Hamburger breakoutOne-line
Qwen3.8 Flash
by QwenReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 37 s
- Cost
- $0.0039
- Output tokens
- 8,161
- Spent thinking
- 0 tokens
- Speed
- 84.7 tok/s
- Console errors
- 0
- First token after
- 748 ms
- First visible text after
- 748 ms
- Prompt tokens
- 376
- Code size
- 21.0 KB
- Finish reason
- stop
- Served by
- Alibaba
- Model id
qwen/qwen3.8-flash- Reasoning sent
{"effort":"none"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 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 · 531 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hamburger Breakout — Hearts Everywhere</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--amber:#ffc247;--cream:#fff4e2;--red:#ff5b6a}
html,body{height:100%}
body{
font-family:"Trebuchet MS","Segoe UI",system-ui,sans-serif;
color:var(--cream);
background:radial-gradient(130% 90% at 50% -12%,#3d2a63 0%,#271a40 42%,#160f26 100%);
display:flex;flex-direction:column;align-items:center;justify-content:center;
min-height:100vh;overflow-x:hidden;padding:14px;user-select:none;-webkit-tap-highlight-color:transparent;
}
.bg{position:fixed;inset:0;z-index:-1;overflow:hidden;pointer-events:none}
.bg span{position:absolute;font-size:26px;opacity:.14;color:#ffd9c0;animation:drift linear infinite;text-shadow:0 0 12px rgba(255,150,150,.4)}
@keyframes drift{0%{transform:translateY(108vh) rotate(0deg)}100%{transform:translateY(-12vh) rotate(360deg)}}
.wrap{display:flex;flex-direction:column;align-items:center;gap:12px;width:min(96vw,640px)}
.title{font-weight:800;letter-spacing:.5px;font-size:clamp(20px,4.4vw,32px);color:var(--amber);text-shadow:0 2px 0 #a35a12,0 0 22px rgba(255,180,60,.35);display:flex;align-items:center;gap:10px}
.title b{color:var(--red);text-shadow:0 2px 0 #7a1220,0 0 22px rgba(255,90,110,.4)}
.hud{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;max-width:520px}
.stat{background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.13);border-radius:14px;padding:7px 14px;display:flex;flex-direction:column;align-items:center;line-height:1.05;min-width:66px}
.stat .k{font-size:10px;letter-spacing:1.5px;text-transform:uppercase;opacity:.7}
.stat .v{font-size:22px;font-weight:800;color:var(--amber)}
.lives{display:flex;gap:5px;align-items:center;margin-top:2px;font-size:15px}
.heart{filter:drop-shadow(0 0 4px rgba(255,90,110,.7));transition:transform .2s}
.heart.lost{opacity:.22;filter:grayscale(1);transform:scale(.8)}
.stage{position:relative;border-radius:18px;overflow:hidden;background:#120a20;
box-shadow:0 22px 60px rgba(0,0,0,.55),inset 0 0 0 2px rgba(255,255,255,.05)}
canvas{display:block;touch-action:none}
.overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;text-align:center;padding:22px;
background:linear-gradient(rgba(16,10,30,.62),rgba(16,10,30,.86));backdrop-filter:blur(2px);z-index:5;cursor:pointer}
.overlay.hidden{display:none}
.bigemoji{font-size:64px;animation:bob 1.6s ease-in-out infinite}
@keyframes bob{0%,100%{transform:translateY(0) rotate(-4deg)}50%{transform:translateY(-12px) rotate(4deg)}}
.ov-title{font-size:clamp(24px,6vw,40px);font-weight:800;color:var(--amber);text-shadow:0 3px 0 #a35a12}
.ov-sub{font-size:14px;opacity:.9;max-width:340px;line-height:1.5}
.prompt{font-size:15px;font-weight:700;color:var(--cream);padding:9px 20px;border-radius:999px;background:rgba(255,90,110,.18);border:1px solid rgba(255,90,110,.5);animation:pulse 1.4s ease-in-out infinite}
@keyframes pulse{0%,100%{transform:scale(1);opacity:.85}50%{transform:scale(1.06);opacity:1}}
.foot{font-size:12px;opacity:.6}
.foot kbd{background:rgba(255,255,255,.12);border-radius:5px;padding:1px 6px;font-family:inherit}
</style>
</head>
<body>
<div class="bg" id="bg"></div>
<div class="wrap">
<div class="title">🍔 Hamburger <b>Breakout</b></div>
<div class="hud">
<div class="stat"><span class="k">Score</span><span class="v" id="score">0</span></div>
<div class="stat"><span class="k">Lives</span><div class="lives" id="lives"></div></div>
<div class="stat"><span class="k">Level</span><span class="v" id="level">1</span></div>
</div>
<div class="stage" id="stage">
<canvas id="game"></canvas>
<div class="overlay" id="overlay">
<div class="bigemoji" id="ovEmoji">🍔</div>
<div class="ov-title" id="ovTitle">Hamburger Breakout</div>
<div class="ov-sub" id="ovSub">Smash every brick with the burger — they explode into a shower of hearts! 💖</div>
<div class="prompt" id="ovPrompt">Click or press any key to start</div>
<div class="foot">Move with your mouse / finger · paddle follows · <kbd>P</kbd> pause · <kbd>R</kbd> restart</div>
</div>
</div>
</div>
<script>
(function(){
"use strict";
const W=520,H=720;
const cvs=document.getElementById('game');
const ctx=cvs.getContext('2d');
const stage=document.getElementById('stage');
const overlay=document.getElementById('overlay');
const scoreEl=document.getElementById('score');
const levelEl=document.getElementById('level');
const livesEl=document.getElementById('lives');
const ovEmoji=document.getElementById('ovEmoji');
const ovTitle=document.getElementById('ovTitle');
const ovSub=document.getElementById('ovSub');
const ovPrompt=document.getElementById('ovPrompt');
let dpr=Math.min(window.devicePixelRatio||1,2);
function fit(){
const availW=Math.min(document.querySelector('.wrap').clientWidth,W);
const scale=availW/W;
const dispW=W*scale, dispH=H*scale;
stage.style.width=dispW+'px'; stage.style.height=dispH+'px';
cvs.style.width=dispW+'px'; cvs.style.height=dispH+'px';
dpr=Math.min(window.devicePixelRatio||1,2);
cvs.width=Math.round(dispW*dpr); cvs.height=Math.round(dispH*dpr);
}
fit(); window.addEventListener('resize',fit);
// ---------- helpers ----------
function clamp(v,a,b){return v<a?a:v>b?b:v;}
function rand(a,b){return a+Math.random()*(b-a);}
function rrp(g,x,y,w,h,r){r=Math.min(r,w/2,h/2);g.beginPath();g.moveTo(x+r,y);g.arcTo(x+w,y,x+w,y+h,r);g.arcTo(x+w,y+h,x,y+h,r);g.arcTo(x,y+h,x,y,r);g.arcTo(x,y,x+w,y,r);g.closePath();}
function heartPath(g,cx,cy,s){g.save();g.translate(cx,cy);g.scale(s,s);g.beginPath();g.moveTo(0,-0.5);g.bezierCurveTo(0.5,-1.1,1.3,-0.4,0,0.9);g.bezierCurveTo(-1.3,-0.4,-0.5,-1.1,0,-0.5);g.closePath();g.restore();}
// ---------- palette ----------
const COL={
bunTop:'#f2b04a',bunBot:'#e0a24a',crust:'#c98a34',seed:'#fdf0cf',
lettuce:'#7cc24b',tomato:'#e24b4b',cheese:'#f6c64b',patty:'#6b3f24',
onion:'#e7dcef',ball:'#ffe08a'
};
const BRICK_COLORS=[COL.tomato,COL.lettuce,COL.cheese,COL.onion,'#e88fb0','#f0a35e'];
const HEART_COLORS=['#ff5b6a','#ff8fa3','#ff2d55','#ffb3c1','#ff6b81','#e63950'];
// ---------- state ----------
let state='demo'; // demo | playing | paused | gameover | win
let score=0,lives=3,level=1;
let shake=0,flash=0;
let bricks=[],particles=[],floaters=[];
let combo=0,lastBrickTime=0;
let ball,paddle;
let pointerX=null;
let lastTs=performance.now(),acc=0;const STEP=1000/60;
const PW_BASE=100,PH_BASE=34,BR=9;
const BALL_SPEEDS=[350,380,410,440,470];
function initEntities(){
ball={x:W/2,y:H-140,vx:0,vy:0,r:BR,trail:[],speed:BALL_SPEEDS[Math.min(level-1,4)]};
paddle={w:PW_BASE-(level-1)*6,x:W/2,y:H-58,targetX:W/2};
if(paddle.w<70)paddle.w=70;
}
initEntities();
function serveBall(fromLeft){
ball.speed=BALL_SPEEDS[Math.min(level-1,4)];
let ang=(fromLeft?-1:1)*(rand(Math.PI*0.16,Math.PI*0.34))+Math.PI/2;
ball.vx=Math.cos(ang)*ball.speed;
ball.vy=-Math.abs(Math.sin(ang))*ball.speed;
ball.x=paddle.x;ball.y=paddle.y-paddle.h/2-ball.r-2;
ball.trail.length=0;
}
function buildLevel(lv){
bricks=[];
const cols=8,gap=6,top=66,left=24,right=24;
const bw=(W-left-right-gap*(cols-1))/cols;
const bh=26;
const rows=Math.min(3+lv,7);
for(let r=0;r<rows;r++){
for(let c=0;c<cols;c++){
const hp=(r%2===0&&lv>1)?2:1;
const cx=left+c*(bw+gap)+bw/2;
const cy=top+r*(bh+gap)+bh/2;
bricks.push({x:cx-bw/2,y:cy-bh/2,w:bw,h:bh,hp,maxhp:hp,color:BRICK_COLORS[(r+c)%BRICK_COLORS.length],alive:true});
}
}
}
function resetGame(){
score=0;lives=3;level=1;combo=0;
particles=[];floaters=[];shake=0;flash=0;
buildLevel(level);initEntities();serveBall(true);updateHUD();
}
function nextLevel(){
level++;buildLevel(level);initEntities();serveBall(true);updateHUD();
}
// ---------- particles ----------
function spawnHearts(x,y,n,opts){
opts=opts||{};
for(let i=0;i<n;i++){
const a=rand(0,Math.PI*2);
const sp=rand(opts.spMin||120,opts.spMax||380);
particles.push({
type:'heart',x:x+rand(-6,6),y:y+rand(-6,6),
vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-rand(40,160),
rot:rand(0,Math.PI*2),vr:rand(-6,6),
size:rand(8,18),life:1,decay:rand(0.008,0.016),
grav:rand(120,260),drag:0.992,
color:HEART_COLORS[i%HEART_COLORS.length]
});
}
}
function spawnBurst(x,y,n,color){
for(let i=0;i<n;i++){
const a=rand(0,Math.PI*2),sp=rand(60,240);
particles.push({type:'spark',x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,
life:1,decay:rand(0.03,0.06),size:rand(2,4),color,grav:60,drag:0.9});
}
}
function spawnCrumbs(x,y,color){
for(let i=0;i<10;i++){
const a=rand(-Math.PI*0.9,-Math.PI*0.1),sp=rand(80,220);
particles.push({type:'crumb',x:x+rand(-10,10),y:y+rand(-8,8),
vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,size:rand(3,6),rot:rand(0,6),vr:rand(-8,8),
life:1,decay:rand(0.01,0.02),grav:420,drag:0.99,color});
}
}
function updateParticles(dt){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];
p.vy+=p.grav*dt;p.vx*=p.drag;p.vy*=p.drag;
p.x+=p.vx*dt;p.y+=p.vy*dt;
if(p.rot!==undefined)p.rot+=p.vr*dt;
p.life-=p.decay;
if(p.life<=0||p.y>H+40)particles.splice(i,1);
}
for(let i=floaters.length-1;i>=0;i--){
const f=floaters[i];f.y-=38*dt;f.life-=dt;
if(f.life<=0)floaters.splice(i,1);
}
}
// ---------- gameplay events ----------
function onBrickHit(b){
const now=performance.now();
combo=(now-lastBrickTime<650)?combo+1:1;lastBrickTime=now;
b.hp--;
const pts=10*combo;score+=pts;
floaters.push({x:b.x+b.w/2,y:b.y,text:'+'+pts+(combo>1?' x'+combo:''),life:0.9,color:combo>1?'#ffd24a':'#fff'});
flash=Math.max(flash,0.12);shake=Math.max(shake,combo>2?7:4);
spawnCrumbs(b.x+b.w/2,b.y+b.h/2,b.color);
spawnBurst(b.x+b.w/2,b.y+b.h/2,14,'#ffffff');
if(b.hp<=0){
b.alive=false;
spawnHearts(b.x+b.w/2,b.y+b.h/2,26,{spMin:140,spMax:420});
spawnHearts(b.x+b.w/2,b.y+b.h/2,10,{spMin:60,spMax:160});
shake=Math.max(shake,9);
} else {
spawnHearts(b.x+b.w/2,b.y+b.h/2,8,{spMin:80,spMax:220});
}
updateHUD();
}
function loseLife(){
lives--;updateHUD();
shake=Math.max(shake,14);flash=Math.max(flash,0.25);
spawnHearts(ball.x,H-20,20,{spMin:120,spMax:300});
if(lives<=0){state='gameover';showOverlay('💀','Game Over','The burger ran out of lives.','Final Score: '+score,'Click or press a key to play again');}
else serveBall(true);
}
function checkWin(){
if(bricks.every(b=>!b.alive)){
if(state==='playing'){state='win';showOverlay('🎉','Level Cleared!','All bricks exploded into hearts!','Score: '+score,'Click or press a key for Level '+(level+1));}
else{nextLevel();}
}
}
// ---------- physics ----------
function step(dt){
// paddle follow
if(state==='playing'||state==='demo'){
paddle.targetX=clamp(pointerX!=null?pointerX:(ball.x),paddle.w/2,W-paddle.w/2);
paddle.x+=(paddle.targetX-paddle.x)*Math.min(1,dt*16);
}
// ball move + substeps
const steps=Math.max(1,Math.ceil((Math.hypot(ball.vx,ball.vy)*dt)/(ball.r*0.8)));
const sdt=dt/steps;
for(let s=0;s<steps;s++){
ball.x+=ball.vx*sdt;ball.y+=ball.vy*sdt;
// walls
if(ball.x-ball.r<0){ball.x=ball.r;ball.vx=Math.abs(ball.vx);spawnSparkWall(ball.x,ball.y);}
if(ball.x+ball.r>W){ball.x=W-ball.r;ball.vx=-Math.abs(ball.vx);spawnSparkWall(ball.x,ball.y);}
if(ball.y-ball.r<0){ball.y=ball.r;ball.vy=Math.abs(ball.vy);spawnSparkWall(ball.x,ball.y);}
// bricks
for(const b of bricks){
if(!b.alive)continue;
if(ball.x+ball.r>b.x&&ball.x-ball.r<b.x+b.w&&ball.y+ball.r>b.y&&ball.y-ball.r<b.y+b.h){
const overlapL=ball.x+ball.r-b.x, overlapR=b.x+b.w-(ball.x-ball.r);
const overlapT=ball.y+ball.r-b.y, overlapB=b.y+b.h-(ball.y-ball.r);
const m=Math.min(overlapL,overlapR,overlapT,overlapB);
if(m===overlapL){ball.x=b.x-ball.r;ball.vx=-Math.abs(ball.vx);}
else if(m===overlapR){ball.x=b.x+b.w+ball.r;ball.vx=Math.abs(ball.vx);}
else if(m===overlapT){ball.y=b.y-ball.r;ball.vy=-Math.abs(ball.vy);}
else{ball.y=b.y+b.h+ball.r;ball.vy=Math.abs(ball.vy);}
onBrickHit(b);break;
}
}
// paddle
if(ball.vy>0&&ball.y+ball.r>paddle.y-paddle.h/2&&ball.y-ball.r<paddle.y+paddle.h/2&&
ball.x>paddle.x-paddle.w/2-ball.r&&ball.x<paddle.x+paddle.w/2+ball.r){
const off=clamp((ball.x-paddle.x)/(paddle.w/2),-1,1);
const maxAng=Math.PI*0.38;
const ang=off*maxAng;
const speed=Math.hypot(ball.vx,ball.vy);
ball.vx=Math.sin(ang)*speed;
ball.vy=-Math.abs(Math.cos(ang)*speed);
ball.y=paddle.y-paddle.h/2-ball.r;
if(state==='playing')combo=0;
spawnHearts(ball.x,paddle.y-paddle.h/2,5,{spMin:60,spMax:140});
}
// bottom
if(ball.y-ball.r>H){loseLife();if(state!=='playing'&&state!=='demo')break;}
}
// trail
ball.trail.push({x:ball.x,y:ball.y});
if(ball.trail.length>14)ball.trail.shift();
checkWin();
// decay fx
shake*=Math.pow(0.86,dt*60);
flash*=Math.pow(0.85,dt*60);
}
function spawnSparkWall(x,y){spawnBurst(x,y,6,'#ffe08a');}
// ---------- drawing ----------
function drawBackground(t){
const g=ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#2c1d4d');g.addColorStop(0.5,'#241738');g.addColorStop(1,'#180f28');
ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
// soft glow
const rg=ctx.createRadialGradient(W/2,H*0.35,20,W/2,H*0.35,W*0.7);
rg.addColorStop(0,'rgba(255,150,120,0.10)');rg.addColorStop(1,'rgba(255,150,120,0)');
ctx.fillStyle=rg;ctx.fillRect(0,0,W,H);
// faint drifting hearts in bg (behind everything)
ctx.save();ctx.globalAlpha=0.06;ctx.fillStyle='#ffb0c0';
for(let i=0;i<10;i++){
const x=(i*97+t*0.02*((i%3)+1))%(W+60)-30;
const y=((i*131+t*0.03*((i%4)+1))%(H+60))-30;
heartPath(ctx,x,y,26);ctx.fill();
}
ctx.restore();
}
function drawBrick(b,t){
const dmg=b.hp/b.maxhp;
ctx.save();
ctx.shadowColor='rgba(0,0,0,.35)';ctx.shadowBlur=6;ctx.shadowOffsetY=3;
rrp(ctx,b.x,b.y,b.w,b.h,7);ctx.fillStyle=b.color;ctx.fill();
ctx.shadowColor='transparent';
// top highlight
rrp(ctx,b.x+2,b.y+2,b.w-4,b.h*0.42,5);ctx.fillStyle='rgba(255,255,255,.22)';ctx.fill();
// bottom shade
rrp(ctx,b.x+2,b.y+b.h*0.6,b.w-4,b.h*0.32,5);ctx.fillStyle='rgba(0,0,0,.16)';ctx.fill();
// little seed dot
ctx.fillStyle='rgba(255,255,255,.35)';
ctx.beginPath();ctx.arc(b.x+b.w*0.5,b.y+b.h*0.4,2,0,7);ctx.fill();
// damage crack
if(dmg<1){
ctx.strokeStyle='rgba(0,0,0,.4)';ctx.lineWidth=1.4;
ctx.beginPath();
ctx.moveTo(b.x+b.w*0.5,b.y+3);
ctx.lineTo(b.x+b.w*0.42,b.y+b.h*0.5);
ctx.lineTo(b.x+b.w*0.58,b.y+b.h*0.6);
ctx.lineTo(b.x+b.w*0.5,b.y+b.h-3);
ctx.stroke();
}
ctx.restore();
}
function drawPaddle(t){
const px=paddle.x,py=paddle.y,pw=paddle.w,ph=paddle.h;
ctx.save();
ctx.translate(px,py);
ctx.rotate(clamp((paddle.targetX-paddle.x)/pw,-0.12,0.12));
const hw=pw/2,hh=ph/2;
ctx.shadowColor='rgba(0,0,0,.4)';ctx.shadowBlur=10;ctx.shadowOffsetY=5;
// bottom bun
rrp(ctx,-hw,hh-12,pw,12,6);ctx.fillStyle=COL.bunBot;ctx.fill();
ctx.shadowColor='transparent';
// patty
rrp(ctx,-hw+3,hh-20,pw-6,9,4);ctx.fillStyle=COL.patty;ctx.fill();
// cheese (dripping zigzag)
ctx.fillStyle=COL.cheese;
ctx.beginPath();
ctx.moveTo(-hw+2,hh-22);ctx.lineTo(hw-2,hh-22);ctx.lineTo(hw-2,hh-16);
for(let x=hw-2;x>-hw+2;x-=10){ctx.lineTo(x-5,hh-11);ctx.lineTo(x-10,hh-16);}
ctx.lineTo(-hw+2,hh-16);ctx.closePath();ctx.fill();
// lettuce
ctx.fillStyle=COL.lettuce;
ctx.beginPath();
const ly=hh-24;
ctx.moveTo(-hw+1,ly+6);
for(let x=-hw+1;x<hw;x+=8){ctx.quadraticCurveTo(x+4,ly-4,x+8,ly+6);}
ctx.lineTo(hw-1,ly+9);ctx.lineTo(-hw+1,ly+9);ctx.closePath();ctx.fill();
// tomato slices
ctx.fillStyle=COL.tomato;
ctx.beginPath();ctx.ellipse(-pw*0.22,ly-2,11,6,0,0,7);ctx.fill();
ctx.beginPath();ctx.ellipse(pw*0.22,ly-2,11,6,0,0,7);ctx.fill();
ctx.fillStyle='rgba(255,255,255,.35)';
ctx.beginPath();ctx.ellipse(-pw*0.22,ly-3,5,2.4,0,0,7);ctx.fill();
ctx.beginPath();ctx.ellipse(pw*0.22,ly-3,5,2.4,0,0,7);ctx.fill();
// top bun dome
rrp(ctx,-hw,-hh,pw,ph*0.55,[hw,hw,10,10]);ctx.fillStyle=COL.bunTop;ctx.fill();
// sesame seeds
ctx.fillStyle=COL.seed;
const seeds=[[-0.55,-0.7],[-0.2,-0.85],[0.15,-0.8],[0.5,-0.65],[-0.35,-0.55],[0.35,-0.5],[0.02,-0.6]];
for(const s of seeds){ctx.save();ctx.translate(s[0]*hw,s[1]*hh);ctx.rotate(s[0]);rrp(ctx,-2.4,-1.4,4.8,2.8,1.4);ctx.fill();ctx.restore();}
ctx.restore();
}
function drawBall(t){
// trail
for(let i=0;i<ball.trail.length;i++){
const p=ball.trail[i],a=i/ball.trail.length;
ctx.save();ctx.globalAlpha=a*0.35;ctx.fillStyle=COL.ball;
ctx.beginPath();ctx.arc(p.x,p.y,ball.r*a,0,7);ctx.fill();ctx.restore();
}
ctx.save();
ctx.shadowColor='rgba(255,200,90,.8)';ctx.shadowBlur=14;
const g=ctx.createRadialGradient(ball.x-3,ball.y-3,1,ball.x,ball.y,ball.r);
g.addColorStop(0,'#fff6d8');g.addColorStop(1,COL.ball);
ctx.fillStyle=g;ctx.beginPath();ctx.arc(ball.x,ball.y,ball.r,0,7);ctx.fill();
ctx.restore();
}
function drawParticles(){
for(const p of particles){
const a=clamp(p.life,0,1);
if(p.type==='heart'){
ctx.save();ctx.globalAlpha=a;
ctx.shadowColor='rgba(255,90,120,.6)';ctx.shadowBlur=8;
ctx.fillStyle=p.color;
heartPath(ctx,p.x,p.y,p.size);ctx.fill();
ctx.restore();
} else if(p.type==='spark'){
ctx.save();ctx.globalAlpha=a;ctx.fillStyle=p.color;
ctx.beginPath();ctx.arc(p.x,p.y,p.size,0,7);ctx.fill();ctx.restore();
} else { // crumb
ctx.save();ctx.globalAlpha=a;ctx.fillStyle=p.color;
ctx.translate(p.x,p.y);ctx.rotate(p.rot);
ctx.fillRect(-p.size/2,-p.size/2,p.size,p.size);ctx.restore();
}
}
}
function drawFloaters(){
ctx.textAlign='center';ctx.font='800 18px "Trebuchet MS",sans-serif';
for(const f of floaters){
ctx.save();ctx.globalAlpha=clamp(f.life/0.9,0,1);
ctx.fillStyle=f.color;ctx.shadowColor='rgba(0,0,0,.5)';ctx.shadowBlur=4;
ctx.fillText(f.text,f.x,f.y);ctx.restore();
}
}
function render(t){
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.clearRect(0,0,W,H);
ctx.save();
if(shake>0.3)ctx.translate(rand(-shake,shake),rand(-shake,shake));
drawBackground(t);
for(const b of bricks)if(b.alive)drawBrick(b,t);
drawPaddle(t);
drawBall(t);
drawParticles();
drawFloaters();
ctx.restore();
if(flash>0.02){ctx.fillStyle='rgba(255,255,255,'+(flash*0.5)+')';ctx.fillRect(0,0,W,H);}
}
// ---------- HUD ----------
function updateHUD(){
scoreEl.textContent=score;
levelEl.textContent=level;
livesEl.innerHTML='';
for(let i=0;i<3;i++){
const s=document.createElement('span');
s.className='heart'+(i<lives?'':' lost');
s.textContent='❤️';livesEl.appendChild(s);
}
}
// ---------- overlay ----------
function showOverlay(emoji,title,sub,extra,prompt){
overlay.classList.remove('hidden');
ovEmoji.textContent=emoji;
ovTitle.textContent=title;
ovSub.innerHTML=sub+(extra?'<br><strong style="color:#ffc247">'+extra+'</strong>':'');
ovPrompt.textContent=prompt;
}
function hideOverlay(){overlay.classList.add('hidden');}
function handleStart(){
if(state==='gameover'){resetGame();state='playing';hideOverlay();return;}
if(state==='win'){nextLevel();state='playing';hideOverlay();return;}
if(state==='paused'){state='playing';hideOverlay();return;}
if(state==='demo'){state='playing';hideOverlay();}
}
// ---------- input ----------
function setPointer(clientX){
const rect=cvs.getBoundingClientRect();
pointerX=clamp((clientX-rect.left)/rect.width*W,0,W);
}
cvs.addEventListener('pointermove',e=>setPointer(e.clientX));
cvs.addEventListener('pointerdown',e=>{setPointer(e.clientX);handleStart();});
window.addEventListener('keydown',e=>{
if(e.key==='p'||e.key==='P'){
if(state==='playing'){state='paused';showOverlay('⏸️','Paused','Take a breather.','Score: '+score,'Press P to resume');}
else if(state==='paused'){state='playing';hideOverlay();}
return;
}
if(e.key==='r'||e.key==='R'){resetGame();state='playing';hideOverlay();return;}
if(!['ArrowLeft','ArrowRight'].includes(e.key))handleStart();
});
document.addEventListener('visibilitychange',()=>{if(document.hidden&&(state==='playing')){state='paused';showOverlay('⏸️','Paused','You tabbed away.','Score: '+score,'Press P to resume');}});
// ---------- loop ----------
function frame(ts){
let dt=(ts-lastTs)/1000;lastTs=ts;
if(dt>0.05)dt=0.05;
acc+=dt*1000;
let guard=0;
while(acc>=STEP&&guard<5){step(STEP/1000);acc-=STEP;guard++;}
updateParticles(dt);
render(ts);
requestAnimationFrame(frame);
}
// ---------- boot ----------
buildLevel(level);
updateHUD();
requestAnimationFrame(frame);
// decorative floating emoji behind page
(function(){
const bg=document.getElementById('bg');
const em=['💖','🍔','❤️','🍟','💕','🧅','🥓','💗'];
for(let i=0;i<16;i++){
const s=document.createElement('span');
s.textContent=em[i%em.length];
s.style.left=(Math.random()*100)+'%';
s.style.animationDuration=(14+Math.random()*20)+'s';
s.style.animationDelay=(-Math.random()*30)+'s';
s.style.fontSize=(18+Math.random()*22)+'px';
bg.appendChild(s);
}
})();
})();
</script>
</body>
</html>