Hamburger breakoutOne-line
Gemini 3.8 Flash
by GoogleReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 6 s
- Cost
- $0.041
- Output tokens
- 10,915
- Spent thinking
- 471 tokens
- Speed
- 186.8 tok/s
- Console errors
- 0
- First token after
- 7.5 s
- First visible text after
- 7.5 s
- Prompt tokens
- 363
- Code size
- 24.1 KB
- Finish reason
- stop
- Served by
- Model id
google/gemini-3.8-flash- Reasoning sent
{"effort":"medium"}- Max output
- 65,536 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
“i want a breakout game where the paddle is a hamburger and every brick you break explodes into tons of little hearts”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 961 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Burger Breakout - Heart Explosion Edition</title>
<style>
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
margin: 0;
padding: 0;
}
body {
background: radial-gradient(circle at 50% 30%, #2b1238, #11051b);
color: #fff;
font-family: 'Segoe UI', Roboto, -apple-system, BlinkMacSystemFont, sans-serif;
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#game-container {
position: relative;
width: 100%;
max-width: 900px;
height: 100%;
max-height: 720px;
box-shadow: 0 10px 40px rgba(255, 60, 120, 0.25), 0 0 100px rgba(0,0,0,0.8);
border-radius: 16px;
overflow: hidden;
border: 2px solid rgba(255, 120, 180, 0.2);
background: #0d0214;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
#overlay {
position: absolute;
inset: 0;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 16px 24px;
}
.hud {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 800;
text-shadow: 0 2px 10px rgba(0,0,0,0.8);
}
.hud-stat {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.08);
padding: 6px 14px;
border-radius: 20px;
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.15);
font-size: 15px;
letter-spacing: 0.5px;
}
.hud-val {
color: #ff528c;
font-size: 18px;
}
#banner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
pointer-events: auto;
cursor: pointer;
background: rgba(20, 5, 28, 0.88);
border: 2px solid rgba(255, 100, 170, 0.5);
padding: 24px 36px;
border-radius: 24px;
box-shadow: 0 0 50px rgba(255, 60, 120, 0.4), inset 0 0 20px rgba(255, 60, 120, 0.2);
transition: transform 0.2s ease, opacity 0.3s ease;
backdrop-filter: blur(12px);
}
#banner:hover {
transform: translate(-50%, -50%) scale(1.02);
}
#banner.hidden {
opacity: 0;
pointer-events: none;
transform: translate(-50%, -50%) scale(0.9);
}
#banner h1 {
font-size: 32px;
margin-bottom: 8px;
background: linear-gradient(45deg, #ffc048, #ff3377, #ff9ff3);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 900;
}
#banner p {
color: #e0d0f0;
font-size: 14px;
line-height: 1.5;
margin-bottom: 14px;
}
.tag-badge {
display: inline-block;
padding: 6px 14px;
background: #ff2a70;
color: #fff;
border-radius: 12px;
font-size: 13px;
font-weight: 700;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); box-shadow: 0 0 10px rgba(255, 42, 112, 0.6); }
50% { transform: scale(1.05); box-shadow: 0 0 20px rgba(255, 42, 112, 0.9); }
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas" width="900" height="720"></canvas>
<div id="overlay">
<div class="hud">
<div class="hud-stat">🍔 BURGER SCORE: <span class="hud-val" id="score-val">0</span></div>
<div class="hud-stat">💖 COMBO: <span class="hud-val" id="combo-val">x1</span></div>
<div class="hud-stat">❤️ LIVES: <span class="hud-val" id="lives-val">3</span></div>
</div>
</div>
<div id="banner">
<h1>🍔 BURGER BREAKOUT 💖</h1>
<p>Bounce the Tomato with your Juicy Burger Paddle!<br>Every brick explodes into a shower of love!</p>
<div class="tag-badge" id="banner-btn">CLICK OR PRESS ANY KEY TO PLAY</div>
</div>
</div>
<script>
// --- AUDIO SYNTHESIS (Zero External Files) ---
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = null;
function initAudio() {
if (!audioCtx) {
audioCtx = new AudioContext();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playSound(type) {
if (!audioCtx) return;
try {
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
if (type === 'paddle') {
osc.type = 'triangle';
osc.frequency.setValueAtTime(140, now);
osc.frequency.exponentialRampToValueAtTime(320, now + 0.12);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
osc.start(now);
osc.stop(now + 0.15);
} else if (type === 'brick') {
osc.type = 'sine';
const base = 440 + Math.random() * 200;
osc.frequency.setValueAtTime(base, now);
osc.frequency.exponentialRampToValueAtTime(base * 1.8, now + 0.15);
gain.gain.setValueAtTime(0.25, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
} else if (type === 'heart') {
// Shimmering chime
osc.type = 'sine';
const notes = [659.25, 783.99, 987.77, 1174.66, 1318.51];
const pitch = notes[Math.floor(Math.random() * notes.length)];
osc.frequency.setValueAtTime(pitch, now);
gain.gain.setValueAtTime(0.12, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
osc.start(now);
osc.stop(now + 0.25);
} else if (type === 'lose') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(280, now);
osc.frequency.linearRampToValueAtTime(90, now + 0.4);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
osc.start(now);
osc.stop(now + 0.4);
}
} catch(e) {}
}
// --- CANVAS SETUP ---
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score-val');
const comboEl = document.getElementById('combo-val');
const livesEl = document.getElementById('lives-val');
const banner = document.getElementById('banner');
const W = 900;
const H = 720;
let isAutoplay = true;
let score = 0;
let lives = 3;
let combo = 1;
let comboTimer = 0;
let shakeTime = 0;
let shakeAmp = 0;
// --- BURGER PADDLE ---
const paddle = {
w: 140,
h: 42,
x: W / 2 - 70,
y: H - 75,
targetX: W / 2 - 70,
squishX: 1,
squishY: 1,
tilt: 0
};
// --- BALL (Cherry Tomato) ---
const balls = [];
function createBall(x, y, vx, vy) {
return {
x: x ?? W / 2,
y: y ?? paddle.y - 20,
r: 12,
vx: vx ?? 4.5 * (Math.random() > 0.5 ? 1 : -1),
vy: vy ?? -6.5,
trail: []
};
}
balls.push(createBall());
// --- BRICKS ---
const BRICK_ROWS = 6;
const BRICK_COLS = 10;
const BRICK_W = 74;
const BRICK_H = 26;
const BRICK_GAP = 12;
const BRICK_OFFSET_X = (W - (BRICK_COLS * BRICK_W + (BRICK_COLS - 1) * BRICK_GAP)) / 2;
const BRICK_OFFSET_Y = 70;
const BRICK_THEMES = [
{ name: 'Berry Glaze', top: '#ff3377', bot: '#c0134f', light: '#ff8fb2' },
{ name: 'Cheddar Gold', top: '#ffb930', bot: '#d97b0a', light: '#ffe17d' },
{ name: 'Crisp Lettuce', top: '#38ef7d', bot: '#11998e', light: '#a8ff78' },
{ name: 'Sweet Ketchup', top: '#ff4b4b', bot: '#b81212', light: '#ffa3a3' },
{ name: 'Pickle Lime', top: '#8ee53f', bot: '#4c9b13', light: '#c8f792' },
{ name: 'Toasted Sesame', top: '#e6a86c', bot: '#a06830', light: '#ffddaa' }
];
let bricks = [];
function initBricks() {
bricks = [];
for (let r = 0; r < BRICK_ROWS; r++) {
const theme = BRICK_THEMES[r % BRICK_THEMES.length];
for (let c = 0; c < BRICK_COLS; c++) {
bricks.push({
x: BRICK_OFFSET_X + c * (BRICK_W + BRICK_GAP),
y: BRICK_OFFSET_Y + r * (BRICK_H + BRICK_GAP),
w: BRICK_W,
h: BRICK_H,
active: true,
theme: theme,
hp: (r === 0 ? 2 : 1),
maxHp: (r === 0 ? 2 : 1),
wobble: 0
});
}
}
}
initBricks();
// --- HEART PARTICLES ---
const hearts = [];
const heartGradients = [
['#ff1493', '#ff69b4'],
['#ff0055', '#ff4d88'],
['#ff3377', '#ffa3c2'],
['#ff4081', '#ffd1dc'],
['#e0115f', '#ff729f'],
['#ffc0cb', '#ffffff']
];
function spawnHearts(bx, by, count = 35) {
playSound('brick');
playSound('heart');
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 2 + Math.random() * 7.5;
const size = 7 + Math.random() * 11;
const palette = heartGradients[Math.floor(Math.random() * heartGradients.length)];
hearts.push({
x: bx,
y: by,
vx: Math.cos(angle) * speed + (Math.random() - 0.5) * 2,
vy: Math.sin(angle) * speed - 2.5,
size: size,
rot: Math.random() * Math.PI * 2,
vRot: (Math.random() - 0.5) * 0.25,
alpha: 1,
decay: 0.012 + Math.random() * 0.018,
wobbleSpeed: 4 + Math.random() * 6,
wobbleAmp: Math.random() * 1.5,
colorA: palette[0],
colorB: palette[1],
sparkle: Math.random() > 0.4
});
}
}
// Floaty ambient hearts rising from bottom occasionally
let ambientTimer = 0;
function spawnAmbientHeart() {
const palette = heartGradients[Math.floor(Math.random() * heartGradients.length)];
hearts.push({
x: Math.random() * W,
y: H + 10,
vx: (Math.random() - 0.5) * 1.2,
vy: -(1.5 + Math.random() * 2),
size: 6 + Math.random() * 8,
rot: Math.random() * Math.PI,
vRot: (Math.random() - 0.5) * 0.08,
alpha: 0.65,
decay: 0.004,
wobbleSpeed: 2,
wobbleAmp: 1,
colorA: palette[0],
colorB: palette[1],
sparkle: false
});
}
// --- DRAWING UTILS ---
// Draw a smooth cute bezier heart
function drawHeart(c, x, y, size, rot, colorA, colorB, alpha) {
c.save();
c.translate(x, y);
c.rotate(rot);
c.globalAlpha = Math.max(0, Math.min(1, alpha));
const grad = c.createLinearGradient(0, -size, 0, size);
grad.addColorStop(0, colorA);
grad.addColorStop(1, colorB);
c.fillStyle = grad;
c.beginPath();
const topCurveHeight = size * 0.35;
c.moveTo(0, size * 0.4);
// top left
c.bezierCurveTo(-size * 0.8, -size * 0.2, -size * 0.8, -size * 0.8, 0, -size * 0.35);
// top right
c.bezierCurveTo(size * 0.8, -size * 0.8, size * 0.8, -size * 0.2, 0, size * 0.4);
c.closePath();
c.fill();
// Gloss highlight
c.fillStyle = 'rgba(255,255,255,0.45)';
c.beginPath();
c.arc(-size * 0.25, -size * 0.4, size * 0.12, 0, Math.PI * 2);
c.fill();
c.restore();
}
// Draw realistic cute hamburger paddle
function drawBurger(c, p) {
c.save();
c.translate(p.x + p.w / 2, p.y + p.h / 2);
c.rotate(p.tilt);
c.scale(p.squishX, p.squishY);
const hw = p.w / 2;
const hh = p.h / 2;
// Shadow under burger
c.shadowColor = 'rgba(255, 60, 130, 0.35)';
c.shadowBlur = 18;
c.fillStyle = 'rgba(0,0,0,0.3)';
c.beginPath();
c.ellipse(0, hh + 6, hw * 0.9, 8, 0, 0, Math.PI * 2);
c.fill();
c.shadowBlur = 0;
// 1. BOTTOM BUN
const botBunGrad = c.createLinearGradient(0, 4, 0, hh);
botBunGrad.addColorStop(0, '#e29b52');
botBunGrad.addColorStop(1, '#b56828');
c.fillStyle = botBunGrad;
c.beginPath();
c.roundRect(-hw + 8, 4, p.w - 16, hh - 2, [4, 4, 14, 14]);
c.fill();
// 2. BEEF PATTY
const pattyGrad = c.createLinearGradient(0, -2, 0, 8);
pattyGrad.addColorStop(0, '#532314');
pattyGrad.addColorStop(0.5, '#3b170c');
pattyGrad.addColorStop(1, '#270c04');
c.fillStyle = pattyGrad;
c.beginPath();
c.roundRect(-hw + 4, -2, p.w - 8, 11, 5);
c.fill();
// Grill marks
c.strokeStyle = '#1e0701';
c.lineWidth = 2.5;
for (let i = -hw + 18; i < hw - 10; i += 18) {
c.beginPath();
c.moveTo(i, -2);
c.lineTo(i + 8, 9);
c.stroke();
}
// 3. MELTED CHEESE (Points dripping down)
c.fillStyle = '#ffb300';
c.beginPath();
c.moveTo(-hw + 6, -1);
c.lineTo(hw - 6, -1);
c.lineTo(hw - 10, 6);
// Drippy cheese triangles
const drips = 7;
const step = (p.w - 20) / drips;
for (let i = 0; i <= drips; i++) {
const cx = (hw - 10) - i * step;
const cy = (i % 2 === 0) ? 9 : 3;
c.lineTo(cx, cy);
}
c.closePath();
c.fill();
// 4. JUICY TOMATO SLICES (2 slices)
c.fillStyle = '#ff2b2b';
c.beginPath();
c.roundRect(-hw + 14, -8, hw - 10, 7, 3);
c.roundRect(0, -8, hw - 14, 7, 3);
c.fill();
// 5. WAVY LETTUCE
c.fillStyle = '#4cd137';
c.beginPath();
c.moveTo(-hw + 2, -7);
for (let lx = -hw + 2; lx <= hw - 2; lx += 10) {
c.quadraticCurveTo(lx + 5, -13, lx + 10, -7);
}
c.lineTo(hw - 2, -3);
c.lineTo(-hw + 2, -3);
c.closePath();
c.fill();
// 6. TOP BUN (Golden plump dome)
const topBunGrad = c.createLinearGradient(0, -hh - 4, 0, -6);
topBunGrad.addColorStop(0, '#f8b467');
topBunGrad.addColorStop(0.6, '#e08c35');
topBunGrad.addColorStop(1, '#c06b1d');
c.fillStyle = topBunGrad;
c.beginPath();
c.moveTo(-hw + 6, -6);
c.quadraticCurveTo(0, -hh - 12, hw - 6, -6);
c.quadraticCurveTo(0, -4, -hw + 6, -6);
c.fill();
// Top bun shiny specular highlight
c.fillStyle = 'rgba(255, 255, 255, 0.4)';
c.beginPath();
c.ellipse(-hw * 0.25, -hh - 1, hw * 0.35, 4, -0.15, 0, Math.PI * 2);
c.fill();
// 7. SESAME SEEDS (Cute little white/cream ovals)
c.fillStyle = '#fff4db';
const seeds = [
[-0.6, -11, 0.2], [-0.35, -16, -0.1], [-0.1, -19, 0.1],
[0.15, -18, -0.2], [0.4, -14, 0.3], [0.6, -10, -0.15],
[-0.45, -8, 0.1], [-0.2, -12, -0.3], [0.0, -14, 0.2],
[0.25, -11, -0.1], [0.45, -7, 0.2]
];
for (let s of seeds) {
c.save();
c.translate(s[0] * (hw - 15), s[1]);
c.rotate(s[2]);
c.beginPath();
c.ellipse(0, 0, 3, 1.6, 0, 0, Math.PI * 2);
c.fill();
c.restore();
}
c.restore();
}
// Draw bouncing Cherry Tomato Ball
function drawBall(c, b) {
// Trail
for (let i = 0; i < b.trail.length; i++) {
const t = b.trail[i];
const progress = (i + 1) / b.trail.length;
c.fillStyle = `rgba(255, 60, 100, ${progress * 0.25})`;
c.beginPath();
c.arc(t.x, t.y, b.r * progress * 0.8, 0, Math.PI * 2);
c.fill();
}
c.save();
c.translate(b.x, b.y);
// Glow
c.shadowColor = '#ff2a6d';
c.shadowBlur = 15;
// Tomato Body
const grad = c.createRadialGradient(-3, -3, 2, 0, 0, b.r);
grad.addColorStop(0, '#ff7070');
grad.addColorStop(0.4, '#ff1a3c');
grad.addColorStop(1, '#ab051f');
c.fillStyle = grad;
c.beginPath();
c.arc(0, 0, b.r, 0, Math.PI * 2);
c.fill();
c.shadowBlur = 0;
// Tomato stem (little green leaves on top)
c.fillStyle = '#4cd137';
for (let ang = 0; ang < Math.PI * 2; ang += (Math.PI * 2) / 4) {
c.save();
c.rotate(ang + 0.2);
c.beginPath();
c.ellipse(0, -b.r + 2, 2, 5, 0.4, 0, Math.PI * 2);
c.fill();
c.restore();
}
// Center stem pip
c.fillStyle = '#2ed573';
c.beginPath();
c.arc(0, -b.r + 2, 2.5, 0, Math.PI * 2);
c.fill();
// Glossy shine highlight
c.fillStyle = 'rgba(255, 255, 255, 0.75)';
c.beginPath();
c.ellipse(-3.5, -3.5, 3.5, 2, -Math.PI / 4, 0, Math.PI * 2);
c.fill();
c.restore();
}
// Draw fancy Brick
function drawBrick(c, b) {
if (!b.active) return;
c.save();
const wobX = Math.sin(b.wobble) * 2;
const wobY = Math.cos(b.wobble) * 2;
c.translate(b.x + wobX, b.y + wobY);
// Gradient body
const grad = c.createLinearGradient(0, 0, 0, b.h);
grad.addColorStop(0, b.theme.top);
grad.addColorStop(1, b.theme.bot);
c.fillStyle = grad;
c.shadowColor = b.theme.top;
c.shadowBlur = b.hp > 1 ? 12 : 6;
c.beginPath();
c.roundRect(0, 0, b.w, b.h, 6);
c.fill();
c.shadowBlur = 0;
// Top highlight border
c.fillStyle = b.theme.light;
c.beginPath();
c.roundRect(2, 2, b.w - 4, 4, 2);
c.fill();
// Mini heart stamp in center of brick
c.save();
c.translate(b.w / 2, b.h / 2);
c.fillStyle = 'rgba(255, 255, 255, 0.28)';
c.beginPath();
const hs = 5;
c.moveTo(0, hs * 0.4);
c.bezierCurveTo(-hs * 0.8, -hs * 0.2, -hs * 0.8, -hs * 0.8, 0, -hs * 0.35);
c.bezierCurveTo(hs * 0.8, -hs * 0.8, hs * 0.8, -hs * 0.2, 0, hs * 0.4);
c.fill();
c.restore();
// Double health indicator
if (b.hp > 1) {
c.strokeStyle = 'rgba(255,255,255,0.7)';
c.lineWidth = 1.5;
c.strokeRect(4, 4, b.w - 8, b.h - 8);
}
c.restore();
}
// --- GAME LOGIC & COLLISION ---
function update(dt) {
// Combo cooldown
if (comboTimer > 0) {
comboTimer -= dt;
if (comboTimer <= 0) {
combo = 1;
comboEl.innerText = 'x1';
}
}
// Screenshake decay
if (shakeTime > 0) {
shakeTime -= dt;
}
// Paddle squish relaxation
paddle.squishX += (1 - paddle.squishX) * 0.15;
paddle.squishY += (1 - paddle.squishY) * 0.15;
paddle.tilt += (0 - paddle.tilt) * 0.1;
// Autoplay or user smoothing
if (isAutoplay) {
// Intelligent AI tracking nearest ball
let targetBall = balls[0];
for (let b of balls) {
if (b.vy > 0 && b.y > (targetBall ? targetBall.y : 0)) {
targetBall = b;
}
}
if (targetBall) {
// Predict ball position slightly
const predX = targetBall.x + targetBall.vx * 3;
paddle.targetX = predX - paddle.w / 2;
}
}
// Bound paddle
paddle.targetX = Math.max(10, Math.min(W - paddle.w - 10, paddle.targetX));
const prevX = paddle.x;
paddle.x += (paddle.targetX - paddle.x) * (isAutoplay ? 0.22 : 0.32);
paddle.tilt = ((paddle.x - prevX) * 0.008);
// Ball update
for (let i = balls.length - 1; i >= 0; i--) {
const b = balls[i];
// Trail
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 8) b.trail.shift();
b.x += b.vx;
b.y += b.vy;
// Walls
if (b.x - b.r < 0) {
b.x = b.r;
b.vx = Math.abs(b.vx);
playSound('paddle');
} else if (b.x + b.r > W) {
b.x = W - b.r;
b.vx = -Math.abs(b.vx);
playSound('paddle');
}
if (b.y - b.r < 0) {
b.y = b.r;
b.vy = Math.abs(b.vy);
playSound('paddle');
}
// Paddle hit
if (
b.y + b.r >= paddle.y &&
b.y - b.r <= paddle.y + paddle.h &&
b.x >= paddle.x - b.r * 0.5 &&
b.x <= paddle.x + paddle.w + b.r * 0.5 &&
b.vy > 0
) {
b.vy = -Math.abs(b.vy);
b.y = paddle.y - b.r;
// Angled bounce based on paddle hit location
const hitOffset = (b.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2);
const speed = Math.hypot(b.vx, b.vy);
const angle = hitOffset * (Math.PI / 3.2); // max 56 deg
b.vx = speed * Math.sin(angle);
b.vy = -speed * Math.cos(angle);
// Squish burger paddle
paddle.squishX = 1.25;
paddle.squishY = 0.75;
playSound('paddle');
// Heart burst on paddle bounce
for (let k = 0; k < 6; k++) {
hearts.push({
x: b.x + (Math.random() - 0.5) * 10,
y: paddle.y,
vx: (Math.random() - 0.5) * 4,
vy: -(2 + Math.random() * 3),
size: 6 + Math.random() * 5,
rot: Math.random() * Math.PI,
vRot: (Math.random() - 0.5) * 0.2,
alpha: 0.9,
decay: 0.03,
wobbleSpeed: 4,
wobbleAmp: 1,
colorA: '#ffc048',
colorB: '#ff3377',
sparkle: true
});
}
}
// Brick collisions
for (let br of bricks) {
if (!br.active) continue;
// Closest point on brick to ball center
const closestX = Math.max(br.x, Math.min(b.x, br.x + br.w));
const closestY = Math.max(br.y, Math.min(b.y, br.y + br.h));
const distX = b.x - closestX;
const distY = b.y - closestY;
const distSq = distX * distX + distY * distY;
if (distSq < b.r * b.r) {
// Collision!
br.hp--;
br.wobble = 1.0;
// Bounce response
const overlapX = (br.w / 2 + b.r) - Math.abs(b.x - (br.x + br.w / 2));
const overlapY = (br.h / 2 + b.r) - Math.abs(b.y - (br.y + br.h / 2));
if (overlapX < overlapY) {
b.vx *= -1;
} else {
b.vy *= -1;
}
// COMBO & SCORE
score += 100 * combo;
combo = Math.min(10, combo + 1);
comboTimer = 2.4;
scoreEl.innerText = score;
comboEl.innerText = `x${combo} 🔥`;
// Explode into hearts!
const heartCount = (br.hp <= 0) ? 35 : 12;
spawnHearts(br.x + br.w / 2, br.y + br.h / 2, heartCount);
shakeTime = 0.12;
shakeAmp = 4;
if (br.hp <= 0) {
br.active = false;
}
break; // One brick hit per ball frame
}
}
// Ball fell below screen
if (b.y - b.r > H) {
balls.splice(i, 1);
}
}
// Check if all balls lost
if (balls.length === 0) {
lives--;
livesEl.innerText = lives;
playSound('lose');
if (lives <= 0) {
// Reset
lives = 3;
score = 0;
combo = 1;
scoreEl.innerText = score;
comboEl.innerText = 'x1';
livesEl.innerText = lives;
initBricks();
}
balls.push(createBall());
}
// Check if all bricks cleared
const remaining = bricks.filter(b => b.active).length;
if (remaining === 0) {
initBricks();
balls.push(createBall(W / 2, paddle.y - 20, 5, -7));
}
// Update Hearts
for (let i = hearts.length - 1; i >= 0; i--) {
const h = hearts[i];
h.x += h.vx + Math.sin(h.rot * h.wobbleSpeed) * h.wobbleAmp;
h.y += h.vy;
h.vy += 0.14; // gentle gravity
h.vx *= 0.985;
h.rot += h.vRot;
h.alpha -= h.decay;
if (h.alpha <= 0 || h.y > H + 50) {
hearts.splice(i, 1);
}
}
// Ambient floaters
ambientTimer += dt;
if (ambientTimer > 0.4) {
ambientTimer = 0;
spawnAmbientHeart();
}
}
// --- RENDER LOOP ---
let lastTime = performance.now();
function render() {
const now = performance.now();
const dt = Math.min((now - lastTime) / 1000, 0.1);
lastTime = now;
update(dt);
ctx.save();
// Screen shake
if (shakeTime > 0) {
const rx = (Math.random() - 0.5) * shakeAmp;
const ry = (Math.random() - 0.5) * shakeAmp;
ctx.translate(rx, ry);
}
// Background
ctx.fillStyle = '#0f0217';
ctx.fillRect(0, 0, W, H);
// Subtle background heart pattern/grid
ctx.fillStyle = 'rgba(255, 60, 140, 0.02)';
for (let x = 30; x < W; x += 60) {
for (let y = 30; y < H; y += 60) {
ctx.fillRect(x, y, 2, 2);
}
}
// Draw Bricks
for (let b of bricks) {
drawBrick(ctx, b);
}
// Draw Hearts (explosions & sparkles)
for (let h of hearts) {
drawHeart(ctx, h.x, h.y, h.size, h.rot, h.colorA, h.colorB, h.alpha);
if (h.sparkle && h.alpha > 0.3) {
ctx.fillStyle = `rgba(255, 255, 255, ${h.alpha * 0.8})`;
ctx.beginPath();
ctx.arc(h.x + (Math.sin(h.rot) * h.size * 0.6), h.y, 1.5, 0, Math.PI * 2);
ctx.fill();
}
}
// Draw Balls
for (let b of balls) {
drawBall(ctx, b);
}
// Draw Hamburger Paddle
drawBurger(ctx, paddle);
ctx.restore();
requestAnimationFrame(render);
}
// --- USER INPUT & DEMO TOGGLE ---
function startGame() {
initAudio();
if (isAutoplay) {
isAutoplay = false;
banner.classList.add('hidden');
// Fresh ball bounce on user start
if (balls.length > 0) {
balls[0].vy = -Math.abs(balls[0].vy);
}
}
}
// Mouse / Pointer Controls
window.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const mouseX = (e.clientX - rect.left) * scaleX;
if (!isAutoplay) {
paddle.targetX = mouseX - paddle.w / 2;
}
});
// Touch Controls
window.addEventListener('touchmove', (e) => {
if (e.touches.length > 0) {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const touchX = (e.touches[0].clientX - rect.left) * scaleX;
if (!isAutoplay) {
paddle.targetX = touchX - paddle.w / 2;
}
}
}, { passive: true });
// Keyboard Controls
const keys = {};
window.addEventListener('keydown', (e) => {
initAudio();
keys[e.code] = true;
startGame();
});
window.addEventListener('keyup', (e) => {
keys[e.code] = false;
});
// Key movement interval
setInterval(() => {
if (!isAutoplay) {
if (keys['ArrowLeft'] || keys['KeyA']) {
paddle.targetX -= 18;
}
if (keys['ArrowRight'] || keys['KeyD']) {
paddle.targetX += 18;
}
}
}, 16);
// Banner / Canvas click
banner.addEventListener('click', startGame);
canvas.addEventListener('click', startGame);
// Start render loop immediately (Autoplay demo ready on frame 1)
requestAnimationFrame(render);
</script>
</body>
</html>