-
-
Save shricodev/8164e2c716bc0f0c362fa379e942f1df to your computer and use it in GitHub Desktop.
Blog - Tetris (OpenAI o3)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8" /> | |
<title>Neon Tetris</title> | |
<meta name="viewport" content="width=device-width,initial-scale=1" /> | |
<style> | |
/* --- Layout -------------------------------------------------------------- */ | |
html, | |
body { | |
margin: 0; | |
height: 100%; | |
background: #0b0d13; | |
color: #eee; | |
font-family: | |
system-ui, | |
Segoe UI, | |
sans-serif; | |
user-select: none; | |
overflow: hidden; | |
} | |
#wrap { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
height: 100%; | |
} | |
canvas { | |
background: #0f1118; | |
outline: none; | |
} | |
/* --- Optional small overlay buttons ------------------------------------- */ | |
#btns { | |
position: fixed; | |
top: 12px; | |
right: 12px; | |
display: flex; | |
gap: 8px; | |
} | |
button { | |
padding: 6px 12px; | |
background: #1e2233; | |
color: #9ce8ff; | |
border: 1px solid #355; | |
border-radius: 6px; | |
font-weight: 600; | |
cursor: pointer; | |
} | |
button:hover { | |
background: #29304d; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="wrap"> | |
<canvas id="game" tabindex="0"></canvas> | |
</div> | |
<div id="btns"> | |
<button id="pauseBtn">Pause (P)</button> | |
<button id="restartBtn">Restart (R)</button> | |
</div> | |
<script> | |
/* =========================================================================== | |
Neon Tetris – Vanilla-JS • HTML5 Canvas • Single File | |
=========================================================================== */ | |
const COLS = 10, | |
ROWS = 20, | |
HIDDEN = 2; // playfield + hidden rows | |
const PREVIEW = 3; // pieces shown in next queue | |
const THEMES = { | |
// neon colors | |
I: "#32e0e0", | |
O: "#ffe76b", | |
T: "#d96bff", | |
S: "#6cff72", | |
Z: "#ff6b6b", | |
J: "#6b90ff", | |
L: "#ff9b36", | |
}; | |
const DARK = "#0f1118", | |
GRID = "#141722"; | |
/* Scoring (classic guideline) */ | |
const LINE_SCORE = [0, 100, 300, 500, 800]; // per line count * level | |
const DROP_SOFT = 1, | |
DROP_HARD = 2; | |
const canvas = document.getElementById("game"); | |
const ctx = canvas.getContext("2d"); | |
let size; // pixel size of one block, set on resize | |
let frameId; // requestAnimationFrame handle | |
/* ---------------------------- Piece definitions --------------------------- */ | |
const SHAPES = { | |
I: [ | |
[ | |
[0, 0, 0, 0], | |
[1, 1, 1, 1], | |
[0, 0, 0, 0], | |
[0, 0, 0, 0], | |
], | |
[ | |
[0, 0, 1, 0], | |
[0, 0, 1, 0], | |
[0, 0, 1, 0], | |
[0, 0, 1, 0], | |
], | |
[ | |
[0, 0, 0, 0], | |
[0, 0, 0, 0], | |
[1, 1, 1, 1], | |
[0, 0, 0, 0], | |
], | |
[ | |
[0, 1, 0, 0], | |
[0, 1, 0, 0], | |
[0, 1, 0, 0], | |
[0, 1, 0, 0], | |
], | |
], | |
J: [ | |
[ | |
[1, 0, 0], | |
[1, 1, 1], | |
[0, 0, 0], | |
], | |
[ | |
[0, 1, 1], | |
[0, 1, 0], | |
[0, 1, 0], | |
], | |
[ | |
[0, 0, 0], | |
[1, 1, 1], | |
[0, 0, 1], | |
], | |
[ | |
[0, 1, 0], | |
[0, 1, 0], | |
[1, 1, 0], | |
], | |
], | |
L: [ | |
[ | |
[0, 0, 1], | |
[1, 1, 1], | |
[0, 0, 0], | |
], | |
[ | |
[0, 1, 0], | |
[0, 1, 0], | |
[0, 1, 1], | |
], | |
[ | |
[0, 0, 0], | |
[1, 1, 1], | |
[1, 0, 0], | |
], | |
[ | |
[1, 1, 0], | |
[0, 1, 0], | |
[0, 1, 0], | |
], | |
], | |
O: [ | |
[ | |
[1, 1], | |
[1, 1], | |
], | |
[ | |
[1, 1], | |
[1, 1], | |
], | |
[ | |
[1, 1], | |
[1, 1], | |
], | |
[ | |
[1, 1], | |
[1, 1], | |
], | |
], | |
S: [ | |
[ | |
[0, 1, 1], | |
[1, 1, 0], | |
[0, 0, 0], | |
], | |
[ | |
[0, 1, 0], | |
[0, 1, 1], | |
[0, 0, 1], | |
], | |
[ | |
[0, 0, 0], | |
[0, 1, 1], | |
[1, 1, 0], | |
], | |
[ | |
[1, 0, 0], | |
[1, 1, 0], | |
[0, 1, 0], | |
], | |
], | |
T: [ | |
[ | |
[0, 1, 0], | |
[1, 1, 1], | |
[0, 0, 0], | |
], | |
[ | |
[0, 1, 0], | |
[0, 1, 1], | |
[0, 1, 0], | |
], | |
[ | |
[0, 0, 0], | |
[1, 1, 1], | |
[0, 1, 0], | |
], | |
[ | |
[0, 1, 0], | |
[1, 1, 0], | |
[0, 1, 0], | |
], | |
], | |
Z: [ | |
[ | |
[1, 1, 0], | |
[0, 1, 1], | |
[0, 0, 0], | |
], | |
[ | |
[0, 0, 1], | |
[0, 1, 1], | |
[0, 1, 0], | |
], | |
[ | |
[0, 0, 0], | |
[1, 1, 0], | |
[0, 1, 1], | |
], | |
[ | |
[0, 1, 0], | |
[1, 1, 0], | |
[1, 0, 0], | |
], | |
], | |
}; | |
/* ------------------------------ SRS kicks -------------------------------- */ | |
const KICK_NORMAL = { | |
"0>1": [ | |
[0, 0], | |
[-1, 0], | |
[-1, 1], | |
[0, -2], | |
[-1, -2], | |
], | |
"1>0": [ | |
[0, 0], | |
[1, 0], | |
[1, -1], | |
[0, 2], | |
[1, 2], | |
], | |
"1>2": [ | |
[0, 0], | |
[1, 0], | |
[1, -1], | |
[0, 2], | |
[1, 2], | |
], | |
"2>1": [ | |
[0, 0], | |
[-1, 0], | |
[-1, 1], | |
[0, -2], | |
[-1, -2], | |
], | |
"2>3": [ | |
[0, 0], | |
[1, 0], | |
[1, 1], | |
[0, -2], | |
[1, -2], | |
], | |
"3>2": [ | |
[0, 0], | |
[-1, 0], | |
[-1, -1], | |
[0, 2], | |
[-1, 2], | |
], | |
"3>0": [ | |
[0, 0], | |
[1, 0], | |
[1, 1], | |
[0, -2], | |
[1, -2], | |
], | |
"0>3": [ | |
[0, 0], | |
[-1, 0], | |
[-1, -1], | |
[0, 2], | |
[-1, 2], | |
], | |
}; | |
const KICK_I = { | |
"0>1": [ | |
[0, 0], | |
[-2, 0], | |
[1, 0], | |
[-2, -1], | |
[1, 2], | |
], | |
"1>0": [ | |
[0, 0], | |
[2, 0], | |
[-1, 0], | |
[2, 1], | |
[-1, -2], | |
], | |
"1>2": [ | |
[0, 0], | |
[-1, 0], | |
[2, 0], | |
[-1, 2], | |
[2, -1], | |
], | |
"2>1": [ | |
[0, 0], | |
[1, 0], | |
[-2, 0], | |
[1, -2], | |
[-2, 1], | |
], | |
"2>3": [ | |
[0, 0], | |
[2, 0], | |
[-1, 0], | |
[2, 1], | |
[-1, -2], | |
], | |
"3>2": [ | |
[0, 0], | |
[-2, 0], | |
[1, 0], | |
[-2, -1], | |
[1, 2], | |
], | |
"3>0": [ | |
[0, 0], | |
[1, 0], | |
[-2, 0], | |
[1, -2], | |
[-2, 1], | |
], | |
"0>3": [ | |
[0, 0], | |
[-1, 0], | |
[2, 0], | |
[-1, 2], | |
[2, -1], | |
], | |
}; | |
/* --------------------------- Utility helpers ----------------------------- */ | |
const rand = (n) => Math.floor(Math.random() * n); | |
function createMatrix(w, h, fill = 0) { | |
return Array.from({ length: h }, () => Array(w).fill(fill)); | |
} | |
function deepCopy(obj) { | |
return JSON.parse(JSON.stringify(obj)); | |
} | |
/* ---------------------------- Game State --------------------------------- */ | |
const game = { | |
board: createMatrix(COLS, ROWS + HIDDEN, 0), | |
current: null, // piece object | |
queue: [], | |
hold: null, | |
canHold: true, | |
score: 0, | |
lines: 0, | |
level: 1, | |
over: false, | |
paused: false, | |
dropTimer: 0, | |
dropDelay: 1000, | |
lockTimer: 0, | |
lockDelay: 500, | |
clearLines: [], // lines currently fading | |
clearTimer: 0, | |
}; | |
/* --------------------------- Initialization ------------------------------ */ | |
window.addEventListener("resize", resizeCanvas); | |
resizeCanvas(); | |
spawnPiece(); | |
fillQueue(); | |
gameLoop(0); | |
/* ------------------------- Event Listeners ------------------------------- */ | |
canvas.focus(); | |
document.addEventListener("keydown", handleKey); | |
document.getElementById("pauseBtn").onclick = () => togglePause(); | |
document.getElementById("restartBtn").onclick = resetGame; | |
/* ----------------------------- Functions --------------------------------- */ | |
function resizeCanvas() { | |
// choose size so board fits 60% height, 40% width | |
const h = window.innerHeight * 0.9; | |
const w = window.innerWidth * 0.5; | |
size = Math.floor(Math.min(h / (ROWS + 2), w / COLS)); | |
canvas.width = size * (COLS + 12); // extra space for UI | |
canvas.height = size * (ROWS + HIDDEN); | |
} | |
function bagGenerator() { | |
const bag = ["I", "J", "L", "O", "S", "T", "Z"]; | |
const out = []; | |
while (bag.length) out.push(bag.splice(rand(bag.length), 1)[0]); | |
return out; | |
} | |
function fillQueue() { | |
while (game.queue.length < PREVIEW + 1) { | |
game.queue.push(...bagGenerator()); | |
} | |
} | |
function spawnPiece() { | |
fillQueue(); | |
const type = game.queue.shift(); | |
const piece = { | |
type, | |
rot: 0, | |
matrix: deepCopy(SHAPES[type][0]), | |
x: Math.floor(COLS / 2) - 2, | |
y: -HIDDEN, | |
}; | |
if (collide(piece, piece.x, piece.y)) { | |
game.over = true; | |
} | |
game.current = piece; | |
game.canHold = true; | |
} | |
function rotate(dir) { | |
if (game.over || game.paused) return; | |
const p = game.current; | |
const oldRot = p.rot; | |
const newRot = (p.rot + dir + 4) % 4; | |
const kicks = (p.type === "I" ? KICK_I : KICK_NORMAL)[ | |
`${oldRot}>${newRot}` | |
]; | |
const newMatrix = SHAPES[p.type][newRot]; | |
for (const [dx, dy] of kicks) { | |
if (!collide({ matrix: newMatrix }, p.x + dx, p.y + dy)) { | |
p.matrix = newMatrix; | |
p.rot = newRot; | |
p.x += dx; | |
p.y += dy; | |
playSound("rotate"); | |
return; | |
} | |
} | |
} | |
function collide(obj, x, y) { | |
const m = obj.matrix; | |
for (let r = 0; r < m.length; r++) { | |
for (let c = 0; c < m[r].length; c++) { | |
if (!m[r][c]) continue; | |
const nx = x + c, | |
ny = y + r; | |
if (nx < 0 || nx >= COLS || ny >= ROWS + HIDDEN) return true; | |
if (ny >= 0 && game.board[ny][nx]) return true; | |
} | |
} | |
return false; | |
} | |
function mergePiece() { | |
const { matrix, x, y, type } = game.current; | |
for (let r = 0; r < matrix.length; r++) { | |
for (let c = 0; c < matrix[r].length; c++) { | |
if (matrix[r][c]) { | |
const ny = y + r; | |
if (ny >= 0) game.board[ny][x + c] = type; | |
} | |
} | |
} | |
} | |
function clearLines() { | |
const lines = []; | |
for (let r = ROWS + HIDDEN - 1; r >= 0; r--) { | |
if (game.board[r].every((v) => v)) { | |
lines.push(r); | |
} | |
} | |
if (lines.length) { | |
game.clearLines = lines; | |
game.clearTimer = 0; | |
animateClear(lines); | |
const pts = LINE_SCORE[lines.length] * game.level; | |
game.score += pts; | |
game.lines += lines.length; | |
if (game.lines >= game.level * 10) { | |
game.level++; | |
game.dropDelay = Math.max( | |
100, | |
1000 * | |
(0.8 - Math.min(0.7, (game.level - 1) * 0.007)) ** | |
(game.level - 1), | |
); | |
} | |
} else { | |
spawnPiece(); | |
} | |
} | |
function animateClear(lines) { | |
// remove lines after fade animation | |
setTimeout(() => { | |
for (const r of lines) { | |
game.board.splice(r, 1); | |
game.board.unshift(Array(COLS).fill(0)); | |
} | |
spawnPiece(); | |
}, 200); // 200ms fade | |
} | |
function hold() { | |
if (!game.canHold) return; | |
playSound("hold"); | |
const cur = game.current.type; | |
if (game.hold) { | |
const swap = game.hold; | |
game.hold = cur; | |
spawnPieceFromHold(swap); | |
} else { | |
game.hold = cur; | |
spawnPiece(); | |
} | |
game.canHold = false; | |
} | |
function spawnPieceFromHold(type) { | |
const piece = { | |
type, | |
rot: 0, | |
matrix: deepCopy(SHAPES[type][0]), | |
x: Math.floor(COLS / 2) - 2, | |
y: -HIDDEN, | |
}; | |
game.current = piece; | |
} | |
function move(dx) { | |
const p = game.current; | |
if (!collide(p, p.x + dx, p.y)) { | |
p.x += dx; | |
playSound("move"); | |
} | |
} | |
function softDrop() { | |
const p = game.current; | |
if (!collide(p, p.x, p.y + 1)) { | |
p.y++; | |
game.score += DROP_SOFT; | |
} else lockPiece(); | |
} | |
function hardDrop() { | |
let n = 0; | |
while (!collide(game.current, game.current.x, game.current.y + 1)) { | |
game.current.y++; | |
n++; | |
} | |
game.score += n * DROP_HARD; | |
lockPiece(true); | |
} | |
function lockPiece(immediate = false) { | |
if (immediate) { | |
mergePiece(); | |
clearLines(); | |
return; | |
} | |
if (game.lockTimer === 0) { | |
game.lockTimer = performance.now(); | |
} | |
} | |
function togglePause() { | |
if (game.over) return; | |
game.paused = !game.paused; | |
document.getElementById("pauseBtn").innerText = game.paused | |
? "Resume (P)" | |
: "Pause (P)"; | |
if (!game.paused) gameLoop(performance.now()); | |
} | |
function resetGame() { | |
Object.assign(game, { | |
board: createMatrix(COLS, ROWS + HIDDEN, 0), | |
queue: [], | |
hold: null, | |
score: 0, | |
lines: 0, | |
level: 1, | |
over: false, | |
paused: false, | |
dropTimer: 0, | |
dropDelay: 1000, | |
lockTimer: 0, | |
clearLines: [], | |
clearTimer: 0, | |
}); | |
fillQueue(); | |
spawnPiece(); | |
canvas.focus(); | |
} | |
function handleKey(e) { | |
if (game.over) return; | |
const kc = e.code; | |
if ( | |
["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Space"].includes( | |
kc, | |
) | |
) | |
e.preventDefault(); | |
if (game.paused && kc !== "KeyP") return; | |
switch (kc) { | |
case "ArrowLeft": | |
move(-1); | |
break; | |
case "ArrowRight": | |
move(1); | |
break; | |
case "ArrowUp": | |
rotate(1); | |
break; | |
case "KeyZ": | |
rotate(-1); | |
break; | |
case "ArrowDown": | |
softDrop(); | |
break; | |
case "Space": | |
hardDrop(); | |
break; | |
case "ShiftLeft": | |
case "ShiftRight": | |
hold(); | |
break; | |
case "KeyP": | |
togglePause(); | |
break; | |
case "KeyR": | |
resetGame(); | |
break; | |
case "KeyF": | |
toggleFull(); | |
break; | |
} | |
} | |
function toggleFull() { | |
if (!document.fullscreenElement) { | |
document.documentElement.requestFullscreen(); | |
} else { | |
document.exitFullscreen(); | |
} | |
} | |
/* ------------------------- Game loop & timing --------------------------- */ | |
function gameLoop(now) { | |
if (game.paused) { | |
cancelAnimationFrame(frameId); | |
return; | |
} | |
const delta = now - (game.prev || now); | |
game.prev = now; | |
/* gravity */ | |
game.dropTimer += delta; | |
if (game.dropTimer > game.dropDelay) { | |
game.dropTimer = 0; | |
if (!collide(game.current, game.current.x, game.current.y + 1)) { | |
game.current.y++; | |
} else { | |
lockPiece(); | |
} | |
} | |
/* lock delay */ | |
if (game.lockTimer) { | |
if (performance.now() - game.lockTimer > game.lockDelay) { | |
mergePiece(); | |
game.lockTimer = 0; | |
clearLines(); | |
} else if ( | |
!collide(game.current, game.current.x, game.current.y + 1) | |
) { | |
game.lockTimer = 0; // lifted | |
} | |
} | |
draw(); | |
if (!game.over) frameId = requestAnimationFrame(gameLoop); | |
else drawGameOver(); | |
} | |
/* ------------------------------ Rendering ------------------------------- */ | |
function draw() { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
drawPlayfield(); | |
drawPiece(game.current); | |
drawGhost(); | |
drawUI(); | |
if (game.paused) drawPause(); | |
} | |
function drawPlayfield() { | |
/* background grid */ | |
for (let r = 0; r < ROWS; r++) { | |
for (let c = 0; c < COLS; c++) { | |
ctx.fillStyle = GRID; | |
ctx.fillRect(c * size, (r + HIDDEN) * size, size - 1, size - 1); | |
const cell = game.board[r + HIDDEN][c]; | |
if (cell) drawBlock(c, r + HIDDEN, THEMES[cell]); | |
} | |
} | |
/* fading lines */ | |
if (game.clearLines.length) { | |
const t = (performance.now() % 200) / 200; | |
ctx.fillStyle = "rgba(255,255,255," + 0.7 * (1 - t) + ")"; | |
for (const r of game.clearLines) { | |
ctx.fillRect(0, r * size, COLS * size, size); | |
} | |
} | |
} | |
function drawBlock(x, y, color, alpha = 1) { | |
const px = x * size, | |
py = y * size; | |
const grd = ctx.createLinearGradient(px, py, px + size, py + size); | |
grd.addColorStop(0, color); | |
grd.addColorStop(1, "#000"); | |
ctx.fillStyle = grd; | |
ctx.globalAlpha = alpha; | |
ctx.beginPath(); | |
const rad = 4; | |
ctx.moveTo(px + rad, py); | |
ctx.lineTo(px + size - rad, py); | |
ctx.quadraticCurveTo(px + size, py, px + size, py + rad); | |
ctx.lineTo(px + size, py + size - rad); | |
ctx.quadraticCurveTo(px + size, py + size, px + size - rad, py + size); | |
ctx.lineTo(px + rad, py + size); | |
ctx.quadraticCurveTo(px, py + size, px, py + size - rad); | |
ctx.lineTo(px, py + rad); | |
ctx.quadraticCurveTo(px, py, px + rad, py); | |
ctx.fill(); | |
ctx.globalAlpha = 1; | |
} | |
function drawPiece(p) { | |
const { matrix, x, y, type } = p; | |
for (let r = 0; r < matrix.length; r++) { | |
for (let c = 0; c < matrix[r].length; c++) { | |
if (matrix[r][c]) { | |
const ny = y + r; | |
if (ny >= 0) drawBlock(x + c, ny, THEMES[type]); | |
} | |
} | |
} | |
} | |
function drawGhost() { | |
const p = game.current; | |
let gy = p.y; | |
while (!collide(p, p.x, gy + 1)) gy++; | |
for (let r = 0; r < p.matrix.length; r++) { | |
for (let c = 0; c < p.matrix[r].length; c++) { | |
if (p.matrix[r][c]) { | |
const ny = gy + r; | |
if (ny >= 0) drawBlock(p.x + c, ny, THEMES[p.type], 0.2); | |
} | |
} | |
} | |
} | |
function drawUI() { | |
const left = COLS * size + size; | |
ctx.fillStyle = "#9ce8ff"; | |
ctx.font = size * 0.8 + "px Segoe UI"; | |
ctx.fillText("NEXT", left, size * 1); | |
for (let i = 0; i < PREVIEW; i++) { | |
const type = game.queue[i]; | |
if (!type) continue; | |
const m = SHAPES[type][0]; | |
const offsetX = left; | |
const offsetY = size * (2 + i * 4); | |
for (let r = 0; r < m.length; r++) { | |
for (let c = 0; c < m[r].length; c++) { | |
if (m[r][c]) { | |
drawMiniBlock( | |
offsetX + c * size * 0.7, | |
offsetY + r * size * 0.7, | |
THEMES[type], | |
); | |
} | |
} | |
} | |
} | |
ctx.fillText("HOLD", left, size * (2 + PREVIEW * 4)); | |
if (game.hold) { | |
const m = SHAPES[game.hold][0]; | |
const offsetX = left; | |
const offsetY = size * (3 + PREVIEW * 4); | |
for (let r = 0; r < m.length; r++) { | |
for (let c = 0; c < m[r].length; c++) { | |
if (m[r][c]) | |
drawMiniBlock( | |
offsetX + c * size * 0.7, | |
offsetY + r * size * 0.7, | |
THEMES[game.hold], | |
); | |
} | |
} | |
} | |
ctx.fillText("SCORE " + game.score, left, size * (ROWS - 6)); | |
ctx.fillText("LINES " + game.lines, left, size * (ROWS - 5)); | |
ctx.fillText("LEVEL " + game.level, left, size * (ROWS - 4)); | |
} | |
function drawMiniBlock(px, py, color) { | |
const s = size * 0.7; | |
const grd = ctx.createLinearGradient(px, py, px + s, py + s); | |
grd.addColorStop(0, color); | |
grd.addColorStop(1, "#000"); | |
ctx.fillStyle = grd; | |
ctx.fillRect(px, py, s, s); | |
} | |
function drawPause() { | |
ctx.fillStyle = "rgba(0,0,0,0.6)"; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
ctx.fillStyle = "#fff"; | |
ctx.font = size * 2 + "px Segoe UI"; | |
ctx.textAlign = "center"; | |
ctx.fillText("PAUSED", canvas.width / 2, canvas.height / 2); | |
ctx.textAlign = "start"; | |
} | |
function drawGameOver() { | |
draw(); | |
ctx.fillStyle = "rgba(0,0,0,0.7)"; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
ctx.fillStyle = "#ff5555"; | |
ctx.font = size * 2 + "px Segoe UI"; | |
ctx.textAlign = "center"; | |
ctx.fillText("GAME OVER", canvas.width / 2, canvas.height / 2); | |
ctx.font = size + "px Segoe UI"; | |
ctx.fillStyle = "#9ce8ff"; | |
ctx.fillText( | |
"Press R to restart", | |
canvas.width / 2, | |
canvas.height / 2 + size * 2, | |
); | |
ctx.textAlign = "start"; | |
} | |
/* ----------------------------- Sounds ----------------------------------- */ | |
const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
function beep(freq, dur) { | |
const osc = audioCtx.createOscillator(); | |
const gain = audioCtx.createGain(); | |
osc.type = "square"; | |
osc.frequency.value = freq; | |
osc.connect(gain); | |
gain.connect(audioCtx.destination); | |
gain.gain.setValueAtTime(0.1, audioCtx.currentTime); | |
gain.gain.exponentialRampToValueAtTime( | |
0.001, | |
audioCtx.currentTime + dur / 1000, | |
); | |
osc.start(); | |
osc.stop(audioCtx.currentTime + dur / 1000); | |
} | |
function playSound(event) { | |
switch (event) { | |
case "move": | |
beep(200, 30); | |
break; | |
case "rotate": | |
beep(300, 40); | |
break; | |
case "hold": | |
beep(150, 30); | |
break; | |
} | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment