Models 44

MiniMax M3

by MiniMaxreasoning defaultClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
1 min 12 s
Cost
$0.0066
Output tokens
6,788
Spent thinking
0 tokens
Speed
98.3 tok/s
Console errors
0
First token after
2.7 s
First visible text after
2.7 s
Prompt tokens
504
Code size
19.7 KB
Finish reason
stop
Served by
GMICloud
Model id
minimax/minimax-m3
Reasoning sent
model default
Max output
512,000 tokens
Generated
Nov 13, 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 · 695 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Heart Burger Breakout</title>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body {
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: #130719;
    font-family: "Trebuchet MS", Arial, sans-serif;
    color: #fff;
  }
  #game {
    position: relative;
    width: 100%;
    height: 100%;
    min-height: 560px;
    display: flex;
    justify-content: center;
    align-items: center;
    background:
      radial-gradient(circle at 50% -20%, #5e1a6b 0%, #2a0d3c 45%, #130719 100%);
  }
  canvas {
    display: block;
    max-width: 100%;
    max-height: 100%;
    touch-action: none;
    box-shadow: 0 0 60px rgba(255, 70, 150, .22);
  }
  #overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    pointer-events: none;
    background: radial-gradient(circle, rgba(20,5,30,.42), rgba(20,5,30,.08));
    transition: opacity .25s;
  }
  #overlay.hidden { opacity: 0; }
  h1 {
    font-size: clamp(30px, 6vw, 66px);
    line-height: .95;
    letter-spacing: -2px;
    color: #fff5fa;
    text-shadow: 0 5px 0 #a71961, 0 8px 24px #ff4d9d;
    transform: rotate(-2deg);
  }
  h1 span { color: #ff77b4; }
  .subtitle {
    margin-top: 18px;
    font-size: clamp(13px, 2vw, 20px);
    color: #ffd8e8;
    font-weight: bold;
    letter-spacing: 1px;
  }
  .prompt {
    margin-top: 28px;
    padding: 13px 25px;
    border-radius: 30px;
    background: #ff4b96;
    color: white;
    font-size: clamp(14px, 2.2vw, 20px);
    font-weight: bold;
    box-shadow: 0 6px 0 #a71b61, 0 10px 25px rgba(255,75,150,.35);
    animation: bob 1.2s ease-in-out infinite;
  }
  @keyframes bob {
    50% { transform: translateY(-5px) scale(1.03); }
  }
  #hud {
    position: absolute;
    top: 16px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    gap: 12px;
    align-items: center;
    pointer-events: none;
    z-index: 3;
  }
  .pill {
    background: rgba(20,4,30,.72);
    border: 2px solid rgba(255,122,181,.55);
    border-radius: 20px;
    padding: 7px 15px;
    color: #ffe5f0;
    font-weight: bold;
    font-size: 14px;
    box-shadow: 0 4px 16px rgba(0,0,0,.25);
    backdrop-filter: blur(5px);
  }
  @media (max-width: 500px) {
    #hud { top: 8px; gap: 6px; }
    .pill { padding: 5px 9px; font-size: 12px; }
  }
</style>
</head>
<body>
<div id="game">
  <canvas id="canvas"></canvas>
  <div id="hud">
    <div class="pill">SCORE <span id="score">0</span></div>
    <div class="pill">♥ <span id="hearts">0</span></div>
    <div class="pill">LEVEL <span id="level">1</span></div>
  </div>
  <div id="overlay">
    <h1>HEART BURGER <span>BREAKOUT</span></h1>
    <div class="subtitle">Catch the burger. Smash the bricks. Make love explode.</div>
    <div class="prompt">CLICK OR PRESS A KEY TO START</div>
  </div>
</div>
<script>
(() => {
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const overlay = document.getElementById('overlay');
  const scoreEl = document.getElementById('score');
  const heartsEl = document.getElementById('hearts');
  const levelEl = document.getElementById('level');

  let W, H, scale;
  let running = false;
  let score = 0, heartCount = 0, level = 1;
  let bricks = [], particles = [], floaters = [], trails = [];
  let paddle, ball, keys = {};
  let shake = 0, flash = 0, levelFlash = 0;
  let particlesEnabled = true;
  let started = false;
  let aiTimer = 0;

  const brickRows = [
    { color: '#ff3f7f', dark: '#ad1852' },
    { color: '#ff5d9e', dark: '#b82c65' },
    { color: '#ff7ab3', dark: '#bf3b73' },
    { color: '#ffa0c8', dark: '#b9557e' },
    { color: '#d9367c', dark: '#941747' },
    { color: '#f04492', dark: '#991d56' }
  ];

  function resize() {
    const rect = document.getElementById('game').getBoundingClientRect();
    const maxW = Math.min(rect.width, 1000);
    const maxH = Math.min(rect.height, 760);
    const aspect = 4 / 3;
    let cw = maxW, ch = maxH;
    if (cw / ch > aspect) cw = ch * aspect;
    else ch = cw / aspect;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.style.width = cw + 'px';
    canvas.style.height = ch + 'px';
    canvas.width = Math.floor(cw * dpr);
    canvas.height = Math.floor(ch * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    W = cw; H = ch; scale = cw / 900;

    paddle = paddle || { x: W / 2, y: H - 55 * scale, w: 125 * scale, h: 28 * scale, vx: 0 };
    if (!started) {
      paddle.x = W / 2;
      paddle.y = H - 55 * scale;
    }
    ball = ball || { x: W / 2, y: paddle.y - 17 * scale, vx: 0, vy: 0, r: 9 * scale };
    layoutBricks();
  }

  function layoutBricks() {
    if (!W || !H) return;
    const cols = W < 520 ? 7 : 10;
    const rows = 5;
    const margin = 38 * scale;
    const gap = 7 * scale;
    const top = 76 * scale;
    const bw = (W - margin * 2 - gap * (cols - 1)) / cols;
    const bh = 24 * scale;
    bricks = [];
    for (let r = 0; r < rows; r++) {
      for (let c = 0; c < cols; c++) {
        bricks.push({
          x: margin + c * (bw + gap),
          y: top + r * (bh + gap),
          w: bw,
          h: bh,
          color: brickRows[(r + level) % brickRows.length].color,
          dark: brickRows[(r + level) % brickRows.length].dark,
          hp: 1,
          alive: true,
          pulse: Math.random() * Math.PI * 2
        });
      }
    }
  }

  function resetGame() {
    score = 0; heartCount = 0; level = 1;
    particles = []; floaters = []; trails = [];
    paddle.w = 125 * scale;
    paddle.x = W / 2;
    paddle.y = H - 55 * scale;
    ball.x = W / 2;
    ball.y = paddle.y - 20 * scale;
    ball.vx = 0;
    ball.vy = 0;
    layoutBricks();
    updateHud();
    running = true;
    started = true;
    overlay.classList.add('hidden');
  }

  function updateHud() {
    scoreEl.textContent = score;
    heartsEl.textContent = heartCount;
    levelEl.textContent = level;
  }

  function launchBall() {
    const speed = 335 * scale;
    const angle = (-Math.PI / 2) + (Math.random() - .5) * .65;
    ball.vx = Math.cos(angle) * speed;
    ball.vy = Math.sin(angle) * speed;
  }

  function startOrLaunch() {
    if (!started) {
      resetGame();
      return;
    }
    if (!running) {
      running = true;
      overlay.classList.add('hidden');
      return;
    }
    if (ball.vx === 0 && ball.vy === 0) launchBall();
  }

  function heartParticle(x, y, big = false) {
    const size = (big ? 8 : 3.2 + Math.random() * 5.4) * scale;
    particles.push({
      type: 'heart',
      x, y,
      vx: (Math.random() - .5) * (big ? 300 : 260) * scale,
      vy: (big ? -80 - Math.random() * 130 : -40 - Math.random() * 160) * scale,
      life: big ? 1.15 : .65 + Math.random() * .8,
      maxLife: big ? 1.15 : 1.45,
      size,
      rot: Math.random() * Math.PI * 2,
      vr: (Math.random() - .5) * 7,
      color: ['#ff4c91', '#ff77ae', '#ff9bc5', '#e92d78', '#ffc0db'][Math.floor(Math.random()*5)]
    });
  }

  function sparkle(x, y, color) {
    for (let i = 0; i < 5; i++) {
      particles.push({
        type: 'spark',
        x, y,
        vx: (Math.random() - .5) * 180 * scale,
        vy: (Math.random() - .5) * 180 * scale,
        life: .35 + Math.random() * .3,
        maxLife: .65,
        size: 1.5 + Math.random() * 2,
        color
      });
    }
  }

  function explodeBrick(b) {
    const cx = b.x + b.w / 2, cy = b.y + b.h / 2;
    const amount = particlesEnabled ? (W < 500 ? 28 : 42) : 0;
    for (let i = 0; i < amount; i++) heartParticle(cx, cy);
    for (let i = 0; i < 10; i++) sparkle(cx, cy, '#ffd3e5');
    floaters.push({ x: cx, y: cy, text: '+10 ♥', life: .9, maxLife: .9, color: '#fff' });
    score += 10;
    heartCount += amount + 1;
    shake = Math.max(shake, 6);
    flash = .18;
  }

  function nextLevel() {
    level++;
    levelFlash = 1;
    layoutBricks();
    ball.x = paddle.x;
    ball.y = paddle.y - 20 * scale;
    ball.vx = 0;
    ball.vy = 0;
    paddle.w = Math.max(88 * scale, paddle.w - 7 * scale);
    updateHud();
  }

  function drawHeart(x, y, size, color, rot = 0, alpha = 1) {
    ctx.save();
    ctx.translate(x, y);
    ctx.rotate(rot);
    ctx.scale(size / 10, size / 10);
    ctx.globalAlpha = alpha;
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.moveTo(0, 7);
    ctx.bezierCurveTo(-12, -2, -7, -11, 0, -5);
    ctx.bezierCurveTo(7, -11, 12, -2, 0, 7);
    ctx.fill();
    ctx.restore();
  }

  function drawBurger() {
    const x = paddle.x, y = paddle.y, w = paddle.w, h = paddle.h;
    const bx = x - w/2, by = y - h/2;
    ctx.save();

    // glow
    const g = ctx.createRadialGradient(x, y, 3, x, y, w*.65);
    g.addColorStop(0, 'rgba(255,100,160,.32)');
    g.addColorStop(1, 'rgba(255,100,160,0)');
    ctx.fillStyle = g;
    ctx.fillRect(bx-w*.3, by-h, w*1.6, h*3);

    // top bun
    ctx.fillStyle = '#f6a940';
    ctx.beginPath();
    ctx.moveTo(bx, by+h*.42);
    ctx.bezierCurveTo(bx+w*.02, by-h*.35, bx+w*.28, by-h*.48, bx+w*.5, by-h*.48);
    ctx.bezierCurveTo(bx+w*.72, by-h*.48, bx+w*.98, by-h*.35, bx+w, by+h*.42);
    ctx.closePath();
    ctx.fill();
    ctx.strokeStyle = '#a95a14';
    ctx.lineWidth = 2 * scale;
    ctx.stroke();

    // sesame
    ctx.fillStyle = '#fff0bd';
    const seeds = [[.22,-.22],[.4,-.31],[.59,-.28],[.76,-.17],[.31,-.12],[.66,-.1]];
    seeds.forEach(([sx,sy]) => {
      ctx.beginPath();
      ctx.ellipse(bx+w*sx, by+h*sy, 2.1*scale, 1.1*scale, -.4, 0, Math.PI*2);
      ctx.fill();
    });

    // lettuce
    ctx.fillStyle = '#67d34d';
    ctx.beginPath();
    ctx.moveTo(bx, by+h*.4);
    for (let i=0; i<=10; i++) {
      const px = bx + w*i/10;
      const py = by+h*(.5 + (i%2 ? .14 : 0));
      ctx.lineTo(px, py);
    }
    ctx.lineTo(bx+w, by+h*.64);
    ctx.lineTo(bx, by+h*.64);
    ctx.closePath();
    ctx.fill();

    // patty
    ctx.fillStyle = '#673321';
    ctx.beginPath();
    ctx.roundRect(bx+w*.02, by+h*.55, w*.96, h*.33, 5*scale);
    ctx.fill();
    ctx.fillStyle = '#85492f';
    ctx.fillRect(bx+w*.04, by+h*.57, w*.92, h*.09);

    // cheese
    ctx.fillStyle = '#ffd43b';
    ctx.beginPath();
    ctx.moveTo(bx+w*.02, by+h*.52);
    ctx.lineTo(bx+w*.98, by+h*.52);
    ctx.lineTo(bx+w*.9, by+h*.82);
    ctx.lineTo(bx+w*.78, by+h*.65);
    ctx.lineTo(bx+w*.65, by+h*.83);
    ctx.lineTo(bx+w*.52, by+h*.64);
    ctx.lineTo(bx+w*.38, by+h*.82);
    ctx.lineTo(bx+w*.25, by+h*.63);
    ctx.lineTo(bx+w*.1, by+h*.78);
    ctx.closePath();
    ctx.fill();

    // bottom bun
    ctx.fillStyle = '#e99735';
    ctx.beginPath();
    ctx.roundRect(bx, by+h*.72, w, h*.34, 7*scale);
    ctx.fill();
    ctx.strokeStyle = '#a95a14';
    ctx.lineWidth = 2 * scale;
    ctx.stroke();

    // little eyes make it adorable
    ctx.fillStyle = '#3c221b';
    ctx.beginPath();
    ctx.arc(bx+w*.39, by+h*.9, 2*scale, 0, Math.PI*2);
    ctx.arc(bx+w*.61, by+h*.9, 2*scale, 0, Math.PI*2);
    ctx.fill();
    ctx.restore();
  }

  function drawBrick(b, time) {
    const p = 1 + Math.sin(time*3 + b.pulse)*.025;
    const x = b.x, y = b.y, w = b.w*p, h = b.h*p;
    ctx.save();
    ctx.fillStyle = 'rgba(0,0,0,.18)';
    ctx.beginPath();
    ctx.roundRect(x+2*scale, y+4*scale, w, h, 5*scale);
    ctx.fill();

    const grad = ctx.createLinearGradient(x, y, x, y+h);
    grad.addColorStop(0, '#fff');
    grad.addColorStop(.12, b.color);
    grad.addColorStop(1, b.dark);
    ctx.globalAlpha = .35;
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.roundRect(x, y, w, h, 5*scale);
    ctx.fill();
    ctx.globalAlpha = 1;

    ctx.strokeStyle = 'rgba(255,255,255,.45)';
    ctx.lineWidth = 1.2*scale;
    ctx.beginPath();
    ctx.roundRect(x, y, w, h, 5*scale);
    ctx.stroke();

    // tiny heart mark
    drawHeart(x+w/2, y+h/2+1*scale, 8*scale, 'rgba(255,255,255,.55)');
    ctx.restore();
  }

  function update(dt) {
    if (!running) return;
    time += dt;

    // Paddle input and gentle demo AI
    const targetSpeed = 620 * scale;
    if (keys.ArrowLeft || keys.a || keys.A) {
      paddle.x -= targetSpeed*dt;
      keys.demo = false;
    } else if (keys.ArrowRight || keys.d || keys.D) {
      paddle.x += targetSpeed*dt;
      keys.demo = false;
    } else {
      aiTimer -= dt;
      if (aiTimer <= 0) {
        aiTimer = .12;
        if (ball.vy < 0) {
          const desired = ball.x;
          const diff = desired - paddle.x;
          if (Math.abs(diff) > 18*scale) {
            paddle.x += Math.sign(diff) * Math.min(Math.abs(diff), 230*scale*dt);
            keys.demo = true;
          }
        } else {
          const desired = W/2 + Math.sin(time*.8)*W*.2;
          const diff = desired - paddle.x;
          paddle.x += Math.sign(diff) * Math.min(Math.abs(diff), 190*scale*dt);
        }
      }
    }
    paddle.x = Math.max(paddle.w/2+4, Math.min(W-paddle.w/2-4, paddle.x));

    if (ball.vx === 0 && ball.vy === 0) {
      ball.x = paddle.x;
      ball.y = paddle.y - 20*scale;
    }

    // Ball trail
    if (ball.vx || ball.vy) {
      trails.push({x:ball.x,y:ball.y,r:ball.r,life:.18,maxLife:.18});
    }

    ball.x += ball.vx*dt;
    ball.y += ball.vy*dt;

    if (ball.x-ball.r < 0) { ball.x=ball.r; ball.vx=Math.abs(ball.vx); sparkle(0,ball.y,'#ff70ae'); }
    if (ball.x+ball.r > W) { ball.x=W-ball.r; ball.vx=-Math.abs(ball.vx); sparkle(W,ball.y,'#ff70ae'); }
    if (ball.y-ball.r < 0) { ball.y=ball.r; ball.vy=Math.abs(ball.vy); sparkle(ball.x,0,'#ff70ae'); }

    // Paddle collision
    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) {
      ball.y = paddle.y-paddle.h/2-ball.r;
      const hit = (ball.x-paddle.x)/(paddle.w/2);
      const speed = Math.min(430*scale, Math.hypot(ball.vx,ball.vy)*1.015);
      const angle = -Math.PI/2 + hit*1.05;
      ball.vx = Math.cos(angle)*speed;
      ball.vy = Math.sin(angle)*speed;
      for(let i=0;i<7;i++) sparkle(ball.x, paddle.y, '#ffd45b');
      heartParticle(ball.x, paddle.y);
    }

    // Brick collisions
    for (const b of bricks) {
      if (!b.alive) continue;
      const nearestX = Math.max(b.x, Math.min(ball.x, b.x+b.w));
      const nearestY = Math.max(b.y, Math.min(ball.y, b.y+b.h));
      const dx = ball.x-nearestX, dy = ball.y-nearestY;
      if (dx*dx+dy*dy < ball.r*ball.r) {
        const overlapX = (b.w/2+ball.r)-Math.abs(ball.x-(b.x+b.w/2));
        const overlapY = (b.h/2+ball.r)-Math.abs(ball.y-(b.y+b.h/2));
        if (overlapX < overlapY) {
          ball.vx *= -1;
          ball.x += ball.x < b.x+b.w/2 ? -overlapX : overlapX;
        } else {
          ball.vy *= -1;
          ball.y += ball.y < b.y+b.h/2 ? -overlapY : overlapY;
        }
        b.alive = false;
        explodeBrick(b);
        break;
      }
    }

    // Particles
    for (let i=particles.length-1;i>=0;i--) {
      const p=particles[i];
      p.life -= dt;
      if (p.life<=0) { particles.splice(i,1); continue; }
      p.x += p.vx*dt;
      p.y += p.vy*dt;
      if (p.type==='heart') {
        p.vy += 290*scale*dt;
        p.vx *= (1-dt*.8);
        p.rot += p.vr*dt;
      } else {
        p.vx *= (1-dt*3);
        p.vy *= (1-dt*3);
      }
    }
    for (let i=trails.length-1;i>=0;i--) {
      trails[i].life -= dt;
      if (trails[i].life<=0) trails.splice(i,1);
    }
    for (let i=floaters.length-1;i>=0;i--) {
      floaters[i].life -= dt;
      floaters[i].y -= 35*scale*dt;
      if (floaters[i].life<=0) floaters.splice(i,1);
    }
    shake = Math.max(0, shake-dt*24);
    flash = Math.max(0, flash-dt);
    levelFlash = Math.max(0, levelFlash-dt*.8);

    if (bricks.every(b=>!b.alive)) {
      nextLevel();
    }

    if (ball.y-ball.r > H+20) {
      running = false;
      overlay.classList.remove('hidden');
      overlay.innerHTML = `<h1>BURGER <span>DROPPED!</span></h1>
        <div class="subtitle">You made ${heartCount} hearts explode — incredible.</div>
        <div class="prompt">CLICK OR PRESS A KEY TO TRY AGAIN</div>`;
      updateHud();
    }
    updateHud();
  }

  function render() {
    ctx.clearRect(0,0,W,H);
    ctx.save();

    if (shake>0) {
      ctx.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake);
    }

    // Background confetti
    ctx.globalAlpha=.14;
    for(let i=0;i<24;i++){
      const x=(i*83+time*12*scale)%W;
      const y=(i*47+time*24*scale)%H;
      ctx.fillStyle=i%2?'#ff6ca7':'#ffd2e5';
      ctx.save(); ctx.translate(x,y); ctx.rotate(time+i); ctx.fillRect(-2,-6,4,12); ctx.restore();
    }
    ctx.globalAlpha=1;

    // Trail
    for(const t of trails){
      ctx.globalAlpha=(t.life/t.maxLife)*.3;
      ctx.fillStyle='#ff6fa8';
      ctx.beginPath(); ctx.arc(t.x,t.y,t.r*.85,0,Math.PI*2); ctx.fill();
    }
    ctx.globalAlpha=1;

    for(const b of bricks) if(b.alive) drawBrick(b,time);

    // Floating text
    for(const f of floaters){
      ctx.globalAlpha=Math.min(1,f.life/.3);
      ctx.fillStyle=f.color;
      ctx.font=`bold ${15*scale}px Trebuchet MS`;
      ctx.textAlign='center';
      ctx.fillText(f.text,f.x,f.y);
    }
    ctx.globalAlpha=1;

    drawBurger();

    // Ball
    if(ball.vx||ball.vy){
      const g=ctx.createRadialGradient(ball.x-3*scale,ball.y-3*scale,1,ball.x,ball.y,ball.r*1.8);
      g.addColorStop(0,'#fff');
      g.addColorStop(.45,'#ff7eb4');
      g.addColorStop(1,'rgba(255,56,140,.1)');
      ctx.fillStyle=g;
      ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r*1.5,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='#fff4f9';
      ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r,0,Math.PI*2); ctx.fill();
    }

    // Hearts
    for(const p of particles){
      const alpha=Math.min(1,p.life/.25);
      if(p.type==='heart') drawHeart(p.x,p.y,p.size,p.color,p.rot,alpha);
      else {
        ctx.globalAlpha=alpha;
        ctx.fillStyle=p.color;
        ctx.beginPath(); ctx.arc(p.x,p.y,p.size,0,Math.PI*2); ctx.fill();
      }
    }
    ctx.globalAlpha=1;

    if(flash>0){
      ctx.fillStyle=`rgba(255,255,255,${flash*.8})`;
      ctx.fillRect(0,0,W,H);
    }
    if(levelFlash>0){
      ctx.fillStyle=`rgba(255,70,150,${levelFlash*.2})`;
      ctx.fillRect(0,0,W,H);
      ctx.fillStyle='white';
      ctx.font=`bold ${28*scale}px Trebuchet MS`;
      ctx.textAlign='center';
      ctx.fillText(`LEVEL ${level}!`, W/2, H*.45);
    }

    ctx.restore();

    if(!started || !running){
      ctx.fillStyle='rgba(10,2,18,.2)';
      ctx.fillRect(0,0,W,H);
    }
  }

  let time=0;
  let last=performance.now();
  function loop(now){
    const dt=Math.min(.033,(now-last)/1000);
    last=now;
    if(started && running) {
      if(ball.vx===0 && ball.vy===0){
        aiTimer-=dt;
        if(aiTimer<=0){ aiTimer=.65; launchBall(); }
      }
    }
    update(dt);
    render();
    requestAnimationFrame(loop);
  }

  window.addEventListener('resize', resize);
  window.addEventListener('keydown', e => {
    if(['ArrowLeft','ArrowRight',' ','a','d','A','D'].includes(e.key)) e.preventDefault();
    keys[e.key]=true;
    startOrLaunch();
  });
  window.addEventListener('keyup', e => { keys[e.key]=false; });
  canvas.addEventListener('pointermove', e => {
    if(!started) return;
    const r=canvas.getBoundingClientRect();
    paddle.x=Math.max(paddle.w/2+4,Math.min(W-paddle.w/2-4,(e.clientX-r.left)));
  });
  canvas.addEventListener('pointerdown', e => {
    e.preventDefault();
    startOrLaunch();
    const r=canvas.getBoundingClientRect();
    paddle.x=Math.max(paddle.w/2+4,Math.min(W-paddle.w/2-4,(e.clientX-r.left)));
  });
  overlay.addEventListener('pointerdown', () => {
    if(!started) resetGame();
    else if(!running) {
      overlay.innerHTML='<h1>HEART BURGER <span>BREAKOUT</span></h1><div class="subtitle">Catch the burger. Smash the bricks. Make love explode.</div><div class="prompt">CLICK OR PRESS A KEY TO START</div>';
      resetGame();
    }
  });

  resize();
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>