!doctype html html lang=ko head meta charset=utf-8 meta name=viewport content=width=device-width,initial-scale=1,viewport-fit=cover titleBrick Breaker - Single Filetitle style root { --bg #0b1020; --panel rgba(255,255,255,0.08); --text rgba(255,255,255,0.92); --muted rgba(255,255,255,0.65); --accent #7dd3fc; --danger #fb7185; --good #34d399; --shadow rgba(0,0,0,0.35); } html, body { height 100%; margin 0; background radial-gradient(1200px 600px at 70% 30%, #1b2a5a 0%, var(--bg) 55%, #070a14 100%); color var(--text); } body { display grid; place-items center; font-family ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Noto Sans KR, Arial, Apple SD Gothic Neo, Malgun Gothic, sans-serif; } .wrap { width min(980px, 96vw); height min(720px, 92vh); display grid; grid-template-rows auto 1fr auto; gap 10px; padding 14px; box-sizing border-box; } .topbar { display flex; align-items center; justify-content space-between; padding 10px 12px; background var(--panel); border-radius 14px; box-shadow 0 12px 35px var(--shadow); backdrop-filter blur(8px); -webkit-backdrop-filter blur(8px); gap 10px; flex-wrap wrap; } .stats { display flex; gap 12px; flex-wrap wrap; align-items center; } .pill { padding 7px 10px; border-radius 999px; background rgba(255,255,255,0.10); font-size 13px; color var(--text); display inline-flex; gap 7px; align-items center; letter-spacing 0.2px; user-select none; } .pill b { font-weight 700; } .kbd { font-family ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace; font-size 12px; padding 3px 7px; border-radius 8px; background rgba(0,0,0,0.25); border 1px solid rgba(255,255,255,0.15); color rgba(255,255,255,0.9); user-select none; } .help { font-size 12px; color var(--muted); display flex; gap 8px; flex-wrap wrap; align-items center; user-select none; } .stage { position relative; background rgba(255,255,255,0.06); border-radius 18px; box-shadow 0 18px 50px var(--shadow); overflow hidden; } canvas { width 100%; height 100%; display block; touch-action none; 모바일 스크롤 방지 } .overlay { position absolute; inset 0; display grid; place-items center; pointer-events none; } .card { pointer-events none; width min(560px, 90%); background rgba(10, 14, 30, 0.65); border 1px solid rgba(255,255,255,0.14); border-radius 18px; padding 18px 18px 14px; backdrop-filter blur(10px); -webkit-backdrop-filter blur(10px); box-shadow 0 24px 60px rgba(0,0,0,0.35); text-align left; } .title { font-size 20px; font-weight 800; margin 0 0 10px; letter-spacing 0.2px; } .desc { margin 0 0 12px; color var(--muted); font-size 14px; line-height 1.55; } .grid { display grid; grid-template-columns 1fr 1fr; gap 10px; margin-top 8px; } .line { display flex; align-items center; justify-content space-between; padding 10px 12px; border-radius 14px; background rgba(255,255,255,0.08); border 1px solid rgba(255,255,255,0.10); color rgba(255,255,255,0.9); font-size 13px; } .line span { color rgba(255,255,255,0.78); } .footer { padding 10px 12px; background var(--panel); border-radius 14px; box-shadow 0 12px 35px var(--shadow); backdrop-filter blur(8px); -webkit-backdrop-filter blur(8px); display flex; align-items center; justify-content space-between; gap 10px; flex-wrap wrap; } .note { color var(--muted); font-size 12px; line-height 1.4; } .badge { color rgba(255,255,255,0.85); font-size 12px; user-select none; } .dot { display inline-block; width 7px; height 7px; border-radius 999px; background var(--good); margin-right 6px; box-shadow 0 0 0 4px rgba(52, 211, 153, 0.18); vertical-align middle; } style head body div class=wrap div class=topbar div class=stats div class=pillScore b id=score0bdiv div class=pillLives b id=lives3bdiv div class=pillLevel b id=level1bdiv div class=pillCombo b id=combo0bdiv div class=pillBest b id=best0bdiv div div class=help span class=kbd←spanspan class=kbd→spanspan class=kbdAspanspan class=kbdDspan 이동 span class=kbdSpacespan 시작 span class=kbdPspan 일시정지 span class=kbdRspan 재시작 span class=kbd마우스터치span 패들 제어 div div div class=stage id=stage canvas id=ccanvas div class=overlay id=overlay div class=card id=overlayCard p class=title id=overlayTitle브릭 브레이커p p class=desc id=overlayDesc Space로 시작하세요. 벽돌을 모두 부수면 다음 레벨로 넘어갑니다.br 파워업(드랍 아이템)을 먹으면 패들 확장공 느려짐라이프+1 효과가 발동합니다. p div class=grid div class=linespan시작 볼 발사spanb class=kbdSpacebdiv div class=linespan일시정지spanb class=kbdPbdiv div class=linespan재시작spanb class=kbdRbdiv div class=linespan패들 이동spanb class=kbd← → A Dbdiv div div div div div class=footer div class=note span class=dotspan 팁 공이 패들에 맞는 위치에 따라 반사 각도가 달라집니다. 중앙은 안정적, 가장자리는 큰 각도로 튕깁니다. div div class=badge id=statusREADYdiv div div script (() = { use strict; ====== DOM ====== const canvas = document.getElementById(c); const stage = document.getElementById(stage); const ctx = canvas.getContext(2d); const elScore = document.getElementById(score); const elLives = document.getElementById(lives); const elLevel = document.getElementById(level); const elCombo = document.getElementById(combo); const elBest = document.getElementById(best); const elStatus = document.getElementById(status); const overlay = document.getElementById(overlay); const overlayTitle = document.getElementById(overlayTitle); const overlayDesc = document.getElementById(overlayDesc); ====== Utilities ====== const clamp = (v, a, b) = Math.max(a, Math.min(b, v)); const lerp = (a, b, t) = a + (b - a) t; function roundRect(ctx, x, y, w, h, r) { r = Math.min(r, w2, h2); ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); } ====== HiDPI resize ====== let W = 0, H = 0, DPR = 1; function resize() { const rect = stage.getBoundingClientRect(); DPR = Math.max(1, Math.min(2, window.devicePixelRatio 1)); 과도한 DPR 제한 W = Math.floor(rect.width); H = Math.floor(rect.height); canvas.width = Math.floor(W DPR); canvas.height = Math.floor(H DPR); ctx.setTransform(DPR, 0, 0, DPR, 0, 0); } window.addEventListener(resize, resize, { passive true }); resize(); ====== Game State ====== const storeKey = brick_breaker_best_v1; let bestScore = Number(localStorage.getItem(storeKey) 0); elBest.textContent = bestScore.toString(); const GAME = { running false, paused false, over false, win false, awaitingServe true, Space로 발사 대기 score 0, lives 3, level 1, combo 0, comboTimer 0, time 0, shake 0, Difficulty scaling baseBallSpeed 360, speedScale 1.0, }; Paddle const paddle = { w 120, h 14, x 0, y 0, vx 0, targetX 0, maxSpeed 900, friction 0.84, baseW 120, widenTimer 0 }; Ball const ball = { r 8, x 0, y 0, vx 0, vy 0, stuckToPaddle true, slowTimer 0 }; Bricks let bricks = []; const BRICK = { cols 10, rows 6, pad 10, top 70, side 26, h 22, radius 10 }; Powerups const powerups = []; const POWER = { dropChance 0.18, brick 파괴시 드랍 확률 size 18, fallSpeed 260 }; const POWER_TYPES = [ { id WIDEN, label W, desc 패들 확장, color #7dd3fc }, { id SLOW, label S, desc 공 느려짐, color #a7f3d0 }, { id LIFE, label +, desc 라이프 +1, color #fb7185 }, ]; Input const input = { left false, right false, pointerActive false, pointerX 0 }; function setStatus(text) { elStatus.textContent = text; } function showOverlay(title, desc) { overlay.style.display = grid; overlayTitle.textContent = title; overlayDesc.innerHTML = desc; } function hideOverlay() { overlay.style.display = none; } ====== Level generation ====== function buildLevel(level) { bricks.length = 0; powerups.length = 0; 레벨이 올라갈수록 rows 증가체력 증가속도 증가 const rows = clamp(5 + Math.floor((level - 1) 0.6), 5, 10); const cols = clamp(9 + Math.floor((level - 1) 0.35), 9, 12); BRICK.rows = rows; BRICK.cols = cols; const hpBase = 1 + Math.floor((level - 1) 0.25); 패턴 레벨에 따라 조금씩 다른 배치 for (let r = 0; r rows; r++) { for (let c = 0; c cols; c++) { 무늬 만들기 대각선홀수줄 빈칸 등을 섞음 let alive = true; if (level = 3 && (r + c) % 11 === 0) alive = false; if (level = 6 && r % 2 === 1 && c % 7 === 0) alive = false; if (!alive) continue; 체력 위쪽 줄이 더 단단 const hp = clamp(hpBase + Math.floor((rows - 1 - r) 0.15), 1, 4); bricks.push({ r, c, hp, maxHp hp, alive true }); } } 난이도 스케일 GAME.speedScale = 1.0 + (level - 1) 0.06; paddle.baseW = clamp(120 - (level - 1) 2, 90, 120); paddle.w = paddle.baseW; 공패들 초기 위치 resetServe(true); } function resetServe(fullResetBall = false) { paddle.w = paddle.widenTimer 0 paddle.w paddle.baseW; paddle.h = 14; paddle.x = (W - paddle.w) 2; paddle.y = H - 54; paddle.vx = 0; paddle.targetX = paddle.x; if (fullResetBall) { ball.r = 8; ball.slowTimer = 0; } ball.stuckToPaddle = true; ball.x = paddle.x + paddle.w 2; ball.y = paddle.y - ball.r - 2; ball.vx = 0; ball.vy = 0; GAME.awaitingServe = true; GAME.running = true; GAME.paused = false; GAME.over = false; GAME.win = false; setStatus(READY); showOverlay( GAME.level === 1 && GAME.score === 0 브릭 브레이커 `LEVEL ${GAME.level}`, `Space로 시작합니다.br키보드마우스터치로 패들을 움직이세요.` ); } function serveBall() { if (!GAME.awaitingServe) return; hideOverlay(); GAME.awaitingServe = false; ball.stuckToPaddle = false; 초기 발사 각도 랜덤(너무 수평수직 피하기) const angle = (Math.random() 0.8 + 0.2) Math.PI; 0.2π ~ 1.0π const speed = GAME.baseBallSpeed GAME.speedScale; ball.vx = Math.cos(angle) speed (Math.random() 0.5 1 -1); ball.vy = -Math.abs(Math.sin(angle) speed); setStatus(PLAY); } ====== Collision helpers ====== function circleRectCollide(cx, cy, cr, rx, ry, rw, rh) { const closestX = clamp(cx, rx, rx + rw); const closestY = clamp(cy, ry, ry + rh); const dx = cx - closestX; const dy = cy - closestY; return (dxdx + dydy) = crcr; } ====== Powerups ====== function spawnPowerup(x, y) { const t = POWER_TYPES[Math.floor(Math.random() POWER_TYPES.length)]; powerups.push({ type t.id, label t.label, desc t.desc, color t.color, x, y, vy POWER.fallSpeed, alive true }); } function applyPowerup(type) { if (type === WIDEN) { paddle.widenTimer = 10.0; seconds paddle.w = clamp(paddle.baseW 1.55, paddle.baseW, 220); GAME.score += 50; setStatus(POWER WIDEN); } else if (type === SLOW) { ball.slowTimer = 8.0; seconds GAME.score += 50; setStatus(POWER SLOW); } else if (type === LIFE) { GAME.lives = Math.min(9, GAME.lives + 1); GAME.score += 80; setStatus(POWER +LIFE); } syncUI(); GAME.shake = 6; } ====== UI sync ====== function syncUI() { elScore.textContent = GAME.score.toString(); elLives.textContent = GAME.lives.toString(); elLevel.textContent = GAME.level.toString(); elCombo.textContent = GAME.combo.toString(); if (GAME.score bestScore) { bestScore = GAME.score; localStorage.setItem(storeKey, String(bestScore)); elBest.textContent = bestScore.toString(); } } ====== Reset Restart ====== function restartAll() { GAME.score = 0; GAME.lives = 3; GAME.level = 1; GAME.combo = 0; GAME.comboTimer = 0; GAME.time = 0; GAME.shake = 0; paddle.widenTimer = 0; ball.slowTimer = 0; buildLevel(GAME.level); syncUI(); setStatus(READY); showOverlay(브릭 브레이커, Space로 시작하세요.br벽돌을 모두 부수면 다음 레벨로 넘어갑니다.); } ====== Input handlers ====== window.addEventListener(keydown, (e) = { const k = e.key.toLowerCase(); if (k === arrowleft k === a) input.left = true; if (k === arrowright k === d) input.right = true; if (k === p) { if (GAME.running && !GAME.over) { GAME.paused = !GAME.paused; if (GAME.paused) { setStatus(PAUSED); showOverlay(일시정지, 다시 진행 Pbr재시작 R); } else { hideOverlay(); setStatus(PLAY); } } } if (k === r) { restartAll(); } if (k === k === spacebar) { if (GAME.over) return; if (GAME.paused) return; serveBall(); } }); window.addEventListener(keyup, (e) = { const k = e.key.toLowerCase(); if (k === arrowleft k === a) input.left = false; if (k === arrowright k === d) input.right = false; }); function pointerToCanvasX(clientX) { const rect = canvas.getBoundingClientRect(); const x = (clientX - rect.left) rect.width W; return x; } stage.addEventListener(mousemove, (e) = { input.pointerActive = true; input.pointerX = pointerToCanvasX(e.clientX); }); stage.addEventListener(mouseleave, () = { input.pointerActive = false; }); stage.addEventListener(touchstart, (e) = { input.pointerActive = true; const t = e.touches[0]; input.pointerX = pointerToCanvasX(t.clientX); }, { passive true }); stage.addEventListener(touchmove, (e) = { input.pointerActive = true; const t = e.touches[0]; input.pointerX = pointerToCanvasX(t.clientX); }, { passive true }); stage.addEventListener(touchend, () = { input.pointerActive = false; }, { passive true }); stage.addEventListener(click, () = { 클릭으로도 발사 가능(초기 진입 편의) if (!GAME.paused && !GAME.over) serveBall(); }); ====== Core loop ====== let last = performance.now(); function loop(now) { const dt = Math.min(0.033, (now - last) 1000); clamp 33ms last = now; if (GAME.running && !GAME.paused) { update(dt); render(dt); } else { paused 상태에서도 살짝 배경만 그려줌 render(dt); } requestAnimationFrame(loop); } function update(dt) { GAME.time += dt; combo decay if (GAME.combo 0) { GAME.comboTimer -= dt; if (GAME.comboTimer = 0) { GAME.combo = 0; GAME.comboTimer = 0; syncUI(); } } power timers if (paddle.widenTimer 0) { paddle.widenTimer -= dt; if (paddle.widenTimer = 0) { paddle.widenTimer = 0; paddle.w = paddle.baseW; setStatus(PLAY); } } if (ball.slowTimer 0) { ball.slowTimer -= dt; if (ball.slowTimer = 0) { ball.slowTimer = 0; setStatus(PLAY); } } paddle movement const accel = 2200; if (input.pointerActive) { pointer 모드 목표 위치로 부드럽게 따라감 const target = input.pointerX - paddle.w 2; paddle.targetX = clamp(target, 10, W - paddle.w - 10); paddle.x = lerp(paddle.x, paddle.targetX, 1 - Math.pow(0.001, dt)); 프레임율 독립 보간 paddle.vx = (paddle.targetX - paddle.x) Math.max(dt, 1e-6); } else { keyboard 모드 가속마찰 if (input.left) paddle.vx -= accel dt; if (input.right) paddle.vx += accel dt; paddle.vx = Math.pow(paddle.friction, dt 60); paddle.vx = clamp(paddle.vx, -paddle.maxSpeed, paddle.maxSpeed); paddle.x += paddle.vx dt; paddle.x = clamp(paddle.x, 10, W - paddle.w - 10); } ball follows paddle before serve if (ball.stuckToPaddle) { ball.x = paddle.x + paddle.w 2; ball.y = paddle.y - ball.r - 2; return; } ball speed modifier (slow power) const slowFactor = ball.slowTimer 0 0.70 1.0; move ball ball.x += ball.vx dt slowFactor; ball.y += ball.vy dt slowFactor; walls collision const wall = 10; if (ball.x - ball.r wall) { ball.x = wall + ball.r; ball.vx = -1; GAME.shake = 2; } if (ball.x + ball.r W - wall) { ball.x = W - wall - ball.r; ball.vx = -1; GAME.shake = 2; } if (ball.y - ball.r wall) { ball.y = wall + ball.r; ball.vy = -1; GAME.shake = 2; } bottom - lose life if (ball.y - ball.r H + 30) { GAME.lives -= 1; syncUI(); if (GAME.lives = 0) { GAME.over = true; GAME.running = false; setStatus(GAME OVER); showOverlay(게임 오버, `Score b${GAME.score}bbr재시작 R`); return; } reset serve (life lost) GAME.combo = 0; GAME.comboTimer = 0; syncUI(); resetServe(false); return; } paddle collision if (circleRectCollide(ball.x, ball.y, ball.r, paddle.x, paddle.y, paddle.w, paddle.h)) { ball is above paddle - reflect const hitPos = (ball.x - (paddle.x + paddle.w 2)) (paddle.w 2); -1..1 const maxBounce = 75 Math.PI 180; 75 degrees const bounceAngle = hitPos maxBounce; const speed = Math.hypot(ball.vx, ball.vy); ball.vx = Math.sin(bounceAngle) speed; ball.vy = -Math.cos(bounceAngle) speed; prevent sticking ball.y = paddle.y - ball.r - 0.5; GAME.shake = 3; setStatus(PLAY); } brick layout sizes computed from current stage const usableW = W - BRICK.side 2; const brickW = (usableW - BRICK.pad (BRICK.cols - 1)) BRICK.cols; const brickH = BRICK.h; bricks collision let hitBrick = null; for (const b of bricks) { if (!b.alive) continue; const x = BRICK.side + b.c (brickW + BRICK.pad); const y = BRICK.top + b.r (brickH + BRICK.pad); if (circleRectCollide(ball.x, ball.y, ball.r, x, y, brickW, brickH)) { hitBrick = { b, x, y, w brickW, h brickH }; break; } } if (hitBrick) { const { b, x, y, w, h } = hitBrick; Determine bounce direction by comparing penetration const prevX = ball.x - ball.vx dt slowFactor; const prevY = ball.y - ball.vy dt slowFactor; const fromLeft = prevX = x; const fromRight = prevX = x + w; const fromTop = prevY = y; const fromBottom = prevY = y + h; If coming clearly from one side, reflect accordingly if (fromLeft fromRight) ball.vx = -1; if (fromTop fromBottom) ball.vy = -1; If ambiguous, choose axis with smaller overlap if (!(fromLeft fromRight fromTop fromBottom)) { const overlapX = Math.min(Math.abs((x) - (ball.x + ball.r)), Math.abs((x + w) - (ball.x - ball.r))); const overlapY = Math.min(Math.abs((y) - (ball.y + ball.r)), Math.abs((y + h) - (ball.y - ball.r))); if (overlapX overlapY) ball.vx = -1; else ball.vy = -1; } brick damage b.hp -= 1; GAME.shake = 4; if (b.hp = 0) { b.alive = false; scoring & combo GAME.combo += 1; GAME.comboTimer = 2.2; seconds const comboBonus = Math.min(400, GAME.combo 8); GAME.score += 100 + comboBonus; powerup drop chance if (Math.random() POWER.dropChance) { const px = x + w 2; const py = y + h 2; spawnPowerup(px, py); } } else { minor score for damaging a brick GAME.score += 20; } syncUI(); win check const remain = bricks.some(br = br.alive); if (!remain) { GAME.win = true; GAME.level += 1; syncUI(); setStatus(LEVEL CLEAR); showOverlay(레벨 클리어!, `다음 레벨 b${GAME.level}bbrSpace로 시작`); buildLevel(GAME.level); return; } } update powerups falling for (const p of powerups) { if (!p.alive) continue; p.y += p.vy dt; catch by paddle const px = p.x - POWER.size2; const py = p.y - POWER.size2; if (px paddle.x + paddle.w && px + POWER.size paddle.x && py paddle.y + paddle.h && py + POWER.size paddle.y) { p.alive = false; applyPowerup(p.type); } out of screen if (p.y H + 40) p.alive = false; } screen shake decay if (GAME.shake 0) GAME.shake = Math.max(0, GAME.shake - 40 dt); } ====== Rendering ====== function render(dt) { background ctx.clearRect(0, 0, W, H); subtle stars drawBackdrop(); shake transform if (GAME.shake 0) { const s = GAME.shake; const ox = (Math.random() 2 - 1) s; const oy = (Math.random() 2 - 1) s; ctx.save(); ctx.translate(ox, oy); drawScene(); ctx.restore(); } else { drawScene(); } } function drawBackdrop() { gradient overlay inside stage const g = ctx.createLinearGradient(0, 0, 0, H); g.addColorStop(0, rgba(255,255,255,0.03)); g.addColorStop(1, rgba(0,0,0,0.12)); ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); moving dust ctx.save(); ctx.globalAlpha = 0.35; for (let i = 0; i 32; i++) { const t = GAME.time (0.04 + i 0.001); const x = (Math.sin(t 1.7 + i) 0.5 + 0.5) W; const y = (Math.cos(t 1.2 + i 1.7) 0.5 + 0.5) H; const r = 1.2 + (i % 5) 0.35; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI 2); ctx.fillStyle = rgba(255,255,255,0.25); ctx.fill(); } ctx.restore(); } function drawScene() { arena border ctx.save(); ctx.strokeStyle = rgba(255,255,255,0.10); ctx.lineWidth = 2; roundRect(ctx, 10, 10, W - 20, H - 20, 18); ctx.stroke(); ctx.restore(); bricks drawBricks(); powerups drawPowerups(); paddle drawPaddle(); ball drawBall(); HUD hint text on serve if (GAME.awaitingServe && !GAME.over) { ctx.save(); ctx.fillStyle = rgba(255,255,255,0.55); ctx.font = 600 14px ui-sans-serif, system-ui; ctx.textAlign = center; ctx.fillText(Space(또는 클릭)로 공 발사, W2, H - 22); ctx.restore(); } } function drawBricks() { const usableW = W - BRICK.side 2; const brickW = (usableW - BRICK.pad (BRICK.cols - 1)) BRICK.cols; const brickH = BRICK.h; for (const b of bricks) { if (!b.alive) continue; const x = BRICK.side + b.c (brickW + BRICK.pad); const y = BRICK.top + b.r (brickH + BRICK.pad); const hpRatio = b.hp b.maxHp; color by row + hp const baseHue = (200 + (b.r 14) + (b.c 6)) % 360; const sat = 75; const light = 62 - (1 - hpRatio) 18; ctx.save(); glow ctx.shadowColor = `hsla(${baseHue}, ${sat}%, ${light}%, 0.38)`; ctx.shadowBlur = 14; ctx.fillStyle = `hsla(${baseHue}, ${sat}%, ${light}%, 0.85)`; roundRect(ctx, x, y, brickW, brickH, BRICK.radius); ctx.fill(); inner highlight ctx.shadowBlur = 0; ctx.strokeStyle = rgba(255,255,255,0.20); ctx.lineWidth = 1; roundRect(ctx, x + 0.8, y + 0.8, brickW - 1.6, brickH - 1.6, BRICK.radius - 2); ctx.stroke(); hp pips (for hp=2) if (b.maxHp = 2) { ctx.globalAlpha = 0.9; ctx.fillStyle = rgba(0,0,0,0.25); const pipCount = b.maxHp; const pipW = (brickW - 16) pipCount; for (let i = 0; i pipCount; i++) { const px = x + 8 + i pipW; const py = y + brickH - 7; ctx.fillRect(px, py, pipW - 2, 3); } ctx.fillStyle = rgba(255,255,255,0.85); for (let i = 0; i b.hp; i++) { const px = x + 8 + i pipW; const py = y + brickH - 7; ctx.fillRect(px, py, pipW - 2, 3); } } ctx.restore(); } } function drawPowerups() { for (const p of powerups) { if (!p.alive) continue; const s = POWER.size; const x = p.x - s2; const y = p.y - s2; ctx.save(); ctx.shadowColor = p.color; ctx.shadowBlur = 18; ctx.fillStyle = rgba(255,255,255,0.08); roundRect(ctx, x, y, s, s, 8); ctx.fill(); ctx.shadowBlur = 0; ctx.strokeStyle = rgba(255,255,255,0.20); ctx.lineWidth = 1; roundRect(ctx, x, y, s, s, 8); ctx.stroke(); ctx.fillStyle = p.color; ctx.font = 800 14px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; ctx.textAlign = center; ctx.textBaseline = middle; ctx.fillText(p.label, p.x, p.y + 0.5); ctx.restore(); } } function drawPaddle() { ctx.save(); const x = paddle.x; const y = paddle.y; const w = paddle.w; const h = paddle.h; glow changes when widened const glow = paddle.widenTimer 0 rgba(125,211,252,0.55) rgba(255,255,255,0.25); ctx.shadowColor = glow; ctx.shadowBlur = 18; const g = ctx.createLinearGradient(x, y, x + w, y); g.addColorStop(0, rgba(255,255,255,0.22)); g.addColorStop(0.5, rgba(255,255,255,0.10)); g.addColorStop(1, rgba(255,255,255,0.20)); ctx.fillStyle = g; roundRect(ctx, x, y, w, h, 10); ctx.fill(); ctx.shadowBlur = 0; ctx.strokeStyle = rgba(255,255,255,0.18); ctx.lineWidth = 1; roundRect(ctx, x + 0.5, y + 0.5, w - 1, h - 1, 10); ctx.stroke(); center mark ctx.globalAlpha = 0.7; ctx.fillStyle = rgba(255,255,255,0.25); ctx.fillRect(x + w2 - 10, y + h2 - 1, 20, 2); ctx.restore(); } function drawBall() { ctx.save(); trail-ish glow const speed = Math.hypot(ball.vx, ball.vy); const glow = clamp(speed (GAME.baseBallSpeed 1.2), 0.4, 1.0); ctx.shadowColor = `rgba(125,211,252,${0.25 + glow0.35})`; ctx.shadowBlur = 18; ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI 2); ctx.fillStyle = rgba(255,255,255,0.92); ctx.fill(); ctx.shadowBlur = 0; ctx.strokeStyle = rgba(0,0,0,0.18); ctx.lineWidth = 1; ctx.stroke(); small highlight ctx.beginPath(); ctx.arc(ball.x - ball.r0.28, ball.y - ball.r0.28, ball.r0.35, 0, Math.PI2); ctx.fillStyle = rgba(255,255,255,0.55); ctx.fill(); ctx.restore(); } ====== Start ====== buildLevel(GAME.level); syncUI(); showOverlay(브릭 브레이커, Space로 시작하세요.br키보드마우스터치로 패들을 움직입니다.); requestAnimationFrame(loop); })(); script body html