MiniMax M3.1 Flash
by MiniMaxReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 38.1 s
- Cost
- $0.0048
- Output tokens
- 7,922
- Spent thinking
- 0 tokens
- Speed
- 210.4 tok/s
- Console errors
- 0
- First token after
- 436 ms
- First visible text after
- 436 ms
- Prompt tokens
- 692
- Code size
- 22.1 KB
- Finish reason
- stop
- Served by
- Parasail
- Model id
minimax/minimax-m3.1-flash- Reasoning sent
{"effort":"low"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 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 · 623 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Breakout</title>
<style>
:root{
--bg:#0b0f1a;
--panel:#141b2d;
--accent:#4dd0e1;
--accent2:#ff7ae0;
--text:#e8f1ff;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
background:radial-gradient(1200px 700px at 50% -10%, #1b2540 0%, var(--bg) 60%);
color:var(--text);
font-family:'Segoe UI',system-ui,-apple-system,Roboto,sans-serif;
display:flex;align-items:center;justify-content:center;
min-height:100vh;padding:16px;
overflow:hidden;
}
#wrap{
width:min(960px,100%);
background:linear-gradient(180deg,#101728,#0a0e18);
border:1px solid rgba(77,208,225,.25);
border-radius:16px;
box-shadow:0 20px 60px rgba(0,0,0,.6), inset 0 0 60px rgba(77,208,225,.05);
padding:14px 14px 12px;
}
header{
display:flex;align-items:center;justify-content:space-between;
gap:10px;margin-bottom:10px;flex-wrap:wrap;
}
h1{
font-size:clamp(16px,2.6vw,22px);letter-spacing:3px;text-transform:uppercase;
background:linear-gradient(90deg,var(--accent),var(--accent2));
-webkit-background-clip:text;background-clip:text;color:transparent;
font-weight:700;
}
.stats{display:flex;gap:8px;flex-wrap:wrap}
.stat{
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.09);
border-radius:9px;padding:5px 11px;font-size:13px;min-width:74px;text-align:center;
}
.stat b{display:block;font-size:17px;color:var(--accent);font-variant-numeric:tabular-nums}
.stat span{opacity:.6;font-size:10px;letter-spacing:1.4px;text-transform:uppercase}
#stage{position:relative;width:100%;}
canvas{
display:block;width:100%;height:auto;border-radius:10px;
background:#070a12;
touch-action:none;
}
#overlay{
position:absolute;inset:0;border-radius:10px;
display:flex;flex-direction:column;align-items:center;justify-content:center;
background:rgba(6,9,16,.82);backdrop-filter:blur(3px);
text-align:center;padding:18px;gap:12px;
}
#overlay.hidden{display:none}
#overlay h2{font-size:clamp(22px,5vw,40px);letter-spacing:2px;text-transform:uppercase;
background:linear-gradient(90deg,var(--accent),var(--accent2));
-webkit-background-clip:text;background-clip:text;color:transparent}
#overlay p{opacity:.8;font-size:14px;max-width:460px;line-height:1.5}
#overlay .hint{opacity:.55;font-size:12px;letter-spacing:1px}
button{
pointer-events:auto;cursor:pointer;border:0;border-radius:10px;
padding:11px 26px;font-size:15px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;
color:#04121a;background:linear-gradient(90deg,var(--accent),var(--accent2));
box-shadow:0 6px 22px rgba(77,208,225,.28);
transition:transform .12s ease, filter .12s ease;
}
button:hover{transform:translateY(-2px);filter:brightness(1.1)}
footer{margin-top:9px;font-size:11.5px;opacity:.5;text-align:center;letter-spacing:.6px}
kbd{background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.18);border-radius:4px;padding:1px 6px;font-size:11px}
</style>
</head>
<body>
<div id="wrap">
<header>
<h1>Neon Breakout</h1>
<div class="stats">
<div class="stat"><span>Score</span><b id="score">0</b></div>
<div class="stat"><span>Lives</span><b id="lives">3</b></div>
<div class="stat"><span>Bricks</span><b id="left">0</b></div>
<div class="stat"><span>Level</span><b id="level">1</b></div>
</div>
</header>
<div id="stage">
<canvas id="cv" width="900" height="600"></canvas>
<div id="overlay">
<h2 id="ovTitle">Neon Breakout</h2>
<p id="ovText">Move the mouse or use <kbd>←</kbd> <kbd>→</kbd> to steer the paddle. Bounce off the paddle's edges for sharp angles. Catch falling power-ups.</p>
<button id="ovBtn">Start Game</button>
<div class="hint" id="ovHint">Click or press any key to start</div>
</div>
</div>
<footer>Mouse / <kbd>←</kbd><kbd>→</kbd> to move • <kbd>Space</kbd> to launch • <kbd>P</kbd> pause • Click or press a key to start</footer>
</div>
<script>
(()=>{
const cv=document.getElementById('cv'), ctx=cv.getContext('2d');
const W=cv.width, H=cv.height;
const overlay=document.getElementById('overlay');
const ovTitle=document.getElementById('ovTitle'), ovText=document.getElementById('ovText'), ovBtn=document.getElementById('ovBtn'), ovHint=document.getElementById('ovHint');
const elScore=document.getElementById('score'), elLives=document.getElementById('lives'), elLeft=document.getElementById('left');
// ---------- Config ----------
const COLS=10, ROWS=6;
const BRICK_W=78, BRICK_H=26, GAP=6, TOP=70, LEFT_PAD=25;
const PLAY_LEFT=LEFT_PAD, PLAY_RIGHT=LEFT_PAD+COLS*(BRICK_W+GAP);
const PADDLE_BASE_W=110, PADDLE_H=13;
const BRICK_COLORS=['#4dd0e1','#5ee0a8','#ffd166','#ff8f6b','#ff7ae0','#8f9bff'];
// ---------- State ----------
let state='menu'; // menu, serve, play, over, win
let score=0, lives=3, level=1;
let bricks=[], balls=[], particles=[], powerups=[];
let paddle, keys={left:false,right:false}, mouseX=null, mouseActive=false;
let wideTimer=0;
let shake=0, flash=0;
let serveTimer=0;
function makePaddle(){
return {x:W/2-PADDLE_BASE_W/2, y:H-40, w:PADDLE_BASE_W, h:PADDLE_H, vx:0, glow:0};
}
paddle=makePaddle();
function levelLayout(){
bricks=[];
const rows=ROWS;
for(let r=0;r<rows;r++){
for(let c=0;c<COLS;c++){
// leave a couple of gaps for style
if(r===2 && (c===4||c===5)) continue;
if(r===4 && c===0) continue;
const hp = (r>=4 || (r===2&&c>=7) || (r===5&&c>=2&&c<=6)) ? 2 : 1;
const hasPU = (r===1&&c===2) || (r===3&&c===7) || (r===5&&c===5);
bricks.push({
x:PLAY_LEFT+c*(BRICK_W+GAP), y:TOP+r*(BRICK_H+GAP),
w:BRICK_W, h:BRICK_H, hp, maxhp:hp,
color:BRICK_COLORS[r%BRICK_COLORS.length],
hasPU, alive:true, flash:0, seed:Math.random()*100
});
}
}
}
function resetLevel(){
levelLayout();
balls=[];particles=[];powerups=[];
paddle=makePaddle(); wideTimer=0;
state='serve'; serveTimer=0;
spawnBall();
updateStats();
}
function spawnBall(extra){
const b={x:paddle.x+paddle.w/2, y:paddle.y-9, vx:(Math.random()-.5)*140, vy:-330, r:7.5, trail:[]};
balls.push(b);
return b;
}
function updateStats(){
elScore.textContent=score;
elLives.textContent=lives;
elLeft.textContent=bricks.filter(b=>b.alive).length;
elLeft.parentElement.querySelector('span').textContent='Bricks';
}
// ---------- Overlay ----------
function showOverlay(title,text,btn,hint){
ovTitle.textContent=title; ovText.innerHTML=text; ovBtn.textContent=btn;
ovHint.textContent=hint||'Click or press any key to start';
overlay.classList.remove('hidden');
}
function hideOverlay(){ overlay.classList.add('hidden'); }
function startGame(){
score=0;lives=3;level=1;resetLevel();
hideOverlay();
}
function gameOver(){
state='over';
showOverlay('Game Over',`Final score: <b>${score}</b><br>You cleared ${level} level(s).`,'Play Again');
}
function victory(){
state='win';
showOverlay('Victory!',`All bricks smashed! Score: <b>${score}</b>`,'Play Again');
}
// ---------- Input ----------
function pointerMove(e){
const r=cv.getBoundingClientRect();
mouseX=(e.clientX-r.left)*(W/r.width);
mouseActive=true;
}
window.addEventListener('mousemove',pointerMove);
cv.addEventListener('touchmove',e=>{e.preventDefault();const r=cv.getBoundingClientRect();mouseX=(e.touches[0].clientX-r.left)*(W/r.width);mouseActive=true;},{passive:false});
cv.addEventListener('touchstart',e=>{e.preventDefault();const r=cv.getBoundingClientRect();mouseX=(e.touches[0].clientX-r.left)*(W/r.width);mouseActive=true; if(state==='menu')startGame();},{passive:false});
window.addEventListener('keydown',e=>{
if(e.key==='ArrowLeft'||e.key==='a'||e.key==='A'){keys.left=true;e.preventDefault();}
if(e.key==='ArrowRight'||e.key==='d'||e.key==='D'){keys.right=true;e.preventDefault();}
if(e.code==='Space'){e.preventDefault(); if(state==='serve')launch();}
if(e.key==='p'||e.key==='P'){ if(state==='play'){state='pause';showOverlay('Paused','Press P or click to resume','Resume');} else if(state==='pause'){state='play';hideOverlay();} }
if(state==='menu'||state==='over'||state==='win'){ startGame(); }
});
window.addEventListener('keyup',e=>{
if(e.key==='ArrowLeft'||e.key==='a'||e.key==='A')keys.left=false;
if(e.key==='ArrowRight'||e.key==='d'||e.key==='D')keys.right=false;
});
ovBtn.addEventListener('click',e=>{e.stopPropagation();startGame();});
overlay.addEventListener('click',()=>{ if(state==='menu'||state==='over'||state==='win')startGame(); else if(state==='pause'){state='play';hideOverlay();} });
window.addEventListener('resize',()=>{});
function launch(){
if(state!=='serve')return;
state='play';
for(const b of balls){
if(Math.abs(b.vy)<120){ b.vy = -Math.abs(b.vy)<0? -330:-330; }
b.vx=(Math.random()-.5)*180;
if(b.vy>0)b.vy=-Math.abs(b.vy);
normalize(b);
}
}
// ---------- Physics helpers ----------
function normalize(b){
const speed=Math.hypot(b.vx,b.vy)||1;
const target=380;
b.vx=b.vx/speed*target; b.vy=b.vy/speed*target;
}
function movePaddle(dt){
const targetW = wideTimer>0 ? PADDLE_BASE_W*1.55 : PADDLE_BASE_W;
paddle.w += (targetW-paddle.w)*Math.min(1,dt*10);
const speed=640;
let dx=0;
if(keys.left)dx-=speed;
if(keys.right)dx+=speed;
if(mouseActive&&dx===0){
const target=Math.max(paddle.x,Math.min(W-paddle.w,mouseX-paddle.w/2));
dx=(target-paddle.x)*Math.min(1,dt*18);
}
paddle.x=Math.max(0,Math.min(W-paddle.w,paddle.x+dx*dt));
}
function brickRectHit(b,r){
return !(b.x+r<b.x || b.x-r>b.x+b.w || b.y+r<b.y || b.y-r>b.y+b.h);
}
function circleRect(cx,cy,r,rx,ry,rw,rh){
const nx=Math.max(rx,Math.min(cx,rx+rw)), ny=Math.max(ry,Math.min(cy,ry+rh));
const dx=cx-nx, dy=cy-ny;
return dx*dx+dy*dy<=r*r;
}
// ---------- Update ----------
function update(dt){
if(state==='menu'||state==='over'||state==='win'||state==='pause')return;
movePaddle(dt);
if(wideTimer>0){wideTimer-=dt; if(wideTimer<=0)paddle.glow=0;}
if(state==='serve'){
serveTimer+=dt;
for(const b of balls){ b.x=paddle.x+paddle.w/2+Math.sin(serveTimer*3)*14; b.y=paddle.y-b.r-2; b.trail=[];}
return;
}
// balls
for(let i=balls.length-1;i>=0;i--){
const b=balls[i];
b.trail.push({x:b.x,y:b.y}); if(b.trail.length>10)b.trail.shift();
// substeps for accuracy
const steps=4;
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<0){b.x=b.r;b.vx=Math.abs(b.vx); wallFx(b.x,b.y);}
if(b.x+b.r>W){b.x=W-b.r;b.vx=-Math.abs(b.vx); wallFx(b.x,b.y);}
if(b.y-b.r<0){b.y=b.r;b.vy=Math.abs(b.vy); wallFx(b.x,b.y);}
// bricks
for(const br of bricks){
if(!br.alive)continue;
if(circleRect(b.x,b.y,b.r,br.x,br.y,br.w,br.h)){
hitBrick(br,b);
break;
}
}
// paddle
if(b.vy>0 && b.y+b.r>=paddle.y && b.y-b.r<=paddle.y+paddle.h &&
b.x>=paddle.x-b.r && b.x<=paddle.x+paddle.w+b.r){
const rel=(b.x-(paddle.x+paddle.w/2))/(paddle.w/2); // -1..1
const clamped=Math.max(-1,Math.min(1,rel));
const angle=clamped*(Math.PI/3); // up to 60 deg
const speed=Math.max(340,Math.hypot(b.vx,b.vy));
b.vx=Math.sin(angle)*speed*Math.sign(b.vx||1)*0 + Math.sin(angle)*speed*1;
// ensure direction follows paddle motion slightly
b.vx = Math.sin(angle)*speed;
b.vy = -Math.abs(Math.cos(angle))*speed;
if(Math.abs(b.vy)<90)b.vy=-90;
b.y=paddle.y-b.r-0.5;
paddle.glow=1;
spark(b.x,paddle.y,8,'#4dd0e1',1.2);
}
}
if(b.y-b.r>H+20){
balls.splice(i,1);
}
}
if(balls.length===0){
lives--;
updateStats();
flash=0.6; shake=8;
if(lives<=0){gameOver();return;}
state='serve';serveTimer=0;
spawnBall();
}
// powerups
for(let i=powerups.length-1;i>=0;i--){
const p=powerups[i];
p.y+=p.vy*dt; p.rot+=p.spin*dt; p.x+=p.vx*dt;
if(p.x<p.r||p.x>W-p.r)p.vx*=-1;
if(p.y>H+30){powerups.splice(i,1);continue;}
if(circleRect(p.x,p.y,p.r+4,paddle.x-2,paddle.y-2,paddle.w+4,paddle.h+6)){
applyPowerup(p);
powerups.splice(i,1);
}
}
// particles
for(let i=particles.length-1;i>=0;i--){
const q=particles[i];
q.x+=q.vx*dt; q.y+=q.vy*dt; q.vy+=q.g*dt; q.life-=dt;
q.vx*=0.99;
if(q.life<=0)particles.splice(i,1);
}
if(shake>0)shake=Math.max(0,shake-dt*22);
if(flash>0)flash=Math.max(0,flash-dt*2);
// win check
if(bricks.every(b=>!b.alive)){ victory(); }
}
function wallFx(x,y){ spark(x,y,5,'#8f9bff',0.8); }
function hitBrick(br,b){
// determine bounce by overlap side
const cx=b.x, cy=b.y;
const overlapL = Math.abs((cx+b.r)-(br.x));
const overlapR = Math.abs((br.x+br.w)-(cx-b.r));
const overlapT = Math.abs((cy+b.r)-(br.y));
const overlapB = Math.abs((br.y+br.h)-(cy-b.r));
const m=Math.min(overlapL,overlapR,overlapT,overlapB);
if(m===overlapT){ b.y=br.y-b.r-0.5; b.vy=-Math.abs(b.vy); }
else if(m===overlapB){ b.y=br.y+br.h+b.r+0.5; b.vy=Math.abs(b.vy); }
else if(m===overlapL){ b.x=br.x-b.r-0.5; b.vx=-Math.abs(b.vx); }
else { b.x=br.x+br.w+b.r+0.5; b.vx=Math.abs(b.vx); }
br.flash=1;
if(br.hp>1){
br.hp--;
score+=5; updateStats();
spark(b.x,b.y,10,br.color,1.0);
shake=Math.max(shake,3);
}else{
br.alive=false;
score+=10; updateStats();
spark(b.x,b.y,22,br.color,1.6);
shake=Math.max(shake,6);
if(br.hasPU) dropPowerup(br);
}
}
function dropPowerup(br){
const type = Math.random()<0.5 ? 'wide' : 'multi';
powerups.push({x:br.x+br.w/2, y:br.y+br.h/2, vy:105, vx:(Math.random()-.5)*60, r:12, type, rot:0, spin:2.2});
}
function applyPowerup(p){
if(p.type==='wide'){
wideTimer=10; paddle.glow=1;
score+=50; updateStats();
floatText(p.x,p.y,'WIDE PADDLE','#ffd166');
}else{
for(let i=0;i<2;i++){
const b=spawnBall();
b.vx=(Math.random()-.5)*300; b.vy=-Math.abs(300+Math.random()*120);
normalize(b);
}
score+=50; updateStats();
floatText(p.x,p.y,'MULTI BALL','#ff7ae0');
}
}
let texts=[];
function floatText(x,y,t,c){texts.push({x,y,t,c,life:1.2});}
function spark(x,y,n,color,scale){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2, sp=(40+Math.random()*220)*scale;
particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-40,g:340,life:0.4+Math.random()*0.5,max:0.9,size:1.5+Math.random()*3,color});
}
}
// ---------- Render ----------
function roundRect(x,y,w,h,r){
ctx.beginPath();
ctx.moveTo(x+r,y);ctx.lineTo(x+w-r,y);ctx.quadraticCurveTo(x+w,y,x+w,y+r);
ctx.lineTo(x+w,y+h-r);ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);
ctx.lineTo(x+r,y+h);ctx.quadraticCurveTo(x,y+h,x,y+h-r);
ctx.lineTo(x,y+r);ctx.quadraticCurveTo(x,y,x+r,y);ctx.closePath();
}
function drawBackground(){
const g=ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#0a0f1d');g.addColorStop(1,'#06080f');
ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
// grid
ctx.strokeStyle='rgba(120,160,255,0.055)';ctx.lineWidth=1;
for(let x=0;x<W;x+=45){ctx.beginPath();ctx.moveTo(x+.5,0);ctx.lineTo(x+.5,H);ctx.stroke();}
for(let y=0;y<H;y+=45){ctx.beginPath();ctx.moveTo(0,y+.5);ctx.lineTo(W,y+.5);ctx.stroke();}
// bottom glow
const g2=ctx.createRadialGradient(W/2,H,20,W/2,H,420);
g2.addColorStop(0,'rgba(77,208,225,0.12)');g2.addColorStop(1,'rgba(77,208,225,0)');
ctx.fillStyle=g2;ctx.fillRect(0,H-220,W,220);
}
function drawBrick(br){
if(!br.alive)return;
ctx.save();
const cracked = br.hp< br.maxhp;
const pad=2;
roundRect(br.x+pad,br.y+pad,br.w-pad*2,br.h-pad*2,5);
const grad=ctx.createLinearGradient(br.x,br.y,br.x,br.y+br.h);
grad.addColorStop(0,br.color);grad.addColorStop(1,shade(br.color,-60));
ctx.fillStyle=grad;ctx.fill();
// top highlight
ctx.fillStyle='rgba(255,255,255,0.28)';
roundRect(br.x+pad+3,br.y+pad,br.w-pad*2-6,4,2);ctx.fill();
if(cracked){
// crack lines
ctx.strokeStyle='rgba(0,0,0,0.55)';ctx.lineWidth=1.6;
const s=br.seed;
ctx.beginPath();
ctx.moveTo(br.x+br.w*0.3+Math.sin(s)*4, br.y+2);
ctx.lineTo(br.x+br.w*0.42+Math.sin(s)*8, br.y+br.h*0.45);
ctx.lineTo(br.x+br.w*0.3+Math.sin(s)*12, br.y+br.h-2);
ctx.moveTo(br.x+br.w*0.66+Math.cos(s)*6, br.y+2);
ctx.lineTo(br.x+br.w*0.58+Math.cos(s)*10, br.y+br.h*0.5);
ctx.lineTo(br.x+br.w*0.7+Math.cos(s)*14, br.y+br.h-2);
ctx.stroke();
ctx.fillStyle='rgba(0,0,0,0.18)';roundRect(br.x+pad,br.y+pad,br.w-pad*2,br.h-pad*2,5);ctx.fill();
}
if(br.hasPU){
// power-up icon badge
const cx=br.x+br.w-13, cy=br.y+br.h/2;
ctx.beginPath();ctx.arc(cx,cy,7.5,0,Math.PI*2);
ctx.fillStyle='rgba(10,14,24,0.85)';ctx.fill();
ctx.strokeStyle='#ffffff';ctx.lineWidth=1.6;ctx.stroke();
ctx.fillStyle='#ffffff';
ctx.font='bold 9px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText('★',cx,cy+0.5);
}
if(br.flash>0){
ctx.fillStyle='rgba(255,255,255,'+(br.flash*0.55)+')';
roundRect(br.x+pad,br.y+pad,br.w-pad*2,br.h-pad*2,5);ctx.fill();
br.flash=Math.max(0,br.flash-0.06);
}
ctx.restore();
}
function drawPaddle(){
ctx.save();
const g=ctx.createLinearGradient(paddle.x,0,paddle.x+paddle.w,0);
if(wideTimer>0){
g.addColorStop(0,'#ffd166');g.addColorStop(0.5,'#fff0a8');g.addColorStop(1,'#ffb347');
}else{
g.addColorStop(0,'#4dd0e1');g.addColorStop(0.5,'#a9f3ff');g.addColorStop(1,'#4dd0e1');
}
ctx.shadowColor=wideTimer>0?'rgba(255,209,102,0.9)':'rgba(77,208,225,0.8)';
ctx.shadowBlur=18+Math.sin(performance.now()/120)*4;
roundRect(paddle.x,paddle.y,paddle.w,paddle.h,7);
ctx.fillStyle=g;ctx.fill();
ctx.shadowBlur=0;
// center marker
ctx.fillStyle='rgba(0,0,0,0.28)';
roundRect(paddle.x+paddle.w/2-8,paddle.y+3,16,3,1.5);ctx.fill();
ctx.restore();
if(wideTimer>0){
ctx.save();
ctx.font='bold 12px sans-serif';ctx.fillStyle='#ffd166';ctx.textAlign='center';
ctx.fillText('WIDE '+wideTimer.toFixed(1)+'s',paddle.x+paddle.w/2,paddle.y-10);
ctx.restore();
}
}
function drawBall(b){
ctx.save();
// trail
for(let i=0;i<b.trail.length;i++){
const t=b.trail[i], k=(i+1)/b.trail.length;
ctx.beginPath();ctx.arc(t.x,t.y,b.r*k*0.75,0,Math.PI*2);
ctx.fillStyle='rgba(255,255,255,'+(k*0.16)+')';ctx.fill();
}
const g=ctx.createRadialGradient(b.x-b.r*0.3,b.y-b.r*0.3,1,b.x,b.y,b.r);
g.addColorStop(0,'#ffffff');g.addColorStop(0.45,'#bff3ff');g.addColorStop(1,'#4dd0e1');
ctx.shadowColor='rgba(77,208,225,0.9)';ctx.shadowBlur=14;
ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,Math.PI*2);ctx.fillStyle=g;ctx.fill();
ctx.restore();
}
function drawPowerup(p){
ctx.save();
ctx.translate(p.x,p.y);ctx.rotate(p.rot);
const color = p.type==='wide' ? '#ffd166' : '#ff7ae0';
ctx.shadowColor=color;ctx.shadowBlur=14;
ctx.beginPath();
for(let i=0;i<6;i++){
const a=i*Math.PI/3-Math.PI/2;
const x=Math.cos(a)*p.r, y=Math.sin(a)*p.r;
if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
}
ctx.closePath();
ctx.fillStyle=color;ctx.fill();
ctx.shadowBlur=0;
ctx.fillStyle='#0b0f1a';ctx.font='bold 13px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText(p.type==='wide'?'W':'×3',0,0.5);
ctx.restore();
}
function draw(){
ctx.save();
if(shake>0)ctx.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake);
drawBackground();
// walls
ctx.fillStyle='rgba(120,170,255,0.18)';
ctx.fillRect(0,0,W,3);ctx.fillRect(0,0,3,H);ctx.fillRect(W-3,0,3,H);
for(const br of bricks)drawBrick(br);
for(const p of powerups)drawPowerup(p);
for(const b of balls)drawBall(b);
drawPaddle();
for(const q of particles){
ctx.globalAlpha=Math.max(0,q.life/q.max);
ctx.fillStyle=q.color;
ctx.fillRect(q.x-q.size/2,q.y-q.size/2,q.size,q.size);
}
ctx.globalAlpha=1;
for(let i=texts.length-1;i>=0;i--){
const t=texts[i];
t.y-=40*0.016; t.life-=0.016;
ctx.save();ctx.globalAlpha=Math.max(0,t.life/1.2);
ctx.fillStyle=t.c;ctx.font='bold 16px sans-serif';ctx.textAlign='center';
ctx.fillText(t.t,t.x,t.y);ctx.restore();
if(t.life<=0)texts.splice(i,1);
}
if(flash>0){
ctx.fillStyle='rgba(255,60,90,'+(flash*0.35)+')';ctx.fillRect(0,0,W,H);
}
ctx.restore();
}
// ---------- Demo AI (auto play in menu) ----------
function demoAI(dt){
// move paddle toward lowest descending ball
let target=null;
for(const b of balls){
if(b.vy>0 && (!target || b.y>target.y)) target=b;
}
if(!target) target=balls[0];
if(target){
const desired=target.x-paddle.w/2;
paddle.x += (desired-paddle.x)*Math.min(1,dt*4.5);
}
// launch automatically
if(state==='serve'){ serveTimer+=0.016; if(serveTimer>0.6)launch(); }
}
// ---------- Loop ----------
let last=performance.now(), demo=false;
function frame(now){
let dt=(now-last)/1000; last=now;
dt=Math.min(dt,0.033);
if(demo){
demoAI(dt);
update(dt);
}else{
update(dt);
}
draw();
requestAnimationFrame(frame);
}
// Start in demo mode so something is visible immediately
resetLevel();
demo=true;
showOverlay('Neon Breakout','Move the mouse or use <kbd>←</kbd><kbd>→</kbd> to steer. Edge hits = sharp angles. Catch the <span style="color:#ffd166">★</span> power-ups.','Start Game');
requestAnimationFrame(frame);
// Stop demo when user starts
const origStart=startGame;
function handleStart(){ demo=false; origStart(); }
ovBtn.addEventListener('click',handleStart);
overlay.addEventListener('click',(e)=>{ if(e.target===ovBtn)return; if(state==='menu'||state==='over'||state==='win')handleStart(); else if(state==='pause'){state='play';hideOverlay();} });
// If user moves mouse or presses keys, exit demo into real game
window.addEventListener('mousemove',()=>{ if(demo){demo=false; if(state==='menu'||state==='over'||state==='win'){origStart();} } });
window.addEventListener('keydown',()=>{ if(demo){demo=false; if(state==='menu'||state==='over'||state==='win'){origStart();} } },true);
// Auto restart demo after game over in demo mode
setInterval(()=>{ if(demo && (state==='over'||state==='win')){ score=0;lives=3;level=1;resetLevel(); hideOverlay(); } },2500);
function shade(hex,amt){
const n=parseInt(hex.slice(1),16);
let r=(n>>16)+amt,g=((n>>8)&255)+amt,b=(n&255)+amt;
r=Math.max(0,Math.min(255,r));g=Math.max(0,Math.min(255,g));b=Math.max(0,Math.min(255,b));
return '#'+((1<<24)+(r<<16)+(g<<8)+b).toString(16).slice(1);
}
})();
</script>
</body>
</html>