Models 44

GPT-6 Luna

by OpenAIReasoningClean
Open in new window

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

How it went

Total time
53.8 s
Cost
$0.0032
Output tokens
6,319
Spent thinking
378 tokens
Speed
155.4 tok/s
Console errors
0
First token after
13.1 s
First visible text after
13.1 s
Prompt tokens
508
Code size
17.6 KB
Finish reason
stop
Served by
OpenAI
Model id
openai/gpt-6-luna
Reasoning sent
{"effort":"low"}
Max output
64,000 tokens
Generated
Nov 10, 2026

What it was asked

The prompt
Build a polished Breakout (brick breaker) game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A paddle controlled with the mouse and with the left/right arrow keys. - The bounce angle off the paddle depends on where the ball hits it (center = straight up, edges = sharp angle). - One level with at least 6 rows of bricks, mixing normal bricks and tougher bricks that need 2 hits and visibly crack after the first one. - Some bricks visibly carry a power-up icon; when broken they drop it, and catching it with the paddle gives either a wider paddle (10 seconds) or multi-ball (3 balls). - The ball must never pass through bricks or the paddle. - Particles when a brick breaks. - 3 lives, score displayed. Game over and victory screens with a restart option.
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 · 405 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Breakout — Neon Arcade</title>
<style>
  :root {
    color-scheme: dark;
    --bg: #090c17;
    --panel: #111629;
    --muted: #929ab5;
    --cyan: #60f5ef;
    --pink: #ff5f9e;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    min-height: 100vh;
    background:
      radial-gradient(ellipse at 50% -20%, rgba(66, 74, 151, .27), transparent 55%),
      var(--bg);
    color: #f4f6ff;
    font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    display: flex;
    justify-content: center;
  }
  .app { width: min(1120px, calc(100% - 40px)); padding: 23px 0 24px; }
  header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    margin-bottom: 18px;
  }
  .brand { display: flex; align-items: center; gap: 12px; }
  .brand-mark {
    width: 42px; height: 42px; border-radius: 13px; display: grid; place-items: center;
    color: var(--cyan); background: linear-gradient(145deg, #1a2940, #101628);
    border: 1px solid rgba(96,245,239,.23); box-shadow: 0 0 24px #60f5ef12;
  }
  .brand-mark svg { width: 23px; height: 23px; }
  h1 { margin: 0; font-size: 18px; letter-spacing: .14em; font-weight: 800; }
  .subtitle { color: var(--muted); font-size: 11px; margin-top: 3px; letter-spacing: .08em; }
  .live { display:flex; align-items:center; gap:8px; color:#b1bbd2; font-size:11px; letter-spacing:.11em; }
  .live-dot { width:7px; height:7px; border-radius:50%; background:#6af5b7; box-shadow:0 0 10px #6af5b7; }
  .hud { display:grid; grid-template-columns:repeat(3, 1fr); gap:10px; margin-bottom:12px; }
  .stat {
    height:64px; padding:11px 16px; border-radius:12px; background:linear-gradient(120deg,#131a2d,#101525);
    border:1px solid #ffffff0d; display:flex; align-items:center; justify-content:space-between;
  }
  .stat-label { font-size:10px; text-transform:uppercase; color:#8993ad; letter-spacing:.13em; }
  .stat-value { font-size:21px; font-weight:750; letter-spacing:.03em; font-variant-numeric:tabular-nums; }
  #score { color:var(--cyan); }
  #lives { color:#ff779c; letter-spacing:.16em; font-size:17px; }
  .arena {
    position:relative; overflow:hidden; border-radius:16px; border:1px solid #39445f;
    background:#0b1020; box-shadow:0 22px 70px #0007, 0 0 40px #6978ff0a;
  }
  canvas { display:block; width:100%; height:auto; aspect-ratio: 960 / 540; }
  .hint {
    position:absolute; top:13px; left:50%; transform:translateX(-50%); z-index:2;
    white-space:nowrap; font-size:10px; letter-spacing:.09em; color:#c8d3e4;
    padding:8px 12px; border-radius:99px; background:#0b1120c9; border:1px solid #ffffff19;
    backdrop-filter:blur(8px); transition:opacity .5s;
    pointer-events:none;
  }
  .hint strong { color:var(--cyan); font-weight:700; }
  .footer { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:12px 4px 0; }
  .controls { display:flex; flex-wrap:wrap; gap:16px; color:#8590aa; font-size:11px; }
  .key { display:inline-grid; place-items:center; min-width:23px; height:21px; padding:0 5px; margin-right:5px;
    border:1px solid #ffffff25; border-bottom-width:2px; border-radius:5px; color:#d5dbee; font-size:10px; }
  .power-legend { display:flex; gap:15px; font-size:11px; color:#a4aec3; }
  .power-legend span { display:flex; align-items:center; gap:6px; }
  .badge { display:grid; place-items:center; width:18px; height:18px; border-radius:5px; font-size:11px; font-weight:800; }
  .wide { color:#6de7ff; background:#6de7ff19; border:1px solid #6de7ff55; }
  .multi { color:#ffd36a; background:#ffd36a19; border:1px solid #ffd36a55; }
  @media (max-width:600px) {
    .app { width:calc(100% - 22px); padding-top:15px; }
    header { margin-bottom:12px; }
    .hud { gap:6px; margin-bottom:8px; }
    .stat { height:56px; padding:8px 9px; }
    .stat-value { font-size:17px; }
    .stat-label { font-size:9px; }
    .hint { font-size:8px; padding:7px 9px; top:8px; }
    .footer { align-items:flex-start; flex-direction:column; }
    .controls { gap:10px; }
    .arena { border-radius:11px; }
  }
</style>
</head>
<body>
<div class="app">
  <header>
    <div class="brand">
      <div class="brand-mark" aria-hidden="true">
        <svg viewBox="0 0 24 24" fill="none"><path d="M4 16.5h16M7 12h10M10 7.5h4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><circle cx="12" cy="4" r="1.5" fill="currentColor"/></svg>
      </div>
      <div><h1>BREAKOUT</h1><div class="subtitle">NEON ARCADE · SECTOR 01</div></div>
    </div>
    <div class="live"><span class="live-dot"></span> ARCADE READY</div>
  </header>

  <section class="hud" aria-label="Game stats">
    <div class="stat"><div class="stat-label">Score</div><div class="stat-value" id="score">00000</div></div>
    <div class="stat"><div class="stat-label">Bricks left</div><div class="stat-value" id="remaining">42</div></div>
    <div class="stat"><div class="stat-label">Lives</div><div class="stat-value" id="lives">♥ ♥ ♥</div></div>
  </section>

  <div class="arena" id="arena">
    <div class="hint" id="hint"><strong>DEMO IN PLAY</strong> · Click or press a key to start playing</div>
    <canvas id="game" width="960" height="540" aria-label="Breakout game canvas"></canvas>
  </div>

  <div class="footer">
    <div class="controls">
      <span><b class="key">←</b><b class="key">→</b> Move paddle</span>
      <span><b class="key">MOUSE</b> Aim &amp; move</span>
      <span><b class="key">R</b> Restart</span>
    </div>
    <div class="power-legend">
      <span><b class="badge wide">↔</b> Wide paddle</span>
      <span><b class="badge multi">✦</b> Multi-ball</span>
    </div>
  </div>
</div>
<script>
(() => {
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const scoreEl = document.getElementById('score');
  const remainingEl = document.getElementById('remaining');
  const livesEl = document.getElementById('lives');
  const hint = document.getElementById('hint');
  const W = 960, H = 540;
  const colors = ['#ff648b','#ff9e58','#ffd66b','#67e5b2','#56caff','#a68bff'];
  const bricks = [];
  const balls = [];
  const drops = [];
  const particles = [];
  let score = 0, lives = 3, remaining = 42;
  let paddle = { x: W / 2, y: 496, w: 110, h: 12, target: W / 2, wideUntil: 0 };
  let mode = 'playing', manual = false, keys = { left:false, right:false };
  let last = performance.now(), elapsed = 0, flash = 0, shake = 0;
  let soundless = true;

  function reset() {
    score = 0; lives = 3; remaining = 42; mode = 'playing'; manual = false;
    paddle = { x: W / 2, y: 496, w: 110, h: 12, target: W / 2, wideUntil: 0 };
    bricks.length = 0; balls.length = 0; drops.length = 0; particles.length = 0;
    const cols = 7, bw = 112, bh = 21, gapX = 9, gapY = 8;
    const startX = (W - (cols * bw + (cols - 1) * gapX)) / 2;
    for (let r = 0; r < 6; r++) {
      for (let c = 0; c < cols; c++) {
        const tough = r < 2 && ((r + c) % 2 === 0);
        const power = (r === 2 && (c === 1 || c === 5)) || (r === 4 && c === 3);
        bricks.push({ x:startX+c*(bw+gapX), y:67+r*(bh+gapY), w:bw, h:bh,
          hp:tough?2:1, max:tough?2:1, color:colors[r], power, alive:true, idx:r*cols+c });
      }
    }
    spawnBall(W/2, 444, 3.3, -4.4);
    updateHud();
    hint.innerHTML = '<strong>DEMO IN PLAY</strong> · Click or press a key to start playing';
    hint.style.opacity = '1';
  }
  function spawnBall(x,y,vx,vy) {
    balls.push({x,y,vx,vy,r:7, trail:[]});
  }
  function updateHud() {
    scoreEl.textContent = String(score).padStart(5,'0');
    remainingEl.textContent = String(remaining).padStart(2,'0');
    livesEl.textContent = '♥ '.repeat(lives).trim() || '—';
  }
  function activate() {
    if (!manual) {
      manual = true;
      hint.innerHTML = '<strong>PLAYER CONTROL</strong> · Move with ← → or your mouse';
      setTimeout(() => { if (manual) hint.style.opacity = '.25'; }, 2400);
    }
  }
  function restart() { reset(); }
  function pointerX(e) {
    const rect = canvas.getBoundingClientRect();
    return (e.clientX - rect.left) * W / rect.width;
  }
  canvas.addEventListener('pointermove', e => {
    activate();
    paddle.target = Math.max(paddle.w/2, Math.min(W-paddle.w/2, pointerX(e)));
  });
  canvas.addEventListener('pointerdown', activate);
  window.addEventListener('keydown', e => {
    if (['ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault();
    activate();
    if (e.key === 'ArrowLeft') keys.left = true;
    if (e.key === 'ArrowRight') keys.right = true;
    if (e.key.toLowerCase() === 'r') restart();
  });
  window.addEventListener('keyup', e => {
    if (e.key === 'ArrowLeft') keys.left = false;
    if (e.key === 'ArrowRight') keys.right = false;
  });

  function burst(x,y,color,count=12) {
    for (let i=0;i<count;i++) {
      const a = Math.random()*Math.PI*2, speed = 45+Math.random()*180;
      particles.push({x,y,vx:Math.cos(a)*speed,vy:Math.sin(a)*speed,life:.35+Math.random()*.42,max:.77,size:2+Math.random()*3,color});
    }
  }
  function hitBrick(b) {
    if (!b || !b.alive) return;
    b.hp--;
    if (b.hp <= 0) {
      b.alive = false; remaining--; score += 100; burst(b.x+b.w/2,b.y+b.h/2,b.color,15);
      if (b.power) drops.push({x:b.x+b.w/2,y:b.y+b.h/2,kind:Math.random()<.5?'wide':'multi',vy:100});
      if (remaining === 0) mode = 'victory';
    } else {
      score += 35; burst(b.x+b.w/2,b.y+b.h/2,'#fff1b0',5);
    }
    updateHud();
  }
  function catchPower(d) {
    if (d.kind === 'wide') {
      paddle.wideUntil = elapsed + 10;
      paddle.w = 168;
      burst(d.x,d.y,'#6de7ff',16);
    } else {
      const source = balls[0];
      if (source) {
        const count = Math.min(3, balls.length);
        for (let i=1;i<count;i++) {
          const angle = (i===1?-.42:.42);
          const speed = Math.hypot(source.vx,source.vy);
          const base = Math.atan2(source.vy,source.vx);
          spawnBall(source.x,source.y,Math.cos(base+angle)*speed,Math.sin(base+angle)*speed);
        }
      }
      burst(d.x,d.y,'#ffd36a',16);
    }
  }

  function update(dt) {
    elapsed += dt;
    if (paddle.wideUntil && elapsed > paddle.wideUntil) { paddle.w = 110; paddle.wideUntil = 0; }
    if (manual) {
      if (keys.left) paddle.target -= 540*dt;
      if (keys.right) paddle.target += 540*dt;
    } else {
      const alive = balls[0];
      if (alive) paddle.target = alive.x + Math.sin(elapsed*1.1)*8;
    }
    paddle.target = Math.max(paddle.w/2+8, Math.min(W-paddle.w/2-8,paddle.target));
    paddle.x += (paddle.target-paddle.x)*Math.min(1,dt*13);

    for (let bi=balls.length-1;bi>=0;bi--) {
      const ball = balls[bi];
      ball.trail.push({x:ball.x,y:ball.y}); if (ball.trail.length>7) ball.trail.shift();
      const speed = Math.hypot(ball.vx,ball.vy);
      const steps = Math.max(1,Math.ceil(speed*dt/3));
      const step = dt/steps;
      let lost = false;
      for (let s=0;s<steps;s++) {
        const ox=ball.x, oy=ball.y;
        ball.x += ball.vx*step; ball.y += ball.vy*step;
        if (ball.x < ball.r) { ball.x=ball.r; ball.vx=Math.abs(ball.vx); }
        if (ball.x > W-ball.r) { ball.x=W-ball.r; ball.vx=-Math.abs(ball.vx); }
        if (ball.y < 38+ball.r) { ball.y=38+ball.r; ball.vy=Math.abs(ball.vy); }
        if (ball.vy > 0 && oy+ball.r <= paddle.y && ball.y+ball.r >= paddle.y &&
            ball.x >= paddle.x-paddle.w/2-ball.r && ball.x <= paddle.x+paddle.w/2+ball.r) {
          ball.y = paddle.y-ball.r;
          const offset = Math.max(-1,Math.min(1,(ball.x-paddle.x)/(paddle.w/2)));
          const angle = offset*1.12;
          const mag = Math.max(5.4,Math.hypot(ball.vx,ball.vy));
          ball.vx = Math.sin(angle)*mag;
          ball.vy = -Math.cos(angle)*mag;
        }
        let struck = false;
        for (const b of bricks) {
          if (!b.alive || ball.x+ball.r < b.x || ball.x-ball.r > b.x+b.w ||
              ball.y+ball.r < b.y || ball.y-ball.r > b.y+b.h) continue;
          const fromX = Math.min(Math.abs(ox-(b.x- ball.r)),Math.abs(ox-(b.x+b.w+ball.r)));
          const fromY = Math.min(Math.abs(oy-(b.y- ball.r)),Math.abs(oy-(b.y+b.h+ball.r)));
          if (fromX < fromY) ball.vx = ox < b.x+b.w/2 ? -Math.abs(ball.vx) : Math.abs(ball.vx);
          else ball.vy = oy < b.y+b.h/2 ? -Math.abs(ball.vy) : Math.abs(ball.vy);
          hitBrick(b); struck=true; break;
        }
        if (struck) {
          ball.x = ox + ball.vx*step;
          ball.y = oy + ball.vy*step;
        }
        if (ball.y > H+ball.r) { lost=true; break; }
      }
      if (lost) balls.splice(bi,1);
    }
    if (!balls.length && mode === 'playing') {
      lives--; updateHud();
      if (lives <= 0) mode = 'gameover';
      else spawnBall(W/2,444,(Math.random()-.5)*3,-5.3);
    }
    for (let i=drops.length-1;i>=0;i--) {
      const d=drops[i]; d.y += d.vy*dt;
      if (d.y+10 >= paddle.y && d.y-10 <= paddle.y+paddle.h &&
          d.x >= paddle.x-paddle.w/2 && d.x <= paddle.x+paddle.w/2) {
        catchPower(d); drops.splice(i,1);
      } else if (d.y > H+15) drops.splice(i,1);
    }
    for (let i=particles.length-1;i>=0;i--) {
      const p=particles[i]; p.life-=dt; p.x+=p.vx*dt; p.y+=p.vy*dt; p.vx*=.985; p.vy=p.vy*.985+30*dt;
      if(p.life<=0) particles.splice(i,1);
    }
  }

  function roundRect(x,y,w,h,r) {
    ctx.beginPath(); ctx.roundRect(x,y,w,h,r);
  }
  function draw() {
    ctx.clearRect(0,0,W,H);
    const bg=ctx.createLinearGradient(0,0,0,H); bg.addColorStop(0,'#10172a'); bg.addColorStop(1,'#090e1c');
    ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
    ctx.save();
    ctx.strokeStyle='rgba(127,157,213,.055)'; ctx.lineWidth=1;
    for(let x=0;x<W;x+=40){ctx.beginPath();ctx.moveTo(x,38);ctx.lineTo(x,H);ctx.stroke();}
    for(let y=38;y<H;y+=40){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
    ctx.restore();
    ctx.fillStyle='#7885a4'; ctx.font='700 10px system-ui'; ctx.textAlign='left'; ctx.letterSpacing='2px';
    ctx.fillText('FIELD 01',24,25);
    ctx.textAlign='right'; ctx.fillStyle='#55617e'; ctx.fillText('CLEAR THE GRID',W-24,25);
    ctx.strokeStyle='#ffffff12'; ctx.beginPath(); ctx.moveTo(0,38); ctx.lineTo(W,38); ctx.stroke();

    for(const b of bricks) {
      if(!b.alive) continue;
      ctx.save();
      ctx.shadowColor=b.color+'55'; ctx.shadowBlur=9;
      roundRect(b.x,b.y,b.w,b.h,5); ctx.fillStyle=b.color; ctx.fill();
      ctx.shadowBlur=0;
      const shine=ctx.createLinearGradient(0,b.y,0,b.y+b.h);
      shine.addColorStop(0,'#ffffff38'); shine.addColorStop(.3,'#ffffff08'); shine.addColorStop(1,'#00000028');
      roundRect(b.x,b.y,b.w,b.h,5); ctx.fillStyle=shine; ctx.fill();
      if(b.hp===2) {
        ctx.strokeStyle='#392339'; ctx.lineWidth=1.5;
        ctx.beginPath(); ctx.moveTo(b.x+b.w*.44,b.y+2); ctx.lineTo(b.x+b.w*.51,b.y+8); ctx.lineTo(b.x+b.w*.46,b.y+12); ctx.lineTo(b.x+b.w*.58,b.y+b.h-2); ctx.stroke();
        ctx.beginPath(); ctx.moveTo(b.x+b.w*.51,b.y+8); ctx.lineTo(b.x+b.w*.62,b.y+11); ctx.stroke();
      }
      if(b.power) {
        ctx.beginPath(); ctx.arc(b.x+b.w-13,b.y+10.5,7,0,Math.PI*2);
        ctx.fillStyle='#10172a'; ctx.fill();
        ctx.fillStyle='#fff'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.font='bold 10px system-ui';
        ctx.fillText('✦',b.x+b.w-13,b.y+10);
      }
      ctx.restore();
    }

    for(const d of drops) {
      const col=d.kind==='wide'?'#6de7ff':'#ffd36a';
      ctx.save(); ctx.shadowColor=col; ctx.shadowBlur=16;
      ctx.beginPath(); ctx.arc(d.x,d.y,11,0,Math.PI*2); ctx.fillStyle='#11192b'; ctx.fill();
      ctx.strokeStyle=col; ctx.lineWidth=2; ctx.stroke();
      ctx.fillStyle=col; ctx.font='bold 14px system-ui'; ctx.textAlign='center'; ctx.textBaseline='middle';
      ctx.fillText(d.kind==='wide'?'↔':'✦',d.x,d.y+1); ctx.restore();
    }
    for(const p of particles) {
      ctx.globalAlpha=Math.max(0,p.life/p.max); ctx.fillStyle=p.color;
      ctx.fillRect(p.x,p.y,p.size,p.size);
    }
    ctx.globalAlpha=1;

    for(const ball of balls) {
      for(let i=0;i<ball.trail.length;i++) {
        const t=ball.trail[i], a=(i+1)/ball.trail.length*.16;
        ctx.globalAlpha=a; ctx.beginPath(); ctx.arc(t.x,t.y,ball.r*(.45+i/ball.trail.length*.45),0,Math.PI*2);
        ctx.fillStyle='#88fff5'; ctx.fill();
      }
      ctx.globalAlpha=1; ctx.save(); ctx.shadowColor='#72fff2'; ctx.shadowBlur=20;
      ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r,0,Math.PI*2);
      ctx.fillStyle='#eaffff'; ctx.fill(); ctx.restore();
    }
    const px=paddle.x-paddle.w/2;
    ctx.save(); ctx.shadowColor=paddle.wide?'#6de7ff':'#58e9ef'; ctx.shadowBlur=20;
    const pg=ctx.createLinearGradient(px,paddle.y,px,paddle.y+paddle.h);
    pg.addColorStop(0,'#e6ffff'); pg.addColorStop(.35,paddle.wide?'#6de7ff':'#65faf1'); pg.addColorStop(1,'#25a9cc');
    roundRect(px,paddle.y,paddle.w,paddle.h,7); ctx.fillStyle=pg; ctx.fill(); ctx.restore();
    ctx.fillStyle='#76f5ed18'; ctx.fillRect(0,H-2,W,2);

    if(mode==='gameover' || mode==='victory') {
      ctx.fillStyle='#070a13bf'; ctx.fillRect(0,0,W,H);
      ctx.textAlign='center'; ctx.textBaseline='middle';
      ctx.fillStyle=mode==='victory'?'#6af5c2':'#ff779c'; ctx.font='800 39px system-ui';
      ctx.fillText(mode==='victory'?'SECTOR CLEARED':'GAME OVER',W/2,H/2-25);
      ctx.fillStyle='#c0c9dc'; ctx.font='15px system-ui';
      ctx.fillText(`FINAL SCORE  ${String(score).padStart(5,'0')}`,W/2,H/2+14);
      ctx.fillStyle='#8b97b1'; ctx.font='12px system-ui'; ctx.fillText('PRESS R OR CLICK TO PLAY AGAIN',W/2,H/2+46);
    }
  }
  canvas.addEventListener('pointerdown', () => { if(mode!=='playing') restart(); });
  function frame(now) {
    const dt=Math.min(.03,(now-last)/1000); last=now;
    if(mode==='playing') update(dt);
    draw();
    requestAnimationFrame(frame);
  }
  reset();
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>