Gemini 3.8 Flash
by GoogleReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 57.5 s
- Cost
- $0.038
- Output tokens
- 9,929
- Spent thinking
- 532 tokens
- Speed
- 194.5 tok/s
- Console errors
- 0
- First token after
- 6.5 s
- First visible text after
- 6.5 s
- Prompt tokens
- 524
- Code size
- 25.1 KB
- Finish reason
- stop
- Served by
- Model id
google/gemini-3.8-flash- Reasoning sent
{"effort":"medium"}- Max output
- 65,536 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 · 875 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CyberBreak - Deluxe Brick Breaker</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
}
body {
background: radial-gradient(circle at center, #0b0f19 0%, #030508 100%);
color: #e2e8f0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
#game-container {
position: relative;
width: 100%;
max-width: 900px;
aspect-ratio: 4 / 3;
max-height: 94vh;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10px 40px rgba(0, 240, 255, 0.15), 0 0 100px rgba(112, 0, 255, 0.1);
border-radius: 12px;
background: #050811;
border: 1px solid rgba(0, 240, 255, 0.25);
}
canvas {
width: 100%;
height: 100%;
display: block;
border-radius: 12px;
cursor: none;
}
.banner {
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: rgba(10, 15, 30, 0.85);
border: 1px solid #00f0ff;
padding: 10px 24px;
border-radius: 30px;
font-size: 15px;
letter-spacing: 2px;
text-transform: uppercase;
font-weight: 700;
color: #00f0ff;
box-shadow: 0 0 20px rgba(0, 240, 255, 0.4);
animation: pulse 1.6s infinite ease-in-out;
pointer-events: none;
white-space: nowrap;
z-index: 10;
}
@keyframes pulse {
0%, 100% { transform: translateX(-50%) scale(1); opacity: 0.95; }
50% { transform: translateX(-50%) scale(1.05); opacity: 0.65; }
}
@media (max-width: 600px) {
.banner { font-size: 12px; padding: 8px 16px; bottom: 12px; }
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas" width="900" height="675"></canvas>
<div id="startBanner" class="banner">Auto-Demo: Click or Press Any Key to Play</div>
</div>
<script>
// Web Audio Context for synthesized retro sound 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) return;
try {
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
if (type === 'hitPaddle') {
osc.type = 'triangle';
osc.frequency.setValueAtTime(220, t);
osc.frequency.exponentialRampToValueAtTime(440, t + 0.08);
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.08);
osc.start(t);
osc.stop(t + 0.09);
} else if (type === 'hitBrickNormal') {
osc.type = 'sine';
osc.frequency.setValueAtTime(520, t);
osc.frequency.exponentialRampToValueAtTime(880, t + 0.08);
gain.gain.setValueAtTime(0.25, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.08);
osc.start(t);
osc.stop(t + 0.09);
} else if (type === 'hitBrickHard') {
osc.type = 'square';
osc.frequency.setValueAtTime(180, t);
osc.frequency.exponentialRampToValueAtTime(120, t + 0.1);
gain.gain.setValueAtTime(0.25, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.1);
osc.start(t);
osc.stop(t + 0.11);
} else if (type === 'powerup') {
osc.type = 'sine';
osc.frequency.setValueAtTime(400, t);
osc.frequency.setValueAtTime(600, t + 0.06);
osc.frequency.setValueAtTime(900, t + 0.12);
gain.gain.setValueAtTime(0.25, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.25);
osc.start(t);
osc.stop(t + 0.26);
} else if (type === 'loseLife') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(220, t);
osc.frequency.exponentialRampToValueAtTime(70, t + 0.25);
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.25);
osc.start(t);
osc.stop(t + 0.26);
} else if (type === 'win') {
osc.type = 'triangle';
osc.frequency.setValueAtTime(440, t);
osc.frequency.setValueAtTime(554.37, t + 0.12);
osc.frequency.setValueAtTime(659.25, t + 0.24);
osc.frequency.setValueAtTime(880, t + 0.36);
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.6);
osc.start(t);
osc.stop(t + 0.61);
}
} catch (e) {
// Audio fallback silent
}
}
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const banner = document.getElementById('startBanner');
const W = canvas.width;
const H = canvas.height;
// Game States
let isDemo = true;
let state = 'PLAYING'; // 'PLAYING', 'GAMEOVER', 'VICTORY'
let score = 0;
let lives = 3;
let shakeTime = 0;
// Controls
let keys = { left: false, right: false };
let mouseActive = false;
let mouseX = W / 2;
// Paddle
const PADDLE_DEFAULT_WIDTH = 120;
const PADDLE_WIDE_WIDTH = 190;
const PADDLE_HEIGHT = 16;
const PADDLE_Y = H - 42;
let paddle = {
x: W / 2 - PADDLE_DEFAULT_WIDTH / 2,
y: PADDLE_Y,
width: PADDLE_DEFAULT_WIDTH,
height: PADDLE_HEIGHT,
speed: 680,
wideTimer: 0
};
// Ball configuration
const BALL_RADIUS = 8;
const BALL_BASE_SPEED = 460;
let balls = [];
function createBall(x, y, vx, vy) {
return {
x: x,
y: y,
vx: vx,
vy: vy,
r: BALL_RADIUS,
trail: []
};
}
function resetBalls() {
balls = [createBall(paddle.x + paddle.width / 2, paddle.y - BALL_RADIUS - 2, 180, -BALL_BASE_SPEED)];
}
// Bricks setup: 7 rows x 10 cols
const BRICK_ROWS = 7;
const BRICK_COLS = 10;
const BRICK_GAP = 8;
const BRICK_OFFSET_TOP = 70;
const BRICK_OFFSET_LEFT = 45;
const BRICK_WIDTH = (W - BRICK_OFFSET_LEFT * 2 - (BRICK_COLS - 1) * BRICK_GAP) / BRICK_COLS;
const BRICK_HEIGHT = 24;
let bricks = [];
function initBricks() {
bricks = [];
const colorTiers = [
{ fill: '#ff0055', stroke: '#ff5599', points: 40 }, // Row 0
{ fill: '#ff7700', stroke: '#ffaa33', points: 30 }, // Row 1
{ fill: '#ffd200', stroke: '#fff066', points: 25 }, // Row 2
{ fill: '#00f0ff', stroke: '#88f9ff', points: 20 }, // Row 3
{ fill: '#00ff88', stroke: '#88ffc2', points: 15 }, // Row 4
{ fill: '#b800ff', stroke: '#e288ff', points: 10 }, // Row 5
{ fill: '#3b82f6', stroke: '#93c5fd', points: 10 } // Row 6
];
for (let r = 0; r < BRICK_ROWS; r++) {
for (let c = 0; c < BRICK_COLS; c++) {
// Rows 0 and 1 are 2-hit tough bricks; Row 2 also has some alternating tough bricks
const isTough = (r <= 1) || (r === 2 && (c % 2 === 0));
const maxHits = isTough ? 2 : 1;
// Power-ups: Wide paddle ('W') or Multi-ball ('M')
let power = null;
if (r === 1 && (c === 2 || c === 7)) power = 'W';
if (r === 2 && (c === 4 || c === 5)) power = 'M';
if (r === 3 && (c === 1 || c === 8)) power = 'W';
if (r === 4 && (c === 3 || c === 6)) power = 'M';
if (r === 5 && c === 4) power = 'W';
bricks.push({
x: BRICK_OFFSET_LEFT + c * (BRICK_WIDTH + BRICK_GAP),
y: BRICK_OFFSET_TOP + r * (BRICK_HEIGHT + BRICK_GAP),
w: BRICK_WIDTH,
h: BRICK_HEIGHT,
r: r,
c: c,
hits: maxHits,
maxHits: maxHits,
color: colorTiers[r],
power: power,
alive: true
});
}
}
}
// Particles
let particles = [];
function spawnParticles(x, y, color, count = 16) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 220 + 40;
particles.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
size: Math.random() * 4 + 2,
life: 1.0,
decay: Math.random() * 1.5 + 1.2,
color: color
});
}
}
// Falling Power-ups
let powerups = [];
function spawnPowerup(x, y, type) {
powerups.push({
x: x,
y: y,
w: 24,
h: 24,
vy: 140,
type: type // 'W' or 'M'
});
}
function startUserGame() {
initAudio();
if (isDemo || state !== 'PLAYING') {
isDemo = false;
banner.style.display = 'none';
if (state !== 'PLAYING') {
score = 0;
lives = 3;
paddle.wideTimer = 0;
paddle.width = PADDLE_DEFAULT_WIDTH;
initBricks();
state = 'PLAYING';
}
resetBalls();
}
}
// Input Listeners
window.addEventListener('keydown', (e) => {
initAudio();
if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
if (e.code === 'Space' || e.code === 'Enter') {
startUserGame();
} else if (isDemo) {
startUserGame();
}
});
window.addEventListener('keyup', (e) => {
if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
});
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
mouseX = (e.clientX - rect.left) * scaleX;
mouseActive = true;
});
canvas.addEventListener('mousedown', () => {
startUserGame();
});
// Touch Support
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (!e.touches[0]) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
mouseX = (e.touches[0].clientX - rect.left) * scaleX;
mouseActive = true;
}, { passive: false });
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
startUserGame();
if (e.touches[0]) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
mouseX = (e.touches[0].clientX - rect.left) * scaleX;
mouseActive = true;
}
}, { passive: false });
// Reset initial game
initBricks();
resetBalls();
// Continuous physics & collision
let lastTime = performance.now();
function update(dt) {
if (shakeTime > 0) shakeTime -= dt;
// Paddle Wide Timer
if (paddle.wideTimer > 0) {
paddle.wideTimer -= dt;
paddle.width = PADDLE_WIDE_WIDTH;
if (paddle.wideTimer <= 0) {
paddle.width = PADDLE_DEFAULT_WIDTH;
}
}
// Move Paddle
if (isDemo) {
// Simple Auto AI: follow closest or lowest falling ball
let targetBall = balls[0];
for (let b of balls) {
if (!targetBall || b.y > targetBall.y) targetBall = b;
}
if (targetBall) {
const destX = targetBall.x - paddle.width / 2;
paddle.x += (destX - paddle.x) * Math.min(1, dt * 14);
}
} else {
if (mouseActive) {
paddle.x = mouseX - paddle.width / 2;
}
if (keys.left) {
paddle.x -= paddle.speed * dt;
mouseActive = false;
}
if (keys.right) {
paddle.x += paddle.speed * dt;
mouseActive = false;
}
}
// Clamp paddle within screen
if (paddle.x < 10) paddle.x = 10;
if (paddle.x + paddle.width > W - 10) paddle.x = W - 10 - paddle.width;
if (state !== 'PLAYING') return;
// Sub-stepping for ultra-reliable collision (prevent tunneling)
const SUB_STEPS = 4;
const subDt = dt / SUB_STEPS;
for (let step = 0; step < SUB_STEPS; step++) {
for (let i = balls.length - 1; i >= 0; i--) {
const ball = balls[i];
ball.x += ball.vx * subDt;
ball.y += ball.vy * subDt;
// Trail record (only record on main steps)
if (step === 0) {
ball.trail.push({ x: ball.x, y: ball.y });
if (ball.trail.length > 8) ball.trail.shift();
}
// Left / Right Walls
if (ball.x - ball.r < 8) {
ball.x = 8 + ball.r;
ball.vx = Math.abs(ball.vx);
} else if (ball.x + ball.r > W - 8) {
ball.x = W - 8 - ball.r;
ball.vx = -Math.abs(ball.vx);
}
// Top Wall
if (ball.y - ball.r < 8) {
ball.y = 8 + ball.r;
ball.vy = Math.abs(ball.vy);
}
// Bottom Wall (Dead ball)
if (ball.y - ball.r > H + 10) {
balls.splice(i, 1);
continue;
}
// Paddle Collision (Angle-based bounce)
// Center = straight up; Edges = sharp angles
if (ball.vy > 0 &&
ball.y + ball.r >= paddle.y &&
ball.y - ball.r <= paddle.y + paddle.height &&
ball.x + ball.r >= paddle.x &&
ball.x - ball.r <= paddle.x + paddle.width) {
ball.y = paddle.y - ball.r; // snap to top of paddle
const hitOffset = (ball.x - (paddle.x + paddle.width / 2)) / (paddle.width / 2);
const clampedOffset = Math.max(-0.92, Math.min(0.92, hitOffset));
// Max bounce angle = ~68 degrees
const maxAngle = Math.PI * 0.38;
const bounceAngle = clampedOffset * maxAngle;
const currentSpeed = Math.hypot(ball.vx, ball.vy);
const newSpeed = Math.min(680, currentSpeed * 1.015); // gentle speed up
ball.vx = newSpeed * Math.sin(bounceAngle);
ball.vy = -newSpeed * Math.cos(bounceAngle);
playSound('hitPaddle');
spawnParticles(ball.x, paddle.y, '#00f0ff', 6);
}
// Brick Collisions
for (let b of bricks) {
if (!b.alive) continue;
// Closest point on brick to ball center
const nearestX = Math.max(b.x, Math.min(ball.x, b.x + b.w));
const nearestY = Math.max(b.y, Math.min(ball.y, b.y + b.h));
const dx = ball.x - nearestX;
const dy = ball.y - nearestY;
const distSq = dx * dx + dy * dy;
if (distSq < ball.r * ball.r) {
// Collision occurred! Determine bounce direction
const overlapX = ball.r - Math.abs(dx);
const overlapY = ball.r - Math.abs(dy);
if (dx === 0 && dy === 0) {
// Center inside brick, push out upwards
ball.vy = -Math.abs(ball.vy);
ball.y = b.y - ball.r;
} else if (overlapX < overlapY) {
ball.vx = dx > 0 ? Math.abs(ball.vx) : -Math.abs(ball.vx);
ball.x += dx > 0 ? overlapX : -overlapX;
} else {
ball.vy = dy > 0 ? Math.abs(ball.vy) : -Math.abs(ball.vy);
ball.y += dy > 0 ? overlapY : -overlapY;
}
// Hit logic
b.hits--;
shakeTime = 0.08;
if (b.hits <= 0) {
b.alive = false;
score += b.color.points * b.maxHits;
playSound('hitBrickNormal');
spawnParticles(b.x + b.w / 2, b.y + b.h / 2, b.color.fill, 20);
if (b.power) {
spawnPowerup(b.x + b.w / 2 - 12, b.y + b.h / 2, b.power);
}
} else {
playSound('hitBrickHard');
score += 10;
spawnParticles(nearestX, nearestY, '#ffffff', 8);
}
// Check Victory
const anyAlive = bricks.some(k => k.alive);
if (!anyAlive) {
state = 'VICTORY';
playSound('win');
}
break; // Break inner loop after colliding with a brick
}
}
}
}
// If all balls are lost
if (balls.length === 0) {
lives--;
playSound('loseLife');
shakeTime = 0.25;
if (lives <= 0) {
state = 'GAMEOVER';
} else {
resetBalls();
}
}
// Powerups Update
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.y += p.vy * dt;
// Check paddle catch
if (p.y + p.h >= paddle.y &&
p.y <= paddle.y + paddle.height &&
p.x + p.w >= paddle.x &&
p.x <= paddle.x + paddle.width) {
playSound('powerup');
spawnParticles(p.x + p.w / 2, paddle.y, p.type === 'W' ? '#00f0ff' : '#ff00ff', 18);
if (p.type === 'W') {
paddle.wideTimer = 10; // 10 seconds of wide paddle
score += 50;
} else if (p.type === 'M') {
score += 50;
// Spawn 2 more balls with varied directions
const base = balls[0] || { x: paddle.x + paddle.width / 2, y: paddle.y - 15 };
balls.push(createBall(base.x, base.y, -260, -BALL_BASE_SPEED));
balls.push(createBall(base.x, base.y, 260, -BALL_BASE_SPEED));
}
powerups.splice(i, 1);
continue;
}
// Fell off screen
if (p.y > H + 30) {
powerups.splice(i, 1);
}
}
// Particles Update
for (let i = particles.length - 1; i >= 0; i--) {
const pt = particles[i];
pt.x += pt.vx * dt;
pt.y += pt.vy * dt;
pt.life -= pt.decay * dt;
if (pt.life <= 0) {
particles.splice(i, 1);
}
}
}
// Render Loop
function draw() {
ctx.save();
// Screen Shake effect
if (shakeTime > 0) {
const mag = shakeTime * 12;
ctx.translate((Math.random() - 0.5) * mag, (Math.random() - 0.5) * mag);
}
// Clear background
ctx.fillStyle = '#060913';
ctx.fillRect(0, 0, W, H);
// Subtle Neon Arena Border
ctx.lineWidth = 3;
ctx.strokeStyle = 'rgba(0, 240, 255, 0.4)';
ctx.strokeRect(6, 6, W - 12, H - 12);
// Background Grid
ctx.strokeStyle = 'rgba(255, 255, 255, 0.025)';
ctx.lineWidth = 1;
for (let x = 40; x < W; x += 40) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, H);
ctx.stroke();
}
for (let y = 40; y < H; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
// Draw Bricks
for (let b of bricks) {
if (!b.alive) continue;
ctx.save();
ctx.shadowBlur = b.maxHits > 1 ? 12 : 6;
ctx.shadowColor = b.color.fill;
// Tough brick vs Normal appearance
if (b.maxHits > 1 && b.hits === 2) {
// Uncracked Tough Brick: metallic gradient
const grad = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
grad.addColorStop(0, '#f1f5f9');
grad.addColorStop(0.5, b.color.fill);
grad.addColorStop(1, '#1e293b');
ctx.fillStyle = grad;
} else {
ctx.fillStyle = b.color.fill;
}
ctx.beginPath();
ctx.roundRect(b.x, b.y, b.w, b.h, 4);
ctx.fill();
// Border highlight
ctx.lineWidth = 1.5;
ctx.strokeStyle = b.color.stroke;
ctx.stroke();
// Visible Cracks on 2-hit brick after 1st hit
if (b.maxHits > 1 && b.hits === 1) {
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.beginPath();
// Jagged crack lines across the brick
ctx.moveTo(b.x + b.w * 0.2, b.y + 2);
ctx.lineTo(b.x + b.w * 0.45, b.y + b.h * 0.55);
ctx.lineTo(b.x + b.w * 0.35, b.y + b.h - 2);
ctx.moveTo(b.x + b.w * 0.45, b.y + b.h * 0.55);
ctx.lineTo(b.x + b.w * 0.8, b.y + b.h * 0.7);
ctx.stroke();
}
// Visible Power-up Icon Badge on Brick
if (b.power) {
ctx.fillStyle = b.power === 'W' ? '#00f0ff' : '#ff00ea';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(b.power === 'W' ? '⚡W' : '★M', b.x + b.w / 2, b.y + b.h / 2 + 1);
}
ctx.restore();
}
// Draw Power-ups
for (let p of powerups) {
ctx.save();
ctx.shadowBlur = 10;
ctx.shadowColor = p.type === 'W' ? '#00f0ff' : '#ff00ff';
ctx.fillStyle = p.type === 'W' ? '#00f0ff' : '#ff00ff';
ctx.beginPath();
ctx.roundRect(p.x, p.y, p.w, p.h, 6);
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#000000';
ctx.font = 'bold 13px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(p.type === 'W' ? 'W' : 'M', p.x + p.w / 2, p.y + p.h / 2);
ctx.restore();
}
// Draw Particles
for (let pt of particles) {
ctx.save();
ctx.globalAlpha = Math.max(0, pt.life);
ctx.fillStyle = pt.color;
ctx.shadowBlur = 8;
ctx.shadowColor = pt.color;
ctx.beginPath();
ctx.arc(pt.x, pt.y, pt.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Draw Paddle
ctx.save();
ctx.shadowBlur = 16;
ctx.shadowColor = paddle.wideTimer > 0 ? '#ff00ff' : '#00f0ff';
const pGrad = ctx.createLinearGradient(paddle.x, paddle.y, paddle.x, paddle.y + paddle.height);
if (paddle.wideTimer > 0) {
pGrad.addColorStop(0, '#ffffff');
pGrad.addColorStop(0.5, '#e000ff');
pGrad.addColorStop(1, '#7000aa');
} else {
pGrad.addColorStop(0, '#e0ffff');
pGrad.addColorStop(0.5, '#00f0ff');
pGrad.addColorStop(1, '#0055aa');
}
ctx.fillStyle = pGrad;
ctx.beginPath();
ctx.roundRect(paddle.x, paddle.y, paddle.width, paddle.height, 8);
ctx.fill();
// Paddle border & center mark
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.fillRect(paddle.x + paddle.width / 2 - 2, paddle.y + 2, 4, paddle.height - 4);
ctx.restore();
// Draw Balls & Trails
for (let ball of balls) {
// Trail
for (let t = 0; t < ball.trail.length; t++) {
const pt = ball.trail[t];
const alpha = (t + 1) / ball.trail.length * 0.45;
const rad = ball.r * (0.4 + 0.6 * ((t + 1) / ball.trail.length));
ctx.beginPath();
ctx.fillStyle = `rgba(0, 240, 255, ${alpha})`;
ctx.arc(pt.x, pt.y, rad, 0, Math.PI * 2);
ctx.fill();
}
// Ball Body
ctx.save();
ctx.shadowBlur = 14;
ctx.shadowColor = '#00f0ff';
const bGrad = ctx.createRadialGradient(ball.x - 2, ball.y - 2, 1, ball.x, ball.y, ball.r);
bGrad.addColorStop(0, '#ffffff');
bGrad.addColorStop(0.6, '#a6f9ff');
bGrad.addColorStop(1, '#00c3ff');
ctx.fillStyle = bGrad;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Top HUD
ctx.save();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 20px -apple-system, sans-serif';
ctx.textBaseline = 'top';
// Score
ctx.textAlign = 'left';
ctx.fillText(`SCORE: ${score}`, 25, 22);
// Power-up Timer indicator
if (paddle.wideTimer > 0) {
ctx.textAlign = 'center';
ctx.fillStyle = '#ff00ff';
ctx.font = 'bold 15px -apple-system, sans-serif';
ctx.fillText(`⚡ WIDE PADDLE: ${paddle.wideTimer.toFixed(1)}s`, W / 2, 24);
}
// Lives
ctx.textAlign = 'right';
ctx.fillStyle = '#ff3366';
let heartIcons = '';
for (let l = 0; l < lives; l++) heartIcons += '♥ ';
ctx.fillText(`LIVES: ${heartIcons}`, W - 25, 22);
ctx.restore();
// Game Over Overlay
if (state === 'GAMEOVER') {
ctx.save();
ctx.fillStyle = 'rgba(5, 8, 16, 0.82)';
ctx.fillRect(0, 0, W, H);
ctx.textAlign = 'center';
ctx.fillStyle = '#ff0055';
ctx.shadowBlur = 24;
ctx.shadowColor = '#ff0055';
ctx.font = '900 52px -apple-system, sans-serif';
ctx.fillText('GAME OVER', W / 2, H / 2 - 40);
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 22px -apple-system, sans-serif';
ctx.fillText(`Final Score: ${score}`, W / 2, H / 2 + 15);
ctx.fillStyle = '#00f0ff';
ctx.font = '16px -apple-system, sans-serif';
ctx.fillText('CLICK OR PRESS ANY KEY TO PLAY AGAIN', W / 2, H / 2 + 65);
ctx.restore();
}
// Victory Overlay
if (state === 'VICTORY') {
ctx.save();
ctx.fillStyle = 'rgba(5, 8, 16, 0.82)';
ctx.fillRect(0, 0, W, H);
ctx.textAlign = 'center';
ctx.fillStyle = '#00ff88';
ctx.shadowBlur = 24;
ctx.shadowColor = '#00ff88';
ctx.font = '900 52px -apple-system, sans-serif';
ctx.fillText('VICTORY!', W / 2, H / 2 - 40);
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 22px -apple-system, sans-serif';
ctx.fillText(`Well Done! Final Score: ${score}`, W / 2, H / 2 + 15);
ctx.fillStyle = '#00f0ff';
ctx.font = '16px -apple-system, sans-serif';
ctx.fillText('CLICK OR PRESS ANY KEY TO RESTART', W / 2, H / 2 + 65);
ctx.restore();
}
ctx.restore();
}
// Main Loop
function loop(now) {
const dt = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
update(dt);
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>