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 1 s
- Cost
- $0.0073
- Output tokens
- 11,987
- Spent thinking
- 0 tokens
- Speed
- 197.8 tok/s
- Console errors
- 0
- First token after
- 769 ms
- First visible text after
- 769 ms
- Prompt tokens
- 692
- Code size
- 33.8 KB
- Finish reason
- stop
- Served by
- Fireworks
- 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
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 · 1056 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>
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
background:
radial-gradient(ellipse 120% 80% at 50% -10%, #1b2a55 0%, #0a0f24 55%, #05070f 100%);
color:#e9efff;
font-family:"Segoe UI",system-ui,-apple-system,Roboto,Helvetica,Arial,sans-serif;
display:flex;align-items:center;justify-content:center;
min-height:100vh;overflow:hidden;
-webkit-user-select:none;user-select:none;
}
#wrap{
position:relative;width:min(96vw,900px);
display:flex;flex-direction:column;align-items:center;gap:10px;
}
header{
width:100%;display:flex;align-items:center;justify-content:space-between;
gap:12px;flex-wrap:wrap;padding:0 4px;
}
.brand{
display:flex;align-items:center;gap:10px;
font-weight:800;letter-spacing:.18em;font-size:clamp(13px,2.2vw,18px);
background:linear-gradient(90deg,#5ee7ff,#b06cff 60%,#ff7ad9);
-webkit-background-clip:text;background-clip:text;color:transparent;
text-shadow:0 0 22px rgba(120,190,255,.25);
}
.brand .dot{width:10px;height:10px;border-radius:50%;background:#5ee7ff;box-shadow:0 0 14px #5ee7ff}
.stats{display:flex;gap:8px;flex-wrap:wrap}
.stat{
background:rgba(255,255,255,.055);border:1px solid rgba(255,255,255,.10);
border-radius:10px;padding:6px 11px;min-width:74px;text-align:center;
backdrop-filter:blur(6px);
}
.stat .lab{font-size:9px;letter-spacing:.18em;color:#8fa0c8;text-transform:uppercase}
.stat .val{font-size:17px;font-weight:800;font-variant-numeric:tabular-nums;line-height:1.25}
#stage{
position:relative;width:100%;
border-radius:16px;overflow:hidden;
border:1px solid rgba(120,170,255,.22);
box-shadow:0 24px 70px rgba(0,0,0,.65),0 0 0 1px rgba(255,255,255,.03) inset,
0 0 60px rgba(70,120,255,.10);
background:#060a16;
}
canvas{display:block;width:100%;height:auto;touch-action:none}
footer{
display:flex;gap:14px;flex-wrap:wrap;justify-content:center;align-items:center;
font-size:11px;color:#7d8db5;letter-spacing:.06em;
}
kbd{
background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.16);
border-bottom-width:2px;border-radius:5px;padding:2px 6px;font-size:10px;
font-family:inherit;color:#cfe0ff;
}
.hint{color:#8b9dc7}
@media (max-width:520px){
.stat{min-width:62px;padding:5px 8px}
.stat .val{font-size:15px}
footer{font-size:10px;gap:9px}
}
</style>
</head>
<body>
<div id="wrap">
<header>
<div class="brand"><span class="dot"></span>NEON BREAKOUT</div>
<div class="stats">
<div class="stat"><div class="lab">Score</div><div class="val" id="score">0</div></div>
<div class="stat"><div class="lab">Lives</div><div class="val" id="lives">♥♥♥</div></div>
<div class="stat"><div class="lab">Level</div><div class="val" id="level">1</div></div>
<div class="stat"><div class="lab">Bricks</div><div class="val" id="left">0</div></div>
<div class="stat"><div class="lab">Best</div><div class="val" id="best">0</div></div>
</div>
</header>
<div id="stage"><canvas id="cv"></canvas></div>
<footer>
<span><kbd>←</kbd><kbd>→</kbd> / mouse to move</span>
<span class="hint">Space = launch & pause</span>
<span class="hint">Catch power-ups: wide paddle (10s) or multi-ball</span>
</footer>
</div>
<script>
(() => {
'use strict';
/* ---------------- Canvas setup ---------------- */
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const stage = document.getElementById('stage');
// Logical resolution
const W = 900, H = 620;
let dpr = Math.min(window.devicePixelRatio || 1, 2);
function resize(){
const rect = stage.getBoundingClientRect();
const cssW = Math.max(300, Math.floor(rect.width));
const cssH = Math.round(cssW * H / W);
stage.style.height = cssH + 'px';
dpr = Math.min(window.devicePixelRatio || 1, 2);
cv.width = Math.floor(cssW * dpr);
cv.height = Math.floor(cssH * dpr);
cv.style.width = cssW + 'px';
cv.style.height = cssH + 'px';
ctx.setTransform(dpr * cssW / W, 0, 0, dpr * cssW / W, 0, 0);
}
window.addEventListener('resize', resize);
const COLS = 13, ROWS = 8;
const TOP = 62;
const SIDE = 26;
const brickAreaW = W - SIDE * 2;
const BW = brickAreaW / COLS; // brick width (cell)
const BH = 26; // brick height
const GAP = 6;
/* ---------------- Game state ---------------- */
const state = {
screen: 'title', // title | play | over | win
score: 0,
lives: 3,
bricksLeft: 0,
balls: [],
powerups: [],
particles: [],
shake: 0,
wideTimer: 0,
flash: 0,
t: 0,
messageTimer: 0,
message: ''
};
const paddle = {
x: W/2, y: H - 46, w: 110, h: 14,
targetX: W/2, speed: 0, glow: 0
};
const BASE_PADDLE_W = 110;
let bricks = [];
let keys = {left:false, right:false};
let mouseActive = false;
let best = 0;
try { best = parseInt(localStorage.getItem('nb_best') || '0', 10) || 0; } catch(e){ best = 0; }
const el = {
score: document.getElementById('score'),
lives: document.getElementById('lives'),
level: document.getElementById('level'),
left: document.getElementById('left'),
best: document.getElementById('best')
};
/* ---------------- Level generation ---------------- */
const NORMAL_COLORS = ['#5ee7ff','#63b3ff','#8f7bff','#c56cff','#ff6fd8','#ff7a9c'];
const HARD_COLORS = ['#ffb347','#ff8a3d'];
function makeLevel(){
bricks = [];
let count = 0;
// Layout: 8 rows of 13. Rows 0-1 fully filled, then a decorative pattern.
for (let r = 0; r < ROWS; r++){
for (let c = 0; c < COLS; c++){
let exists = false;
if (r <= 1) exists = true;
else {
// wave pattern in lower rows
const wave = Math.abs(Math.sin((c + r*1.7) * 0.55));
exists = wave > 0.34 || (r === 7 && c % 3 === 0);
}
if (!exists) continue;
// tough bricks: some in lower-middle & edges
let tough = (r >= 3 && ((c + r) % 4 === 0)) || (r === ROWS-1 && c % 2 === 0);
// keep some tough bricks but ensure >= 20 normal bricks
let hasPower = false;
if (!tough && ((c * 7 + r * 3) % 11 === 0) || (tough && (c*3+r) % 17 === 0)){
hasPower = true;
}
const x = SIDE + c * BW + GAP/2;
const y = TOP + r * (BH + GAP);
const w = BW - GAP;
const h = BH;
bricks.push({
x, y, w, h,
hp: tough ? 2 : 1,
maxHp: tough ? 2 : 1,
tough,
hasPower,
color: tough ? HARD_COLORS[(c+r)%HARD_COLORS.length] : NORMAL_COLORS[(r*2+c)%NORMAL_COLORS.length],
alive: true,
crack: 0,
pulse: Math.random()*Math.PI*2,
hitFlash: 0
});
count++;
}
}
// Guarantee at least 6 rows of bricks exist
for (let r = 0; r < 6; r++){
const hasRow = bricks.some(b => b.alive && Math.abs((b.y - TOP)/(BH+GAP) - r) < 0.4);
if (!hasRow){
for (let c = 0; c < COLS; c += 2){
const x = SIDE + c * BW + GAP/2;
const y = TOP + r * (BH + GAP);
bricks.push({
x, y, w: BW - GAP, h: BH, hp:1, maxHp:1, tough:false,
hasPower:(c+r)%5===0, color:NORMAL_COLORS[r%NORMAL_COLORS.length],
alive:true, crack:0, pulse:Math.random()*Math.PI*2, hitFlash:0
});
}
}
}
state.bricksLeft = bricks.filter(b=>b.alive).length;
}
function refreshHUD(){
el.score.textContent = state.score;
el.lives.textContent = state.lives > 0 ? '♥'.repeat(state.lives) : '—';
el.lives.style.color = state.lives <= 1 ? '#ff6b8a' : '#ff8fb0';
el.level.textContent = '1';
el.left.textContent = state.bricksLeft;
el.best.textContent = Math.max(best, state.score);
}
/* ---------------- Ball ---------------- */
function makeBall(x, y, vx, vy, r){
return { x, y, vx, vy, r: r||8, speed: 0, trail: [], stuck: true, attached: null };
}
function launch(b){
b.stuck = false;
b.attached = null;
const speed = 430;
const ang = -Math.PI/2 + (Math.random()*0.9 - 0.45);
b.vx = Math.cos(ang)*speed;
b.vy = Math.sin(ang)*speed;
}
function resetBall(){
state.balls = [makeBall(paddle.x, paddle.y - 22, 0, 0)];
state.balls[0].stuck = true;
state.balls[0].attached = paddle;
}
/* ---------------- Input ---------------- */
function pointerX(clientX){
const rect = cv.getBoundingClientRect();
return (clientX - rect.left) / rect.width * W;
}
window.addEventListener('pointermove', (e)=>{
const px = pointerX(e.clientX);
if (state.screen === 'title'){ startGame(); }
paddle.targetX = px;
mouseActive = true;
});
cv.addEventListener('pointerdown', (e)=>{
const px = pointerX(e.clientX);
paddle.targetX = px; mouseActive = true;
if (state.screen === 'title'){ startGame(); return; }
if (state.screen === 'over' || state.screen === 'win'){ startGame(); return; }
if (state.screen === 'play'){
// launch any stuck ball
for(const b of state.balls) if (b.stuck) launch(b);
}
});
window.addEventListener('keydown', (e)=>{
if (e.code === 'ArrowLeft' || e.code === 'ArrowRight'){
keys[e.code === 'ArrowLeft' ? 'left' : 'right'] = true;
mouseActive = false;
e.preventDefault();
if (state.screen === 'title') startGame();
}
if (e.code === 'Space' || e.code === 'Enter'){
e.preventDefault();
if (state.screen === 'title' || state.screen === 'over' || state.screen === 'win'){ startGame(); return; }
if (state.screen === 'play'){
for(const b of state.balls) if (b.stuck) launch(b);
}
}
if (e.code === 'KeyP' && state.screen === 'play'){
state.screen = state.screen === 'pause' ? 'play' : 'pause';
}
});
window.addEventListener('keyup', (e)=>{
if (e.code === 'ArrowLeft') keys.left = false;
if (e.code === 'ArrowRight') keys.right = false;
});
window.addEventListener('blur', ()=>{ keys.left=false; keys.right=false; });
/* ---------------- Start / restart ---------------- */
function startGame(){
state.score = 0;
state.lives = 3;
state.wideTimer = 0;
state.screen = 'play';
state.powerups.length = 0;
state.particles.length = 0;
makeLevel();
paddle.w = BASE_PADDLE_W;
paddle.x = paddle.targetX = W/2;
resetBall();
refreshHUD();
}
function gameOver(){
state.screen = 'over';
if (state.score > best){
best = state.score;
try { localStorage.setItem('nb_best', String(best)); } catch(e){}
}
refreshHUD();
}
function victory(){
state.screen = 'win';
if (state.score > best){
best = state.score;
try { localStorage.setItem('nb_best', String(best)); } catch(e){}
}
refreshHUD();
}
/* ---------------- Power-ups ---------------- */
const POWERS = [
{ type:'wide', label:'WIDE', color:'#6cffb0', icon:'⇔' },
{ type:'multi', label:'MULTI', color:'#ffd166', icon:'⋮' },
{ type:'wide', label:'WIDE', color:'#6cffb0', icon:'⇔' }
];
function dropPower(b){
const p = POWERS[Math.floor(Math.random()*POWERS.length)];
state.powerups.push({
x: b.x + b.w/2, y: b.y + b.h/2,
vy: 150, type: p.type, color: p.color, icon: p.icon,
label: p.label, life: 12, rot: 0
});
}
function applyPower(p){
if (p.type === 'wide'){
paddle.w = BASE_PADDLE_W * 1.75;
state.wideTimer = 10;
state.message = 'WIDE PADDLE!';
state.messageTimer = 1.6;
} else {
const src = state.balls.filter(b=>!b.stuck);
const base = src.length ? src[0] : state.balls[0];
for (let i = 0; i < 2; i++){
const ang = Math.atan2(base.vy, base.vx) + (i===0 ? 0.45 : -0.45);
const sp = Math.hypot(base.vx, base.vy) || 430;
state.balls.push(makeBall(base.x, base.y, Math.cos(ang)*sp, Math.sin(ang)*sp, base.r));
}
state.message = 'MULTI-BALL!';
state.messageTimer = 1.6;
}
}
/* ---------------- Particles ---------------- */
function spawnParticles(x, y, color, n, power){
for (let i = 0; i < n; i++){
const a = Math.random()*Math.PI*2;
const sp = (60 + Math.random()*260) * (power||1);
state.particles.push({
x, y, vx: Math.cos(a)*sp, vy: Math.sin(a)*sp - 40,
life: 0.4 + Math.random()*0.55, max: 0.95,
size: 1.5 + Math.random()*3.2,
color, g: 460
});
}
}
function spawnShock(x, y, color){
state.particles.push({ring:true, x, y, r:6, vr:340, life:0.45, max:0.45, color});
}
/* ---------------- Collision helpers ---------------- */
function clampBall(b){
const speed = Math.hypot(b.vx, b.vy);
const min = 300, max = 640;
if (speed > max){ b.vx = b.vx/speed*max; b.vy = b.vy/speed*max; }
if (speed < min){ b.vx = b.vx/speed*min; b.vy = b.vy/speed*min; }
// avoid near-horizontal
const horiz = Math.abs(b.vx) < 0.22*Math.abs(b.vy);
if (horiz){
const sign = b.vx >= 0 ? 1 : -1;
const ang = Math.atan2(b.vy, b.vx);
let na = ang;
if (Math.abs(na) < 0.24) na = 0.24*sign;
if (Math.abs(Math.abs(na) - Math.PI) < 0.24) na = Math.PI - 0.24*sign;
const sp = Math.hypot(b.vx, b.vy);
b.vx = Math.cos(na)*sp; b.vy = Math.sin(na)*sp;
}
}
function rectCircle(rx, ry, rw, rh, cx, cy, cr){
const nx = Math.max(rx, Math.min(cx, rx+rw));
const ny = Math.max(ry, Math.min(cy, ry+rh));
const dx = cx-nx, dy = cy-ny;
return dx*dx + dy*dy <= cr*cr;
}
function reflectFromRect(b, rect){
// Determine the closest edge and push out along its normal.
const overlaps = [
{d: Math.abs((b.x + b.r) - rect.x), nx: 1, ny: 0},
{d: Math.abs(rect.x + rect.w - (b.x - b.r)), nx: -1, ny: 0},
{d: Math.abs((b.y + b.r) - rect.y), nx: 0, ny: -1},
{d: Math.abs(rect.y + rect.h - (b.y - b.r)), nx: 0, ny: 1}
];
overlaps.sort((a,c)=>a.d-c.d);
const o = overlaps[0];
if (o.nx !== 0){ b.vx = Math.abs(b.vx)*o.nx; b.x += o.nx * 2; }
else { b.vy = Math.abs(b.vy)*o.ny; b.y += o.ny * 2; }
}
function hitBrick(b, br){
if (!br.alive) return false;
br.hitFlash = 1;
if (br.hp > 1){
br.hp--;
br.crack = 1;
spawnParticles(b.x, b.y, '#ffffff', 6, 0.6);
state.score += 5;
return true;
}
br.alive = false;
state.bricksLeft--;
state.score += 10;
state.shake = Math.min(9, state.shake + 3);
spawnParticles(br.x + br.w/2, br.y + br.h/2, br.color, 20, 1);
spawnShock(br.x + br.w/2, br.y + br.h/2, br.color);
if (br.hasPower) dropPower(br);
return true;
}
/* ---------------- Update ---------------- */
function update(dt){
state.t += dt;
state.shake *= Math.pow(0.0015, dt);
state.flash = Math.max(0, state.flash - dt*3);
if (state.messageTimer > 0) state.messageTimer -= dt;
// particles always animate
for (let i = state.particles.length - 1; i >= 0; i--){
const p = state.particles[i];
p.life -= dt;
if (p.ring){
p.r += p.vr*dt;
if (p.life <= 0) state.particles.splice(i,1);
continue;
}
p.vy += p.g*dt;
p.x += p.vx*dt;
p.y += p.vy*dt;
p.vx *= Math.pow(0.35, dt);
if (p.life <= 0) state.particles.splice(i,1);
}
if (state.screen === 'title'){
// Idle demo animation: paddle drifts, ball bounces on a mini-preview
updateParticlesOnly(dt);
return;
}
if (state.screen !== 'play') return;
// wide paddle timer
if (state.wideTimer > 0){
state.wideTimer -= dt;
if (state.wideTimer <= 0){
paddle.w = BASE_PADDLE_W;
state.wideTimer = 0;
state.message = 'PADDLE NORMALIZED';
state.messageTimer = 1.2;
}
}
// paddle movement
const prevX = paddle.x;
if (keys.left || keys.right){
const sp = 620;
if (keys.left) paddle.x -= sp*dt;
if (keys.right) paddle.x += sp*dt;
paddle.targetX = paddle.x;
} else {
const diff = paddle.targetX - paddle.x;
paddle.x += diff * Math.min(1, dt*18);
}
paddle.x = Math.max(paddle.w/2 + 4, Math.min(W - paddle.w/2 - 4, paddle.x));
paddle.speed = (paddle.x - prevX)/Math.max(dt, 0.0001);
// power-ups
for (let i = state.powerups.length - 1; i >= 0; i--){
const p = state.powerups[i];
p.y += p.vy*dt;
p.rot += dt*2.4;
p.life -= dt;
if (p.life <= 0){ state.powerups.splice(i,1); continue; }
// paddle catch
if (p.y + 12 >= paddle.y - paddle.h/2 && p.y - 12 <= paddle.y + paddle.h/2 &&
p.x >= paddle.x - paddle.w/2 - 6 && p.x <= paddle.x + paddle.w/2 + 6){
applyPower(p);
spawnParticles(p.x, p.y, p.color, 18, 1);
spawnShock(p.x, p.y, p.color);
state.powerups.splice(i,1);
continue;
}
if (p.y > H + 30){ state.powerups.splice(i,1); }
}
// balls
const subSteps = 3;
const sdt = dt / subSteps;
for (let bi = 0; bi < state.balls.length; bi++){
const b = state.balls[bi];
if (b.stuck){
b.x = paddle.x;
b.y = paddle.y - paddle.h/2 - b.r - 2;
continue;
}
for (let s = 0; s < subSteps; 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); }
if (b.x + b.r > W){ b.x = W - b.r; b.vx = -Math.abs(b.vx); }
if (b.y - b.r < 0){ b.y = b.r; b.vy = Math.abs(b.vy); }
// paddle
if (b.vy > 0 &&
rectCircle(paddle.x - paddle.w/2, paddle.y - paddle.h/2, paddle.w, paddle.h, b.x, b.y, b.r)){
// angle based on hit offset
const rel = (b.x - paddle.x) / (paddle.w/2); // -1..1
const clamped = Math.max(-1, Math.min(1, rel));
let ang = -Math.PI/2 + clamped * (Math.PI/3.1); // max ~58deg
const sp = Math.max(330, Math.min(640, Math.hypot(b.vx, b.vy) + Math.abs(paddle.speed)*0.06));
b.vx = Math.cos(ang)*sp;
b.vy = Math.sin(ang)*sp;
b.y = paddle.y - paddle.h/2 - b.r - 0.5;
clampBall(b);
spawnParticles(b.x, paddle.y - paddle.h/2, '#9fd8ff', 4, 0.5);
}
// bricks
for (let k = 0; k < bricks.length; k++){
const br = bricks[k];
if (!br.alive) continue;
if (rectCircle(br.x, br.y, br.w, br.h, b.x, b.y, b.r)){
reflectFromRect(b, br);
hitBrick(b, br);
clampBall(b);
break;
}
}
// bottom
if (b.y - b.r > H){
b.dead = true;
break;
}
}
// trail
b.trail.push({x:b.x, y:b.y});
if (b.trail.length > 12) b.trail.shift();
}
// remove dead balls
for (let i = state.balls.length - 1; i >= 0; i--){
if (state.balls[i].dead) state.balls.splice(i,1);
}
if (state.balls.length === 0){
state.lives--;
state.shake = 8;
state.flash = 1;
if (state.lives <= 0){
refreshHUD();
gameOver();
return;
}
state.wideTimer = 0;
paddle.w = BASE_PADDLE_W;
resetBall();
state.message = 'BALL LOST — ' + state.lives + (state.lives===1?' LIFE LEFT':' LIVES LEFT');
state.messageTimer = 2;
}
if (state.bricksLeft <= 0){
victory();
}
refreshHUD();
}
function updateParticlesOnly(dt){
for (let i = state.particles.length - 1; i >= 0; i--){
const p = state.particles[i];
p.life -= dt;
if (p.ring){ p.r += p.vr*dt; if (p.life<=0) state.particles.splice(i,1); continue; }
p.vy += p.g*dt; p.x += p.vx*dt; p.y += p.vy*dt;
if (p.life <= 0) state.particles.splice(i,1);
}
}
/* ---------------- Rendering helpers ---------------- */
function roundRect(c, x, y, w, h, r){
r = Math.min(r, w/2, h/2);
c.beginPath();
c.moveTo(x+r, y);
c.lineTo(x+w-r, y); c.quadraticCurveTo(x+w, y, x+w, y+r);
c.lineTo(x+w, y+h-r); c.quadraticCurveTo(x+w, y+h, x+w-r, y+h);
c.lineTo(x+r, y+h); c.quadraticCurveTo(x, y+h, x, y+h-r);
c.lineTo(x, y+r); c.quadraticCurveTo(x, y, x+r, y);
c.closePath();
}
function drawGrid(t){
ctx.save();
ctx.strokeStyle = 'rgba(120,170,255,0.055)';
ctx.lineWidth = 1;
for (let x = 0; x <= W; x += 45){ ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); }
for (let y = 0; y <= H; y += 45){ ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }
// subtle moving glow bar
const gy = (t*40) % (H+300) - 150;
const grad = ctx.createLinearGradient(0, gy-140, 0, gy+140);
grad.addColorStop(0, 'rgba(90,160,255,0)');
grad.addColorStop(0.5, 'rgba(90,160,255,0.055)');
grad.addColorStop(1, 'rgba(90,160,255,0)');
ctx.fillStyle = grad;
ctx.fillRect(0, gy-140, W, 280);
ctx.restore();
}
function drawBricks(t){
for (const b of bricks){
if (!b.alive) continue;
const pulse = 0.5 + 0.5*Math.sin(t*2.2 + b.pulse);
ctx.save();
// glow
ctx.shadowColor = b.color;
ctx.shadowBlur = b.tough ? 16 : 10;
// body
const g = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
g.addColorStop(0, b.color);
g.addColorStop(1, shade(b.color, -0.42));
roundRect(ctx, b.x, b.y, b.w, b.h, 5);
ctx.fillStyle = g;
ctx.globalAlpha = b.tough ? 0.95 : 0.9;
ctx.fill();
ctx.shadowBlur = 0;
ctx.globalAlpha = 1;
// top highlight
ctx.fillStyle = 'rgba(255,255,255,'+(0.18 + pulse*0.14)+')';
roundRect(ctx, b.x+2, b.y+2, b.w-4, Math.max(3, b.h*0.24), 3);
ctx.fill();
// border
ctx.strokeStyle = b.tough ? 'rgba(255,255,255,0.55)' : 'rgba(255,255,255,0.22)';
ctx.lineWidth = b.tough ? 1.6 : 1;
ctx.stroke();
// tough brick rivets
if (b.tough){
ctx.fillStyle = 'rgba(255,255,255,0.75)';
ctx.beginPath(); ctx.arc(b.x+7, b.y+b.h/2, 2, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(b.x+b.w-7, b.y+b.h/2, 2, 0, Math.PI*2); ctx.fill();
}
// cracks after first hit
if (b.crack > 0){
ctx.strokeStyle = 'rgba(10,12,22,0.85)';
ctx.lineWidth = 1.6;
ctx.beginPath();
const mx = b.x + b.w*0.5, my = b.y + b.h*0.5;
ctx.moveTo(b.x+4, b.y+3);
ctx.lineTo(mx-2, my-1);
ctx.lineTo(b.x+b.w*0.62, b.y+b.h-3);
ctx.moveTo(mx-2, my-1);
ctx.lineTo(b.x+b.w-4, b.y+4);
ctx.moveTo(mx-2, my-1);
ctx.lineTo(b.x+6, b.y+b.h-3);
ctx.stroke();
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
ctx.lineWidth = 0.8;
ctx.stroke();
// damage tint
ctx.fillStyle = 'rgba(255,120,90,0.10)';
roundRect(ctx, b.x, b.y, b.w, b.h, 5);
ctx.fill();
}
// hit flash
if (b.hitFlash > 0){
ctx.fillStyle = 'rgba(255,255,255,'+(b.hitFlash*0.55)+')';
roundRect(ctx, b.x, b.y, b.w, b.h, 5);
ctx.fill();
b.hitFlash = Math.max(0, b.hitFlash - 0.06);
}
// power-up icon
if (b.hasPower){
ctx.save();
ctx.translate(b.x + b.w/2, b.y + b.h/2);
ctx.rotate(Math.sin(t*1.6 + b.pulse)*0.14);
ctx.fillStyle = 'rgba(12,16,30,0.55)';
ctx.beginPath(); ctx.arc(0,0, Math.min(b.w,b.h)*0.32, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#0b1020';
ctx.font = 'bold ' + Math.round(b.h*0.62) + 'px "Segoe UI Symbol", "Segoe UI", sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('⚡', 0, 1);
ctx.restore();
}
ctx.restore();
}
}
function shade(hex, amt){
const n = parseInt(hex.slice(1), 16);
let r = (n>>16)&255, g = (n>>8)&255, b = n&255;
const f = amt < 0 ? 1+amt : 1;
const add = amt > 0 ? 255*amt : 0;
r = Math.round(r*f + add); g = Math.round(g*f + add); b = Math.round(b*f + add);
return 'rgb('+Math.max(0,Math.min(255,r))+','+Math.max(0,Math.min(255,g))+','+Math.max(0,Math.min(255,b))+')';
}
function drawPaddle(t){
const x = paddle.x - paddle.w/2, y = paddle.y - paddle.h/2;
ctx.save();
ctx.shadowColor = state.wideTimer > 0 ? '#6cffb0' : '#5ee7ff';
ctx.shadowBlur = 22;
const g = ctx.createLinearGradient(x, y, x, y+paddle.h);
g.addColorStop(0, state.wideTimer > 0 ? '#b6ffd8' : '#d7f4ff');
g.addColorStop(0.45, state.wideTimer > 0 ? '#6cffb0' : '#5ee7ff');
g.addColorStop(1, state.wideTimer > 0 ? '#1f9c6a' : '#2a86c9');
roundRect(ctx, x, y, paddle.w, paddle.h, 7);
ctx.fillStyle = g; ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.lineWidth = 1; ctx.stroke();
// center marker
ctx.fillStyle = 'rgba(255,255,255,0.55)';
ctx.fillRect(paddle.x - 9, y + paddle.h*0.28, 18, 2.5);
ctx.restore();
}
function drawBalls(t){
for (const b of state.balls){
// trail
for (let i = 0; i < b.trail.length; i++){
const p = b.trail[i];
const a = (i/b.trail.length) * 0.42;
ctx.fillStyle = 'rgba(150,215,255,'+a+')';
ctx.beginPath(); ctx.arc(p.x, p.y, b.r*(i/b.trail.length)*0.95, 0, Math.PI*2); ctx.fill();
}
ctx.save();
ctx.shadowColor = '#9fe8ff'; ctx.shadowBlur = 20;
const g = ctx.createRadialGradient(b.x - b.r*0.35, b.y - b.r*0.4, 1, b.x, b.y, b.r);
g.addColorStop(0, '#ffffff');
g.addColorStop(0.45, '#bdf0ff');
g.addColorStop(1, '#3fa9e8');
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
}
function drawPowerups(t){
for (const p of state.powerups){
ctx.save();
ctx.translate(p.x, p.y);
// capsule
ctx.rotate(Math.sin(t*3 + p.rot)*0.12);
ctx.shadowColor = p.color; ctx.shadowBlur = 18;
const h = 24, w = 24;
roundRect(ctx, -w/2, -h/2, w, h, 8);
const g = ctx.createLinearGradient(0,-h/2,0,h/2);
g.addColorStop(0, '#ffffff');
g.addColorStop(0.3, p.color);
g.addColorStop(1, shade(p.color,-0.5));
ctx.fillStyle = g; ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = 'rgba(255,255,255,0.7)'; ctx.lineWidth = 1.2; ctx.stroke();
ctx.fillStyle = '#0b1020';
ctx.font = 'bold 14px "Segoe UI Symbol", "Segoe UI", sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(p.type === 'wide' ? '↔' : '✦', 0, 0.5);
ctx.restore();
// label
ctx.save();
ctx.globalAlpha = 0.9;
ctx.fillStyle = p.color;
ctx.font = 'bold 10px "Segoe UI", sans-serif';
ctx.textAlign = 'center';
ctx.fillText(p.label, p.x, p.y - 20);
ctx.restore();
}
}
function drawParticles(){
for (const p of state.particles){
if (p.ring){
ctx.save();
ctx.globalAlpha = Math.max(0, p.life/p.max);
ctx.strokeStyle = p.color; ctx.lineWidth = 3;
ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2); ctx.stroke();
ctx.restore();
continue;
}
const a = Math.max(0, p.life/p.max);
ctx.save();
ctx.globalAlpha = a;
ctx.fillStyle = p.color;
ctx.shadowColor = p.color; ctx.shadowBlur = 8;
ctx.beginPath(); ctx.arc(p.x, p.y, p.size*a + 0.4, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
}
function drawHudOverlay(){
// top in-canvas status bar
ctx.save();
ctx.fillStyle = 'rgba(8,12,26,0.55)';
ctx.fillRect(0, 0, W, 40);
ctx.strokeStyle = 'rgba(120,170,255,0.18)';
ctx.beginPath(); ctx.moveTo(0, 40.5); ctx.lineTo(W, 40.5); ctx.stroke();
ctx.font = 'bold 13px "Segoe UI", sans-serif';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#8fa0c8';
ctx.textAlign = 'left';
ctx.fillText('SCORE', 18, 20);
ctx.fillStyle = '#e9efff';
ctx.font = 'bold 17px "Segoe UI", sans-serif';
ctx.fillText(String(state.score), 18, 20);
ctx.textAlign = 'center';
ctx.fillStyle = '#8fa0c8'; ctx.font = 'bold 13px "Segoe UI", sans-serif';
ctx.fillText('LIVES', W/2 - 34, 20);
ctx.fillStyle = '#ff8fb0'; ctx.font = 'bold 17px "Segoe UI", sans-serif';
ctx.fillText('♥'.repeat(Math.max(0,state.lives)) || '-', W/2 + 16, 20);
ctx.textAlign = 'right';
ctx.fillStyle = '#8fa0c8'; ctx.font = 'bold 13px "Segoe UI", sans-serif';
ctx.fillText('BRICKS', W - 74, 20);
ctx.fillStyle = '#e9efff'; ctx.font = 'bold 17px "Segoe UI", sans-serif';
ctx.fillText(String(state.bricksLeft), W - 18, 20);
ctx.restore();
// wide paddle timer bar
if (state.wideTimer > 0){
ctx.save();
const w = 160, x = W/2 - w/2, y = 48;
ctx.fillStyle = 'rgba(108,255,176,0.18)';
roundRect(ctx, x, y, w, 7, 4); ctx.fill();
ctx.fillStyle = '#6cffb0';
roundRect(ctx, x, y, w*(state.wideTimer/10), 7, 4); ctx.fill();
ctx.fillStyle = '#b6ffd8';
ctx.font = 'bold 10px "Segoe UI", sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
ctx.fillText('WIDE PADDLE ' + state.wideTimer.toFixed(1) + 's', W/2, y - 3);
ctx.restore();
}
// center message
if (state.messageTimer > 0 && state.screen === 'play'){
const a = Math.min(1, state.messageTimer/0.5);
ctx.save();
ctx.globalAlpha = a;
ctx.fillStyle = 'rgba(10,16,32,0.55)';
roundRect(ctx, W/2 - 160, H/2 - 26, 320, 46, 12); ctx.fill();
ctx.strokeStyle = 'rgba(150,200,255,0.25)'; ctx.lineWidth = 1; ctx.stroke();
ctx.fillStyle = '#e9f3ff';
ctx.font = 'bold 20px "Segoe UI", sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(state.message, W/2, H/2 - 2);
ctx.restore();
}
}
function drawTitle(t){
// dim the field
ctx.save();
ctx.fillStyle = 'rgba(4,7,16,0.62)';
ctx.fillRect(0,0,W,H);
ctx.restore();
const cx = W/2, cy = H/2 - 30;
ctx.save();
ctx.textAlign = 'center';
// title
const g = ctx.createLinearGradient(0, cy-70, 0, cy+10);
g.addColorStop(0, '#7ff0ff');
g.addColorStop(0.5, '#b06cff');
g.addColorStop(1, '#ff7ad9');
ctx.font = '900 62px "Segoe UI", sans-serif';
ctx.fillStyle = g;
ctx.shadowColor = 'rgba(140,120,255,0.6)'; ctx.shadowBlur = 26;
ctx.fillText('NEON BREAKOUT', cx, cy);
ctx.shadowBlur = 0;
ctx.fillStyle = '#a8bcdd';
ctx.font = '15px "Segoe UI", sans-serif';
ctx.fillText('Clear every brick. Catch the ⚡ power-ups. Don’t lose the ball.', cx, cy + 32);
ctx.restore();
// pulsing prompt
const pulse = 0.55 + 0.45*Math.sin(t*4);
ctx.save();
ctx.globalAlpha = pulse;
ctx.fillStyle = '#eaf4ff';
ctx.font = 'bold 20px "Segoe UI", sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('Click or press a key to start', cx, cy + 96);
ctx.restore();
// demo hint panel
ctx.save();
ctx.globalAlpha = 0.85;
ctx.fillStyle = 'rgba(12,18,36,0.6)';
roundRect(ctx, cx-230, cy+128, 460, 46, 12); ctx.fill();
ctx.strokeStyle = 'rgba(120,170,255,0.18)'; ctx.lineWidth = 1; ctx.stroke();
ctx.fillStyle = '#9db3d9';
ctx.font = '13px "Segoe UI", sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('← → or mouse to move the paddle • Space to launch the ball', cx, cy+151);
ctx.restore();
}
function drawEndScreen(t, win){
ctx.save();
ctx.fillStyle = win ? 'rgba(6,20,14,0.66)' : 'rgba(18,6,14,0.68)';
ctx.fillRect(0,0,W,H);
ctx.restore();
const cx = W/2, cy = H/2 - 20;
ctx.save();
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
const g = ctx.createLinearGradient(0, cy-60, 0, cy+20);
if (win){ g.addColorStop(0,'#8dffc4'); g.addColorStop(1,'#4fd1ff'); }
else { g.addColorStop(0,'#ff9bb5'); g.addColorStop(1,'#c86cff'); }
ctx.font = '900 58px "Segoe UI", sans-serif';
ctx.fillStyle = g;
ctx.shadowColor = win ? 'rgba(90,255,180,0.55)' : 'rgba(255,110,160,0.5)';
ctx.shadowBlur = 26;
ctx.fillText(win ? 'VICTORY!' : 'GAME OVER', cx, cy-10);
ctx.shadowBlur = 0;
ctx.fillStyle = '#c6d6f2';
ctx.font = 'bold 20px "Segoe UI", sans-serif';
ctx.fillText('Final score: ' + state.score, cx, cy + 34);
ctx.fillStyle = '#8fa0c8';
ctx.font = '14px "Segoe UI", sans-serif';
ctx.fillText('Best: ' + Math.max(best, state.score), cx, cy + 60);
ctx.restore();
const pulse = 0.55 + 0.45*Math.sin(t*4);
ctx.save();
ctx.globalAlpha = pulse;
ctx.fillStyle = '#eaf4ff';
ctx.font = 'bold 19px "Segoe UI", sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('Click or press a key to play again', cx, cy + 108);
ctx.restore();
}
/* ---------------- Main render ---------------- */
function render(){
ctx.save();
// screen shake
if (state.shake > 0.2){
const sx = (Math.random()*2-1)*state.shake;
const sy = (Math.random()*2-1)*state.shake;
ctx.translate(sx, sy);
}
// background
const bg = ctx.createLinearGradient(0,0,0,H);
bg.addColorStop(0,'#0a1024');
bg.addColorStop(0.5,'#070b18');
bg.addColorStop(1,'#050810');
ctx.fillStyle = bg;
ctx.fillRect(-20,-20,W+40,H+40);
drawGrid(state.t);
drawBricks(state.t);
drawPowerups(state.t);
drawPaddle(state.t);
drawBalls(state.t);
drawParticles();
ctx.restore();
// walls glow
ctx.save();
ctx.strokeStyle = 'rgba(110,170,255,0.35)';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(1.5, 40); ctx.lineTo(1.5, H); ctx.moveTo(W-1.5, 40); ctx.lineTo(W-1.5, H);
ctx.stroke();
ctx.restore();
drawHudOverlay();
if (state.screen === 'title') drawTitle(state.t);
else if (state.screen === 'over') drawEndScreen(state.t, false);
else if (state.screen === 'win') drawEndScreen(state.t, true);
else if (state.screen === 'pause'){
ctx.save();
ctx.fillStyle = 'rgba(4,8,18,0.6)'; ctx.fillRect(0,0,W,H);
ctx.fillStyle = '#eaf4ff'; ctx.font='bold 40px "Segoe UI",sans-serif';
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText('PAUSED', W/2, H/2);
ctx.font='14px "Segoe UI",sans-serif'; ctx.fillStyle='#93a7cc';
ctx.fillText('Press P or click to resume', W/2, H/2+34);
ctx.restore();
}
// ready prompt when ball is stuck
if (state.screen === 'play' && state.balls.length && state.balls[0].stuck){
const pulse = 0.55 + 0.45*Math.sin(state.t*5);
ctx.save();
ctx.globalAlpha = pulse;
ctx.fillStyle = '#dff0ff';
ctx.font = 'bold 16px "Segoe UI", sans-serif';
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText('Click or press SPACE to launch', W/2, H - 96);
ctx.restore();
}
}
/* ---------------- Loop ---------------- */
let last = performance.now();
let demoTimer = 0;
function frame(now){
let dt = (now - last)/1000;
last = now;
if (dt > 0.05) dt = 0.05;
if (state.screen === 'title'){
// Self-playing demo behind the title: paddle tracks a demo ball bouncing around.
demoTimer += dt;
if (demoTimer > 2.4){ demoTimer = 0; }
if (!state.demoBall) state.demoBall = {x: W/2, y: 120, vx: 190, vy: 230};
const db = state.demoBall;
db.x += db.vx*dt; db.y += db.vy*dt;
if (db.x < 12 || db.x > W-12) db.vx *= -1;
if (db.y < 60) db.vy = Math.abs(db.vy);
if (db.y > H-70){ db.vy = -Math.abs(db.vy); db.x = Math.max(40, Math.min(W-40, paddle.x + (Math.random()*2-1)*220)); }
paddle.targetX = db.x;
paddle.x += (db.x - paddle.x) * Math.min(1, dt*3.2);
updateParticlesOnly(dt);
} else {
update(dt);
}
render();
requestAnimationFrame(frame);
}
/* ---------------- Init ---------------- */
function init(){
resize();
makeLevel();
resetBall();
// hide bricks during title? keep them for visual richness
refreshHUD();
requestAnimationFrame(frame);
}
init();
})();
</script>
</body>
</html>