Gemini 4 Argon
by Googlereasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 2 min 7 s
- Cost
- $0.129
- Output tokens
- 12,793
- Spent thinking
- 0 tokens
- Speed
- 112.5 tok/s
- Console errors
- 0
- First token after
- 13.2 s
- First visible text after
- 13.2 s
- Prompt tokens
- 563
- Code size
- 30.5 KB
- Finish reason
- stop
- Served by
- Google AI Studio
- Model id
google/gemini-4-argon- Reasoning sent
model default- Max output
- 943,718 tokens
- Generated
- Nov 12, 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 · 1158 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Neon Breakout</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
-webkit-user-select: none;
}
body {
background: radial-gradient(circle at center, #13182b 0%, #070913 100%);
color: #fff;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
overflow: hidden;
}
#game-wrapper {
position: relative;
width: 100%;
height: 100%;
max-width: 900px;
max-height: 700px;
aspect-ratio: 9 / 7;
display: flex;
justify-content: center;
align-items: center;
padding: 12px;
}
canvas {
width: 100%;
height: 100%;
background: #0a0d1a;
border-radius: 14px;
box-shadow: 0 0 35px rgba(0, 240, 255, 0.18), inset 0 0 25px rgba(0, 240, 255, 0.08);
border: 2px solid rgba(0, 240, 255, 0.35);
display: block;
cursor: pointer;
touch-action: none;
}
</style>
</head>
<body>
<div id="game-wrapper">
<canvas id="gameCanvas" width="900" height="700"></canvas>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const W = 900;
const H = 700;
// Game State
let state = 'DEMO'; // DEMO, PLAYING, GAMEOVER, VICTORY
let score = 0;
let lives = 3;
let combo = 0;
let shakeTime = 0;
let shakeMag = 0;
// Input State
const keys = { ArrowLeft: false, ArrowRight: false, a: false, d: false };
let lastInputType = 'mouse';
let mouseX = W / 2;
// Entities
const paddle = {
baseW: 115,
wideW: 185,
w: 115,
targetW: 115,
h: 16,
x: W / 2 - 115 / 2,
y: H - 48,
speed: 680,
wideTimer: 0
};
let balls = [];
let bricks = [];
let powerups = [];
let particles = [];
let floatingTexts = [];
let stars = [];
// Audio Context (Synthesized Effects)
let audioCtx = null;
function initAudio() {
if (!audioCtx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) audioCtx = new AudioContext();
}
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playSound(type) {
if (!audioCtx || state === 'DEMO') return;
try {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
const now = audioCtx.currentTime;
if (type === 'paddle') {
osc.type = 'sine';
osc.frequency.setValueAtTime(320, now);
osc.frequency.exponentialRampToValueAtTime(480, now + 0.08);
gain.gain.setValueAtTime(0.15, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.08);
osc.start(now);
osc.stop(now + 0.08);
} else if (type === 'brick') {
osc.type = 'triangle';
const pitch = 380 + Math.min(combo * 25, 400);
osc.frequency.setValueAtTime(pitch, now);
osc.frequency.exponentialRampToValueAtTime(pitch * 1.4, now + 0.09);
gain.gain.setValueAtTime(0.18, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.09);
osc.start(now);
osc.stop(now + 0.09);
} else if (type === 'crack') {
osc.type = 'square';
osc.frequency.setValueAtTime(190, now);
osc.frequency.exponentialRampToValueAtTime(120, now + 0.07);
gain.gain.setValueAtTime(0.12, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.07);
osc.start(now);
osc.stop(now + 0.07);
} else if (type === 'powerup') {
osc.type = 'sine';
osc.frequency.setValueAtTime(400, now);
osc.frequency.exponentialRampToValueAtTime(880, now + 0.22);
gain.gain.setValueAtTime(0.2, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.22);
osc.start(now);
osc.stop(now + 0.22);
} else if (type === 'lose') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(250, now);
osc.frequency.linearRampToValueAtTime(90, now + 0.35);
gain.gain.setValueAtTime(0.2, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.35);
osc.start(now);
osc.stop(now + 0.35);
}
} catch (e) {}
}
// Background Stars
for (let i = 0; i < 65; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H,
r: Math.random() * 1.6 + 0.4,
alpha: Math.random() * 0.6 + 0.2,
speed: Math.random() * 14 + 4
});
}
const rowColors = [
{ fill: '#ff2a6d', glow: '#ff2a6d', light: '#ff7aa2' }, // Row 0: Neon Pink
{ fill: '#ff7700', glow: '#ff7700', light: '#ffaa55' }, // Row 1: Neon Orange
{ fill: '#ffdd00', glow: '#ffdd00', light: '#ffee77' }, // Row 2: Neon Gold
{ fill: '#05ffa1', glow: '#05ffa1', light: '#77ffca' }, // Row 3: Neon Mint
{ fill: '#00e5ff', glow: '#00e5ff', light: '#88f3ff' }, // Row 4: Neon Cyan
{ fill: '#b967ff', glow: '#b967ff', light: '#d9adff' } // Row 5: Neon Purple
];
// Deterministic pattern for tough bricks (rows 0, 1, 2, 4 have reinforced armor bricks)
const toughMap = [
[1, 0, 1, 1, 0, 1, 1, 0, 1, 1],
[0, 1, 0, 1, 1, 1, 1, 0, 1, 0],
[1, 1, 0, 0, 1, 1, 0, 0, 1, 1],
[0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
[0, 1, 0, 1, 0, 0, 1, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
// Powerup map ('W' = wide paddle, 'M' = multi-ball)
// Placed heavily in rows 4 & 5 so they drop within the first 2 seconds of gameplay/demo
const powerupMap = [
[null, 'M', null, null, 'W', null, null, 'M', null, null],
[null, null, 'W', null, null, null, 'M', null, null, 'W' ],
['M', null, null, null, 'W', 'M', null, null, null, null],
[null, 'W', null, 'M', null, null, 'W', null, 'M', null],
[null, null, 'M', null, 'W', 'M', null, 'W', null, null],
['W', null, null, 'M', null, null, 'W', null, 'M', null]
];
function createCrackPattern(w, h) {
// Pre-compute deterministic-looking jagged crack lines per brick
return [
[
{ x: w * 0.28, y: 0 },
{ x: w * 0.36, y: h * 0.38 },
{ x: w * 0.24, y: h * 0.72 },
{ x: w * 0.38, y: h }
],
[
{ x: w * 0.36, y: h * 0.38 },
{ x: w * 0.56, y: h * 0.55 },
{ x: w * 0.68, y: h * 0.25 }
],
[
{ x: w * 0.72, y: h },
{ x: w * 0.64, y: h * 0.62 },
{ x: w * 0.80, y: h * 0.32 }
]
];
}
function initLevel() {
bricks = [];
powerups = [];
particles = [];
floatingTexts = [];
combo = 0;
const rows = 6;
const cols = 10;
const padX = 55;
const padY = 82;
const gap = 8;
const brickW = (W - padX * 2 - gap * (cols - 1)) / cols; // 71.8px
const brickH = 28;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const isTough = toughMap[r][c] === 1;
const pType = powerupMap[r][c];
bricks.push({
r, c,
x: padX + c * (brickW + gap),
y: padY + r * (brickH + gap),
w: brickW,
h: brickH,
maxHp: isTough ? 2 : 1,
hp: isTough ? 2 : 1,
color: rowColors[r],
powerup: pType,
hitAnim: 0,
cracks: createCrackPattern(brickW, brickH)
});
}
}
}
function spawnBall(x, y, vx, vy) {
balls.push({
x, y,
vx, vy,
r: 8,
baseSpeed: 500,
trail: []
});
}
function resetBallAndPaddle(isDemoStart = false) {
paddle.w = paddle.baseW;
paddle.targetW = paddle.baseW;
paddle.wideTimer = 0;
paddle.x = (W - paddle.w) / 2;
balls = [];
powerups = [];
combo = 0;
const speed = 500;
// Aim directly toward a power-up brick in row 5 on initial launch
const angle = isDemoStart ? -Math.PI / 2 + 0.23 : -Math.PI / 2 + (Math.random() * 0.5 - 0.25);
spawnBall(
paddle.x + paddle.w / 2,
paddle.y - 12,
Math.cos(angle) * speed,
Math.sin(angle) * speed
);
}
function startDemo() {
state = 'DEMO';
score = 0;
lives = 3;
initLevel();
resetBallAndPaddle(true);
}
function startGame() {
initAudio();
state = 'PLAYING';
score = 0;
lives = 3;
initLevel();
resetBallAndPaddle(false);
}
// Input Listeners
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'a' || e.key === 'd' || e.key === 'A' || e.key === 'D') {
keys[e.key] = true;
lastInputType = 'keyboard';
}
if (state === 'DEMO' || state === 'GAMEOVER' || state === 'VICTORY') {
if (e.key.length === 1 || e.key.startsWith('Arrow') || e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
startGame();
}
}
});
window.addEventListener('keyup', (e) => {
if (e.key in keys) keys[e.key] = false;
});
function updatePointerPos(clientX) {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
mouseX = (clientX - rect.left) * scaleX;
lastInputType = 'mouse';
}
canvas.addEventListener('mousemove', (e) => {
if (state === 'PLAYING') {
updatePointerPos(e.clientX);
}
});
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (state === 'PLAYING' && e.touches.length > 0) {
updatePointerPos(e.touches[0].clientX);
}
}, { passive: false });
canvas.addEventListener('mousedown', (e) => {
updatePointerPos(e.clientX);
if (state !== 'PLAYING') {
startGame();
}
});
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
if (e.touches.length > 0) updatePointerPos(e.touches[0].clientX);
if (state !== 'PLAYING') {
startGame();
}
}, { passive: false });
// Particle & Visual Effects
function spawnBrickParticles(brick, destroyed) {
const count = destroyed ? 20 : 8;
const cx = brick.x + brick.w / 2;
const cy = brick.y + brick.h / 2;
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * (destroyed ? 240 : 130) + 40;
particles.push({
x: cx + (Math.random() - 0.5) * brick.w * 0.7,
y: cy + (Math.random() - 0.5) * brick.h * 0.7,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
size: Math.random() * 5 + 3,
color: destroyed ? brick.color.fill : '#ffffff',
alpha: 1,
life: Math.random() * 0.45 + 0.25,
maxLife: 0.7
});
}
}
function addFloatingText(text, x, y, color = '#fff') {
floatingTexts.push({
text, x, y,
vy: -55,
alpha: 1,
life: 0.85,
color
});
}
function activatePowerup(type, x, y) {
playSound('powerup');
if (type === 'W') {
paddle.wideTimer = 10.0;
paddle.targetW = paddle.wideW;
addFloatingText('WIDE PADDLE (10s)', x, y - 10, '#05ffa1');
} else if (type === 'M') {
addFloatingText('MULTI-BALL x3!', x, y - 10, '#ffaa00');
const source = balls[0] || {
x: paddle.x + paddle.w / 2,
y: paddle.y - 12,
vx: 0,
vy: -500,
baseSpeed: 500
};
const speed = Math.hypot(source.vx, source.vy) || 500;
const baseAngle = Math.atan2(source.vy, source.vx);
const offsets = [-0.32, 0.32];
let idx = 0;
while (balls.length < 3 && idx < 4) {
const a = baseAngle + offsets[idx % 2] * (idx < 2 ? 1 : 1.6);
// Ensure new balls travel mostly vertically
let vx = Math.cos(a) * speed;
let vy = Math.sin(a) * speed;
if (Math.abs(vy) < speed * 0.35) {
vy = -speed * 0.7;
vx = (vx >= 0 ? 1 : -1) * Math.sqrt(speed * speed - vy * vy);
}
spawnBall(source.x, source.y, vx, vy);
idx++;
}
}
score += 50;
}
// Swept Circle-AABB Collision Detection (Prevents Tunneling)
function intersectBallRect(ball, rect) {
const closestX = Math.max(rect.x, Math.min(ball.x, rect.x + rect.w));
const closestY = Math.max(rect.y, Math.min(ball.y, rect.y + rect.h));
const dx = ball.x - closestX;
const dy = ball.y - closestY;
const distSq = dx * dx + dy * dy;
if (distSq > ball.r * ball.r) return null;
const dist = Math.sqrt(distSq);
let nx = 0, ny = 0, overlap = 0;
if (dist > 0.0001) {
nx = dx / dist;
ny = dy / dist;
overlap = ball.r - dist;
} else {
// Ball center is inside the rectangle; push out along shallowest axis
const leftPen = ball.x - rect.x;
const rightPen = (rect.x + rect.w) - ball.x;
const topPen = ball.y - rect.y;
const botPen = (rect.y + rect.h) - ball.y;
const minPen = Math.min(leftPen, rightPen, topPen, botPen);
if (minPen === leftPen) { nx = -1; overlap = leftPen + ball.r; }
else if (minPen === rightPen) { nx = 1; overlap = rightPen + ball.r; }
else if (minPen === topPen) { ny = -1; overlap = topPen + ball.r; }
else { ny = 1; overlap = botPen + ball.r; }
}
return { nx, ny, overlap };
}
function updatePaddle(dt) {
// Smooth width transition for Wide Power-up
if (paddle.wideTimer > 0) {
paddle.wideTimer -= dt;
if (paddle.wideTimer <= 0) {
paddle.wideTimer = 0;
paddle.targetW = paddle.baseW;
}
}
if (Math.abs(paddle.w - paddle.targetW) > 0.5) {
const prevCenter = paddle.x + paddle.w / 2;
paddle.w += (paddle.targetW - paddle.w) * Math.min(1, dt * 12);
paddle.x = prevCenter - paddle.w / 2;
} else {
paddle.w = paddle.targetW;
}
if (state === 'DEMO') {
// Smart AI controller for Demo mode
let targetX = W / 2;
let bestBall = null;
let maxDanger = -Infinity;
for (const b of balls) {
const danger = b.y + (b.vy > 0 ? 350 : 0);
if (danger > maxDanger) {
maxDanger = danger;
bestBall = b;
}
}
if (bestBall) {
// Aim with a gentle wave offset so the AI hits varied angles
const aimOffset = Math.sin(performance.now() * 0.0023) * (paddle.w * 0.28);
targetX = bestBall.x + aimOffset;
}
// If a power-up is about to land and the ball is safe, grab the powerup
for (const p of powerups) {
if (p.y > H * 0.45 && (!bestBall || bestBall.y < H * 0.55 || bestBall.vy < 0)) {
targetX = p.x;
break;
}
}
const desiredX = targetX - paddle.w / 2;
const diff = desiredX - paddle.x;
const maxStep = paddle.speed * 0.95 * dt;
if (Math.abs(diff) <= maxStep) paddle.x = desiredX;
else paddle.x += Math.sign(diff) * maxStep;
} else if (state === 'PLAYING') {
const leftDown = keys.ArrowLeft || keys.a || keys.A;
const rightDown = keys.ArrowRight || keys.d || keys.D;
if (leftDown || rightDown) {
if (leftDown) paddle.x -= paddle.speed * dt;
if (rightDown) paddle.x += paddle.speed * dt;
} else if (lastInputType === 'mouse') {
paddle.x = mouseX - paddle.w / 2;
}
}
// Clamp paddle inside walls
if (paddle.x < 12) paddle.x = 12;
if (paddle.x + paddle.w > W - 12) paddle.x = W - 12;
}
function stepBallPhysics(ball, dt) {
// Record trail
ball.trail.push({ x: ball.x, y: ball.y });
if (ball.trail.length > 9) ball.trail.shift();
// Sub-step movement so ball never moves more than 3px per collision check
const speed = Math.hypot(ball.vx, ball.vy);
const totalDist = speed * dt;
const maxStepDist = 3.0;
const steps = Math.max(1, Math.ceil(totalDist / maxStepDist));
const subDt = dt / steps;
for (let s = 0; s < steps; s++) {
ball.x += ball.vx * subDt;
ball.y += ball.vy * subDt;
// Wall collisions
if (ball.x - ball.r < 10) {
ball.x = 10 + ball.r;
ball.vx = Math.abs(ball.vx);
} else if (ball.x + ball.r > W - 10) {
ball.x = W - 10 - ball.r;
ball.vx = -Math.abs(ball.vx);
}
if (ball.y - ball.r < 54) {
ball.y = 54 + ball.r;
ball.vy = Math.abs(ball.vy);
}
// Paddle collision
if (ball.vy > 0) {
const hit = intersectBallRect(ball, paddle);
if (hit) {
// Push ball cleanly above paddle
ball.y = paddle.y - ball.r - 0.5;
// Calculate bounce angle based on where the ball hits the paddle
// Center (0) = straight up (-PI/2), Edges (-1 or +1) = sharp 68 degree angle
const paddleCenter = paddle.x + paddle.w / 2;
let hitOffset = (ball.x - paddleCenter) / (paddle.w / 2);
hitOffset = Math.max(-1, Math.min(1, hitOffset));
const maxBounceAngle = 68 * (Math.PI / 180); // 68 degrees from vertical
const bounceAngle = hitOffset * maxBounceAngle;
const curSpeed = Math.min(680, Math.hypot(ball.vx, ball.vy) * 1.012);
ball.vx = curSpeed * Math.sin(bounceAngle);
ball.vy = -curSpeed * Math.cos(bounceAngle);
combo = 0;
playSound('paddle');
// Subtle spark on paddle impact
for (let i = 0; i < 5; i++) {
particles.push({
x: ball.x,
y: paddle.y,
vx: (Math.random() - 0.5) * 140,
vy: -Math.random() * 100 - 20,
size: 3,
color: '#00f0ff',
alpha: 1,
life: 0.25,
maxLife: 0.25
});
}
}
}
// Brick collisions
for (let i = bricks.length - 1; i >= 0; i--) {
const b = bricks[i];
const hit = intersectBallRect(ball, b);
if (hit) {
// Resolve penetration
ball.x += hit.nx * hit.overlap;
ball.y += hit.ny * hit.overlap;
// Reflect velocity along collision normal
if (Math.abs(hit.nx) > Math.abs(hit.ny)) {
ball.vx = (hit.nx > 0 ? 1 : -1) * Math.abs(ball.vx);
} else {
ball.vy = (hit.ny > 0 ? 1 : -1) * Math.abs(ball.vy);
}
// Damage brick
b.hp--;
b.hitAnim = 1.0;
combo++;
if (b.hp <= 0) {
playSound('brick');
const pts = (b.maxHp === 2 ? 25 : 10) + Math.min(combo * 2, 30);
score += pts;
spawnBrickParticles(b, true);
shakeTime = 0.08;
shakeMag = 3;
if (b.powerup) {
powerups.push({
x: b.x + b.w / 2,
y: b.y + b.h / 2,
vy: 175,
type: b.powerup,
r: 14,
pulse: 0
});
}
bricks.splice(i, 1);
} else {
playSound('crack');
score += 5;
spawnBrickParticles(b, false);
}
// Only break one brick per sub-step to prevent double reflections
break;
}
}
}
}
function update(dt) {
// Update background stars
for (const st of stars) {
st.y += st.speed * dt;
if (st.y > H) {
st.y = 0;
st.x = Math.random() * W;
}
}
// Screen shake decay
if (shakeTime > 0) shakeTime = Math.max(0, shakeTime - dt);
// Update brick hit flash animations
for (const b of bricks) {
if (b.hitAnim > 0) b.hitAnim = Math.max(0, b.hitAnim - dt * 6);
}
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 260 * dt; // subtle gravity
p.life -= dt;
p.alpha = Math.max(0, p.life / p.maxLife);
if (p.life <= 0) particles.splice(i, 1);
}
// Update floating texts
for (let i = floatingTexts.length - 1; i >= 0; i--) {
const ft = floatingTexts[i];
ft.y += ft.vy * dt;
ft.life -= dt;
ft.alpha = Math.max(0, ft.life / 0.85);
if (ft.life <= 0) floatingTexts.splice(i, 1);
}
if (state !== 'PLAYING' && state !== 'DEMO') return;
updatePaddle(dt);
// Update balls
for (let i = balls.length - 1; i >= 0; i--) {
const b = balls[i];
stepBallPhysics(b, dt);
if (b.y - b.r > H) {
balls.splice(i, 1);
}
}
// Update falling power-ups
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.y += p.vy * dt;
p.pulse += dt * 6;
// Check paddle catch
if (
p.x + p.r > paddle.x &&
p.x - p.r < paddle.x + paddle.w &&
p.y + p.r > paddle.y &&
p.y - p.r < paddle.y + paddle.h
) {
activatePowerup(p.type, p.x, paddle.y);
powerups.splice(i, 1);
continue;
}
if (p.y - p.r > H) {
powerups.splice(i, 1);
}
}
// Check Victory
if (bricks.length === 0) {
if (state === 'DEMO') {
initLevel();
resetBallAndPaddle(true);
} else {
state = 'VICTORY';
}
return;
}
// Check All Balls Lost
if (balls.length === 0) {
if (state === 'DEMO') {
resetBallAndPaddle(true);
} else {
lives--;
playSound('lose');
shakeTime = 0.2;
shakeMag = 6;
if (lives <= 0) {
state = 'GAMEOVER';
} else {
resetBallAndPaddle(false);
}
}
}
}
// Rendering Helpers
function drawRoundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function drawPowerupBadge(cx, cy, type, scale = 1) {
ctx.save();
ctx.translate(cx, cy);
ctx.scale(scale, scale);
if (type === 'W') {
// Wide Paddle Icon: Pill with left-right outward arrows
ctx.fillStyle = 'rgba(5, 255, 161, 0.22)';
ctx.strokeStyle = '#05ffa1';
ctx.lineWidth = 1.8;
drawRoundRect(-15, -8, 30, 16, 8);
ctx.fill();
ctx.stroke();
ctx.beginPath();
// Left arrow
ctx.moveTo(-4, 0); ctx.lineTo(-10, 0);
ctx.moveTo(-7, -3.5); ctx.lineTo(-10.5, 0); ctx.lineTo(-7, 3.5);
// Right arrow
ctx.moveTo(4, 0); ctx.lineTo(10, 0);
ctx.moveTo(7, -3.5); ctx.lineTo(10.5, 0); ctx.lineTo(7, 3.5);
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1.8;
ctx.stroke();
} else if (type === 'M') {
// Multi-ball Icon: 3 glowing mini orbs in a triangle cluster
ctx.fillStyle = 'rgba(255, 200, 0, 0.24)';
ctx.strokeStyle = '#ffdd00';
ctx.lineWidth = 1.8;
drawRoundRect(-14, -9, 28, 18, 9);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#ffffff';
const coords = [[0, -3.5], [-4.5, 3.2], [4.5, 3.2]];
for (const [bx, by] of coords) {
ctx.beginPath();
ctx.arc(bx, by, 2.6, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
function drawBrick(b) {
ctx.save();
// Base brick fill
ctx.fillStyle = b.color.fill;
drawRoundRect(b.x, b.y, b.w, b.h, 5);
ctx.fill();
// Top bevel highlight
ctx.fillStyle = 'rgba(255, 255, 255, 0.22)';
drawRoundRect(b.x + 2, b.y + 2, b.w - 4, b.h * 0.38, 3);
ctx.fill();
// Tough brick metallic frame (2-Hit Armor)
if (b.maxHp === 2) {
ctx.strokeStyle = b.hp === 2 ? '#ffffff' : 'rgba(255, 255, 255, 0.65)';
ctx.lineWidth = 2.4;
drawRoundRect(b.x + 1.5, b.y + 1.5, b.w - 3, b.h - 3, 4);
ctx.stroke();
// Corner rivets for intact armor bricks
if (b.hp === 2) {
ctx.fillStyle = '#ffffff';
const r = 1.7;
ctx.fillRect(b.x + 4, b.y + 4, r, r);
ctx.fillRect(b.x + b.w - 4 - r, b.y + 4, r, r);
ctx.fillRect(b.x + 4, b.y + b.h - 4 - r, r, r);
ctx.fillRect(b.x + b.w - 4 - r, b.y + b.h - 4 - r, r, r);
}
}
// Visible cracks when a 2-hit brick has taken 1 hit
if (b.maxHp === 2 && b.hp === 1) {
ctx.strokeStyle = '#090c17';
ctx.lineWidth = 2.5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
for (const line of b.cracks) {
ctx.beginPath();
ctx.moveTo(b.x + line[0].x, b.y + line[0].y);
for (let i = 1; i < line.length; i++) {
ctx.lineTo(b.x + line[i].x, b.y + line[i].y);
}
ctx.stroke();
}
// Bright fracture edge highlight
ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(b.x + b.cracks[0][0].x + 1, b.y + b.cracks[0][0].y);
for (let i = 1; i < b.cracks[0].length; i++) {
ctx.lineTo(b.x + b.cracks[0][i].x + 1, b.y + b.cracks[0][i].y);
}
ctx.stroke();
}
// Power-up icon visibly embedded on brick
if (b.powerup) {
drawPowerupBadge(b.x + b.w / 2, b.y + b.h / 2, b.powerup, 0.95);
}
// Hit flash overlay
if (b.hitAnim > 0) {
ctx.fillStyle = `rgba(255, 255, 255, ${b.hitAnim * 0.75})`;
drawRoundRect(b.x, b.y, b.w, b.h, 5);
ctx.fill();
}
ctx.restore();
}
function drawHUD() {
// Top bar background
ctx.fillStyle = 'rgba(14, 20, 40, 0.85)';
ctx.fillRect(10, 10, W - 20, 42);
ctx.strokeStyle = 'rgba(0, 240, 255, 0.3)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(10, 52);
ctx.lineTo(W - 10, 52);
ctx.stroke();
// Score
ctx.fillStyle = '#88f3ff';
ctx.font = 'bold 18px "Segoe UI", sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`SCORE: ${String(score).padStart(5, '0')}`, 26, 37);
// Active Wide-Paddle Timer Bar
if (paddle.wideTimer > 0) {
const barW = 150;
const bx = W / 2 - barW / 2;
const by = 22;
ctx.fillStyle = 'rgba(5, 255, 161, 0.18)';
drawRoundRect(bx, by, barW, 18, 9);
ctx.fill();
const pct = Math.max(0, paddle.wideTimer / 10);
ctx.fillStyle = '#05ffa1';
drawRoundRect(bx + 2, by + 2, (barW - 4) * pct, 14, 7);
ctx.fill();
ctx.fillStyle = '#070913';
ctx.font = 'bold 11px "Segoe UI", sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`WIDE PADDLE ${paddle.wideTimer.toFixed(1)}s`, W / 2, by + 13);
} else {
// Legend in center of HUD
ctx.font = '600 13px "Segoe UI", sans-serif';
ctx.fillStyle = 'rgba(255, 255, 255, 0.65)';
ctx.textAlign = 'center';
ctx.fillText('POWER-UPS: [↔ WIDE PADDLE] [••• MULTI-BALL]', W / 2, 36);
}
// Lives indicator
ctx.textAlign = 'right';
ctx.fillStyle = '#ff7aa2';
ctx.font = 'bold 16px "Segoe UI", sans-serif';
ctx.fillText('LIVES:', W - 108, 37);
for (let i = 0; i < 3; i++) {
const lx = W - 85 + i * 26;
const ly = 31;
ctx.beginPath();
ctx.arc(lx, ly, 7.5, 0, Math.PI * 2);
if (i < lives) {
ctx.fillStyle = '#ff2a6d';
ctx.shadowColor = '#ff2a6d';
ctx.shadowBlur = 8;
ctx.fill();
ctx.shadowBlur = 0;
} else {
ctx.strokeStyle = 'rgba(255, 42, 109, 0.35)';
ctx.lineWidth = 2;
ctx.stroke();
}
}
}
function drawOverlay() {
if (state === 'PLAYING') return;
if (state === 'DEMO') {
// Translucent floating banner in lower-middle area so bricks & action stay 100% clear
const boxW = 490;
const boxH = 128;
const bx = (W - boxW) / 2;
const by = H * 0.56;
ctx.save();
ctx.fillStyle = 'rgba(8, 12, 26, 0.84)';
ctx.strokeStyle = 'rgba(0, 240, 255, 0.65)';
ctx.lineWidth = 2;
ctx.shadowColor = 'rgba(0, 240, 255, 0.25)';
ctx.shadowBlur = 20;
drawRoundRect(bx, by, boxW, boxH, 16);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
ctx.textAlign = 'center';
ctx.fillStyle = '#00f0ff';
ctx.font = 'bold 26px "Segoe UI", sans-serif';
ctx.fillText('NEON BREAKOUT — DEMO MODE', W / 2, by + 40);
const pulse = 0.65 + 0.35 * Math.sin(performance.now() * 0.006);
ctx.fillStyle = `rgba(255, 255, 255, ${pulse})`;
ctx.font = 'bold 20px "Segoe UI", sans-serif';
ctx.fillText('Click or press any key to start', W / 2, by + 76);
ctx.fillStyle = 'rgba(200, 225, 255, 0.7)';
ctx.font = '14px "Segoe UI", sans-serif';
ctx.fillText('Controls: Mouse or Left / Right Arrow Keys', W / 2, by + 106);
ctx.restore();
return;
}
// Victory / Game Over Modal
ctx.save();
ctx.fillStyle = 'rgba(6, 9, 20, 0.78)';
ctx.fillRect(0, 0, W, H);
const boxW = 460;
const boxH = 250;
const bx = (W - boxW) / 2;
const by = (H - boxH) / 2;
const isWin = state === 'VICTORY';
const accent = isWin ? '#05ffa1' : '#ff2a6d';
ctx.fillStyle = '#0d1326';
ctx.strokeStyle = accent;
ctx.lineWidth = 3;
ctx.shadowColor = accent;
ctx.shadowBlur = 25;
drawRoundRect(bx, by, boxW, boxH, 18);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
ctx.textAlign = 'center';
ctx.fillStyle = accent;
ctx.font = 'bold 42px "Segoe UI", sans-serif';
ctx.fillText(isWin ? 'VICTORY!' : 'GAME OVER', W / 2, by + 68);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 24px "Segoe UI", sans-serif';
ctx.fillText(`FINAL SCORE: ${score}`, W / 2, by + 122);
// Restart button
const btnW = 280;
const btnH = 50;
const btnX = (W - btnW) / 2;
const btnY = by + 162;
ctx.fillStyle = accent;
drawRoundRect(btnX, btnY, btnW, btnH, 10);
ctx.fill();
ctx.fillStyle = '#070913';
ctx.font = 'bold 19px "Segoe UI", sans-serif';
ctx.fillText('CLICK OR PRESS KEY TO RESTART', W / 2, btnY + 31);
ctx.restore();
}
function render() {
ctx.clearRect(0, 0, W, H);
ctx.save();
if (shakeTime > 0) {
const dx = (Math.random() - 0.5) * shakeMag * 2;
const dy = (Math.random() - 0.5) * shakeMag * 2;
ctx.translate(dx, dy);
}
// Starfield background
for (const st of stars) {
ctx.fillStyle = `rgba(180, 220, 255, ${st.alpha})`;
ctx.beginPath();
ctx.arc(st.x, st.y, st.r, 0, Math.PI * 2);
ctx.fill();
}
// Playfield border
ctx.strokeStyle = 'rgba(0, 240, 255, 0.25)';
ctx.lineWidth = 2;
ctx.strokeRect(10, 10, W - 20, H - 20);
// Bricks
for (const b of bricks) {
drawBrick(b);
}
// Falling Power-ups
for (const p of powerups) {
const scale = 1.15 + Math.sin(p.pulse) * 0.1;
ctx.save();
ctx.shadowColor = p.type === 'W' ? '#05ffa1' : '#ffdd00';
ctx.shadowBlur = 12;
drawPowerupBadge(p.x, p.y, p.type, scale);
ctx.restore();
}
// Paddle
ctx.save();
const padColor = paddle.wideTimer > 0 ? '#05ffa1' : '#00f0ff';
ctx.shadowColor = padColor;
ctx.shadowBlur = 14;
ctx.fillStyle = padColor;
drawRoundRect(paddle.x, paddle.y, paddle.w, paddle.h, 8);
ctx.fill();
ctx.shadowBlur = 0;
// Paddle center core highlight
ctx.fillStyle = '#ffffff';
drawRoundRect(paddle.x + 8, paddle.y + 3, paddle.w - 16, 4, 2);
ctx.fill();
ctx.restore();
// Balls & Motion Trails
for (const b of balls) {
for (let i = 0; i < b.trail.length; i++) {
const t = b.trail[i];
const ratio = (i + 1) / b.trail.length;
ctx.fillStyle = `rgba(0, 240, 255, ${ratio * 0.35})`;
ctx.beginPath();
ctx.arc(t.x, t.y, b.r * ratio * 0.9, 0, Math.PI * 2);
ctx.fill();
}
ctx.save();
ctx.shadowColor = '#00f0ff';
ctx.shadowBlur = 12;
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Particles
for (const p of particles) {
ctx.save();
ctx.globalAlpha = p.alpha;
ctx.fillStyle = p.color;
ctx.translate(p.x, p.y);
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
ctx.restore();
}
// Floating Texts
for (const ft of floatingTexts) {
ctx.save();
ctx.globalAlpha = ft.alpha;
ctx.font = 'bold 16px "Segoe UI", sans-serif';
ctx.fillStyle = ft.color;
ctx.textAlign = 'center';
ctx.shadowColor = '#000';
ctx.shadowBlur = 4;
ctx.fillText(ft.text, ft.x, ft.y);
ctx.restore();
}
drawHUD();
ctx.restore();
drawOverlay();
}
// Main Loop
let lastTime = performance.now();
function loop(now) {
const dt = Math.min(0.033, (now - lastTime) / 1000);
lastTime = now;
update(dt);
render();
requestAnimationFrame(loop);
}
// Start in self-playing Demo Mode immediately
startDemo();
requestAnimationFrame(loop);
</script>
</body>
</html>