!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