Hamburger breakoutOne-line
MiniMax M3.1 Flash
by MiniMaxReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 42 s
- Cost
- $0.014
- Output tokens
- 23,877
- Spent thinking
- 11,289 tokens
- Speed
- 234.9 tok/s
- Console errors
- 0
- First token after
- 478 ms
- First visible text after
- 1 min 1 s
- Prompt tokens
- 541
- Code size
- 33.0 KB
- Finish reason
- stop
- Served by
- Nebius
- Model id
minimax/minimax-m3.1-flash- Reasoning sent
{"effort":"xhigh"}- 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 · 925 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Burger Breakout — every brick explodes into hearts</title>
<style>
*{box-sizing:border-box}
html,body{height:100%;margin:0}
body{
background:radial-gradient(1200px 700px at 50% -10%, #2a1a52 0%, #120c26 55%, #0a0716 100%);
color:#fff; overflow:hidden;
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
display:flex; flex-direction:column;
-webkit-user-select:none;user-select:none; touch-action:none;
}
header{
display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;
padding:12px 16px 10px; padding-top:max(12px,env(safe-area-inset-top));
}
.brand{display:flex;align-items:center;gap:12px;min-width:0}
.brand h1{
margin:0;font-size:clamp(16px,2.4vw,24px);letter-spacing:.5px;line-height:1.05;
background:linear-gradient(90deg,#ffd98a,#ff9db5 60%,#ff6f91);
-webkit-background-clip:text;background-clip:text;color:transparent;
text-shadow:0 0 22px rgba(255,120,160,.18);
}
.brand p{margin:2px 0 0;font-size:11px;opacity:.62;letter-spacing:.4px}
.stats{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.chip{
background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.12);
border-radius:12px;padding:5px 11px;display:flex;align-items:center;gap:8px;
backdrop-filter:blur(6px);
}
.chip .label{font-size:9px;letter-spacing:1.4px;opacity:.55;font-weight:700}
.chip .val{font-size:15px;font-weight:800;font-variant-numeric:tabular-nums}
.lives .val{display:flex;gap:3px}
.iconbtn{
width:36px;height:36px;border-radius:12px;border:1px solid rgba(255,255,255,.14);
background:rgba(255,255,255,.07);color:#fff;font-size:16px;cursor:pointer;
display:grid;place-items:center;transition:.15s;
}
.iconbtn:hover{background:rgba(255,255,255,.16);transform:translateY(-1px)}
/* CSS burger */
.bk{position:relative;display:inline-block;width:var(--s,44px);height:var(--s,44px);flex:none}
.bk i{position:absolute;left:2%;right:2%;display:block}
.bk .bt{top:0;height:34%;background:linear-gradient(#f7c579,#e2a04c);border-radius:40% 40% 20% 20%/65% 65% 32% 32%}
.bk .bm{top:32%;height:7%;background:#e05252;border-radius:3px}
.bk .bl{top:38%;height:10%;background:#63bf4a;border-radius:5px}
.bk .bc{top:47%;height:7%;background:#ffc93c;border-radius:3px}
.bk .bp{top:52%;height:16%;background:#6d3a21;border-radius:4px}
.bk .bb{top:65%;height:35%;background:linear-gradient(#e2a04c,#c98433);border-radius:18% 18% 44% 44%/26% 26% 72% 72%}
main{
position:relative;flex:1 1 auto;min-height:0;margin:0 12px 12px;
border-radius:18px;overflow:hidden;
box-shadow:0 24px 70px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.08) inset;
background:#0d0a20;
}
canvas{display:block;width:100%;height:100%;cursor:none}
#badge{
position:absolute;left:12px;top:10px;font-size:10px;font-weight:800;letter-spacing:1.6px;
padding:5px 10px;border-radius:999px;background:rgba(10,6,26,.55);
border:1px solid rgba(255,255,255,.16);color:#ffb8cd;backdrop-filter:blur(6px);
pointer-events:none;transition:.25s;
}
#badge.live{color:#9ff5c4;border-color:rgba(120,255,190,.35)}
#overlay{
position:absolute;inset:0;display:grid;place-items:center;padding:20px;
background:radial-gradient(600px 300px at 50% 45%, rgba(20,10,45,.55), rgba(8,5,20,.82));
backdrop-filter:blur(3px);transition:opacity .35s, visibility .35s;
}
#overlay.hidden{opacity:0;visibility:hidden}
.card{
text-align:center;max-width:460px;padding:26px 26px 22px;border-radius:20px;
background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.16);
box-shadow:0 20px 60px rgba(0,0,0,.5);animation:float 3.4s ease-in-out infinite;
}
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
.card h2{margin:14px 0 8px;font-size:clamp(18px,3.2vw,26px);letter-spacing:.4px}
.card p{margin:0;font-size:12.5px;line-height:1.6;opacity:.72}
.card kbd{
display:inline-block;padding:1px 7px;border-radius:6px;background:rgba(255,255,255,.14);
border:1px solid rgba(255,255,255,.2);font-size:11px;font-family:inherit;margin:0 1px;
}
.pulse{animation:pulse 1.6s ease-in-out infinite;display:inline-block}
@keyframes pulse{0%,100%{opacity:.55}50%{opacity:1}}
footer{
display:flex;justify-content:space-between;gap:10px;padding:0 16px 10px;
font-size:10.5px;opacity:.45;letter-spacing:.4px;
}
@media (max-width:520px){
header{padding:9px 11px 8px}
.brand p{display:none}
.chip{padding:4px 8px}
.chip .label{font-size:8px}
.chip .val{font-size:13px}
main{margin:0 8px 8px;border-radius:14px}
footer{font-size:9px;padding:0 11px 8px}
}
</style>
</head>
<body>
<header>
<div class="brand">
<span class="bk" style="--s:46px"><i class="bt"></i><i class="bm"></i><i class="bl"></i><i class="bc"></i><i class="bp"></i><i class="bb"></i></span>
<div>
<h1>BURGER BREAKOUT</h1>
<p>every brick you smash explodes into a storm of little hearts</p>
</div>
</div>
<div class="stats">
<div class="chip"><span class="label">SCORE</span><span class="val" id="score">0</span></div>
<div class="chip"><span class="label">LEVEL</span><span class="val" id="level">1</span></div>
<div class="chip"><span class="label">BEST</span><span class="val" id="best">0</span></div>
<div class="chip lives"><span class="label">LIVES</span><span class="val" id="lives"></span></div>
<button class="iconbtn" id="soundBtn" title="Toggle sound">🔈</button>
<button class="iconbtn" id="resetBtn" title="Restart">⟳</button>
</div>
</header>
<main id="stage">
<canvas id="c"></canvas>
<div id="badge">AI DEMO</div>
<div id="overlay">
<div class="card" id="card">
<span class="bk" style="--s:76px"><i class="bt"></i><i class="bm"></i><i class="bl"></i><i class="bc"></i><i class="bp"></i><i class="bb"></i></span>
<h2 id="ovTitle">Click or press a key to start</h2>
<p id="ovBody">The AI is already playing a demo — every brick bursts into hearts.<br/>
Take over any time with <kbd>←</kbd><kbd>→</kbd>, your mouse, or a finger.</p>
</div>
</div>
</main>
<footer>
<span>Move: mouse · touch · <span style="opacity:.8">← →</span></span>
<span>Space: serve ball · R: restart</span>
</footer>
<script>
(() => {
'use strict';
const cvs = document.getElementById('c');
const ctx = cvs.getContext('2d');
const stage = document.getElementById('stage');
const overlay = document.getElementById('overlay');
const card = document.getElementById('card');
const ovTitle = document.getElementById('ovTitle');
const ovBody = document.getElementById('ovBody');
const badge = document.getElementById('badge');
const scoreEl = document.getElementById('score');
const levelEl = document.getElementById('level');
const bestEl = document.getElementById('best');
const livesEl = document.getElementById('lives');
const TAU = Math.PI * 2;
const clamp = (v,a,b)=> v<a?a:(v>b?b:v);
const rand = (a,b)=> a + Math.random()*(b-a);
const pick = a => a[(Math.random()*a.length)|0];
/* ---------- audio ---------- */
let actx = null, muted = false;
function ensureAudio(){
if(!actx){ try{ actx = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ actx=null; } }
if(actx && actx.state === 'suspended'){ try{ actx.resume(); }catch(e){} }
}
function blip(freq, dur, type, vol, slide){
if(!actx || muted) return;
try{
const t = actx.currentTime;
const o = actx.createOscillator(), g = actx.createGain();
o.type = type||'square';
o.frequency.setValueAtTime(freq, t);
if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(40, freq+slide), t+dur);
g.gain.setValueAtTime(vol||0.06, t);
g.gain.exponentialRampToValueAtTime(0.0001, t+dur);
o.connect(g); g.connect(actx.destination);
o.start(t); o.stop(t+dur+0.02);
}catch(e){}
}
/* ---------- state ---------- */
let W=0, H=0, DPR=1;
let bricks=[], balls=[], hearts=[], sparks=[], texts=[], rings=[], powers=[];
let paddle=null;
let mode='demo'; // demo | play | over
let score=0, bestScore=0, level=1, lives=3, combo=0;
let shake=0, time=0, banner=null, serveTimer=0, heartTimer=0, wideUntil=0;
let pointerX=null, keyLeft=false, keyRight=false;
const PALETTE = ['#ff5d73','#ff9046','#ffd23f','#6bd968','#3fd0c9','#7a8cff','#c084fc'];
const HEART_COLORS = ['#ff4d6d','#ff7aa2','#ff9fbd','#ff2e63','#ffc2d4','#ff5fa2','#ffd1e0'];
/* ---------- helpers ---------- */
function rr(c,x,y,w,h,r){
let a = Array.isArray(r)? r.slice() : [r,r,r,r];
const mx = Math.min(w,h)/2;
a = a.map(v=> clamp(v,0,mx));
c.beginPath();
c.moveTo(x+a[0],y);
c.lineTo(x+w-a[1],y); c.quadraticCurveTo(x+w,y,x+w,y+a[1]);
c.lineTo(x+w,y+h-a[2]); c.quadraticCurveTo(x+w,y+h,x+w-a[2],y+h);
c.lineTo(x+a[3],y+h); c.quadraticCurveTo(x,y+h,x,y+h-a[3]);
c.lineTo(x,y+a[0]); c.quadraticCurveTo(x,y,x+a[0],y);
c.closePath();
}
function shade(hex, amt){
const n = parseInt(hex.slice(1),16);
let r=(n>>16)&255, g=(n>>8)&255, b=n&255;
if(amt>0){ r=r+(255-r)*amt; g=g+(255-g)*amt; b=b+(255-b)*amt; }
else { r=r*(1+amt); g=g*(1+amt); b=b*(1+amt); }
return 'rgb('+(r|0)+','+(g|0)+','+(b|0)+')';
}
function roundRectPath(x,y,w,h,r){ rr(ctx,x,y,w,h,r); }
/* ---------- burger drawing ---------- */
function drawBurger(cx, cy, w, h){
const x = cx - w/2, y = cy - h/2;
ctx.save();
// soft shadow / glow
const gl = ctx.createRadialGradient(cx, cy+h*0.35, 2, cx, cy+h*0.35, w*0.75);
gl.addColorStop(0,'rgba(255,170,80,0.30)'); gl.addColorStop(1,'rgba(255,170,80,0)');
ctx.fillStyle = gl;
ctx.fillRect(cx-w*0.8, cy-h, w*1.6, h*3);
const rBot = Math.min(w*0.16, h*0.26);
// bottom bun
let yy = y + h*0.72, hh = h*0.28;
let g = ctx.createLinearGradient(0,yy,0,yy+hh);
g.addColorStop(0,'#e5a44f'); g.addColorStop(1,'#c47f2e');
ctx.fillStyle = g; rr(ctx, x, yy, w, hh, [0,0,rBot,rBot]); ctx.fill();
// patty
yy = y + h*0.55; hh = h*0.20;
g = ctx.createLinearGradient(0,yy,0,yy+hh);
g.addColorStop(0,'#7d4224'); g.addColorStop(1,'#54291a');
ctx.fillStyle = g; rr(ctx, x+w*0.005, yy, w*0.99, hh, h*0.09); ctx.fill();
// cheese (with drips over the patty)
yy = y + h*0.47; hh = h*0.11;
ctx.fillStyle = '#ffcb3d'; rr(ctx, x+w*0.01, yy, w*0.98, hh, 2); ctx.fill();
ctx.fillStyle = '#ffcb3d';
const drips = 4;
for(let i=0;i<drips;i++){
const dx = x + w*(0.12 + 0.25*i) + (i%2? w*0.04:0);
ctx.beginPath();
ctx.moveTo(dx- w*0.05, yy+hh);
ctx.lineTo(dx+w*0.05, yy+hh);
ctx.lineTo(dx, yy+hh+h*0.16);
ctx.closePath(); ctx.fill();
}
// lettuce (wavy)
yy = y + h*0.37; hh = h*0.13;
ctx.fillStyle = '#5fbf49';
ctx.beginPath();
ctx.moveTo(x, yy+hh*0.55);
const seg = 6, sw = w/seg;
for(let i=0;i<=seg;i++){
const px = x + sw*i;
const py = yy + (i%2 ? hh*0.05 : hh*0.95);
ctx.quadraticCurveTo(px - sw*0.5, yy + (i%2? hh*0.95: hh*0.05), px, py);
}
ctx.lineTo(x+w, yy); ctx.lineTo(x, yy);
ctx.closePath(); ctx.fill();
// tomato
yy = y + h*0.31; hh = h*0.08;
ctx.fillStyle = '#e24b4b'; rr(ctx, x+w*0.015, yy, w*0.97, hh, hh*0.45); ctx.fill();
// top bun
yy = y; hh = h*0.32;
g = ctx.createLinearGradient(0,yy,0,yy+hh);
g.addColorStop(0,'#f8c880'); g.addColorStop(1,'#dd9a45');
ctx.fillStyle = g; rr(ctx, x, yy, w, hh, [w*0.18,w*0.18,2,2]); ctx.fill();
// sesame seeds
ctx.fillStyle = 'rgba(255,246,214,0.92)';
const seeds = [[0.22,0.34],[0.44,0.22],[0.66,0.36],[0.83,0.24],[0.34,0.62],[0.58,0.60]];
for(const s of seeds){
ctx.save();
ctx.translate(x + w*s[0], yy + hh*s[1]);
ctx.rotate(rand(-0.6,0.6));
ctx.beginPath(); ctx.ellipse(0,0, w*0.017, w*0.009, 0, 0, TAU); ctx.fill();
ctx.restore();
}
// top highlight
ctx.fillStyle = 'rgba(255,255,255,0.18)';
rr(ctx, x+w*0.10, yy+hh*0.14, w*0.36, hh*0.16, hh*0.08); ctx.fill();
ctx.restore();
}
/* ---------- heart drawing ---------- */
function heartPath(c, s){
c.beginPath();
c.moveTo(0, s*0.30);
c.bezierCurveTo(-s*0.62, -s*0.05, -s*0.58, -s*0.62, -s*0.22, -s*0.62);
c.bezierCurveTo(-s*0.08, -s*0.62, 0, -s*0.52, 0, -s*0.44);
c.bezierCurveTo(0, -s*0.52, s*0.08, -s*0.62, s*0.22, -s*0.62);
c.bezierCurveTo(s*0.58, -s*0.62, s*0.62, -s*0.05, 0, s*0.30);
c.closePath();
}
function drawHeart(x,y,size,rot,alpha,color,glow){
ctx.save();
ctx.globalAlpha = alpha;
ctx.translate(x,y);
ctx.rotate(rot);
if(glow){
ctx.shadowColor = color; ctx.shadowBlur = size*0.9;
}
ctx.fillStyle = color;
heartPath(ctx, size);
ctx.fill();
ctx.shadowBlur = 0;
ctx.globalAlpha = alpha*0.75;
ctx.fillStyle = 'rgba(255,255,255,0.85)';
ctx.beginPath();
ctx.ellipse(-size*0.22, -size*0.20, size*0.12, size*0.08, -0.6, 0, TAU);
ctx.fill();
ctx.restore();
}
/* ---------- particles ---------- */
function burstHearts(x, y, n, power){
for(let i=0;i<n;i++){
const a = rand(0, TAU);
const sp = rand(60, 340) * (power||1);
hearts.push({
x: x + rand(-8,8), y: y + rand(-6,6),
vx: Math.cos(a)*sp, vy: Math.sin(a)*sp - rand(40,180),
rot: rand(0,TAU), vr: rand(-7,7),
size: rand(5,13) * (Math.random()<0.18 ? 1.7 : 1),
life: rand(1.6,3.1), max: 3.1,
color: pick(HEART_COLORS), glow: Math.random()<0.35
});
}
}
function burstSparks(x,y,n,color){
for(let i=0;i<n;i++){
const a = rand(0,TAU), sp = rand(40,260);
sparks.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-30,life:rand(.25,.7),max:.7,size:rand(1.5,4),color:color||'#fff'});
}
}
function ring(x,y,color){
rings.push({x,y,r:6,life:.45,max:.45,color});
}
/* ---------- level building ---------- */
function buildPattern(lv, rows, cols){
const g=[];
for(let r=0;r<rows;r++){
let row='';
for(let c=0;c<cols;c++){
let ch='#';
const m = lv % 5;
if(m===0){ ch = (Math.random()<0.05 && r===0)?'.':'#'; }
else if(m===1){ ch = (Math.random()<0.10)?'.':'#'; }
else if(m===2){
const dr=Math.abs(r-(rows-1)/2), dc=Math.abs(c-(cols-1)/2);
ch = (dr*1.25 + dc*0.9) < (rows*1.35) ? '#' : '.';
}
else if(m===3){ ch = ((r+c)%2===0)?'#':'.'; }
else {
const half = (cols/2) * ((r+1)/rows);
ch = (Math.abs(c-(cols-1)/2) <= half)?'#':'.';
}
if(ch==='#' && lv>=3 && Math.random()<0.10) ch='X';
row+=ch;
}
g.push(row);
}
return g;
}
function buildLevel(lv){
const margin = Math.max(12, W*0.025);
const gap = Math.max(4, Math.min(7, W*0.008));
let cols = W < 480 ? 7 : (W < 700 ? 9 : 11);
let bh = clamp(H*0.035, 13, 26);
let rows = clamp(4 + Math.floor((lv-1)/2), 4, 8);
const top = Math.max(26, H*0.06);
while(rows > 3 && top + rows*(bh+gap) > H*0.58){ rows--; }
const pat = buildPattern(lv, rows, cols);
const usableW = W - margin*2;
const bw = (usableW - gap*(cols-1)) / cols;
bricks = [];
for(let r=0;r<pat.length;r++){
const rowStr = pat[r];
const startX = margin + (usableW - (rowStr.length*bw + (rowStr.length-1)*gap))/2;
for(let c=0;c<rowStr.length;c++){
const ch = rowStr[c];
if(ch==='.') continue;
const hp = ch==='X' ? 2 : 1;
bricks.push({
x: startX + c*(bw+gap), y: top + r*(bh+gap), w: bw, h: bh,
hp, maxHp: hp, color: hp===2 ? '#8b7bd8' : PALETTE[r % PALETTE.length],
flash: 0, dead:false
});
}
}
levelEl.textContent = lv;
}
/* ---------- ball & paddle ---------- */
function newBall(x,y,vx,vy,speed){
return {x,y,vx,vy,r:clamp(W*0.011,6,10),speed,trail:[],emit:0};
}
function serveBall(){
const b = newBall(paddle.x, paddle.y - 40, rand(-1,1)*0.4, -1, ballSpeed());
const len = Math.hypot(b.vx,b.vy);
b.vx = b.vx/len*b.speed; b.vy = b.vy/len*b.speed;
balls.push(b);
}
function ballSpeed(){
const s = clamp(W/780, 0.72, 1.12);
return clamp((300 + level*22) * s, 200, 700);
}
function resetPaddle(){
const wBase = clamp(W*0.115, 74, 132);
paddle = {x: W/2, y: H - clamp(H*0.075, 34, 62), w: wBase, wBase: wBase, h: clamp(H*0.035, 16, 26), vx:0};
}
/* ---------- input ---------- */
function pointerTo(e){
const rect = cvs.getBoundingClientRect();
pointerX = e.clientX - rect.left;
}
stage.addEventListener('pointermove', e=>{ pointerTo(e); });
stage.addEventListener('pointerdown', e=>{ ensureAudio(); pointerTo(e); startPlay(); });
stage.addEventListener('touchmove', e=>{ if(e.touches[0]) pointerTo(e.touches[0]); }, {passive:true});
window.addEventListener('keydown', e=>{
ensureAudio();
const k = e.key;
if(k==='ArrowLeft'||k==='a'||k==='A'){ keyLeft=true; e.preventDefault(); }
if(k==='ArrowRight'||k==='d'||k==='D'){ keyRight=true; e.preventDefault(); }
if(k===' '||k==='Enter'){ e.preventDefault(); startPlay(); if(serveTimer>0) serveTimer=0.05; }
if(k==='r'||k==='R'){ restart(); }
if(mode==='over' && (k===' '||k==='Enter')) restart();
startPlay();
});
window.addEventListener('keyup', e=>{
const k=e.key;
if(k==='ArrowLeft'||k==='a'||k==='A') keyLeft=false;
if(k==='ArrowRight'||k==='d'||k==='D') keyRight=false;
});
function startPlay(){
if(mode==='demo'){
mode='play';
badge.textContent='YOU ARE PLAYING'; badge.classList.add('live');
overlay.classList.add('hidden');
}
}
/* ---------- restart ---------- */
function restart(){
score=0; level=1; lives=3; combo=0;
hearts=[]; sparks=[]; texts=[]; rings=[]; powers=[]; balls=[];
resetPaddle(); buildLevel(1);
serveTimer = 0.4;
mode='demo';
badge.textContent='AI DEMO'; badge.classList.remove('live');
overlay.classList.remove('hidden');
ovTitle.textContent='Click or press a key to start';
ovBody.innerHTML='The AI is already playing a demo — every brick bursts into hearts.<br/>Take over any time with <kbd>←</kbd><kbd>→</kbd>, your mouse, or a finger.';
updateHud();
}
/* ---------- HUD ---------- */
function updateHud(){
scoreEl.textContent = score.toLocaleString();
bestEl.textContent = bestScore.toLocaleString();
livesEl.innerHTML = '';
for(let i=0;i<Math.max(0,lives);i++){
const s = document.createElement('span');
s.className='bk'; s.style.setProperty('--s','20px');
s.innerHTML = '<i class="bt"></i><i class="bm"></i><i class="bl"></i><i class="bc"></i><i class="bp"></i><i class="bb"></i>';
livesEl.appendChild(s);
}
}
/* ---------- resize ---------- */
function resize(){
const rect = stage.getBoundingClientRect();
const nw = Math.max(260, rect.width), nh = Math.max(220, rect.height);
const sx = W ? nw/W : 1, sy = H ? nh/H : 1;
W = nw; H = nh;
DPR = Math.min(window.devicePixelRatio||1, 2);
cvs.width = Math.round(W*DPR); cvs.height = Math.round(H*DPR);
cvs.style.width = W+'px'; cvs.style.height = H+'px';
ctx.setTransform(DPR,0,0,DPR,0,0);
if(paddle){
paddle.x = clamp(paddle.x*sx, paddle.w/2, W-paddle.w/2);
paddle.y = H - clamp(H*0.075, 34, 62);
paddle.h = clamp(H*0.035, 16, 26);
paddle.wBase = clamp(W*0.115, 74, 132);
paddle.w = paddle.wBase;
}else{
resetPaddle();
}
if(bricks.length){
for(const b of bricks){
b.x*=sx; b.y*=sy; b.w*=sx; b.h=clamp(b.h*sy,11,28);
}
}
for(const b of balls){ b.x*=sx; b.y*=sy; b.r=clamp(W*0.011,6,10); }
}
window.addEventListener('resize', resize);
/* ---------- update ---------- */
function update(dt){
time += dt;
// paddle
const wide = time < wideUntil;
const targetW = paddle.wBase * (wide ? 1.45 : 1);
paddle.w += (targetW - paddle.w) * Math.min(1, dt*8);
const px0 = paddle.x;
if(mode==='demo'){
let target = W/2;
let danger=null;
for(const b of balls){ if(b.vy>0){ const d = paddle.y - b.y; if(!danger || d<danger.d) danger={b,d}; } }
if(danger){
const b = danger.b;
const t = (paddle.y - b.y) / Math.max(1, b.vy);
target = b.x + b.vx*t + rand(-7,7);
if(Math.random()<0.012) target += rand(-45,45);
} else if(balls.length){
target = balls[0].x + rand(-10,10);
}
target = clamp(target, paddle.w/2+3, W-paddle.w/2-3);
const sp = paddle.wBase*2.6;
const d = target - paddle.x;
paddle.x += clamp(d, -sp*dt, sp*dt);
}else{
let move = 0;
if(keyLeft) move -= 1;
if(keyRight) move += 1;
if(move!==0){
paddle.x += move * paddle.wBase*2.4 * dt;
}else if(pointerX!=null){
const d = (pointerX) - paddle.x;
paddle.x += clamp(d, -paddle.wBase*8*dt, paddle.wBase*8*dt);
}
}
paddle.x = clamp(paddle.x, paddle.w/2+2, W-paddle.w/2-2);
paddle.vx = (paddle.x - px0)/Math.max(dt,0.0001);
// serve
if(balls.length===0){
serveTimer -= dt;
if(serveTimer<=0){ serveBall(); }
}
// balls
for(let i=balls.length-1;i>=0;i--){
const b = balls[i];
b.trail.unshift({x:b.x,y:b.y}); if(b.trail.length>14) b.trail.pop();
// heart trail
b.emit -= dt;
if(b.emit<=0){
b.emit = 0.055;
hearts.push({
x:b.x+rand(-4,4), y:b.y+rand(-4,4), vx:rand(-30,30), vy:rand(10,60),
rot:rand(0,TAU), vr:rand(-5,5), size:rand(3.5,7.5), life:rand(0.9,1.7), max:1.7,
color: pick(HEART_COLORS), glow:false
});
}
const steps = Math.max(1, Math.ceil(b.speed*dt/3));
const sdt = dt/steps;
for(let s=0;s<steps;s++){
b.x += b.vx*sdt; b.y += b.vy*sdt;
if(b.x < b.r){ b.x=b.r; b.vx=Math.abs(b.vx); wallFx(b.x,b.y); }
if(b.x > W-b.r){ b.x=W-b.r; b.vx=-Math.abs(b.vx); wallFx(b.x,b.y); }
if(b.y < b.r+2){ b.y=b.r+2; b.vy=Math.abs(b.vy); wallFx(b.x,b.y); }
// paddle
const pw = paddle.w, ph = paddle.h;
const pxx = paddle.x - pw/2, pyy = paddle.y - ph/2;
if(b.vy>0 && b.y+b.r >= pyy && b.y+b.r <= pyy+ph+10 && b.x >= pxx-6 && b.x <= pxx+pw+6){
const off = clamp((b.x - paddle.x)/(pw/2), -1, 1);
let ang = off*1.05;
const spd = b.speed;
let vx = Math.sin(ang)*spd, vy = -Math.cos(ang)*spd;
if(Math.abs(vy) < spd*0.36){ const s2=Math.sign(vy)||-1; vy = s2*spd*0.36; vx = Math.sign(vx||1)*Math.sqrt(Math.max(0,spd*spd-vy*vy)); }
b.vx = vx + paddle.vx*0.10;
b.vy = vy;
const n = Math.hypot(b.vx,b.vy);
b.vx = b.vx/n*spd; b.vy = b.vy/n*spd;
b.y = pyy - b.r - 0.5;
burstSparks(b.x, pyy, 7, '#ffd9a0');
hearts.push({x:b.x,y:pyy-4,vx:rand(-60,60),vy:rand(-140,-40),rot:rand(0,TAU),vr:rand(-6,6),size:rand(5,9),life:rand(.8,1.4),max:1.4,color:pick(HEART_COLORS),glow:false});
blip(200+off*90, 0.07, 'triangle', 0.07, -60);
combo = 0;
}
// bricks
for(const br of bricks){
if(br.dead) continue;
const ox = Math.min((b.x+b.r)-br.x, (br.x+br.w)-(b.x-b.r));
const oy = Math.min((b.y+b.r)-br.y, (br.y+br.h)-(b.y-b.r));
if(ox>0 && oy>0){
if(ox<oy){ b.x += (b.x < br.x+br.w/2) ? -ox : ox; b.vx = -b.vx; }
else { b.y += (b.y < br.y+br.h/2) ? -oy : oy; b.vy = -b.vy; }
hitBrick(br,b);
break;
}
}
}
if(b.y > H+40){ balls.splice(i,1); }
}
// hearts
for(let i=hearts.length-1;i>=0;i--){
const h = hearts[i];
h.vy += 780*dt;
h.vx *= 0.995;
h.x += h.vx*dt; h.y += h.vy*dt; h.rot += h.vr*dt;
h.life -= dt;
if(h.life<=0 || h.y > H+30) hearts.splice(i,1);
}
// sparks
for(let i=sparks.length-1;i>=0;i--){
const s = sparks[i];
s.vy += 400*dt; s.x += s.vx*dt; s.y += s.vy*dt; s.life -= dt;
if(s.life<=0) sparks.splice(i,1);
}
// rings
for(let i=rings.length-1;i>=0;i--){
const r = rings[i]; r.r += 420*dt; r.life -= dt;
if(r.life<=0) rings.splice(i,1);
}
// texts
for(let i=texts.length-1;i>=0;i--){
const t = texts[i]; t.y -= 46*dt; t.life -= dt;
if(t.life<=0) texts.splice(i,1);
}
// powerups
for(let i=powers.length-1;i>=0;i--){
const p = powers[i];
p.vy += 520*dt; p.x += p.vx*dt; p.y += p.vy*dt; p.rot += p.vr*dt; p.life -= dt;
if(p.y > H+40 || p.life<=0){ powers.splice(i,1); continue; }
const pyy = paddle.y - paddle.h/2;
if(p.y + 12 >= pyy && p.y - 12 <= pyy+paddle.h && Math.abs(p.x - paddle.x) < paddle.w/2 + 12){
applyPower(p.type);
burstHearts(p.x, p.y, 18, 1.2);
powers.splice(i,1);
}
}
// level clear
if(bricks.length===0 && balls.length>0){
level++;
score += 250*level;
buildLevel(level);
banner = {text:'LEVEL '+level, life:1.6, max:1.6};
combo = 0;
blip(520,0.14,'sine',0.09,420);
setTimeout(()=>blip(760,0.18,'sine',0.07,260),120);
}
// lose life
if(balls.length===0 && serveTimer<=0){
if(mode==='demo'){
lives = 3;
serveTimer = 0.7;
}else{
lives--;
combo = 0;
updateHud();
if(lives<=0){ gameOver(); }
else { serveTimer = 0.8; banner={text:'OUCH!', life:1.0, max:1.0}; blip(220,0.3,'sawtooth',0.07,-140); }
}
}
if(shake>0) shake = Math.max(0, shake - dt*38);
if(banner){ banner.life -= dt; if(banner.life<=0) banner=null; }
}
function wallFx(x,y){ burstSparks(x,y,3,'#bcd7ff'); blip(520+Math.random()*180,0.035,'square',0.03,-80); }
function hitBrick(br,b){
br.hp--; br.flash=1;
if(br.hp>0){
burstSparks(br.x+br.w/2, br.y+br.h/2, 6, br.color);
burstHearts(br.x+br.w/2, br.y+br.h/2, 8, 0.7);
blip(760,0.06,'square',0.05,-200);
return;
}
br.dead=true;
const i = bricks.indexOf(br); if(i>=0) bricks.splice(i,1);
combo = Math.min(9, combo+1);
const gain = 50*combo;
score += gain;
if(score>bestScore) bestScore=score;
updateHud();
const cx = br.x+br.w/2, cy = br.y+br.h/2;
// TONS of hearts
burstHearts(cx, cy, 24 + ((Math.random()*10)|0), 1);
for(let k=0;k<6;k++){
hearts.push({
x:cx+rand(-br.w/2,br.w/2), y:cy+rand(-br.h/2,br.h/2),
vx:rand(-220,220), vy:rand(-330,-90), rot:rand(0,TAU), vr:rand(-8,8),
size:rand(9,17), life:rand(2.2,3.4), max:3.4, color:pick(HEART_COLORS), glow:true
});
}
burstSparks(cx,cy,16,br.color);
ring(cx,cy,'rgba(255,120,160,0.85)');
shake = Math.min(9, shake+3.2);
texts.push({x:cx, y:cy, text:'+'+gain, life:0.9, max:0.9, color:'#ffd3e2'});
blip(300+Math.random()*160, 0.08, 'square', 0.07, 260);
setTimeout(()=>blip(900,0.07,'square',0.05,-500),60);
// powerup drop
if(Math.random()<0.11){
const types = ['multi','wide','heart'];
powers.push({x:cx,y:cy,vx:rand(-40,40),vy:rand(-120,-40),rot:0,vr:rand(-3,3),life:6,type:pick(types)});
}
}
function applyPower(type){
if(type==='multi'){
for(const b of balls){
const ang = Math.atan2(b.vy,b.vx);
for(const d of [-0.42,0.42]){
const nb = newBall(b.x,b.y,Math.cos(ang+d)*b.speed,Math.sin(ang+d)*b.speed,b.speed);
balls.push(nb);
}
}
banner={text:'TRIPLE BURGER!',life:1.2,max:1.2};
blip(420,0.16,'square',0.07,300);
}else if(type==='wide'){
wideUntil = time + 11;
banner={text:'WIDE PADDLE!',life:1.2,max:1.2};
blip(380,0.16,'triangle',0.08,280);
}else{
score += 300; if(score>bestScore) bestScore=score; updateHud();
for(const b of balls) burstHearts(b.x,b.y,10,0.8);
banner={text:'+300 LOVE!',life:1.2,max:1.2};
blip(660,0.2,'sine',0.09,300);
}
}
function gameOver(){
mode='over';
overlay.classList.remove('hidden');
ovTitle.textContent = 'GAME OVER';
ovBody.innerHTML = 'Final score <b>'+score.toLocaleString()+'</b> — level '+level+'. <br/>Click or press <kbd>Space</kbd> to play again.';
blip(200,0.5,'sawtooth',0.08,-150);
updateHud();
}
/* ---------- render ---------- */
const bokeh = [];
for(let i=0;i<26;i++) bokeh.push({x:Math.random(), y:Math.random(), r:rand(8,42), s:rand(0.05,0.22), a:rand(0.03,0.10), h:pick(['#ff6f91','#8ea2ff','#ffd166','#7ef0c6'])});
function render(){
ctx.clearRect(0,0,W,H);
// background
const g = ctx.createLinearGradient(0,0,W*0.4,H);
g.addColorStop(0,'#1b1140'); g.addColorStop(0.55,'#150d2e'); g.addColorStop(1,'#0c0820');
ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
const rg = ctx.createRadialGradient(W*0.5,H*0.95,10,W*0.5,H*0.95,W*0.85);
rg.addColorStop(0,'rgba(255,140,90,0.16)'); rg.addColorStop(1,'rgba(255,140,90,0)');
ctx.fillStyle=rg; ctx.fillRect(0,0,W,H);
// bokeh
for(const b of bokeh){
b.y += b.s*0.004; if(b.y>1.1){ b.y=-0.1; b.x=Math.random(); }
ctx.globalAlpha = b.a;
ctx.fillStyle = b.h;
ctx.beginPath(); ctx.arc(b.x*W, b.y*H, b.r, 0, TAU); ctx.fill();
}
ctx.globalAlpha = 1;
ctx.save();
if(shake>0){
ctx.translate((Math.random()-0.5)*shake, (Math.random()-0.5)*shake);
}
// ambient hearts
ctx.globalAlpha = 0.16;
for(let i=0;i<7;i++){
const t = time*0.25 + i*1.7;
const x = (Math.sin(t*0.6+i)*0.5+0.5)*W;
const y = (H - ((time*18 + i*90) % (H+120)) + 60);
drawHeart(x, y, 12+i, t*0.4, 0.22, HEART_COLORS[i%HEART_COLORS.length], false);
}
ctx.globalAlpha = 1;
// bricks
for(const br of bricks){
const gg = ctx.createLinearGradient(0,br.y,0,br.y+br.h);
gg.addColorStop(0, shade(br.color,0.30));
gg.addColorStop(0.45, br.color);
gg.addColorStop(1, shade(br.color,-0.34));
ctx.fillStyle = gg;
rr(ctx, br.x, br.y, br.w, br.h, Math.min(7, br.h*0.28));
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.20)';
rr(ctx, br.x+br.w*0.06, br.y+br.h*0.13, br.w*0.88, Math.max(2,br.h*0.20), Math.min(4,br.h*0.18));
ctx.fill();
if(br.maxHp===2){
ctx.strokeStyle='rgba(255,255,255,0.5)'; ctx.lineWidth=1.5;
rr(ctx, br.x+1.5, br.y+1.5, br.w-3, br.h-3, Math.min(6,br.h*0.25)); ctx.stroke();
ctx.fillStyle='rgba(255,255,255,0.75)';
for(let i=0;i<2;i++){ ctx.beginPath(); ctx.arc(br.x+6+i*(br.w-12), br.y+br.h/2, Math.max(1.4,br.h*0.08),0,TAU); ctx.fill(); }
}
if(br.flash>0){
ctx.fillStyle='rgba(255,255,255,'+(br.flash*0.65)+')';
rr(ctx, br.x, br.y, br.w, br.h, Math.min(7, br.h*0.28)); ctx.fill();
br.flash = Math.max(0, br.flash-0.12);
}
}
// rings
for(const r of rings){
const a = r.life/r.max;
ctx.strokeStyle = r.color; ctx.globalAlpha = a*0.8; ctx.lineWidth = 3*a+1;
ctx.beginPath(); ctx.arc(r.x,r.y,r.r,0,TAU); ctx.stroke();
}
ctx.globalAlpha=1;
// powerups
for(const p of powers){
ctx.save();
ctx.translate(p.x,p.y); ctx.rotate(p.rot);
const gg = ctx.createLinearGradient(-12,-12,12,12);
if(p.type==='multi'){ gg.addColorStop(0,'#ffd166'); gg.addColorStop(1,'#ff7a3d'); }
else if(p.type==='wide'){ gg.addColorStop(0,'#7ef0c6'); gg.addColorStop(1,'#2fa8ff'); }
else { gg.addColorStop(0,'#ff9fbd'); gg.addColorStop(1,'#ff3f7f'); }
ctx.fillStyle=gg;
rr(ctx,-13,-13,26,26,8); ctx.fill();
ctx.strokeStyle='rgba(255,255,255,0.7)'; ctx.lineWidth=2; ctx.stroke();
ctx.fillStyle='#fff'; ctx.font='bold 15px system-ui,sans-serif';
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(p.type==='multi'?'★':(p.type==='wide'?'⇔':'♥'),0,1);
ctx.restore();
}
// sparks
for(const s of sparks){
ctx.globalAlpha = clamp(s.life/s.max,0,1);
ctx.fillStyle = s.color;
ctx.beginPath(); ctx.arc(s.x,s.y,s.size,0,TAU); ctx.fill();
}
ctx.globalAlpha=1;
// hearts
for(const h of hearts){
const a = clamp(h.life/0.55,0,1);
drawHeart(h.x,h.y,h.size,h.rot,a*0.95,h.color,h.glow);
}
// balls
for(const b of balls){
for(let i=b.trail.length-1;i>=0;i--){
const t=b.trail[i];
const f = 1 - i/b.trail.length;
ctx.globalAlpha = f*0.35;
ctx.fillStyle = '#ffd8e6';
ctx.beginPath(); ctx.arc(t.x,t.y,b.r*f*0.95,0,TAU); ctx.fill();
}
ctx.globalAlpha=1;
const bg = ctx.createRadialGradient(b.x-b.r*0.3,b.y-b.r*0.4,1,b.x,b.y,b.r*1.6);
bg.addColorStop(0,'#fff'); bg.addColorStop(0.55,'#ffe3ef'); bg.addColorStop(1,'#ff9ec4');
ctx.fillStyle=bg;
ctx.shadowColor='rgba(255,150,200,0.8)'; ctx.shadowBlur=14;
ctx.beginPath(); ctx.arc(b.x,b.y,b.r,0,TAU); ctx.fill();
ctx.shadowBlur=0;
}
// paddle
drawBurger(paddle.x, paddle.y, paddle.w, paddle.h);
// floating texts
ctx.textAlign='center'; ctx.textBaseline='middle';
for(const t of texts){
const a = clamp(t.life/t.max,0,1);
ctx.globalAlpha=a;
ctx.fillStyle=t.color;
ctx.font='bold '+(13+ (1-a)*4)+'px system-ui,sans-serif';
ctx.fillText(t.text,t.x,t.y);
}
ctx.globalAlpha=1;
ctx.restore();
// banner
if(banner){
const a = clamp(banner.life/banner.max,0,1);
ctx.globalAlpha = a;
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='900 '+Math.round(clamp(W*0.06,24,44))+'px system-ui,sans-serif';
ctx.fillStyle='rgba(0,0,0,0.35)';
ctx.fillText(banner.text, W/2+2, H*0.42+3);
const gg = ctx.createLinearGradient(0,H*0.42-24,0,H*0.42+24);
gg.addColorStop(0,'#ffe9a8'); gg.addColorStop(1,'#ff6f91');
ctx.fillStyle=gg;
ctx.fillText(banner.text, W/2, H*0.42);
ctx.globalAlpha=1;
}
// combo indicator
if(combo>1){
ctx.textAlign='left'; ctx.textBaseline='top';
ctx.font='800 12px system-ui,sans-serif';
ctx.fillStyle='rgba(255,255,255,0.75)';
ctx.fillText('COMBO x'+combo, 14, 34);
}
}
/* ---------- loop ---------- */
let last = performance.now();
function loop(now){
let dt = (now-last)/1000; last=now;
if(dt>0.05) dt=0.05;
update(dt);
render();
requestAnimationFrame(loop);
}
/* ---------- init ---------- */
document.getElementById('resetBtn').addEventListener('click', e=>{ e.stopPropagation(); ensureAudio(); restart(); });
document.getElementById('soundBtn').addEventListener('click', e=>{
e.stopPropagation(); ensureAudio(); muted=!muted;
e.currentTarget.textContent = muted?'🔇':'🔈';
if(!muted) blip(700,0.08,'sine',0.06,200);
});
resize();
resetPaddle();
buildLevel(1);
updateHud();
serveTimer = 0.5;
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Reasoning trace (11,289 tokens)
Let me build a breakout game with a hamburger paddle and bricks that explode into hearts.
Key features:
- Canvas-based breakout
- Hamburger paddle (drawn with CSS-like layers: bun top, lettuce, cheese, patty, bun bottom)
- Bricks that explode into many little hearts with physics (gravity, rotation, fade)
- Nice visuals, gradient background, particles
- Self-playing demo mode: AI moves the paddle, behind "Click or press a key to start"
- Responsive: 1280x800 down to 375px wide
Let me write a single HTML file.
Design:
- Full-screen-ish canvas with max width, centered. Use canvas sized to container with devicePixelRatio.
- Game area with paddle, ball(s), brick grid with different colored rows.
- On brick break: spawn ~18 hearts that fly out with gravity, rotate, fade. Hearts drawn as vector path (or emoji? emoji may render differently; draw vector heart path for reliability).
- Score, lives, level.
- Demo AI: paddle tracks ball x with some lag/jitter.
- Click to start → user control with mouse/touch/arrows.
Let me carefully implement.
Canvas logical size: width = min(1000, container width), height = width * 0.62 maybe. For 375px wide, height ~ 240? That's short but ok. Better: keep aspect ratio 4:3 or 16:10, and for small screens use 3:4 portrait? Breakout works better in landscape. Let's do responsive: logical width = container width, logical height = container height determined by aspect ratio clamped. Actually let's compute canvas size from element size and set internal resolution via dpr, and use logical coordinates = CSS pixels of the canvas. Simpler: set canvas width/height to element's client size * dpr, and scale ctx by dpr, then game in CSS pixel coordinate space. Resize handler recomputes layout.
Layout: game bounds with side walls, top wall, bottom is death line. Bricks arranged in grid at top.
For narrow screens, fewer columns and smaller bricks, smaller paddle.
Let me implement:
```
const cvs = document.getElementById('c');
const ctx = cvs.getContext('2d');
let W,H,DPR;
function resize(){
const rect = wrap.getBoundingClientRect();
W = rect.width; H = rect.height;
DPR = Math.min(devicePixelRatio||1, 2);
cvs.width = W*DPR; cvs.height = H*DPR;
cvs.style.width = W+'px'; cvs.style.height=H+'px';
ctx.setTransform(DPR,0,0,DPR,0,0);
layout();
}
```
Wrap: a container div with aspect-ratio: 16/10 and max-height. Actually make the whole page: header with title & score chips, then game area filling remaining viewport height. Use flexbox column, body height 100vh, game area flex:1 with min-height 0. Canvas absolutely fills game area.
Bricks: rows=5, cols based on width: cols = clamp(floor(W/90), 5, 12). Brick width computed.
Paddle: width = clamp(W*0.12, 70, 130). Height ~ 20. Hamburger drawn.
Ball: radius 8.
Ball trail: draw a few fading circles.
Hearts particle: {x,y,vx,vy,rot,vr,size,life,color}. Draw heart shape path scaled. Some hearts bigger with glow.
Ball physics: bounce off walls, paddle with angle based on hit offset, bricks collision (circle vs rect).
Ball can break brick; on break, explode hearts + brick shards + score + particle burst.
Levels: every N bricks, next level with new layout/colors. Keep simple: 3-4 level patterns cycling, and speed increases.
Also "every brick you break explodes into tons of little hearts" — spawn ~20-28 hearts per brick, plus maybe a ring burst.
Also hearts could be collected by paddle for bonus points? Keep hearts purely decorative but also give small score bonus when paddle catches some hearts? That could be fun but complicates. Maybe hearts that fall to the bottom give small score? Let's just make them decorative with nice trail. Actually adding: catching a heart with paddle gives +5 and a pop. I'll keep decorative for safety but add subtle sparkle.
Hmm, the hearts falling should probably disappear at bottom with a little splash.
Let me also add a "combo" indicator.
AI demo: paddle target = ball.x + prediction; move with max speed; with slight error. When demo active, show "AI DEMO" badge. When user clicks/keys, switch to manual mode.
Start overlay: "CLICK OR PRESS A KEY TO START" big text, pulsing. Behind it, the game runs in demo mode (AI playing) — so visible immediately: bricks, hearts exploding as AI breaks bricks. Yes: run AI demo from the start, overlay on top semi-transparent.
Let's write the heart path:
```
function heartPath(ctx, s){
ctx.beginPath();
ctx.moveTo(0, s*0.35);
ctx.bezierCurveTo(0, -s*0.1, -s*0.5, -s*0.35, -s*0.5, s*0.05);
...
}
```
Simpler standard heart:
```
ctx.beginPath();
ctx.moveTo(0, -0.3*s);
ctx.bezierCurveTo(0, -0.8*s, -0.7*s, -0.5*s, -0.7*s, -0.05*s);
ctx.bezierCurveTo(-0.7*s, 0.45*s, -0.2*s, 0.6*s, 0, 0.85*s);
ctx.bezierCurveTo(0.2*s, 0.6*s, 0.7*s, 0.45*s, 0.7*s, -0.05*s);
ctx.bezierCurveTo(0.7*s, -0.5*s, 0, -0.8*s, 0, -0.3*s);
```
with s being scale where heart width ~1.4s. Fine.
Alternatively draw filled with bezier using scale factor.
Let me define drawHeart(ctx, x, y, size, rot, alpha, color) using ctx.save/translate/rotate.
Now burger paddle: draw layered:
- bottom bun: rounded rect, color #d9913f
- patty: dark brown #6b3b22
- cheese: yellow #ffcc33 with drips
- lettuce: green wavy
- tomato: red
- top bun: #e8a94c with sesame seeds
Draw using roundRect and some wave shapes.
I'll write a function drawBurger(ctx, x, y, w, h) where (x,y) is center of paddle.
Simplify: total height h (~26). Layers from bottom:
- bottom bun h*0.28 (rounded bottom)
- patty h*0.22
- cheese h*0.10
- lettuce h*0.12
- tomato h*0.08
- top bun h*0.30 rounded top
I'll draw them with rects with rounded corners, plus sesame seeds on top bun and cheese drip triangles.
Round rect helper: ctx.roundRect is available in modern Chrome. Use it (Chrome 99+). Should be fine in "latest Chrome". I'll add a fallback polyfill just in case.
Colors palette for background: deep purple/night gradient with stars? Let's do a vibrant retro gradient: dark navy #14132b → #2b1b4d, with subtle radial glow, floating dots.
Bricks: rounded rect with gradient fill per row color, highlight top edge, and a small "burger" icon? Could put a tiny burger emoji-like drawing in each brick? That would be cute: each brick looks like a mini burger. Hmm, might be too much. Instead, bricks look like burger buns? Let's make bricks with a horizontal stripe pattern (like a sandwich) — colored with a lighter top strip. I'll draw each brick with a glossy top highlight and a darker bottom, and a tiny burger icon drawn small in the center for flavor. Actually drawing a mini burger per brick is fine and adds charm. Let's do simple: brick with rounded corners, gradient, plus 2 small white shine lines. Keep it clean.
Rows colors: red, orange, yellow, green, teal, pink.
Add screen shake on brick break (small).
Score display in header, plus level, lives (as burger icons).
Lives lost when ball falls below paddle → respawn ball, reset combo.
Level progression: when all bricks cleared, next level, new pattern, brief banner "LEVEL 2".
Patterns: array of functions that fill grid with some bricks missing. Use bitmask arrays.
Let's define levels as arrays of strings where '#' = brick, '.' = empty, 'X' = tough (2 hits).
Level 1: all '#'.
Level 2: some gaps.
Level 3: pyramid.
Level 4: checker.
Implement generation with rows/cols counts per level, e.g. cols = base but pattern centered.
Simplify: define pattern strings, center them horizontally.
Let me write patterns:
L1: 5 rows of 10 '#'.
L2: rows: "#### ####", etc. I'll write code generating patterns programmatically with row index.
Simpler: level index determines layout function returning 2D array of hp.
```
function buildLevel(n){
const rows = 5, cols = C;
const grid = [];
for(r...) for(c...) hp=1;
if(n%4===1){ // full
} else if(n%4===2){ // diagonal gaps
} else if(n%4===3){ // pyramid
} else { // checkerboard
}
}
```
Actually pattern strings are easier to read. Let me do strings with variable length, centered.
patterns = [
['##########','##########','##########','##########','##########'],
['###..####','####..###','..####..','##....##','####..###'],
['....####....','..######..','.########.','..######..','....####....'], // variable lengths center
['##.##.##.##','#.#######.#','.##.###.##.','##.##.##.##','#.#######.#'],
]
Level = (level-1) % patterns.length, with difficulty scaling: rows.
OK. Also add a "tough" brick type appearing at higher levels (hp 2, different color, breaks into even more hearts).
Now write code.
Collision: ball vs brick AABB. Use swept-ish: check overlap, determine which side by comparing distances, reflect velocity.
Standard approach: compute closest point on rect to ball center; if dist < r → collision. Determine normal via penetration minimal axis.
Let's implement:
```
function collideBricks(){
for(const b of bricks){
if(b.dead) continue;
const cx = clamp(ball.x, b.x, b.x+b.w);
const cy = clamp(ball.y, b.y, b.y+b.h);
const dx = ball.x-cx, dy = ball.y-cy;
if(dx*dx+dy*dy < ball.r*ball.r){ ... }
}
```
But for deep overlap the closest-point method may give wrong normal. Use overlap method: compute overlaps with each side:
```
const ox = Math.min(ball.x + r - b.x, b.x + b.w - (ball.x - r));
const oy = Math.min(ball.y + r - b.y, b.y + b.h - (ball.y - r));
if(ox>0&&oy>0){ if(ox<oy){ reflect x, ball.x = ...} else {reflect y} }
```
That's robust. But need to handle multiple bricks in one frame — break one and continue.
For collision resolution with sub-steps: move ball in small steps (dt split) to avoid tunneling: split movement into steps of max 4px.
Let's implement step loop: compute steps = ceil(speed*dt/3), each step move ball and check collisions.
Ball speed increases slightly per level and per brick broken (cap).
Paddle bounce: if ball moving down and overlaps paddle top → set ball.y = paddle top - r, compute angle based on offset: maxAngle 60°; angle = offset*(maxAngle) mapped; vx = sin, vy=-cos. Ensure vy not too small (min 0.35 vertical component).
Also add English/spin? Not needed.
Particles: hearts + sparkles + brick dust.
Let me also add sound? Web Audio simple beeps might be nice but could be annoying; keep optional with a mute toggle, default muted? I'll include a small sound toggle button, default on but quiet. Actually audio autoplay policies require user gesture; audio context will be suspended until interaction. Since demo starts without interaction, no sound in demo — fine, we create context lazily on first interaction. I'll include sound with a toggle. Keep it simple: short blip on brick break (chime), thud on paddle, and a "pop" sound for heart splash. Might be risky but harmless. I'll add with try/catch.
Let me include a mute button in the header.
UI:
Header: title "BURGER BREAKOUT" with 🍔 emoji? Emoji in header fine. Plus stats: score, level, lives, and buttons: mute, restart.
Below header: canvas area.
Overlay: centered card "CLICK OR PRESS ANY KEY TO START" with subtitle "Demo mode: the AI is playing — take over anytime". And a pulsing arrow.
When user interacts, overlay fades out, mode = 'play'.
Also add "demo" badge in the corner of canvas when in demo mode.
Controls hint at bottom: "Move: mouse / touch / ← →".
Responsive: on narrow screens, header wraps; canvas min height.
Let me code the CSS.
```
*{box-sizing:border-box}
html,body{margin:0;height:100%;}
body{background:#0d0a1a;color:#fff;font-family:...;display:flex;flex-direction:column;overflow:hidden}
header{...}
#stage{flex:1;position:relative;min-height:0;margin:0 12px 12px;border-radius:16px;overflow:hidden;box-shadow:...}
canvas{display:block;width:100%;height:100%}
```
Font: use system font stack; maybe a Google Font? Not allowed unless CDN listed — task doesn't list allowed CDNs, so no external fonts. Use system fonts with heavy weight.
Now game loop with requestAnimationFrame, dt clamped.
Let's write the full code.
Details on drawing hearts "tons of little hearts": spawn 24 per brick + 6 bigger ones. Colors: pinks/reds (#ff4d6d, #ff8fab, #ffb3c6, #ff2e63), some golden.
Heart physics: initial velocity random upward/outward, gravity 900 px/s², spin, drag, fade near end of life, shrink.
Also a shockwave ring expanding on break.
Screen shake: shakeAmt decays; translate context.
Let's write the code now.
For the ball trail, store last N positions.
Also add a slight paddle glow, and a "grill" effect? nah.
Let me now write the hamburger drawing carefully.
drawBurger(ctx, cx, cy, w, h):
```
const x = cx - w/2, y = cy - h/2;
// shadow
ctx.save();
// bottom bun
let bh = h*0.30;
roundRect(x, y+h-bh, w, bh, [w*0.18, w*0.18, h*0.28, h*0.28]) -> hmm roundRect radii array order top-left, top-right, bottom-right, bottom-left
```
Bottom bun should have rounded bottom corners and flat top (hidden under patty). Use radii [2,2, w*0.16, w*0.16]? Let's define bottom bun height bh = h*0.3; draw rect from y+h-bh to y+h with radius bottom corners = min(w*0.18, bh*0.9).
Use custom helper roundRectPath(ctx,x,y,w,h, r) with r as number, and separate for per-corner maybe use ctx.roundRect with array (supported). I'll use ctx.roundRect with arrays: top-left, top-right, bottom-right, bottom-left.
Bottom bun: roundRect(x, yTop, w, bh, [0,0,rB,rB]) where rB = min(w*0.18, bh*0.8).
Patty: roundRect(x+ w*0.01, yPatty, w*0.98, hPatty, rPatty=3) color #7a3b1d with darker edge.
Cheese: yellow layer with drips: draw rect plus small triangles pointing down at intervals.
Lettuce: green wavy — draw a rounded rect with wavy bottom using quadratic curves.
Tomato: red rounded rect with slight curvature.
Top bun: roundRect(x, y, w, hTop, [r,r,0,0])? Top bun has rounded top corners. But top bun should be a dome. Use roundRect with big radius rTop = min(w*0.2, hTop*0.9) plus an ellipse-ish? Let's approximate: roundRect(x, y, w, hTop, [rTop, rTop, 2, 2]) and then add a subtle arc highlight. Good enough. Sesame seeds: small ellipses rotated, color #fff3d6.
Top bun color gradient: #f2b25c → #d98b36.
Also a "shine" on top.
Height allocation for h=26:
- top bun: h*0.34 ≈ 9
- tomato: h*0.09 ≈ 2.3
- lettuce: h*0.14 ≈ 3.6
- cheese: h*0.12 ≈ 3.1
- patty: h*0.2 ≈ 5.2
- bottom bun: h*0.25 ≈ 6.5
Sum ≈ 29.7 > 26. Adjust: total layers sum to h.
Let: topBun 0.32, tomato 0.08, lettuce 0.12, cheese 0.10, patty 0.20, botBun 0.28 → 1.10. Fix: topBun 0.30, tomato 0.08, lettuce 0.12, cheese 0.10, patty 0.18, botBun 0.22 = 1.00. Good. But cheese drips extend below into patty area — fine, draw cheese over patty.
Order drawing: bottom bun, patty, cheese, lettuce, tomato, top bun (top bun drawn last covers top of tomato).
Actually cheese drips should hang over the patty — draw cheese after patty, drips extend down over patty. Then lettuce over cheese. Fine.
Now the ball: white circle with glow + trail.
Also "tons of little hearts" — maybe also add floating ambient hearts in the background for atmosphere.
Let me write code.
Paddle movement:
- manual: target x from mouse position (canvas coords) or arrow keys.
- demo AI: predict ball landing x on paddle y: simple: if ball.vy>0, t = (paddleY - ball.y)/ball.vy, targetX = ball.x + ball.vx*t. Move toward target with speed limit and small jitter error (±10px) and reaction delay. Also add "rubber band" clamp to walls.
Also demo AI should occasionally miss slightly to keep it fun.
Implementation: paddle.x (center). In manual, mouse sets target; keyboard changes velocity.
Let me handle pointer events: mousemove on canvas → targetX = e.clientX - rect.left. Also touchmove.
Keyboard: ArrowLeft/Right or A/D adjust velocity.
Now lives: 3. Ball lost → lives--, respawn after 0.8s, combo reset. When lives = 0 → game over screen with "GAME OVER — press R / click to restart" but keep demo? In demo mode AI shouldn't lose. Actually in demo, if AI loses all lives → restart level automatically? Let's have demo infinite: when lives reach 0 in demo, reset lives to 3 and continue (keep it playing forever). In manual, game over overlay → click restarts.
Better: In demo mode, keep lives at 3 always (never lose). In manual mode, show lives.
Let's write.
Also score popups ("+50") floating text.
I'll now write the whole HTML.
Canvas background drawn each frame: gradient + vignette + ambient floating hearts + grid dots? Let's do: linear gradient dark; radial glow behind paddle; some slowly drifting soft circles (bokeh).
Let me produce final code.
One more: The overlay message must be visible on first paint and the game running behind. Use a DOM overlay div with pointer-events none? It should not block clicks — clicking anywhere on the stage starts. Set overlay pointer-events:none and listen on window for click/keydown.
Also "restart" button in header.
Let me write the code carefully now.
```js
const TAU = Math.PI*2;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rand=(a,b)=>a+Math.random()*(b-a);
const pick=arr=>arr[(Math.random()*arr.length)|0];
```
State:
```
const state = {
mode:'demo', // 'demo' | 'play' | 'over'
score:0, lives:3, level:1, combo:0, best:...
};
```
Ball: {x,y,vx,vy,r,speed}
Trail: array.
Bricks: array of {x,y,w,h,hp,maxHp,color,row,col,dead,flash}
Levels: when all bricks dead → level++, rebuild, ball reset to paddle, banner.
Let's write buildLevel:
```
function makeLevel(n){
const pat = PATTERNS[(n-1)%PATTERNS.length];
const rows = pat.length;
const margin = 18;
const gap = 6;
const usableW = W - margin*2;
const cols = Math.max(...pat.map(r=>r.length));
const bw = (usableW - gap*(cols-1))/cols;
const bh = Math.min(26, Math.max(16, H*0.045));
const top = 34;
bricks=[];
pat.forEach((rowStr,r)=>{
const startX = margin + (usableW - (rowStr.length*bw + (rowStr.length-1)*gap))/2;
for(let c=0;c<rowStr.length;c++){
const ch = rowStr[c]; if(ch==='.')continue;
const hp = ch==='X'?2:1;
bricks.push({x:startX+c*(bw+gap), y:top+r*(bh+gap), w:bw, h:bh, hp, maxHp:hp, color:..., dead:false, flash:0});
}
});
}
```
Brick colors by row: palette array; tough bricks get a distinct color (#c084fc or dark with border).
Brick drawing: rounded rect with vertical gradient from color to darker; a lighter top strip (glossy); if hp===2, draw a metallic border and rivets.
Brick break: spawnHearts(b.x+b.w/2, b.y+b.h/2, count).
Hearts count: 22 + rand 8.
Also add sparkle particles (small circles) and brick dust rectangles.
Now the draw for hearts: draw with ctx.globalAlpha, fill color, and a highlight.
Let's write drawHeart:
```
function drawHeart(ctx,size){
ctx.beginPath();
const s=size;
ctx.moveTo(0, s*0.35);
ctx.bezierCurveTo(-s*0.55, -s*0.05, -s*0.5, -s*0.6, -s*0.2, -s*0.6);
...
}
```
Standard heart path (from common snippet), scale factor 1 → roughly width 1.4, height 1.4:
```
ctx.moveTo(0, 0.3);
ctx.bezierCurveTo(0, -0.5, -0.7, -0.5, -0.7, 0.05);
ctx.bezierCurveTo(-0.7, 0.45, -0.25, 0.6, 0, 0.85);
ctx.bezierCurveTo(0.25, 0.6, 0.7, 0.45, 0.7, 0.05);
ctx.bezierCurveTo(0.7, -0.5, 0, -0.5, 0, 0.3);
ctx.closePath();
```
Hmm the bottom point: after (-0.7,0.45) to (0,0.85) then mirror. That looks like a heart with the point at bottom. Good.
We draw with transform: translate(x,y), rotate(rot), scale(s,s) where s = size (heart extends about -0.6..0.85 vertically). Then fill.
Add highlight: small white ellipse at upper-left with alpha.
Now let's write everything. I'll be thorough but concise.
Also for the "Click or press a key to start" overlay, the iframe receives keyboard only after click; but we display the message anyway.
Let me handle the case where the demo runs but user presses key → switch to play mode, hide overlay.
Also add a small "DEMO" chip in the top-left of the canvas and "YOU" when playing.
Edge case: resize during play — recompute layout, reposition bricks proportionally. Simplest: on resize, rebuild level (keeping score) and reset ball onto paddle. Slight disruption but acceptable. Better: scale positions proportionally. I'll do: store old W,H; scale brick x,y by ratio; reposition paddle proportionally; ball reposition proportionally.
Let's write resize:
```
function resize(){
const rect = stage.getBoundingClientRect();
const nw = Math.max(280, rect.width), nh = Math.max(220, rect.height);
const sx = W? nw/W:1, sy = H? nh/H:1;
W=nw;H=nh;
...
if(bricks.length){ bricks.forEach(b=>{b.x*=sx;b.y*=sy;b.w*=sx;b.h*=sy;}); }
paddle.x *= sx; paddle.y = H - paddleH - 18;
ball.x*=sx; ball.y*=sy;
}
```
But H changes, and paddle.y resets to bottom. Fine.
Brick layout at level build uses W/H. On resize with proportional scaling, gaps may change slightly; acceptable.
Hmm, but bh scaling with sy could be odd if aspect changes a lot. Fine.
Now, initial: W=0 so first resize builds level.
Let me write the code in full.
I also want the paddle to have a subtle "grease" glow and maybe little fries? no.
Let me write the render order:
1. background gradient + bokeh
2. ambient hearts (background, slow drift, low alpha)
3. bricks
4. particles (hearts, sparkles)
5. ball trail + ball
6. paddle
7. floating score texts
8. banners (level up) - DOM or canvas text. I'll do canvas text with fade.
Let's finalize.
Sound: I'll add minimal WebAudio:
- brick: short square blip with descending pitch, random.
- paddle: low thud (sine 180Hz quick decay).
- heart pop: high blip.
- life lost: descending.
Volume low (0.12). Mute button toggles.
Create AudioContext on first user gesture (click/keydown). In demo mode before interaction, no audio.
OK, writing final code.
Also ensure no horizontal scrollbar: body overflow hidden, stage margin small.
For 375px width: header should wrap nicely; use flex-wrap and smaller fonts.
Let me write HTML structure:
```html
<div id="app">
<header>
<div class="brand"><span class="logo">🍔</span><div><h1>Burger Breakout</h1><p>every brick explodes into hearts</p></div></div>
<div class="stats">
<div class="chip"><span class="label">SCORE</span><span class="val" id="score">0</span></div>
<div class="chip"><span class="label">LEVEL</span><span class="val" id="level">1</span></div>
<div class="chip"><span class="label">BEST</span><span class="val" id="best">0</span></div>
<div class="chip lives"><span class="label">LIVES</span><span class="val" id="lives">🍔🍔🍔</span></div>
<button id="soundBtn" class="iconbtn" title="Sound">🔊</button>
<button id="resetBtn" class="iconbtn" title="Restart">⟳</button>
</div>
</header>
<main id="stage">
<canvas id="c"></canvas>
<div id="overlay">
<div class="card">
<div class="burger-big">🍔</div>
<h2>Click or press a key to start</h2>
<p>The AI is demoing it for you — take over any time.<br/>Move with <kbd>←</kbd><kbd>→</kbd> or your mouse / finger.</p>
</div>
</div>
<div id="badge">AI DEMO</div>
</main>
</div>
```
Badge top-left of stage, switches text to "YOU'RE PLAYING".
Overlay card styled with backdrop blur, pulsing animation.
When game over (manual), show overlay with "GAME OVER" and score, click to restart.
Implement overlay functions: showOverlay(title, html), hideOverlay().
Now code the JS fully.
I'll write it now, carefully.
```js
(() => {
const cvs = document.getElementById('c');
const ctx = cvs.getContext('2d');
const stage = document.getElementById('stage');
const overlay = document.getElementById('overlay');
const badge = document.getElementById('badge');
const scoreEl=..., levelEl, livesEl, bestEl;
const TAU=Math.PI*2, clamp=(v,a,b)=>..., rand=..., pick=...;
let W=0,H=0,DPR=1;
let bricks=[], balls=[], hearts=[], sparks=[], texts=[], rings=[];
let paddle;
let mode='demo';
let score=0, level=1, lives=3, combo=0;
let shake=0, banner=null;
let particlesEnabled = true;
const best = ... // no localStorage! use in-memory variable only. "BEST" chip would reset on reload — fine, keep in-memory.
```
No localStorage allowed. Use a module variable `bestScore`.
Ball array for multi-ball powerups? Could add powerup: some bricks drop a powerup (multi-ball, wide paddle). That adds interest but complexity. The core request is hearts. I'll add a simple occasional powerup: 8% chance a brick drops a "fries" powerup giving wider paddle, or heart-rain bonus. Hmm — maybe keep scope manageable but add multi-ball since it's fun: let's add powerups:
- 'multi' (split into 3 balls)
- 'wide' (paddle wider for 10s)
- 'hearts' (bonus hearts burst +500)
These are drawn as little icons. I'll implement 'multi' and 'wide' and 'bonus' (score). Keep simple.
Actually let's include: when a brick breaks, small chance (10%) to drop a powerup that falls with gravity; paddle catches → effect. Powerup drawn as small rounded square with emoji-ish icon (use text glyph: ✦ for multi, ⇔ for wide, ♥ for bonus). Using unicode symbols is safe.
I'll implement it—moderate code.
Let me limit: powerups array, drawn as rounded rect with gradient and glyph, physics fall with slight spin, catch by paddle → effect.
Effects:
- 'multi': balls.push two extra with angled velocity; also all current balls get slight speed boost.
- 'wide': paddleWideUntil = time+10; paddle width grows with easing.
- 'heart': +300 score and spawn 40 hearts burst.
Also demo AI should handle multi-ball (track the lowest/most dangerous ball).
Ball loss: when all balls gone → lose life.
Let's code the ball update with steps.
Ball speed base: min(W,H)*0.55? Let's use absolute: 320 px/s at start, scaled: speed = 300 + level*20, cap 640. On narrow screens maybe scale by min(1, W/700). Let's scale speeds by s = clamp(W/760, 0.7, 1.15).
Now writing.
Physics loop:
```
function update(dt){
time += dt;
// paddle
...
// balls
for(let i=balls.length-1;i>=0;i--){
const b=balls[i];
// trail
b.trail.unshift({x:b.x,y:b.y}); if(b.trail.length>12)b.trail.pop();
const steps = Math.ceil(b.speed*dt/3);
const sdt = dt/steps;
for(let s=0;s<steps;s++){
b.x+=b.vx*sdt; b.y+=b.vy*sdt;
// walls
if(b.x<b.r){b.x=b.r;b.vx=Math.abs(b.vx); wallFx();}
if(b.x>W-b.r){b.x=W-b.r;b.vx=-Math.abs(b.vx);}
if(b.y<b.r+topOffset){...} top wall at 0
// paddle
...
// bricks
...
}
if(b.y > H+30){ remove ball }
}
}
```
Top wall: y < r → bounce. Also the bricks start at top=34, so ball can travel between top and bricks.
Paddle collision: paddle rect: px = paddle.x - pw/2, py = paddle.y - ph/2, pw, ph. Check b.vy>0 && b.y+b.r >= py && b.y-b.r <= py+ph && b.x in [px-?, px+pw+?]. Use circle-rect overlap with only top-face resolution.
Simplify: if(b.vy>0 && b.y + b.r >= py && b.y + b.r <= py + ph + 6 && b.x > px - b.r*0.5 && b.x < px + pw + b.r*0.5) → bounce:
offset = clamp((b.x - paddle.x)/(pw/2), -1, 1); angle = offset * 1.05 (60°);
speed = b.speed; b.vx = Math.sin(angle)*speed; b.vy = -Math.cos(angle)*speed;
ensure |vy| >= 0.35*speed → if too small, adjust.
b.y = py - b.r - 0.1.
Add small english from paddle velocity: b.vx += paddle.vx*0.12.
Also "bounce" should feel good; add particle burst at contact and sound.
Brick collision handled per step; when a brick breaks, remove it.
Now rendering hearts: hearts array with {x,y,vx,vy,r,rot,vr,life,maxLife,size,color,glow}.
Update: vy += 900*dt; vx *= 0.99; x+=vx*dt; y+=vy*dt; rot+=vr*dt; life-=dt. When y > H+20 → remove with a little splash (skip).
Draw: alpha = clamp(life/0.6) etc; scale pulse.
Count: on break, 26 hearts. With 50 bricks that's 1300 hearts over time — need perf care. Hearts have lifetime ~2.5s so at most ~100 alive typically. Fine.
Also add "tons" - maybe 30 per brick. Let's do 24 + 8*random, plus 6 large hearts.
Now let me also make the ball leave a heart trail occasionally? Nice touch: every ~0.12s the ball emits a tiny heart. That adds a lot of hearts. Yes! Ball heart trail in demo... Let's do it always: ball emits a small heart every 0.08s with small size. That's very charming and matches "tons of little hearts".
OK.
Let me write the code now, final.
For the AI paddle:
```
function aiTarget(){
// choose ball with largest vy and closest to paddle
let best=null;
for(const b of balls){ if(b.vy<=0) continue; const dist = paddle.y - b.y; if(!best||dist<best.dist) best={b,dist}; }
if(!best) best = balls[0];
const b=best;
let tx = b.x + b.vx * ((paddle.y - b.y)/Math.max(1,b.vy));
tx += rand(-1,1)* (aiError);
return tx;
}
```
AI error: 6px, plus speed limit ~ paddle speed*1.1. Also clamp target to [pw/2+4, W-pw/2-4].
AI misses occasionally: if random chance, offset by 40px. That keeps things lively and doesn't matter since demo lives are infinite.
In demo mode, lives stay 3 (never decrement) — actually let's decrement but reset to 3 when 0, showing "AI keeps playing".
Now let's write final code. I'll write it out.
Also the top-of-canvas badge "AI DEMO" / "PLAYER".
Also display combo multiplier: combo bricks without losing → multiplier. Score += 50*combo.
Floating text "+100" at brick position.
Let me write the drawing of the paddle glow and a soft radial gradient under it.
Final code writing now. I'll be careful with syntax.
One more consideration: `ctx.roundRect` — supported Chrome 99+. I'll add a polyfill function rr(ctx,x,y,w,h,r) that handles number or array.
Let me write:
```
function rr(c,x,y,w,h,r){
const rad = Array.isArray(r)? r : [r,r,r,r];
if(c.roundRect){ c.beginPath(); c.roundRect(x,y,w,h,rad); return; }
// fallback manual
...
}
```
Actually just always use manual path to be safe? Manual is a bit long but fine. I'll use ctx.roundRect with try/catch fallback manual implementation.
I'll write manual path helper to avoid any issue:
```
function rr(c,x,y,w,h,r){
let a = Array.isArray(r)?r.slice():[r,r,r,r];
const max = Math.min(w,h)/2;
a = a.map(v=>Math.max(0,Math.min(v,max)));
c.beginPath();
c.moveTo(x+a[0], y);
c.lineTo(x+w-a[1], y);
c.quadraticCurveTo(x+w, y, x+w, y+a[1]);
c.lineTo(x+w, y+h-a[2]);
c.quadraticCurveTo(x+w, y+h, x+w-a[2], y+h);
c.lineTo(x+a[3], y+h);
c.quadraticCurveTo(x, y+h, x, y+h-a[3]);
c.lineTo(x, y+a[0]);
c.quadraticCurveTo(x, y, x+a[0], y);
c.closePath();
}
```
Good, uses quadratic (not exact arc but fine visually).
Now write the full file.
Also: keep `best` in memory and update DOM.
Let me write it.
For the intro, also a subtle hint animation.
I'll write the final HTML now.
Check: no localStorage, no alert, no window.open. Good.
Time to produce the answer.
Additional detail: the stage height. body is 100vh flex column; header auto height; stage flex:1. On a 375x800 mobile, stage height ~ 800-header(~90)-margins ≈ 690, width 351. That's a tall portrait play area. Breakout in portrait: bricks at top 5 rows, paddle at bottom. Works fine.
Aspect ratio issue: W=351,H=690 → very tall. Ball speed scaled by clamp(W/760,0.7,1.15)=0.7 → 210 px/s; travel distance 690 takes ~3s. OK.
Maybe better to constrain the stage to a max aspect ratio (like 16:10) and center it, so the game area looks good on tall screens. Let's set stage: flex:1; and also max-height? Hmm. Alternatively set the app to have the stage with aspect-ratio and margin auto. On desktop 1280x800: stage width 1256 → aspect 16/10 → height 785, too tall. Use aspect-ratio with max-height: min(100%, ...). Let's do: stage { flex:1 1 auto; min-height:0; } and inside it, a wrapper div centered with aspect-ratio 4/3? Hmm, complexity.
Simplest approach: let the canvas fill the stage entirely (any aspect). It looks fine on both. On desktop 1280x800, stage ≈ 1256 x 660 → good. On mobile portrait 351x690 → tall but playable.
But bricks: 5 rows in a tall field leaves lots of empty space; that's fine visually (space for hearts falling).
I'll go with fill.
Maybe on very tall/narrow screens, reduce number of columns so bricks aren't tiny: bw computed from cols; with cols=10 in 351px → bw=27px, small but ok. Let's set cols based on width: if W<520 use 8 columns. Patterns with 10 chars won't fit; I'll instead generate patterns programmatically based on cols. Let's do that — build patterns as functions of (rows, cols) using character masks.
Plan: define pattern functions returning 2D array of chars.
```
function patternFull(rows,cols){ return Array.from({length:rows},()=> '#'.repeat(cols)); }
function patternChecker(rows,cols){...}
function patternDiamond(rows,cols){...}
function patternGaps(rows,cols){...}
```
Diamond: cell is '#' if |row - (rows-1)/2| + |col - (cols-1)/2| <= something. Let's compute: for diamond, keep cells where dist <= rows*0.75 etc.
Let's define:
- level 1: full grid.
- level 2: full with a few random gaps (seeded random ok).
- level 3: diamond/hollow.
- level 4: checkerboard.
- level 5: pyramid.
I'll implement 4 pattern generators and cycle with increasing rows (5,5,6,6,7) capped by available height.
Tough bricks: 'X' appears in higher levels (level>=3) at certain positions.
Let's code:
```
function buildPattern(level, rows, cols){
const g = [];
for(let r=0;r<rows;r++){
let row='';
for(let c=0;c<cols;c++){
let ch='#';
const m = level % 5;
if(m===0){ ch='#'; }
else if(m===1){ ch = (Math.random()<0.08)?'.':'#'; }
else if(m===2){ // hollow diamond
const dr=Math.abs(r-(rows-1)/2), dc=Math.abs(c-(cols-1)/2);
ch = (dr+dc*0.9) < rows*0.85 ? '#':'.';
}
else if(m===3){ ch = ((r+c)%2===0)?'#':'.'; }
else { // pyramid
const half = Math.floor((cols*(r+1))/rows/2);
const center = Math.floor(cols/2);
ch = (Math.abs(c-center) <= half)?'#':'.';
}
if(ch==='#' && level>=4 && Math.random()<0.08) ch='X';
row+=ch;
}
g.push(row);
}
return g;
}
```
Good enough.
Rows: 5 + floor((level-1)/2) capped at 8, but also ensure vertical space: rows*bh+gap < H*0.55.
Let's compute bh = clamp(H*0.035, 14, 26) and rows limited so that top+rows*(bh+gap) < H*0.6.
Now write the final code.
Also brick colors by row index from palette; tough = metallic gray-purple.
Let me write the render of brick:
```
rr(ctx,b.x,b.y,b.w,b.h, Math.min(6,b.h*0.28));
const g=ctx.createLinearGradient(b.x,b.y,b.x,b.y+b.h);
g.addColorStop(0, lighten(color,0.25)); g.addColorStop(0.5,color); g.addColorStop(1, darken(color,0.3));
ctx.fillStyle=g; ctx.fill();
// gloss
ctx.fillStyle='rgba(255,255,255,0.18)';
rr(ctx,b.x+b.w*0.06,b.y+b.h*0.12,b.w*0.88,b.h*0.22, 3); ctx.fill();
```
Need color helpers: parse hex → adjust lightness.
Implement:
```
function shade(hex, amt){ // amt -1..1
const n=parseInt(hex.slice(1),16); let r=n>>16,g=(n>>8)&255,b=n&255;
if(amt>0){r=r+(255-r)*amt;g=g+(255-g)*amt;b=b+(255-b)*amt;} else {r=r*(1+amt);g=g*(1+amt);b=b*(1+amt);}
return `rgb(${r|0},${g|0},${b|0})`;
}
```
Good.
Now the full code. Let me write it.
I also want the hearts to look "cute" with a white highlight.
Finally, the header burger logo: use a CSS-drawn burger? Emoji 🍔 is simplest and renders on most systems (Chrome on Linux may lack emoji font → shows tofu). Risky in sandbox screenshot. Safer: draw burger with CSS divs for the logo. Let's make a CSS mini burger: stacked divs with border-radius. That's safe and cute.
CSS burger:
```
.burger{width:38px;height:38px;position:relative}
.burger span{position:absolute;left:0;right:0;border-radius:6px}
.burger .bun-top{top:0;height:11px;background:#f0b05e}
.burger .tom{top:10px;height:4px;background:#e5484d}
.burger .lett{top:13px;height:5px;background:#57b64a}
.burger .cheese{top:17px;height:4px;background:#ffcb3d}
.burger .patty{top:20px;height:6px;background:#6b3b22}
.burger .bun-bot{top:25px;height:11px;background:#d98b36;border-radius:0 0 8px 8px}
```
Good — no emoji dependency.
For lives display, use small CSS burgers too (mini versions) or just "♥ ♥ ♥"? Lives as burger icons: I'll render lives as text "🍔" replaced by a small span with class mini-burger. Simpler: show lives as number with heart glyph? Hearts are central; show lives as small burger CSS. Let's do 3 mini-burgers via spans with class 'mburger' using the same layered approach but smaller. Might be a bit much. I'll just show a numeric lives count with a small burger icon next to it. Simpler: "LIVES 3". But a burger-themed icon is nicer. I'll do the layered CSS with a modifier class for small size.
Let me define:
```
.burger{ --w:44px; ...}
```
Use a reusable markup: <span class="bk"><i class="t"></i><i class="to"></i>...</span>. I'll write a JS function to generate burger HTML for the logo and lives.
```
function burgerHTML(size){
return `<span class="bk" style="--s:${size}px"><i class="bt"></i><i class="bm"></i><i class="bl"></i><i class="bc"></i><i class="bp"></i><i class="bb"></i></span>`;
}
```
CSS with --s controlling width and each layer's height/position percentages. Using percentages relative to the parent height.
```
.bk{position:relative;display:inline-block;width:var(--s);height:var(--s);vertical-align:middle}
.bk i{position:absolute;left:2%;right:2%;display:block}
.bk .bt{top:0;height:34%;background:linear-gradient(#f6c274,#e2a04c);border-radius:38% 38% 18% 18% / 60% 60% 30% 30%}
.bk .bm{top:32%;height:7%;background:#e04b4b;border-radius:3px}
.bk .bl{top:38%;height:9%;background:#63bf4a;border-radius:4px}
.bk .bc{top:46%;height:7%;background:#ffc93c;border-radius:3px}
.bk .bp{top:51%;height:16%;background:#6d3a21;border-radius:4px}
.bk .bb{top:64%;height:34%;background:linear-gradient(#e2a04c,#c98433);border-radius:18% 18% 42% 42% / 26% 26% 70% 70%}
```
Good.
Now the canvas burger paddle is drawn manually.
Alright, write it all out.
Also add keyboard handling: ArrowLeft/ArrowRight, a/d. And Space to launch? Ball starts attached to paddle? In demo mode the ball should be in play immediately. So in demo, ball is launched automatically. In manual mode when user takes over, keep current balls. If the game is in "ready" state (ball on paddle), space/click launches.
I'll implement `serve()` that places ball on paddle with slight angle; auto-launch after 0.6s in demo; in manual, launch on click/space.
Simplify: always auto-launch after 0.7s; in manual mode clicking also launches.
OK writing final answer now.
Careful: ensure `overlay` transitions: use class 'hidden' with opacity 0, pointer-events none.
Game over overlay: set innerHTML of .card.
Let's write.
I'll now carefully compose the code.
```js
// ---------- audio ----------
let actx=null, muted=false;
function ensureAudio(){ if(!actx){ try{ actx=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } if(actx&&actx.state==='suspended') actx.resume(); }
function blip(freq, dur=0.08, type='square', vol=0.08, slide=0){
if(!actx||muted) return;
try{
const o=actx.createOscillator(), g=actx.createGain();
o.type=type; o.frequency.setValueAtTime(freq, actx.currentTime);
if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(40,freq+slide), actx.currentTime+dur);
g.gain.setValueAtTime(vol, actx.currentTime);
g.gain.exponentialRampToValueAtTime(0.0001, actx.currentTime+dur);
o.connect(g); g.connect(actx.destination); o.start(); o.stop(actx.currentTime+dur+0.02);
}catch(e){}
}
```
Now write everything.
Time to output. I'll produce the final file.