MiMo-V2.6-Pro
by Xiaomireasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 2 min 44 s
- Cost
- $0.0091
- Output tokens
- 13,753
- Spent thinking
- 0 tokens
- Speed
- 84.9 tok/s
- Console errors
- 0
- First token after
- 2.3 s
- First visible text after
- 2.3 s
- Prompt tokens
- 513
- Code size
- 39.2 KB
- Finish reason
- stop
- Served by
- GMICloud
- Model id
xiaomi/mimo-v2.6-pro- Reasoning sent
model default- Max output
- 131,072 tokens
- Generated
- Nov 13, 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 · 1350 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Neon Breaker</title>
<style>
:root{
--bg0:#070912;
--bg1:#101629;
--ink:#eef2ff;
--muted:#8e9ac2;
--cyan:#42e8ff;
--pink:#ff4d7d;
--gold:#ffd166;
}
*{box-sizing:border-box;}
html,body{
margin:0; padding:0;
height:100%;
background:
radial-gradient(1100px 700px at 20% -10%, #1c2a4d 0%, transparent 60%),
radial-gradient(900px 600px at 110% 110%, #2a1436 0%, transparent 55%),
linear-gradient(160deg, var(--bg0), var(--bg1));
color:var(--ink);
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
overflow:hidden;
-webkit-font-smoothing:antialiased;
user-select:none;
-webkit-user-select:none;
touch-action:none;
}
body{
display:flex;
align-items:center;
justify-content:center;
padding:clamp(10px, 2.2vh, 26px) clamp(10px, 2vw, 26px);
}
.shell{
width:min(1120px, 100%);
display:flex;
flex-direction:column;
gap:clamp(10px, 1.6vh, 18px);
position:relative;
z-index:1;
}
header{
display:flex;
align-items:flex-end;
justify-content:space-between;
gap:18px;
padding:0 4px;
}
.brand{display:flex; align-items:center; gap:12px;}
.mark{
width:42px; height:42px; flex:none;
border-radius:13px;
background:linear-gradient(145deg, var(--cyan), #6b6cff 55%, var(--pink));
box-shadow:0 10px 28px rgba(66,232,255,.28), inset 0 -8px 18px rgba(0,0,0,.28);
position:relative;
overflow:hidden;
}
.mark::after{
content:""; position:absolute; inset:0;
background:linear-gradient(120deg, transparent 35%, rgba(255,255,255,.55) 48%, transparent 62%);
mix-blend-mode:overlay;
}
h1{
margin:0;
font-size:clamp(18px, 2.4vw, 26px);
letter-spacing:.12em;
font-weight:800;
text-transform:uppercase;
}
.subtitle{
margin:3px 0 0;
color:var(--muted);
font-size:clamp(11px, 1.3vw, 13px);
letter-spacing:.16em;
text-transform:uppercase;
}
.legend{
display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end;
}
.chip{
display:inline-flex; align-items:center; gap:7px;
padding:7px 11px;
border-radius:999px;
background:rgba(255,255,255,.06);
border:1px solid rgba(255,255,255,.10);
color:#c6d2f5;
font-size:11px;
letter-spacing:.09em;
text-transform:uppercase;
white-space:nowrap;
}
.chip b{color:#fff; font-weight:700;}
.dot{width:9px; height:9px; border-radius:50%;}
.dot.c{background:var(--cyan); box-shadow:0 0 10px var(--cyan);}
.dot.p{background:var(--pink); box-shadow:0 0 10px var(--pink);}
.dot.g{background:var(--gold); box-shadow:0 0 10px var(--gold);}
.stage{
position:relative;
width:100%;
border-radius:26px;
padding:clamp(8px, 1.4vw, 16px);
background:
linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.03));
border:1px solid rgba(255,255,255,.12);
box-shadow:
0 40px 90px rgba(0,0,0,.45),
0 2px 0 rgba(255,255,255,.06) inset;
overflow:hidden;
}
.stage::before{
content:"";
position:absolute; inset:0;
background:
radial-gradient(520px 320px at 50% -10%, rgba(66,232,255,.16), transparent 70%),
radial-gradient(420px 280px at 8% 110%, rgba(255,77,125,.14), transparent 70%);
pointer-events:none;
}
.canvasWrap{
position:relative;
width:100%;
border-radius:16px;
overflow:hidden;
background:rgba(6,9,20,.72);
box-shadow:0 18px 50px rgba(0,0,0,.35) inset, 0 1px 0 rgba(255,255,255,.08);
}
canvas{
display:block;
width:100%;
height:auto;
aspect-ratio: 16 / 10;
cursor:crosshair;
touch-action:none;
}
.overlay{
position:absolute;
inset:0;
display:flex;
align-items:center;
justify-content:center;
text-align:center;
padding:22px;
background:rgba(5,8,20,.62);
backdrop-filter: blur(7px) saturate(120%);
-webkit-backdrop-filter: blur(7px) saturate(120%);
opacity:1;
transition:opacity .28s ease;
z-index:3;
}
.overlay.hidden{opacity:0; pointer-events:none;}
.panel{
width:min(560px, 94%);
padding:clamp(20px, 3.4vw, 34px) clamp(18px, 3vw, 32px);
border-radius:22px;
background:
linear-gradient(180deg, rgba(22,32,62,.92), rgba(13,18,38,.94));
border:1px solid rgba(150,190,255,.22);
box-shadow:0 30px 80px rgba(0,0,0,.5), 0 0 60px rgba(66,232,255,.12) inset;
}
.kicker{
display:inline-block;
padding:6px 11px;
border-radius:999px;
background:rgba(66,232,255,.12);
border:1px solid rgba(66,232,255,.28);
color:var(--cyan);
font-size:11px;
letter-spacing:.18em;
text-transform:uppercase;
margin-bottom:14px;
}
.panel h2{
margin:0 0 12px;
font-size:clamp(26px, 5vw, 46px);
letter-spacing:.04em;
line-height:1.02;
background:linear-gradient(180deg, #ffffff, #b9c9ff);
-webkit-background-clip:text;
background-clip:text;
color:transparent;
}
.panel p{
margin:0 auto 22px;
max-width:440px;
color:#a9b6da;
font-size:clamp(13px, 1.8vw, 15px);
line-height:1.65;
}
.controls{
display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin-bottom:22px;
}
.key{
padding:8px 12px;
border-radius:11px;
background:rgba(255,255,255,.07);
border:1px solid rgba(255,255,255,.12);
color:#c9d6f8;
font-size:12px;
letter-spacing:.08em;
text-transform:uppercase;
}
.key b{color:#fff; font-weight:700; letter-spacing:.12em;}
button{
appearance:none; border:0; cursor:pointer;
font-family:inherit; font-weight:800;
letter-spacing:.12em; text-transform:uppercase;
padding:15px 30px;
border-radius:13px;
font-size:13px;
color:#031022;
background:linear-gradient(135deg, var(--cyan), #8ef1ff 40%, #b6c2ff);
box-shadow:0 14px 32px rgba(66,232,255,.28), 0 1px 0 rgba(255,255,255,.5) inset;
transition:transform .18s ease, box-shadow .18s ease, filter .18s ease;
}
button:hover{transform:translateY(-2px); filter:brightness(1.05);}
button:active{transform:translateY(1px) scale(.985);}
.hint{
margin-top:14px;
color:#6f7fae;
font-size:11px;
letter-spacing:.12em;
text-transform:uppercase;
}
.resultRow{
display:flex; gap:10px; justify-content:center; margin-bottom:22px;
}
.stat{
flex:1; min-width:120px;
padding:13px 10px;
border-radius:14px;
background:rgba(255,255,255,.07);
border:1px solid rgba(255,255,255,.12);
}
.stat .v{font-size:clamp(20px,4vw,28px); font-weight:800;}
.stat .l{color:#8b9bc6; font-size:10px; letter-spacing:.16em; text-transform:uppercase; margin-top:4px;}
.foot{
display:flex; justify-content:space-between; gap:14px;
color:#7a89b3; font-size:11px; letter-spacing:.12em; text-transform:uppercase;
padding:0 6px;
}
.foot .right{color:#65749c;}
@media (max-width: 620px){
header{flex-direction:column; align-items:flex-start; gap:12px;}
.legend{justify-content:flex-start;}
.foot{flex-direction:column; gap:6px;}
.panel{padding:22px 16px;}
}
@media (max-height: 620px){
header{display:none;}
.foot{display:none;}
body{padding:10px;}
}
</style>
</head>
<body>
<div class="shell">
<header>
<div class="brand">
<div class="mark" aria-hidden="true"></div>
<div>
<h1>Neon Breaker</h1>
<p class="subtitle">Single-level arcade demolition</p>
</div>
</div>
<div class="legend" aria-hidden="true">
<span class="chip"><span class="dot c"></span> Catch the <b>W</b> for wide paddle</span>
<span class="chip"><span class="dot g"></span> Catch the <b>M</b> for multi-ball</span>
<span class="chip"><span class="dot p"></span> Cracked bricks take <b>2 hits</b></span>
</div>
</header>
<div class="stage">
<div class="canvasWrap">
<canvas id="game" width="1280" height="800" aria-label="Breakout game"></canvas>
<div class="overlay" id="overlay">
<div class="panel">
<div class="kicker" id="kicker">Click or press a key to start</div>
<h2 id="overlayTitle">Break every brick</h2>
<p id="overlayText">
Steer the paddle with your mouse or the arrow keys. The ball bounces at sharper angles
when it catches the edge of the paddle — use that to carve through the wall.
</p>
<div class="controls" id="controls">
<div class="key"><b>Mouse</b> / <b>← →</b> move</div>
<div class="key"><b>Space</b> launch</div>
<div class="key"><b>P</b> pause</div>
</div>
<button id="startBtn">Start Game</button>
<div class="hint" id="hint">A self-playing demo runs until you take control.</div>
</div>
</div>
</div>
</div>
<div class="foot">
<div>3 lives · score & lives tracked above the field</div>
<div class="right">Vanilla JS · Canvas 2D</div>
</div>
</div>
<script>
(() => {
"use strict";
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d", { alpha: true });
const overlay = document.getElementById("overlay");
const overlayTitle = document.getElementById("overlayTitle");
const overlayText = document.getElementById("overlayText");
const kicker = document.getElementById("kicker");
const controls = document.getElementById("controls");
const hint = document.getElementById("hint");
const startBtn = document.getElementById("startBtn");
const W = 1280, H = 800;
// ---------- Physics (fixed step) ----------
const STEP = 1 / 120; // 120 Hz physics
const SUBSTEPS = 8; // swept ball -> collision samples
const MAX_BALL_SPEED = 1060;
// ---------- Layout ----------
const HUD_H = 88;
const FIELD_TOP = HUD_H;
const WALL = 14;
const BRICK_COLS = 12;
const BRICK_ROWS = 7;
const BRICK_GAP = 10;
const BRICK_H = 38;
const BRICK_TOP = FIELD_TOP + 54;
const BRICK_AREA_W = W - WALL * 2 - 26;
const BRICK_W = (BRICK_AREA_W - (BRICK_COLS - 1) * BRICK_GAP) / BRICK_COLS;
const BRICK_LEFT = WALL + 13;
// ---------- Palette ----------
const PALETTE = [
["#ff5f7e", "#ff2f65"],
["#ff9052", "#ff5d2e"],
["#ffd05c", "#ff9d2e"],
["#8fe86b", "#35c96b"],
["#42e8ff", "#2b8fff"],
["#7c8dff", "#4b4dff"],
["#d477ff", "#8b3dff"]
];
// ---------- State ----------
let state = "demo"; // demo | playing | paused | gameover | victory
let score = 0;
let lives = 3;
let combo = 0;
let lastTime = 0;
let accumulator = 0;
let flash = 0;
let shake = 0;
let message = null;
let messageTimer = 0;
const keys = Object.create(null);
let mouseActive = false;
const paddle = {
x: W / 2,
y: H - 74,
w: 190,
baseW: 190,
maxW: 320,
h: 22,
speed: 900,
wideTimer: 0,
glow: 0
};
let balls = [];
let bricks = [];
let drops = [];
let particles = [];
let sparkles = [];
// ---------- Utility ----------
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const lerp = (a, b, t) => a + (b - a) * t;
const rand = (a, b) => a + Math.random() * (b - a);
function roundRectPath(c, x, y, w, h, r) {
r = Math.min(r, w / 2, h / 2);
c.beginPath();
c.moveTo(x + r, y);
c.arcTo(x + w, y, x + w, y + h, r);
c.arcTo(x + w, y + h, x, y + h, r);
c.arcTo(x, y + h, x, y, r);
c.arcTo(x, y, x + w, y, r);
c.closePath();
}
function hexToRgb(hex) {
const h = hex.replace("#", "");
const n = parseInt(h, 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
function shade(hex, amt) {
const [r, g, b] = hexToRgb(hex);
const f = (v) => clamp(Math.round(v + amt), 0, 255);
return `rgb(${f(r)},${f(g)},${f(b)})`;
}
// ---------- Brick build ----------
function buildBricks() {
bricks = [];
for (let r = 0; r < BRICK_ROWS; r++) {
for (let c = 0; c < BRICK_COLS; c++) {
const x = BRICK_LEFT + c * (BRICK_W + BRICK_GAP);
const y = BRICK_TOP + r * (BRICK_H + BRICK_GAP);
// tougher bricks toward the middle of the wall
const centerBias = Math.abs(c - (BRICK_COLS - 1) / 2);
const toughChance = 0.16 + (3.4 - centerBias) * 0.10;
const tough = Math.random() < toughChance;
let power = null;
if (!tough && Math.random() < 0.16) {
power = Math.random() < 0.55 ? "wide" : "multi";
}
bricks.push({
x, y, w: BRICK_W, h: BRICK_H,
hp: tough ? 2 : 1,
maxHp: tough ? 2 : 1,
colors: PALETTE[r % PALETTE.length],
power,
alive: true,
hitFlash: 0,
row: r, col: c
});
}
}
}
// ---------- Balls ----------
function makeBall(x, y, vx, vy, stuck = false) {
return {
x, y,
r: 12,
vx, vy,
stuck,
stuckOffset: 0,
trail: [],
lastPaddleHit: -1
};
}
function launchBall(b, angleBias = 0) {
const angle = -Math.PI / 2 + angleBias;
const speed = 520;
b.vx = Math.cos(angle) * speed;
b.vy = Math.sin(angle) * speed;
b.stuck = false;
b.trail.length = 0;
}
function resetBallOnPaddle() {
balls = [];
const b = makeBall(paddle.x, paddle.y - paddle.h / 2 - 13, 0, -520, true);
b.stuckOffset = 0;
balls.push(b);
}
function totalBricksAlive() {
let n = 0;
for (const b of bricks) if (b.alive) n++;
return n;
}
// ---------- Effects ----------
function burst(x, y, colors, count = 18, speed = 420) {
for (let i = 0; i < count; i++) {
const a = Math.random() * Math.PI * 2;
const s = speed * (0.35 + Math.random() * 0.9);
particles.push({
x, y,
vx: Math.cos(a) * s,
vy: Math.sin(a) * s - rand(0, 90),
life: 0.5 + Math.random() * 0.55,
maxLife: 0.5 + Math.random() * 0.55,
size: 3 + Math.random() * 7,
color: colors[Math.floor(Math.random() * colors.length)],
rot: Math.random() * Math.PI,
vr: rand(-8, 8),
gravity: 420 + Math.random() * 380,
square: Math.random() < 0.65
});
}
for (let i = 0; i < 8; i++) {
const a = Math.random() * Math.PI * 2;
sparkles.push({
x, y,
vx: Math.cos(a) * speed * 0.35,
vy: Math.sin(a) * speed * 0.35,
life: 0.35 + Math.random() * 0.35,
maxLife: 0.5,
size: 2 + Math.random() * 3,
color: "#ffffff"
});
}
}
function showMessage(text, duration = 1.4) {
message = text;
messageTimer = duration;
}
// ---------- Collisions ----------
function collidePaddle(b, dt) {
const px = paddle.x - paddle.w / 2;
const py = paddle.y - paddle.h / 2;
if (b.vy > 0 &&
b.x + b.r > px &&
b.x - b.r < px + paddle.w &&
b.y + b.r > py &&
b.y - b.r < py + paddle.h + 6) {
b.y = py - b.r - 0.5;
const rel = clamp((b.x - paddle.x) / (paddle.w / 2), -1, 1);
const angle = rel * (Math.PI * 0.42); // center = straight up, edges = sharp
const speed = clamp(Math.hypot(b.vx, b.vy) * 1.012, 470, MAX_BALL_SPEED);
b.vx = Math.sin(angle) * speed + paddle.vx * 0.16;
b.vy = -Math.abs(Math.cos(angle) * speed);
const norm = Math.hypot(b.vx, b.vy);
b.vx = b.vx / norm * speed;
b.vy = b.vy / norm * speed;
combo = 0;
paddle.glow = 1;
burst(b.x, py, ["#42e8ff", "#8ef1ff", "#ffffff"], 7, 180);
}
}
function collideBrick(b, brick) {
if (!brick.alive) return false;
const bx = brick.x, by = brick.y, bw = brick.w, bh = brick.h;
const closestX = clamp(b.x, bx, bx + bw);
const closestY = clamp(b.y, by, by + bh);
const dx = b.x - closestX;
const dy = b.y - closestY;
const d2 = dx * dx + dy * dy;
if (d2 > b.r * b.r) return false;
// Determine dominant axis of penetration
const overlapX = (bw / 2 + b.r) - Math.abs(b.x - (bx + bw / 2));
const overlapY = (bh / 2 + b.r) - Math.abs(b.y - (by + bh / 2));
if (overlapX < overlapY) {
if (b.x < bx + bw / 2) b.x = bx - b.r - 0.2;
else b.x = bx + bw + b.r + 0.2;
b.vx = -b.vx;
} else {
if (b.y < by + bh / 2) b.y = by - b.r - 0.2;
else b.y = by + bh + b.r + 0.2;
b.vy = -b.vy;
}
hitBrick(brick);
return true;
}
function hitBrick(brick) {
brick.hp--;
brick.hitFlash = 1;
const colors = [brick.colors[0], brick.colors[1], "#ffffff"];
burst(brick.x + brick.w / 2, brick.y + brick.h / 2, colors, 7, 220);
if (brick.hp <= 0) {
brick.alive = false;
combo++;
const base = 100 + (brick.maxHp - 1) * 60;
const gain = base + Math.min(combo, 12) * 12;
score += gain;
burst(brick.x + brick.w / 2, brick.y + brick.h / 2, colors, 20, 430);
shake = Math.min(shake + 0.12, 0.42);
if (brick.power) {
drops.push({
x: brick.x + brick.w / 2,
y: brick.y + brick.h / 2,
vy: 180,
vx: 0,
r: 19,
type: brick.power,
wobble: Math.random() * Math.PI * 2
});
}
if (totalBricksAlive() === 0 && state === "playing") {
state = "victory";
showEndScreen(true);
}
} else {
// tough brick cracked after first hit
score += 25;
burst(brick.x + brick.w / 2, brick.y + brick.h / 2, ["#ffffff", brick.colors[0]], 5, 180);
}
}
function hitWall(b) {
if (b.x - b.r < WALL) {
b.x = WALL + b.r;
b.vx = Math.abs(b.vx);
burst(b.x, b.y, ["#42e8ff", "#8ef1ff"], 3, 120);
} else if (b.x + b.r > W - WALL) {
b.x = W - WALL - b.r;
b.vx = -Math.abs(b.vx);
burst(b.x, b.y, ["#42e8ff", "#8ef1ff"], 3, 120);
}
if (b.y - b.r < FIELD_TOP) {
b.y = FIELD_TOP + b.r;
b.vy = Math.abs(b.vy);
burst(b.x, b.y, ["#42e8ff", "#8ef1ff"], 3, 120);
}
}
// ---------- Power-ups ----------
function collectDrop(d) {
if (d.type === "wide") {
paddle.wideTimer = 10;
paddle.w = Math.min(paddle.w + 110, paddle.maxW);
showMessage("WIDE PADDLE · 10s");
burst(d.x, d.y, ["#42e8ff", "#8ef1ff", "#ffffff"], 18, 320);
} else {
// Multi-ball: top up to 3 live balls
const existing = balls.filter(b => !b.stuck).length;
const target = 3;
const need = Math.max(0, target - existing);
const source = balls.find(b => !b.stuck) || balls[0];
for (let i = 0; i < need; i++) {
const angle = -Math.PI / 2 + (i % 2 === 0 ? -1 : 1) * (0.32 + i * 0.12);
const speed = Math.hypot(source.vx, source.vy) || 520;
balls.push(makeBall(
source.x, source.y,
Math.cos(angle) * speed,
Math.sin(angle) * speed,
false
));
}
showMessage("MULTI-BALL · 3 balls");
burst(d.x, d.y, ["#ffd166", "#fff1b8", "#ffffff"], 22, 380);
}
}
// ---------- Update ----------
function update(dt) {
// Paddle
let dir = 0;
if (keys["ArrowLeft"] || keys["a"] || keys["A"]) dir -= 1;
if (keys["ArrowRight"] || keys["d"] || keys["D"]) dir += 1;
const prevX = paddle.x;
if (dir !== 0) {
paddle.x += dir * paddle.speed * dt;
mouseActive = false;
}
paddle.x = clamp(paddle.x, WALL + paddle.w / 2 + 4, W - WALL - paddle.w / 2 - 4);
paddle.vx = (paddle.x - prevX) / Math.max(dt, 0.0001);
// Wide paddle timer
if (paddle.wideTimer > 0) {
paddle.wideTimer -= dt;
if (paddle.wideTimer <= 0) {
paddle.wideTimer = 0;
paddle.w = paddle.baseW;
}
}
paddle.glow = Math.max(0, paddle.glow - dt * 2.2);
// Balls
for (const b of balls) {
if (b.stuck) {
b.x = paddle.x + b.stuckOffset;
b.y = paddle.y - paddle.h / 2 - b.r - 1;
continue;
}
const speed = Math.hypot(b.vx, b.vy);
const steps = clamp(Math.ceil(speed * dt / 4), 1, SUBSTEPS);
const sdt = dt / steps;
for (let s = 0; s < steps; s++) {
const prevX2 = b.x, prevY2 = b.y;
b.x += b.vx * sdt;
b.y += b.vy * sdt;
hitWall(b);
collidePaddle(b, sdt);
// Bricks: test all bricks, resolve only nearest overlap
let best = null, bestDist = Infinity;
for (const brick of bricks) {
if (!brick.alive) continue;
const cx = clamp(b.x, brick.x, brick.x + brick.w);
const cy = clamp(b.y, brick.y, brick.y + brick.h);
const dx = b.x - cx, dy = b.y - cy;
const d = dx * dx + dy * dy;
if (d <= b.r * b.r && d < bestDist) {
bestDist = d;
best = brick;
}
}
if (best) {
collideBrick(b, best);
// one resolution per substep keeps tunneling out
}
// avoid weird full vertical lock
if (Math.abs(b.vy) < 80) b.vy = b.vy >= 0 ? 80 : -80;
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 10) b.trail.shift();
}
}
// Ball removal
for (let i = balls.length - 1; i >= 0; i--) {
const b = balls[i];
if (b.y - b.r > H + 40) balls.splice(i, 1);
}
if (balls.length === 0 && state === "playing") {
lives--;
if (lives <= 0) {
state = "gameover";
showEndScreen(false);
} else {
resetBallOnPaddle();
showMessage("LIFE LOST", 1.1);
}
}
// Drops
for (let i = drops.length - 1; i >= 0; i--) {
const d = drops[i];
d.wobble += dt * 3;
d.x += Math.sin(d.wobble) * 26 * dt;
d.y += d.vy * dt;
const px = paddle.x - paddle.w / 2;
const py = paddle.y - paddle.h / 2;
if (d.y + d.r > py && d.y - d.r < py + paddle.h &&
d.x > px - d.r && d.x < px + paddle.w + d.r) {
collectDrop(d);
drops.splice(i, 1);
} else if (d.y - d.r > H) {
drops.splice(i, 1);
}
}
// Particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.life <= 0) { particles.splice(i, 1); continue; }
p.vy += p.gravity * dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.rot += p.vr * dt;
}
for (let i = sparkles.length - 1; i >= 0; i--) {
const s = sparkles[i];
s.life -= dt;
if (s.life <= 0) { sparkles.splice(i, 1); continue; }
s.x += s.vx * dt;
s.y += s.vy * dt;
s.vx *= 0.94;
s.vy *= 0.94;
}
flash = Math.max(0, flash - dt * 1.8);
shake = Math.max(0, shake - dt * 0.9);
if (messageTimer > 0) {
messageTimer -= dt;
if (messageTimer <= 0) message = null;
}
// Demo autopilot
if (state === "demo") {
const target = nearestBallTarget();
const desired = clamp(target, WALL + paddle.w / 2, W - WALL - paddle.w / 2);
paddle.x += (desired - paddle.x) * Math.min(1, dt * 2.6);
for (const b of balls) {
if (b.stuck) {
b.stuck = false;
launchBall(b, rand(-0.35, 0.35));
}
}
}
}
function nearestBallTarget() {
let best = balls[0];
if (!best) return W / 2;
// aim toward a gap: use ball x plus a little steering
const lead = best.vx * 0.10;
return best.x + lead;
}
// ---------- Drawing ----------
function draw() {
ctx.clearRect(0, 0, W, H);
// Background glow
const bg = ctx.createLinearGradient(0, 0, W, H);
bg.addColorStop(0, "#0b1226");
bg.addColorStop(0.55, "#0a1022");
bg.addColorStop(1, "#120c22");
ctx.fillStyle = bg;
ctx.fillRect(0, 0, W, H);
// Subtle grid
ctx.save();
ctx.globalAlpha = 0.16;
ctx.strokeStyle = "#28406d";
ctx.lineWidth = 1;
for (let x = WALL; x <= W - WALL; x += 42) {
ctx.beginPath();
ctx.moveTo(x, FIELD_TOP);
ctx.lineTo(x, H);
ctx.stroke();
}
for (let y = FIELD_TOP; y <= H; y += 42) {
ctx.beginPath();
ctx.moveTo(WALL, y);
ctx.lineTo(W - WALL, y);
ctx.stroke();
}
ctx.restore();
// Field vignette
const vign = ctx.createRadialGradient(W / 2, H * 0.42, 120, W / 2, H * 0.5, W * 0.72);
vign.addColorStop(0, "rgba(30,60,120,0.18)");
vign.addColorStop(1, "rgba(0,0,0,0.42)");
ctx.fillStyle = vign;
ctx.fillRect(0, FIELD_TOP, W, H - FIELD_TOP);
drawWalls();
drawBricks();
drawDrops();
drawPaddle();
drawBalls();
drawParticles();
drawHUD();
if (flash > 0) {
ctx.fillStyle = `rgba(120,220,255,${flash * 0.16})`;
ctx.fillRect(0, 0, W, H);
}
if (message) {
ctx.save();
ctx.globalAlpha = clamp(messageTimer * 1.8, 0, 1);
ctx.font = "800 22px ui-sans-serif, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.fillStyle = "#ffffff";
ctx.shadowColor = "rgba(66,232,255,0.8)";
ctx.shadowBlur = 18;
ctx.fillText(message, W / 2, FIELD_TOP + 34);
ctx.restore();
}
if (state === "paused") {
ctx.save();
ctx.fillStyle = "rgba(8,12,28,0.55)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#ffffff";
ctx.font = "800 30px ui-sans-serif, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.fillText("PAUSED", W / 2, H / 2);
ctx.font = "500 15px ui-sans-serif, system-ui, sans-serif";
ctx.fillStyle = "#9fb0dc";
ctx.fillText("Press P to resume", W / 2, H / 2 + 30);
ctx.restore();
}
}
function drawWalls() {
const wallGrad = ctx.createLinearGradient(0, FIELD_TOP, 0, H);
wallGrad.addColorStop(0, "rgba(66,232,255,0.22)");
wallGrad.addColorStop(1, "rgba(66,232,255,0.06)");
ctx.fillStyle = wallGrad;
ctx.fillRect(0, FIELD_TOP, WALL, H - FIELD_TOP);
ctx.fillRect(W - WALL, FIELD_TOP, WALL, H - FIELD_TOP);
ctx.fillRect(0, FIELD_TOP, W, 8);
ctx.strokeStyle = "rgba(120,220,255,0.42)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(WALL, FIELD_TOP); ctx.lineTo(WALL, H);
ctx.moveTo(W - WALL, FIELD_TOP); ctx.lineTo(W - WALL, H);
ctx.moveTo(WALL, FIELD_TOP + 4); ctx.lineTo(W - WALL, FIELD_TOP + 4);
ctx.stroke();
}
function drawBricks() {
for (const b of bricks) {
if (!b.alive) continue;
const [c1, c2] = b.colors;
const g = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
g.addColorStop(0, shade(c1, 24));
g.addColorStop(0.42, c1);
g.addColorStop(1, c2);
ctx.save();
ctx.shadowColor = b.hp > 1 ? "rgba(255,120,160,0.32)" : "rgba(80,170,255,0.25)";
ctx.shadowBlur = b.hitFlash > 0 ? 22 : 12;
ctx.shadowOffsetY = 3;
roundRectPath(ctx, b.x, b.y, b.w, b.h, 9);
ctx.fillStyle = g;
ctx.fill();
// top highlight
roundRectPath(ctx, b.x + 3, b.y + 3, b.w - 6, 9, 5);
ctx.fillStyle = "rgba(255,255,255,0.22)";
ctx.fill();
ctx.restore();
// border
ctx.strokeStyle = "rgba(255,255,255,0.16)";
ctx.lineWidth = 1.2;
roundRectPath(ctx, b.x + 0.5, b.y + 0.5, b.w - 1, b.h - 1, 9);
ctx.stroke();
// cracked state after first hit
if (b.hp < b.maxHp) {
ctx.save();
ctx.strokeStyle = "rgba(255,255,255,0.72)";
ctx.lineWidth = 2;
ctx.shadowColor = "rgba(255,255,255,0.55)";
ctx.shadowBlur = 6;
ctx.beginPath();
ctx.moveTo(b.x + b.w * 0.16, b.y + 4);
ctx.lineTo(b.x + b.w * 0.36, b.y + b.h * 0.42);
ctx.lineTo(b.x + b.w * 0.27, b.y + b.h - 4);
ctx.moveTo(b.x + b.w * 0.36, b.y + b.h * 0.42);
ctx.lineTo(b.x + b.w * 0.56, b.y + b.h * 0.28);
ctx.moveTo(b.x + b.w * 0.36, b.y + b.h * 0.42);
ctx.lineTo(b.x + b.w * 0.62, b.y + b.h * 0.76);
ctx.moveTo(b.x + b.w * 0.72, b.y + 5);
ctx.lineTo(b.x + b.w * 0.62, b.y + b.h * 0.76);
ctx.stroke();
ctx.restore();
}
// power-up icon badge
if (b.power) {
const cx = b.x + b.w - 15;
const cy = b.y + 14;
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, 11, 0, Math.PI * 2);
ctx.fillStyle = b.power === "wide" ? "#42e8ff" : "#ffd166";
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = "rgba(255,255,255,0.7)";
ctx.stroke();
ctx.fillStyle = "#0b1730";
ctx.font = "800 11px ui-sans-serif, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(b.power === "wide" ? "W" : "M", cx, cy + 0.5);
ctx.restore();
}
if (b.hitFlash > 0) {
ctx.save();
ctx.globalAlpha = b.hitFlash * 0.55;
roundRectPath(ctx, b.x, b.y, b.w, b.h, 9);
ctx.fillStyle = "#ffffff";
ctx.fill();
ctx.restore();
b.hitFlash = Math.max(0, b.hitFlash - 0.06);
}
}
}
function drawDrops() {
for (const d of drops) {
const col = d.type === "wide" ? "#42e8ff" : "#ffd166";
ctx.save();
ctx.translate(d.x, d.y);
ctx.rotate(Math.sin(d.wobble) * 0.12);
ctx.shadowColor = col;
ctx.shadowBlur = 22;
const g = ctx.createLinearGradient(-d.r, -d.r, d.r, d.r);
g.addColorStop(0, shade(col, 36));
g.addColorStop(1, shade(col, -30));
roundRectPath(ctx, -d.r, -d.r * 0.72, d.r * 2, d.r * 1.44, 10);
ctx.fillStyle = g;
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = "rgba(255,255,255,0.7)";
ctx.lineWidth = 2;
roundRectPath(ctx, -d.r + 1, -d.r * 0.72 + 1, d.r * 2 - 2, d.r * 1.44 - 2, 9);
ctx.stroke();
ctx.fillStyle = "#0a1730";
ctx.font = "800 16px ui-sans-serif, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(d.type === "wide" ? "W" : "M", 0, 1);
// glow trail
ctx.restore();
}
}
function drawPaddle() {
const x = paddle.x - paddle.w / 2;
const y = paddle.y - paddle.h / 2;
// glow
ctx.save();
const glow = ctx.createRadialGradient(paddle.x, paddle.y, 10, paddle.x, paddle.y, paddle.w * 0.8);
glow.addColorStop(0, `rgba(66,232,255,${0.22 + paddle.glow * 0.18})`);
glow.addColorStop(1, "rgba(66,232,255,0)");
ctx.fillStyle = glow;
ctx.fillRect(x - 80, y - 50, paddle.w + 160, paddle.h + 100);
ctx.restore();
const g = ctx.createLinearGradient(x, y, x, y + paddle.h);
g.addColorStop(0, "#8ef1ff");
g.addColorStop(0.35, "#42e8ff");
g.addColorStop(1, "#2b6fff");
ctx.save();
ctx.shadowColor = "rgba(66,232,255,0.55)";
ctx.shadowBlur = 22;
roundRectPath(ctx, x, y, paddle.w, paddle.h, 11);
ctx.fillStyle = g;
ctx.fill();
ctx.restore();
// top gloss
roundRectPath(ctx, x + 6, y + 3, paddle.w - 12, 7, 4);
ctx.fillStyle = "rgba(255,255,255,0.55)";
ctx.fill();
// dark underside
roundRectPath(ctx, x + 8, y + paddle.h - 6, paddle.w - 16, 3, 2);
ctx.fillStyle = "rgba(10,30,80,0.45)";
ctx.fill();
if (paddle.wideTimer > 0) {
const pct = paddle.wideTimer / 10;
ctx.fillStyle = "rgba(66,232,255,0.18)";
roundRectPath(ctx, x, y + paddle.h + 8, paddle.w, 4, 2);
ctx.fill();
ctx.fillStyle = "#42e8ff";
roundRectPath(ctx, x, y + paddle.h + 8, paddle.w * pct, 4, 2);
ctx.fill();
}
}
function drawBalls() {
for (const b of balls) {
// trail
for (let i = 0; i < b.trail.length; i++) {
const t = b.trail[i];
const alpha = (i + 1) / b.trail.length * 0.22;
const r = b.r * (0.35 + (i / b.trail.length) * 0.65);
ctx.beginPath();
ctx.arc(t.x, t.y, r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(120,230,255,${alpha})`;
ctx.fill();
}
const g = ctx.createRadialGradient(b.x - 4, b.y - 5, 2, b.x, b.y, b.r * 1.1);
g.addColorStop(0, "#ffffff");
g.addColorStop(0.32, "#b9f4ff");
g.addColorStop(1, "#2b7dff");
ctx.save();
ctx.shadowColor = "rgba(80,210,255,0.85)";
ctx.shadowBlur = 22;
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fillStyle = g;
ctx.fill();
ctx.restore();
}
}
function drawParticles() {
for (const p of particles) {
const a = clamp(p.life / p.maxLife, 0, 1);
ctx.save();
ctx.globalAlpha = a;
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.fillStyle = p.color;
if (p.square) {
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
} else {
ctx.beginPath();
ctx.arc(0, 0, p.size / 2, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
for (const s of sparkles) {
const a = clamp(s.life / s.maxLife, 0, 1);
ctx.save();
ctx.globalAlpha = a;
ctx.fillStyle = s.color;
ctx.shadowColor = s.color;
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
function drawHUD() {
// background strip
const hud = ctx.createLinearGradient(0, 0, 0, HUD_H);
hud.addColorStop(0, "rgba(8,14,32,0.95)");
hud.addColorStop(1, "rgba(10,18,40,0.72)");
ctx.fillStyle = hud;
ctx.fillRect(0, 0, W, HUD_H);
ctx.strokeStyle = "rgba(120,190,255,0.18)";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, HUD_H - 1); ctx.lineTo(W, HUD_H - 1);
ctx.stroke();
ctx.textBaseline = "middle";
ctx.textAlign = "left";
ctx.fillStyle = "#7f92c1";
ctx.font = "700 13px ui-sans-serif, system-ui, sans-serif";
ctx.fillText("SCORE", 34, 28);
ctx.fillStyle = "#ffffff";
ctx.font = "800 30px ui-sans-serif, system-ui, sans-serif";
ctx.fillText(String(score).padStart(6, "0"), 34, 58);
ctx.fillStyle = "#7f92c1";
ctx.font = "700 13px ui-sans-serif, system-ui, sans-serif";
ctx.fillText("BRICKS", 260, 28);
ctx.fillStyle = "#ffffff";
ctx.font = "800 30px ui-sans-serif, system-ui, sans-serif";
ctx.fillText(String(totalBricksAlive()), 260, 58);
// lives
ctx.fillStyle = "#7f92c1";
ctx.font = "700 13px ui-sans-serif, system-ui, sans-serif";
ctx.fillText("LIVES", W - 220, 28);
for (let i = 0; i < 3; i++) {
const cx = W - 208 + i * 38;
const cy = 58;
const alive = i < lives;
ctx.save();
ctx.globalAlpha = alive ? 1 : 0.22;
const g = ctx.createLinearGradient(cx - 14, cy - 12, cx + 14, cy + 12);
g.addColorStop(0, "#ffffff");
g.addColorStop(1, "#42b7ff");
ctx.fillStyle = g;
ctx.shadowColor = "rgba(66,232,255,0.7)";
ctx.shadowBlur = alive ? 16 : 0;
ctx.beginPath();
ctx.arc(cx, cy, 12, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// center title / power status
ctx.textAlign = "center";
ctx.fillStyle = "rgba(170,205,255,0.72)";
ctx.font = "700 12px ui-sans-serif, system-ui, sans-serif";
ctx.fillText("NEON BREAKER", W / 2, 26);
if (paddle.wideTimer > 0) {
ctx.fillStyle = "#42e8ff";
ctx.font = "700 12px ui-sans-serif, system-ui, sans-serif";
ctx.fillText(`WIDE ${paddle.wideTimer.toFixed(1)}s`, W / 2, 56);
} else {
ctx.fillStyle = "rgba(150,180,235,0.62)";
ctx.font = "500 12px ui-sans-serif, system-ui, sans-serif";
ctx.fillText("MOUSE / ARROWS TO MOVE", W / 2, 56);
}
}
// ---------- End screens ----------
function showEndScreen(won) {
overlay.classList.remove("hidden");
if (won) {
kicker.textContent = "Victory · wall demolished";
overlayTitle.textContent = "Perfect clear!";
overlayText.textContent =
`You smashed every brick for a final score of ${score}. Try again and chase a higher combo bonus.`;
controls.innerHTML = `
<div class="key"><b>Score</b> ${score}</div>
<div class="key"><b>Lives left</b> ${lives}</div>
<div class="key"><b>Bricks</b> 0 remaining</div>`;
hint.textContent = "Press Enter or click below to play again.";
} else {
kicker.textContent = "Game over";
overlayTitle.textContent = "Out of lives";
overlayText.textContent =
`The wall got the better of you. Final score: ${score}. Ready for another run?`;
controls.innerHTML = `
<div class="key"><b>Score</b> ${score}</div>
<div class="key"><b>Lives</b> 0</div>
<div class="key"><b>Bricks left</b> ${totalBricksAlive()}</div>`;
hint.textContent = "Press Enter or click below to try again.";
}
startBtn.textContent = "Restart Game";
startBtn.focus();
}
// ---------- Flow ----------
function startGame() {
score = 0;
lives = 3;
combo = 0;
paddle.w = paddle.baseW;
paddle.wideTimer = 0;
paddle.x = W / 2;
drops = [];
particles = [];
sparkles = [];
buildBricks();
resetBallOnPaddle();
state = "playing";
overlay.classList.add("hidden");
flash = 0.8;
showMessage("GET READY", 1.2);
}
function pauseToggle() {
if (state === "playing") state = "paused";
else if (state === "paused") state = "playing";
}
// ---------- Input ----------
window.addEventListener("keydown", (e) => {
keys[e.key] = true;
if (["ArrowLeft", "ArrowRight", " ", "Spacebar"].includes(e.key)) {
e.preventDefault();
}
if (state === "demo") {
// any key takes control
startGame();
return;
}
if (e.key === " " || e.key === "Spacebar") {
if (state === "playing") {
for (const b of balls) if (b.stuck) launchBall(b, rand(-0.12, 0.12));
}
}
if (e.key === "p" || e.key === "P") {
pauseToggle();
}
if (e.key === "Enter") {
if (state === "gameover" || state === "victory" || state === "paused") {
startGame();
} else if (state === "playing") {
for (const b of balls) if (b.stuck) launchBall(b, rand(-0.12, 0.12));
}
}
}, { passive: false });
window.addEventListener("keyup", (e) => {
keys[e.key] = false;
});
function pointerToGame(e) {
const rect = canvas.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * W;
return x;
}
canvas.addEventListener("pointermove", (e) => {
if (state === "demo") return;
const x = pointerToGame(e);
paddle.x = clamp(x, WALL + paddle.w / 2 + 4, W - WALL - paddle.w / 2 - 4);
mouseActive = true;
});
canvas.addEventListener("pointerdown", (e) => {
if (state === "demo") {
startGame();
return;
}
if (state === "playing") {
for (const b of balls) if (b.stuck) launchBall(b, rand(-0.12, 0.12));
}
});
startBtn.addEventListener("click", () => {
if (state === "demo" || state === "gameover" || state === "victory" || state === "paused") {
startGame();
}
});
// ---------- Boot ----------
function boot() {
buildBricks();
resetBallOnPaddle();
// give the demo ball motion immediately
launchBall(balls[0], -0.18);
state = "demo";
kicker.textContent = "Click or press a key to start";
overlayTitle.textContent = "Break every brick";
overlayText.textContent =
"Steer the paddle with your mouse or the arrow keys. The ball bounces at sharper angles when it catches the edge of the paddle — use that to carve through the wall.";
hint.textContent = "A self-playing demo runs until you take control.";
}
function frame(now) {
if (!lastTime) lastTime = now;
let dt = (now - lastTime) / 1000;
lastTime = now;
if (dt > 0.25) dt = 0.25;
if (state !== "paused") {
accumulator += dt;
let guard = 0;
while (accumulator >= STEP && guard < 40) {
update(STEP);
accumulator -= STEP;
guard++;
}
}
draw();
requestAnimationFrame(frame);
}
boot();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>