Qwen3.8 Flash
by QwenReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 45 s
- Cost
- $0.0039
- Output tokens
- 8,238
- Spent thinking
- 0 tokens
- Speed
- 82.1 tok/s
- Console errors
- 0
- First token after
- 4.8 s
- First visible text after
- 4.8 s
- Prompt tokens
- 535
- Code size
- 21.2 KB
- Finish reason
- stop
- Served by
- Alibaba
- Model id
qwen/qwen3.8-flash- Reasoning sent
{"effort":"none"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 2026
What it was asked
The prompt
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 · 547 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>
*{margin:0;padding:0;box-sizing:border-box;}
html,body{height:100%;}
body{
background:#070b16;
display:flex;align-items:center;justify-content:center;
font-family:'Segoe UI',system-ui,-apple-system,sans-serif;
overflow:hidden;color:#eaf2ff;user-select:none;-webkit-user-select:none;
}
#wrap{position:relative;width:min(94vw,calc(92vh*0.75));aspect-ratio:3/4;}
canvas{display:block;width:100%;height:100%;border-radius:16px;background:#0a1122;
box-shadow:0 24px 70px rgba(0,0,0,.6),0 0 0 1px rgba(120,160,255,.12),0 0 60px rgba(60,120,255,.12);}
</style>
</head>
<body>
<div id="wrap"><canvas id="game"></canvas></div>
<script>
(function(){
"use strict";
/* ---------- constants ---------- */
const W=600,H=800;
const BRICKS_PER_ROW=10, ROWS=6;
const SIDE_PAD=24, TOP_OFFSET=96, GAP=6;
const BRICK_W=(W-SIDE_PAD*2-GAP*(BRICKS_PER_ROW-1))/BRICKS_PER_ROW; // 50
const BRICK_H=26;
const PADDLE_W=104, PADDLE_H=16, PADDLE_Y=H-58, BALL_R=8, BASE_SPEED=370;
const MAX_BALLS=7;
const COLS=['#ff4d6d','#ff8c42','#ffd23f','#3ddc97','#3ab0ff','#b06bff'];
const POWERUP_COLORS={wide:'#3ab0ff',multi:'#ffd23f'};
const FONT="'Segoe UI',system-ui,sans-serif";
/* ---------- canvas setup ---------- */
const cv=document.getElementById('game');
cv.width=W; cv.height=H;
const ctx=cv.getContext('2d');
function fitCanvas(){
const dpr=Math.min(window.devicePixelRatio||1,2);
cv.width=Math.round(W*dpr); cv.height=Math.round(H*dpr);
ctx.setTransform(dpr,0,0,dpr,0,0);
}
fitCanvas(); window.addEventListener('resize',fitCanvas);
/* ---------- audio (optional, guarded) ---------- */
let AC=null;
function ensureAudio(){ if(!AC){ try{AC=new (window.AudioContext||window.webkitAudioContext)();}catch(e){AC=null;} } if(AC&&AC.state==='suspended'){AC.resume().catch(()=>{});} }
function tone(f,d,type,g,slideTo){
if(!AC)return;
try{
const o=AC.createOscillator(),ga=AC.createGain();
o.type=type||'square'; o.frequency.value=f;
if(slideTo)o.frequency.exponentialRampToValueAtTime(Math.max(20,slideTo),AC.currentTime+d);
ga.gain.setValueAtTime(g||0.06,AC.currentTime);
ga.gain.exponentialRampToValueAtTime(0.0001,AC.currentTime+d);
o.connect(ga).connect(AC.destination); o.start(); o.stop(AC.currentTime+d);
}catch(e){}
}
function sfx(n){
switch(n){
case 'wall':tone(240,0.05,'square',0.035);break;
case 'paddle':tone(330,0.07,'square',0.05,430);break;
case 'brick':tone(560,0.05,'triangle',0.05,760);break;
case 'crack':tone(190,0.06,'square',0.04);break;
case 'powerup':tone(520,0.09,'sine',0.06,900);setTimeout(()=>tone(820,0.12,'sine',0.05,1250),70);break;
case 'life':tone(300,0.2,'sawtooth',0.06,90);break;
case 'win':[523,659,784,1046].forEach((f,i)=>setTimeout(()=>tone(f,0.18,'triangle',0.06),i*120));break;
case 'over':[400,300,200,120].forEach((f,i)=>setTimeout(()=>tone(f,0.22,'sawtooth',0.06,f*0.7),i*150));break;
}
}
/* ---------- state ---------- */
const game={state:'menu',score:0,lives:3,best:0};
let bricks=[],balls=[],particles=[],powerups=[];
const paddle={x:(W-PADDLE_W)/2,w:PADDLE_W,h:PADDLE_H,y:PADDLE_Y,targetX:null,glow:0};
const powerupsActive={wide:0,multi:0};
let shake=0,flash=0;
let demoTimer=0,serveTimer=0,ballSpeedMul=1;
const keys={left:false,right:false};
let lastTime=performance.now();
/* ---------- helpers ---------- */
function clamp(v,a,b){return v<a?a:v>b?b:v;}
function rand(a,b){return a+Math.random()*(b-a);}
function rr(x,y,w,h,r){r=Math.min(r,w/2,h/2);ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);ctx.arcTo(x+w,y+h,x,y+h,r);ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);ctx.closePath();}
function hexA(hex,a){const n=parseInt(hex.slice(1),16);return `rgba(${(n>>16)&255},${(n>>8)&255},${n&255},${a})`;}
function mix(h,t){const n=parseInt(h.slice(1),16);const r=(n>>16)&255,g=(n>>8)&255,b=n&255;return `rgb(${Math.round(r+(t-r)*0.5)},${Math.round(g+(t-g)*0.5)},${Math.round(b+(t-b)*0.5)})`;}
function lightCol(c){return mix(c,255);}
function darkCol(c){return mix(c,0);}
function roundRectFill(x,y,w,h,r,fill){rr(x,y,w,h,r);ctx.fillStyle=fill;ctx.fill();}
/* ---------- level ---------- */
function makeBall(cx,cy,vx,vy){return {x:cx,y:cy,vx,vy,r:BALL_R,trail:[],active:true,dead:false,justBounced:false};}
function buildLevel(){
bricks=[];
for(let r=0;r<ROWS;r++)for(let c=0;c<BRICKS_PER_ROW;c++){
const x=SIDE_PAD+c*(BRICK_W+GAP), y=TOP_OFFSET+r*(BRICK_H+GAP);
let hp=1; if(r===0||r===1)hp=2; else if(Math.random()<0.22)hp=2;
bricks.push({x,y,w:BRICK_W,h:BRICK_H,color:COLS[r%COLS.length],hp,maxHp:hp,alive:true,powerup:null});
}
const pool=[];
bricks.forEach((b,i)=>{if(b.alive&&b.hp===1)pool.push(i);});
for(let i=0;i<4&&pool.length;i++){const idx=Math.floor(Math.random()*pool.length);bricks[pool[idx]].powerup=Math.random()<0.5?'wide':'multi';pool.splice(idx,1);}
}
function resetGame(){
game.score=0; game.lives=3;
buildLevel();
particles=[]; powerups=[]; balls=[];
powerupsActive.wide=0; powerupsActive.multi=0;
paddle.w=PADDLE_W; paddle.x=(W-paddle.w)/2; paddle.targetX=null;
serveTimer=0.9; ballSpeedMul=1;
game.state='playing';
}
/* ---------- serving & spawning ---------- */
function spawnServeBall(){
const cx=paddle.x+paddle.w/2, cy=paddle.y-BALL_R-2;
const vx=rand(-140,140), vy=-BASE_SPEED;
balls=[makeBall(cx,cy,vx,vy)];
}
function spawnPowerup(b){
powerups.push({x:b.x+b.w/2,y:b.y+b.h/2,vy:135,type:b.powerup,r:15,spin:0});
}
function applyPowerup(p){
if(p.type==='wide'){powerupsActive.wide=10;sfx('powerup');}
else{multiball();sfx('powerup');}
}
function multiball(){
const src=balls.find(b=>b.active&&!b.dead)||null;
const base=src?{x:src.x,y:src.y}: {x:paddle.x+paddle.w/2,y:paddle.y-BALL_R-2};
const spd=BASE_SPEED*ballSpeedMul;
let added=0;
[-0.6,0.6].forEach(off=>{
if(balls.length>=MAX_BALLS)return;
const ang=Math.atan2(base.vy,base.vx)+off;
balls.push(makeBall(base.x,base.y,Math.cos(ang)*spd,Math.sin(ang)*spd));
added++;
});
if(added===0&&balls.length<MAX_BALLS){
balls.push(makeBall(base.x,base.y,rand(-160,160),-spd));
}
}
/* ---------- collisions ---------- */
function hitBrick(b,bx,by,prevX,prevY,ball){
b.hp--; game.score+=10;
if(b.hp<=0){
b.alive=false; game.score+=15; spawnParticles(b,true);
if(b.powerup)spawnPowerup(b);
sfx('brick'); flash=0.12;
}else{
spawnParticles(b,false); sfx('crack');
}
const dx=bx-prevX, dy=by-prevY, len=Math.hypot(dx,dy)||1;
ball.vx=dx/len*Math.abs(ball.vx); ball.vy=dy/len*Math.abs(ball.vy);
ball.justBounced=true;
}
function resolveBallBrick(ball,b){
const overlapX=Math.min(ball.x+ball.r,b.x+b.w)-Math.max(ball.x-ball.r,b.x);
const overlapY=Math.min(ball.y+ball.r,b.y+b.h)-Math.max(ball.y-ball.r,b.y);
if(overlapX<=0||overlapY<=0)return false;
const prevX=ball.x-ball.vx*dt, prevY=ball.y-ball.vy*dt;
if(overlapX<overlapY){
if(ball.x<b.x+b.w/2){ball.x=b.x-ball.r;ball.vx=-Math.abs(ball.vx);}
else{ball.x=b.x+b.w+ball.r;ball.vx=Math.abs(ball.vx);}
hitBrick(b,ball.x,ball.y,prevX,prevY,ball); return true;
}else{
if(ball.y<b.y+b.h/2){ball.y=b.y-ball.r;ball.vy=-Math.abs(ball.vy);}
else{ball.y=b.y+b.h+ball.r;ball.vy=Math.abs(ball.vy);}
hitBrick(b,ball.x,ball.y,prevX,prevY,ball); return true;
}
}
function collidePaddle(ball){
if(ball.vy<=0)return;
const p=paddle;
if(ball.y+ball.r>=p.y && ball.y-ball.r<=p.y+p.h && ball.x>=p.x-ball.r && ball.x<=p.x+p.w+ball.r){
const rel=clamp((ball.x-(p.x+p.w/2))/(p.w/2),-1,1);
const ang=rel*(Math.PI*0.42);
const sp=BASE_SPEED*ballSpeedMul;
ball.vx=Math.sin(ang)*sp; ball.vy=-Math.cos(ang)*sp;
ball.y=p.y-ball.r-0.5; ball.justBounced=true;
p.glow=1; sfx('paddle');
}
}
/* ---------- particles ---------- */
function spawnParticles(b,big){
const col=b.color, n=big?18:7;
for(let i=0;i<n;i++){
particles.push({
x:b.x+Math.random()*b.w, y:b.y+Math.random()*b.h,
vx:rand(-150,150), vy:rand(-220,-40), g:520,
life:rand(0.4,0.9), maxLife:0, size:rand(2,big?5:4), color:col
});
particles[particles.length-1].maxLife=particles[particles.length-1].life;
}
}
function updateParticles(dt){
for(const p of particles){p.life-=dt;p.vy+=p.g*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;}
particles=particles.filter(p=>p.life>0);
}
/* ---------- AI ---------- */
function aiTarget(){
let best=null,bestT=Infinity;
for(const b of balls){
if(!b.active||b.dead||b.vy<=0)continue;
const t=(paddle.y-BALL_R-b.y)/b.vy; if(t<=0)continue;
let px=b.x+b.vx*t;
const span=W-2*BALL_R;
let m=(px-BALL_R)%span; if(m<0)m+=span;
const cyc=m/span;
px=BALL_R+m; if(cyc>0.5)px=W-BALL_R-m;
const cost=t+Math.abs(px-(paddle.x+paddle.w/2))/1400;
if(cost<bestT){bestT=cost;best=px;}
}
if(best==null)best=paddle.x+paddle.w/2;
return best;
}
/* ---------- update ---------- */
function update(dt){
if(game.state!=='playing')return;
// paddle control
if(demoMode()){paddle.targetX=aiTarget()-paddle.w/2;}
else{
if(keys.left)paddle.targetX=(paddle.targetX==null?paddle.x:paddle.targetX)-640*dt;
if(keys.right)paddle.targetX=(paddle.targetX==null?paddle.x:paddle.targetX)+640*dt;
if(paddle.targetX!=null)paddle.targetX=clamp(paddle.targetX,0,W-paddle.w);
}
if(paddle.targetX!=null){
paddle.x+=(paddle.targetX-paddle.x)*Math.min(1,dt*18);
if(Math.abs(paddle.targetX-paddle.x)<0.4)paddle.x=paddle.targetX;
}
paddle.x=clamp(paddle.x,0,W-paddle.w);
paddle.glow*=Math.pow(0.0008,dt);
// power-up timers
if(powerupsActive.wide>0){
powerupsActive.wide-=dt;
if(powerupsActive.wide<=0){
powerupsActive.wide=0;
const nc=(paddle.x+paddle.w/2); paddle.w=PADDLE_W; paddle.x=clamp(nc-paddle.w/2,0,W-paddle.w);
}
}
// serve / speed
if(serveTimer>0){serveTimer-=dt;if(serveTimer<=0)spawnServeBall();}
ballSpeedMul=Math.min(1.7,1+game.score/2600);
// balls
for(const ball of balls){
if(!ball.active||ball.dead)continue;
ball.trail.unshift({x:ball.x,y:ball.y}); if(ball.trail.length>12)ball.trail.pop();
const steps=Math.max(1,Math.ceil(Math.max(Math.abs(ball.vx),Math.abs(ball.vy))*dt/6));
const sdt=dt/steps;
for(let s=0;s<steps;s++){
ball.justBounced=false;
ball.x+=ball.vx*sdt; ball.y+=ball.vy*sdt;
// walls
if(ball.x-ball.r<0){ball.x=ball.r;ball.vx=Math.abs(ball.vx);ball.justBounced=true;sfx('wall');}
if(ball.x+ball.r>W){ball.x=W-ball.r;ball.vx=-Math.abs(ball.vx);ball.justBounced=true;sfx('wall');}
if(ball.y-ball.r<TOP_OFFSET-40){ball.y=TOP_OFFSET-40+ball.r;ball.vy=Math.abs(ball.vy);ball.justBounced=true;sfx('wall');}
// bricks
for(const b of bricks){
if(!b.alive)continue;
if(ball.x+ball.r>b.x&&ball.x-ball.r<b.x+b.w&&ball.y+ball.r>b.y&&ball.y-ball.r<b.y+b.h){
if(resolveBallBrick(ball,b))break;
}
}
// paddle
collidePaddle(ball);
// bottom
if(ball.y-ball.r>H){ball.dead=true;break;}
// anti-stuck
if(!ball.justBounced){
if(ball.vy<-BASE_SPEED*0.32)ball.vy=-BASE_SPEED*0.32;
if(ball.vy>BASE_SPEED*0.32)ball.vy=BASE_SPEED*0.32;
}
const sp=Math.hypot(ball.vx,ball.vy), target=BASE_SPEED*ballSpeedMul;
if(sp>1){const k=target/sp;ball.vx*=k;ball.vy*=k;}
}
}
balls=balls.filter(b=>!b.dead);
// powerups falling
for(const p of powerups){
p.y+=p.vy*dt; p.spin+=dt*4;
if(p.y+p.r>=paddle.y && p.y-p.r<=paddle.y+paddle.h && p.x>=paddle.x && p.x<=paddle.x+paddle.w){
applyPowerup(p); p.collected=true;
}
}
powerups=powerups.filter(p=>!p.collected && p.y<H+30);
// lose a life
if(balls.length===0){
game.lives--; sfx('life'); shake=0.5;
if(game.lives<=0){game.best=Math.max(game.best,game.score);game.state='over';sfx('over');}
else{powerupsActive.wide=0;paddle.w=PADDLE_W;paddle.x=(W-paddle.w)/2;paddle.targetX=null;serveTimer=1.1;}
}
// win
if(bricks.every(b=>!b.alive)){game.best=Math.max(game.best,game.score);game.state='win';sfx('win');}
updateParticles(dt);
shake=Math.max(0,shake-dt);
flash=Math.max(0,flash-dt);
}
function demoMode(){return game.state==='playing'&&paddle.targetX==null;}
/* ---------- render ---------- */
function drawBackground(){
const g=ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#0c1730');g.addColorStop(0.5,'#0a1122');g.addColorStop(1,'#070b16');
ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
ctx.save();ctx.globalAlpha=0.5;
const rg=ctx.createRadialGradient(W/2,TOP_OFFSET,20,W/2,TOP_OFFSET,420);
rg.addColorStop(0,'rgba(70,110,220,0.22)');rg.addColorStop(1,'rgba(70,110,220,0)');
ctx.fillStyle=rg;ctx.fillRect(0,0,W,H);ctx.restore();
ctx.save();ctx.strokeStyle='rgba(120,160,255,0.05)';ctx.lineWidth=1;
for(let x=0;x<=W;x+=40){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
ctx.restore();
}
function drawHUD(){
ctx.textBaseline='alphabetic';
ctx.font='bold 22px '+FONT;ctx.fillStyle='#fff';ctx.textAlign='left';
ctx.fillText(String(game.score).padStart(5,'0'),18,40);
ctx.font='11px '+FONT;ctx.fillStyle='rgba(180,200,230,0.6)';
ctx.fillText('SCORE',18,54);
ctx.textAlign='center';ctx.font='bold 13px '+FONT;ctx.fillStyle='rgba(180,200,230,0.5)';
ctx.fillText('NEON BREAKOUT',W/2,34);
ctx.textAlign='right';ctx.font='11px '+FONT;ctx.fillStyle='rgba(180,200,230,0.6)';
ctx.fillText('BEST '+String(game.best).padStart(5,'0'),W-18,40);
// lives
ctx.textAlign='left';ctx.font='11px '+FONT;ctx.fillStyle='rgba(180,200,230,0.6)';
ctx.fillText('LIVES',18,72);
for(let i=0;i<3;i++){
const x=64+i*20, y=67, alive=i<game.lives;
ctx.beginPath();ctx.arc(x,y,7,0,7);
if(alive){ctx.fillStyle='#3ddc97';ctx.shadowColor='#3ddc97';ctx.shadowBlur=8;ctx.fill();ctx.shadowBlur=0;}
else{ctx.strokeStyle='rgba(120,140,170,0.5)';ctx.lineWidth=1.5;ctx.stroke();}
}
// active power-ups
let px=18,py=H-14;
if(powerupsActive.wide>0){
ctx.font='bold 11px '+FONT;ctx.fillStyle=POWERUP_COLORS.wide;ctx.textAlign='left';
ctx.fillText('◈ WIDE '+Math.ceil(powerupsActive.wide)+'s',px,py);
px+=ctx.measureText('◈ WIDE '+Math.ceil(powerupsActive.wide)+'s').width+16;
}
if(balls.length>1){
ctx.font='bold 11px '+FONT;ctx.fillStyle=POWERUP_COLORS.multi;ctx.textAlign='left';
ctx.fillText('✦ ×'+balls.length+' BALLS',px,py);
}
}
function drawCracks(b){
const cx=b.x+b.w/2, cy=b.y+b.h/2;
ctx.strokeStyle='rgba(0,0,0,0.5)';ctx.lineWidth=1.4;
ctx.beginPath();
ctx.moveTo(cx-8,cy-9);ctx.lineTo(cx+3,cy-1);ctx.lineTo(cx-4,cy+8);
ctx.moveTo(cx+3,cy-1);ctx.lineTo(cx+11,cy+3);
ctx.moveTo(cx+3,cy-1);ctx.lineTo(cx+9,cy-8);
ctx.stroke();
}
function drawBricks(){
for(const b of bricks){
if(!b.alive)continue;
const tough=b.maxHp===2, cracked=tough&&b.hp===1;
ctx.save();
ctx.shadowColor=b.color;ctx.shadowBlur=12;
const g=ctx.createLinearGradient(0,b.y,0,b.y+b.h);
g.addColorStop(0,lightCol(b.color));g.addColorStop(0.5,b.color);g.addColorStop(1,darkCol(b.color));
rr(b.x,b.y,b.w,b.h,5);ctx.fillStyle=g;ctx.fill();
ctx.shadowBlur=0;
ctx.strokeStyle='rgba(255,255,255,0.35)';ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(b.x+3,b.y+2);ctx.lineTo(b.x+b.w-3,b.y+2);ctx.stroke();
ctx.strokeStyle='rgba(0,0,0,0.28)';
ctx.beginPath();ctx.moveTo(b.x+2,b.y+b.h-2);ctx.lineTo(b.x+b.w-2,b.y+b.h-2);ctx.stroke();
if(tough){
ctx.strokeStyle='rgba(0,0,0,0.45)';ctx.lineWidth=2;
rr(b.x+3,b.y+3,b.w-6,b.h-6,3);ctx.stroke();
ctx.fillStyle='rgba(255,255,255,0.18)';
ctx.fillRect(b.x+b.w/2-8,b.y+b.h/2-1,16,2);
ctx.fillRect(b.x+b.w/2-1,b.y+b.h/2-8,2,16);
}
if(cracked)drawCracks(b);
if(b.powerup){
ctx.beginPath();ctx.arc(b.x+b.w/2,b.y+b.h/2,7,0,7);
ctx.fillStyle='rgba(255,255,255,0.16)';ctx.fill();
ctx.strokeStyle='rgba(255,255,255,0.55)';ctx.lineWidth=1.2;ctx.stroke();
ctx.fillStyle='#fff';ctx.font='bold 11px '+FONT;ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText(b.powerup==='wide'?'⇔':'✦',b.x+b.w/2,b.y+b.h/2+0.5);
ctx.textBaseline='alphabetic';
}
ctx.restore();
}
}
function drawPaddle(){
const p=paddle, glow=Math.max(p.glow,powerupsActive.wide>0?0.45:0);
ctx.save();
ctx.shadowColor=glow>0.05?'#3ab0ff':'#8fd0ff';ctx.shadowBlur=10+glow*26;
const g=ctx.createLinearGradient(0,p.y,0,p.y+p.h);
g.addColorStop(0,'#dff3ff');g.addColorStop(0.45,'#7cc4ff');g.addColorStop(1,'#2f7fe0');
rr(p.x,p.y,p.w,p.h,p.h/2);ctx.fillStyle=g;ctx.fill();
ctx.shadowBlur=0;
ctx.fillStyle='rgba(255,255,255,0.5)';
rr(p.x+6,p.y+2,p.w-12,3,2);ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.14)';
ctx.fillRect(p.x+p.w/2-1,p.y+3,2,p.h-6);
ctx.restore();
}
function drawBall(b){
for(let i=b.trail.length-1;i>=0;i--){
const t=b.trail[i], f=(i+1)/b.trail.length;
ctx.beginPath();ctx.arc(t.x,t.y,b.r*f*0.9,0,7);
ctx.fillStyle=`rgba(180,225,255,${0.05*f})`;ctx.fill();
}
ctx.save();
ctx.shadowColor='#bfe6ff';ctx.shadowBlur=16;
const g=ctx.createRadialGradient(b.x-b.r*0.35,b.y-b.r*0.35,1,b.x,b.y,b.r);
g.addColorStop(0,'#ffffff');g.addColorStop(0.6,'#cdeeff');g.addColorStop(1,'#7fb8ff');
ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,7);ctx.fillStyle=g;ctx.fill();
ctx.restore();
}
function drawPowerups(){
for(const p of powerups){
const col=POWERUP_COLORS[p.type];
ctx.save();
ctx.translate(p.x,p.y);
ctx.shadowColor=col;ctx.shadowBlur=14;
ctx.rotate(Math.sin(p.spin)*0.25);
rr(-p.r,-p.r,p.r*2,p.r*2,6);
const g=ctx.createLinearGradient(0,-p.r,0,p.r);
g.addColorStop(0,lightCol(col));g.addColorStop(1,col);
ctx.fillStyle=g;ctx.fill();
ctx.shadowBlur=0;
ctx.strokeStyle='rgba(255,255,255,0.6)';ctx.lineWidth=1.5;ctx.stroke();
ctx.fillStyle='#fff';ctx.font='bold 16px '+FONT;ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText(p.type==='wide'?'⇔':'✦',0,1);
ctx.restore();
}
}
function drawParticles(){
for(const p of particles){
const a=clamp(p.life/p.maxLife,0,1);
ctx.globalAlpha=a;ctx.fillStyle=p.color;
ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);
}
ctx.globalAlpha=1;
}
function overlay(alpha){ctx.fillStyle=`rgba(5,8,18,${alpha})`;ctx.fillRect(0,0,W,H);}
function centerText(txt,size,color,y,weight){
ctx.font=(weight||'bold')+' '+size+'px '+FONT;ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillStyle=color;ctx.fillText(txt,W/2,y);
}
function drawMenu(){
overlay(0.35);
ctx.save();ctx.shadowColor='#3ab0ff';ctx.shadowBlur=30;
centerText('NEON BREAKOUT',48,'#eaf5ff',150);ctx.restore();
centerText('Break every brick • Catch the drops',15,'rgba(190,210,240,0.8)',200);
const pulse=0.5+0.5*Math.sin(performance.now()/300);
ctx.globalAlpha=0.55+0.45*pulse;
centerText('▶ CLICK OR PRESS A KEY TO START',20,'#ffd23f',258);
ctx.globalAlpha=1;
centerText('MOUSE or ← → move the paddle • SPACE to launch',13,'rgba(170,190,220,0.7)',300);
centerText('◈ Wide paddle ✦ Multi-ball',13,'rgba(170,190,220,0.7)',324);
}
function drawEnd(win){
overlay(0.6);
ctx.save();ctx.shadowColor=win?'#3ddc97':'#ff4d6d';ctx.shadowBlur=30;
centerText(win?'VICTORY!':'GAME OVER',46,win?'#c9ffe6':'#ffd0d8',300);ctx.restore();
centerText('Score '+String(game.score).padStart(5,'0'),22,'#fff',360);
centerText('Best '+String(game.best).padStart(5,'0'),15,'rgba(190,210,240,0.8)',392);
const pulse=0.5+0.5*Math.sin(performance.now()/300);
ctx.globalAlpha=0.55+0.45*pulse;
centerText('▶ CLICK OR PRESS R TO PLAY AGAIN',18,'#ffd23f',452);
ctx.globalAlpha=1;
}
function render(){
ctx.clearRect(0,0,W,H);
ctx.save();
if(shake>0){const m=shake*10;ctx.translate(rand(-m,m),rand(-m,m));}
drawBackground();
drawBricks();
drawPowerups();
drawPaddle();
for(const b of balls)drawBall(b);
drawParticles();
ctx.restore();
drawHUD();
if(flash>0){ctx.fillStyle=`rgba(255,255,255,${flash*0.5})`;ctx.fillRect(0,0,W,H);}
if(game.state==='menu')drawMenu();
else if(game.state==='over')drawEnd(false);
else if(game.state==='win')drawEnd(true);
}
/* ---------- loop ---------- */
function frame(now){
let dt=(now-lastTime)/1000; lastTime=now;
if(dt>0.033)dt=0.033; if(dt<0)dt=0;
update(dt); render();
requestAnimationFrame(frame);
}
/* ---------- input ---------- */
function startFromMenu(){ensureAudio();resetGame();}
cv.addEventListener('mousemove',e=>{
if(game.state!=='playing'||demoMode())return;
const rect=cv.getBoundingClientRect();
const mx=(e.clientX-rect.left)*(W/rect.width);
paddle.targetX=clamp(mx-paddle.w/2,0,W-paddle.w);
});
cv.addEventListener('mousedown',()=>{
ensureAudio();
if(game.state==='menu')startFromMenu();
else if(game.state==='playing'){paddle.targetX=null;if(serveTimer>0)serveTimer=0;}
else if(game.state==='over'||game.state==='win')resetGame();
});
window.addEventListener('keydown',e=>{
if(['ArrowLeft','ArrowRight','Space'].includes(e.code))e.preventDefault();
ensureAudio();
if(e.code==='ArrowLeft')keys.left=true;
if(e.code==='ArrowRight')keys.right=true;
if(e.code==='KeyR'){if(game.state==='over'||game.state==='win')resetGame();}
if(game.state==='menu'){startFromMenu();}
else if(game.state==='playing'){
if(e.code==='Space'&&serveTimer>0){serveTimer=0;}
if(e.code==='ArrowLeft'||e.code==='ArrowRight')paddle.targetX=null;
}
});
window.addEventListener('keyup',e=>{
if(e.code==='ArrowLeft')keys.left=false;
if(e.code==='ArrowRight')keys.right=false;
});
/* ---------- boot ---------- */
buildLevel();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>