Run details
- Model
- sakana/fugu-ultra-v2
- Route
- OpenRouter API, one user message
- Served by
- Sakana AI
- Effort
- xhigh, OpenRouter's default for this model
- Limits
- 60 min · 66,666 output tokens (the model's limit)
- Date
- 9 Nov 2026
- Time
- 22 min 51 s
- Size
- 63 kB
- Tokens
- 93,387 in, 139,823 out (9,526 of them thinking)
- List price
- $4.66 at today's price ($5.00 in / $30.00 out per M)
- Paid
- $4.64
Exact prompt
Make a Breakout game I can play right now with the keyboard or the mouse. Give it several levels, a score and some juice. It must be one complete, self-contained HTML file: inline all CSS and JavaScript, and make no network requests of any kind (no CDNs, web fonts or remote images). Reply with the file in a single ```html code block.
Source
63 kB 2,337 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Neon Breakout</title>
<style>
* { box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: radial-gradient(1100px 800px at 50% -10%, #172452 0%, #090d20 55%, #03040b 100%);
color: #dff5ff;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Courier New", monospace;
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
#wrap {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
canvas {
display: block;
border-radius: 14px;
touch-action: none;
outline: none;
box-shadow:
0 0 0 1px rgba(125, 210, 255, .2),
0 0 55px rgba(50, 125, 255, .25),
0 25px 85px rgba(0, 0, 0, .75);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
</style>
</head>
<body>
<div id="wrap">
<canvas id="game" width="900" height="680" tabindex="0" aria-label="Neon Breakout game"></canvas>
</div>
<p class="sr-only">
Neon Breakout. Move with the mouse, the left and right arrow keys, or A and D.
Press Space, Enter, or click to launch the ball and to fire lasers.
Press P or Escape to pause, M to mute, R to restart.
</p>
<script>
(function () {
'use strict';
/* ============================== CONSTANTS ============================== */
var W = 900, H = 680;
var HUD_H = 68, WALL = 14;
var FL = WALL, FR = W - WALL, FT = HUD_H + 8;
var COLS = 12, MAX_ROWS = 10, GRID_PAD = 10;
var BRICK_GAP = 4;
var BRICK_W = (FR - FL - GRID_PAD * 2) / COLS;
var BRICK_H = 26, BRICK_TOP = FT + 46;
var BALL_R = 8, MAX_BALLS = 8, MAX_SPEED = 880;
var PADDLE_Y = H - 58, PADDLE_H = 16;
var PADDLE_BASE = 112, PADDLE_WIDE = 178, PADDLE_SMALL = 72;
var MAX_BOUNCE = 1.05;
var MAX_DT = 0.05;
var MAX_PARTICLES = 760;
var FONT_DISPLAY = '"Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif';
var FONT_MONO = 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Courier New", monospace';
var BULLET = '\u2022';
var ARROW_L = '\u2190';
var ARROW_R = '\u2192';
var STAR = '\u2605';
var HP_COLORS = {
1: { a: '#5ff7e6', b: '#0f9b8c' },
2: { a: '#a6f759', b: '#3f9418' },
3: { a: '#ff79dc', b: '#a1198a' },
4: { a: '#ffd36b', b: '#bf7710' },
S: { a: '#c3d2e2', b: '#58677a' }
};
/* Level grammar: '.' empty, '1'-'4' hit points, 'S' indestructible steel. */
var LEVELS = [
{ name: 'FIRST SERVE', rows: [
'............',
'111111111111',
'111111111111',
'222222222222'
] },
{ name: 'PYRAMID', rows: [
'.....11.....',
'....1221....',
'...122221...',
'..12233221..',
'.1223333221.'
] },
{ name: 'FORTRESS', rows: [
'SS........SS',
'.2222222222.',
'.2SSSSSSSS2.',
'.2333333332.',
'.2222222222.'
] },
{ name: 'CHECKPOINT', rows: [
'1.1.1.1.1.1.',
'.2.2.2.2.2.2',
'3.3.3.3.3.3.',
'.2.2.2.2.2.2',
'1.1.1.1.1.1.'
] },
{ name: 'ZIGZAG', rows: [
'..3......3..',
'.333....333.',
'33333..33333',
'.222....222.',
'..1......1..'
] },
{ name: 'THE VAULT', rows: [
'222222222222',
'2..........2',
'2.SS3333SS.2',
'2.S333333S.2',
'2..........2',
'333333333333'
] },
{ name: 'DOWNPOUR', rows: [
'4.4.4.4.4.4.',
'.S.S.S.S.S.S',
'3.3.3.3.3.3.',
'.S.S.S.S.S.S',
'2.2.2.2.2.2.',
'.1.1.1.1.1.1'
] },
{ name: 'CORE MELTDOWN', rows: [
'SS44444444SS',
'.3333333333.',
'..S333333S..',
'...333333...',
'....3333....',
'.....22.....'
] }
];
var POWER_TYPES = [
{ key: 'wide', weight: 16, color: '#4ad7ff', icon: 'W', label: 'WIDE PADDLE', duration: 14 },
{ key: 'multi', weight: 14, color: '#ff79dc', icon: 'M', label: 'MULTI BALL', duration: 0 },
{ key: 'slow', weight: 12, color: '#a6f759', icon: 'S', label: 'SLOW MOTION', duration: 10 },
{ key: 'laser', weight: 12, color: '#ffd36b', icon: 'L', label: 'LASERS', duration: 13 },
{ key: 'sticky', weight: 10, color: '#c9a3ff', icon: 'C', label: 'STICKY PADDLE', duration: 13 },
{ key: 'life', weight: 6, color: '#ff6b7a', icon: '+', label: 'EXTRA LIFE', duration: 0 },
{ key: 'small', weight: 7, color: '#ff8a3d', icon: '-', label: 'SHRINK!', duration: 9 }
];
var POWER_BY_KEY = {};
(function () {
for (var i = 0; i < POWER_TYPES.length; i++) {
POWER_BY_KEY[POWER_TYPES[i].key] = POWER_TYPES[i];
}
})();
var TIMED_EFFECTS = ['wide', 'small', 'slow', 'laser', 'sticky'];
/* =============================== HELPERS =============================== */
function clamp(v, a, b) { return v < a ? a : (v > b ? b : v); }
function lerp(a, b, t) { return a + (b - a) * t; }
function rand(a, b) { return a + Math.random() * (b - a); }
function pick(list) { return list[(Math.random() * list.length) | 0]; }
function pad(n, length) {
var s = String(Math.max(0, Math.floor(n)));
while (s.length < length) s = '0' + s;
return s;
}
function roundedRect(c, x, y, w, h, r) {
r = Math.min(r, Math.abs(w) / 2, Math.abs(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();
}
var Store = {
get: function (key, fallback) {
try {
var value = localStorage.getItem(key);
return value === null ? fallback : value;
} catch (e) {
return fallback;
}
},
set: function (key, value) {
try { localStorage.setItem(key, value); } catch (e) {}
}
};
/* ================================ AUDIO ================================ */
var muted = Store.get('neon_breakout_muted', '0') === '1';
var Sound = (function () {
var audio = null;
var master = null;
var broken = false;
function ready() {
if (muted || broken) return null;
if (!audio) {
try {
var Ctor = window.AudioContext || window.webkitAudioContext;
if (!Ctor) { broken = true; return null; }
audio = new Ctor();
master = audio.createGain();
master.gain.value = 0.24;
master.connect(audio.destination);
} catch (e) {
broken = true;
audio = null;
return null;
}
}
if (audio.state === 'suspended') {
try { audio.resume(); } catch (e) {}
}
return audio;
}
function envelope(node, start, duration, volume, attack) {
var gain = audio.createGain();
attack = attack || 0.005;
gain.gain.setValueAtTime(0.0001, start);
gain.gain.exponentialRampToValueAtTime(Math.max(0.0001, volume), start + attack);
gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
node.connect(gain);
gain.connect(master);
return gain;
}
function tone(frequency, duration, type, volume, slide, delay) {
var a = ready();
if (!a) return;
try {
var start = a.currentTime + (delay || 0);
var osc = a.createOscillator();
osc.type = type || 'square';
osc.frequency.setValueAtTime(frequency, start);
if (slide) {
osc.frequency.exponentialRampToValueAtTime(Math.max(20, slide), start + duration);
}
envelope(osc, start, duration, volume == null ? 0.3 : volume);
osc.start(start);
osc.stop(start + duration + 0.03);
} catch (e) {}
}
function noise(duration, volume, filterFrequency, slide) {
var a = ready();
if (!a) return;
try {
var start = a.currentTime;
var count = Math.max(1, Math.floor(a.sampleRate * duration));
var buffer = a.createBuffer(1, count, a.sampleRate);
var data = buffer.getChannelData(0);
for (var i = 0; i < count; i++) {
data[i] = (Math.random() * 2 - 1) * (1 - i / count);
}
var source = a.createBufferSource();
source.buffer = buffer;
var filter = a.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(filterFrequency || 1200, start);
if (slide) {
filter.frequency.exponentialRampToValueAtTime(Math.max(60, slide), start + duration);
}
source.connect(filter);
envelope(filter, start, duration, volume == null ? 0.25 : volume, 0.004);
source.start(start);
} catch (e) {}
}
function sequence(base, steps, duration, type, volume, gap) {
for (var i = 0; i < steps.length; i++) {
tone(base * Math.pow(2, steps[i] / 12), duration, type, volume, null, i * gap);
}
}
return {
unlock: function () { ready(); },
paddle: function () { tone(300, 0.07, 'square', 0.22, 470); },
wall: function () { tone(195, 0.05, 'sine', 0.14, 175); },
brick: function (combo) {
var f = 240 * Math.pow(1.0595, Math.min(combo || 0, 26));
tone(f, 0.08, 'triangle', 0.26, f * 1.5);
},
hard: function () { tone(130, 0.1, 'sawtooth', 0.17, 72); noise(0.07, 0.12, 900); },
steel: function () { noise(0.09, 0.2, 2800, 600); tone(540, 0.05, 'square', 0.07, 380); },
power: function () { sequence(440, [0, 4, 7], 0.1, 'square', 0.18, 0.06); },
life: function () { sequence(523, [0, 7, 12], 0.14, 'sine', 0.22, 0.07); },
laser: function () { tone(900, 0.08, 'sawtooth', 0.1, 180); },
lose: function () { tone(300, 0.5, 'sawtooth', 0.28, 60); noise(0.4, 0.16, 800, 100); },
clear: function () { sequence(330, [0, 2, 4, 7, 12], 0.22, 'triangle', 0.2, 0.08); },
over: function () { sequence(300, [0, -3, -7, -12], 0.4, 'sawtooth', 0.2, 0.15); },
win: function () { sequence(392, [0, 4, 7, 12, 16, 19, 24], 0.3, 'square', 0.15, 0.09); },
ui: function () { tone(660, 0.06, 'square', 0.13, 880); }
};
})();
/* ================================ STATE ================================ */
var canvas = document.getElementById('game');
var ctx = canvas.getContext('2d');
var highScore = parseInt(Store.get('neon_breakout_high', '0'), 10) || 0;
var game = {
state: 'title',
previousState: 'serve',
score: 0,
lives: 3,
level: 0,
loop: 0,
combo: 0,
bestCombo: 0,
bricksLeft: 0,
bricksTotal: 1,
timer: 0,
intro: 0,
levelBonus: 0,
trauma: 0,
hitStop: 0,
flash: 0,
flashColor: '255,255,255',
time: 0,
newRecord: false,
error: null
};
var paddle = {
cx: W / 2, w: PADDLE_BASE, h: PADDLE_H, y: PADDLE_Y,
vx: 0, squash: 0, flash: 0, cooldown: 0
};
var effects = { wide: 0, small: 0, slow: 0, laser: 0, sticky: 0 };
var balls = [];
var bricks = [];
var powerups = [];
var bolts = [];
var particles = [];
var floatingTexts = [];
var waves = [];
var keys = Object.create(null);
var pointerMode = false;
var pointerX = W / 2;
var fireHeld = false;
var lastWallSound = -1;
var demoBall = { x: 260, y: 380, vx: 250, vy: -215, r: 8, trail: [] };
/* ========================== BRICK SPRITE CACHE ========================= */
var brickSprites = {};
(function buildBrickSprites() {
var names = ['1', '2', '3', '4', 'S'];
for (var i = 0; i < names.length; i++) {
var name = names[i];
var colors = HP_COLORS[name] || HP_COLORS[1];
var scale = 2;
var width = BRICK_W - BRICK_GAP;
var height = BRICK_H;
var sprite = document.createElement('canvas');
sprite.width = Math.round(width * scale);
sprite.height = Math.round(height * scale);
var g = sprite.getContext('2d');
g.scale(scale, scale);
var gradient = g.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, colors.a);
gradient.addColorStop(0.55, colors.b);
gradient.addColorStop(1, colors.a);
roundedRect(g, 1, 1, width - 2, height - 2, 6);
g.fillStyle = gradient;
g.fill();
g.globalAlpha = 0.45;
roundedRect(g, 3.5, 3, width - 7, height * 0.36, 4);
g.fillStyle = '#ffffff';
g.fill();
g.globalAlpha = 1;
roundedRect(g, 1, 1, width - 2, height - 2, 6);
g.lineWidth = 1.4;
g.strokeStyle = 'rgba(255,255,255,.4)';
g.stroke();
if (name === 'S') {
g.fillStyle = 'rgba(255,255,255,.55)';
var rivets = [[7, height / 2], [width - 7, height / 2]];
for (var r = 0; r < rivets.length; r++) {
g.beginPath();
g.arc(rivets[r][0], rivets[r][1], 2.2, 0, Math.PI * 2);
g.fill();
}
g.strokeStyle = 'rgba(0,0,0,.25)';
g.lineWidth = 1;
g.beginPath();
g.moveTo(10, height - 4);
g.lineTo(width - 10, 4);
g.stroke();
}
brickSprites[name] = sprite;
}
})();
/* =============================== LEVELS ================================ */
function baseSpeed() {
return clamp(395 + game.level * 16 + game.loop * 55, 320, MAX_SPEED);
}
function loadLevel(index) {
var normalized = ((index % LEVELS.length) + LEVELS.length) % LEVELS.length;
var definition = LEVELS[normalized];
bricks.length = 0;
powerups.length = 0;
bolts.length = 0;
waves.length = 0;
var count = 0;
var rows = (definition && definition.rows) || [];
for (var rowIndex = 0; rowIndex < rows.length && rowIndex < MAX_ROWS; rowIndex++) {
var row = String(rows[rowIndex] || '');
while (row.length < COLS) row += '.';
row = row.slice(0, COLS);
for (var column = 0; column < COLS; column++) {
var character = row.charAt(column);
if (character === '.' || character === ' ') continue;
var steel = (character === 'S' || character === 's');
var hp = steel ? 1 : clamp(parseInt(character, 10) || 1, 1, 4);
bricks.push({
x: FL + GRID_PAD + BRICK_GAP / 2 + column * BRICK_W,
y: BRICK_TOP + rowIndex * (BRICK_H + BRICK_GAP),
w: BRICK_W - BRICK_GAP,
h: BRICK_H,
hp: hp,
maxHp: hp,
steel: steel,
alive: true,
flash: 0,
shake: 0,
offY: -(260 + rowIndex * 22 + rand(0, 70))
});
if (!steel) count++;
}
}
game.bricksLeft = count;
game.bricksTotal = Math.max(1, count);
return count;
}
function resetEffects() {
for (var i = 0; i < TIMED_EFFECTS.length; i++) {
effects[TIMED_EFFECTS[i]] = 0;
}
powerups.length = 0;
bolts.length = 0;
paddle.cooldown = 0;
}
function startLevel() {
loadLevel(game.level);
resetEffects();
paddle.w = PADDLE_BASE;
paddle.cx = clamp(paddle.cx, FL + PADDLE_BASE / 2, FR - PADDLE_BASE / 2);
paddle.vx = 0;
serveBall();
game.combo = 0;
game.intro = 1.7;
game.state = 'serve';
game.flash = 0.45;
game.flashColor = '140,210,255';
}
function startGame() {
game.error = null;
game.score = 0;
game.lives = 3;
game.level = 0;
game.loop = 0;
game.combo = 0;
game.bestCombo = 0;
game.newRecord = false;
game.hitStop = 0;
game.trauma = 0;
particles.length = 0;
floatingTexts.length = 0;
startLevel();
Sound.ui();
}
function nextLoop() {
game.loop++;
game.level = 0;
game.lives = Math.max(game.lives, 3);
startLevel();
Sound.ui();
}
function advanceLevel() {
if (game.level + 1 >= LEVELS.length) {
game.state = 'win';
saveHighScore();
Sound.win();
return;
}
game.level++;
startLevel();
}
function saveHighScore() {
if (game.score > highScore) {
highScore = game.score;
game.newRecord = true;
}
Store.set('neon_breakout_high', String(highScore));
}
/* ================================ BALLS ================================ */
function makeBall(x, y, vx, vy, speed) {
return {
x: x, y: y, vx: vx, vy: vy, r: BALL_R, speed: speed,
trail: [], stuck: false, offset: 0, dead: false, idle: 0
};
}
function serveBall() {
balls.length = 0;
var ball = makeBall(paddle.cx, paddle.y - BALL_R - 2, 0, 0, baseSpeed());
ball.stuck = true;
balls.push(ball);
}
function normalizeBall(ball) {
var vx = ball.vx;
var vy = ball.vy;
var length = Math.sqrt(vx * vx + vy * vy);
if (!(length > 0.0001)) { vx = 0; vy = -1; length = 1; }
vx /= length;
vy /= length;
var minY = 0.26;
if (Math.abs(vy) < minY) {
vy = (vy > 0 ? 1 : -1) * minY;
vx = (vx >= 0 ? 1 : -1) * Math.sqrt(Math.max(0, 1 - minY * minY));
}
var minX = 0.08;
if (Math.abs(vx) < minX) {
vx = (vx >= 0 ? 1 : -1) * minX;
vy = (vy > 0 ? 1 : -1) * Math.sqrt(Math.max(0, 1 - minX * minX));
}
var speed = ball.speed * (effects.slow > 0 ? 0.66 : 1);
ball.vx = vx * speed;
ball.vy = vy * speed;
}
function releaseBall(ball) {
if (!ball.stuck) return;
ball.stuck = false;
var angle = -Math.PI / 2 +
clamp(ball.offset / Math.max(1, paddle.w / 2), -1, 1) * 0.6 +
rand(-0.06, 0.06);
ball.vx = Math.cos(angle) * ball.speed;
ball.vy = Math.sin(angle) * ball.speed;
normalizeBall(ball);
}
function launchBalls() {
var launched = false;
for (var i = 0; i < balls.length; i++) {
if (balls[i].stuck) {
releaseBall(balls[i]);
launched = true;
}
}
if (launched) {
Sound.paddle();
paddle.squash = 1;
if (game.state === 'serve') game.state = 'play';
}
return launched;
}
function multiball() {
var originals = balls.slice();
for (var i = 0; i < originals.length; i++) {
if (balls.length >= MAX_BALLS) break;
var source = originals[i];
if (source.stuck) releaseBall(source);
var baseAngle = Math.atan2(source.vy, source.vx);
var offsets = [-0.42, 0.42];
for (var j = 0; j < offsets.length; j++) {
if (balls.length >= MAX_BALLS) break;
var angle = baseAngle + offsets[j];
balls.push(makeBall(
source.x, source.y,
Math.cos(angle) * source.speed,
Math.sin(angle) * source.speed,
source.speed
));
}
}
if (game.state === 'serve') game.state = 'play';
}
/* =============================== SCORING =============================== */
function addScore(points, x, y, color, size) {
game.score += points;
if (game.score > highScore) {
highScore = game.score;
game.newRecord = true;
}
if (x != null) {
floatingTexts.push({
x: x, y: y, text: '+' + points,
life: 0.9, maxLife: 0.9,
color: color || '#ffffff', size: size || 16, vy: -48
});
}
}
function addFloatingText(text, x, y, color, size) {
floatingTexts.push({
x: x, y: y, text: text,
life: 1.1, maxLife: 1.1,
color: color || '#ffffff', size: size || 16, vy: -36
});
}
/* ============================== PARTICLES ============================== */
function spawnParticle(x, y, color, speed, options) {
if (particles.length > MAX_PARTICLES) particles.shift();
options = options || {};
var angle = options.angle == null
? rand(0, Math.PI * 2)
: options.angle + rand(-0.6, 0.6);
var velocity = speed * rand(0.35, 1);
var particle = {
x: x, y: y,
vx: Math.cos(angle) * velocity,
vy: Math.sin(angle) * velocity,
life: options.life || rand(0.35, 0.9),
maxLife: 1,
size: options.size || rand(2, 5),
color: color,
gravity: options.gravity == null ? 540 : options.gravity,
rotation: rand(0, Math.PI * 2),
rotationSpeed: rand(-10, 10),
shape: options.shape || 'rect',
drag: options.drag || 0.88
};
particle.maxLife = particle.life;
particles.push(particle);
}
function sparks(x, y, color, count, speed) {
for (var i = 0; i < count; i++) {
spawnParticle(x, y, color, speed, {
size: rand(1.5, 3), life: rand(0.18, 0.45),
gravity: 170, shape: 'spark', drag: 0.9
});
}
}
function shockwave(x, y, radius, maxRadius, life, color) {
waves.push({
x: x, y: y, radius: radius, maxRadius: maxRadius,
life: life, maxLife: life, color: color
});
}
function explodeBrick(brick) {
var colors = HP_COLORS[brick.steel ? 'S' : brick.maxHp] || HP_COLORS[1];
var drawY = brick.y + brick.offY;
var centerX = brick.x + brick.w / 2;
var centerY = drawY + brick.h / 2;
for (var i = 0; i < 14; i++) {
spawnParticle(
rand(brick.x, brick.x + brick.w),
rand(drawY, drawY + brick.h),
i % 4 === 0 ? '#ffffff' : (i % 2 ? colors.a : colors.b),
rand(90, 340),
{ size: rand(2.5, 6.5), life: rand(0.4, 1.05) }
);
}
shockwave(centerX, centerY, 4, 62 + Math.min(game.combo, 12) * 6, 0.35, colors.a);
}
/* =============================== BRICKS ================================ */
function damageBrick(brick, damage, hitX, hitY) {
if (!brick || !brick.alive) return false;
if (brick.steel) {
brick.flash = 1;
brick.shake = 1;
Sound.steel();
sparks(hitX, hitY, '#dbe7f5', 6, 170);
game.trauma = Math.min(1, game.trauma + 0.05);
return false;
}
brick.hp -= damage;
brick.flash = 1;
brick.shake = 1;
if (brick.hp > 0) {
Sound.hard();
sparks(hitX, hitY, (HP_COLORS[brick.hp] || HP_COLORS[1]).a, 7, 190);
addScore(10, null);
game.trauma = Math.min(1, game.trauma + 0.05);
return false;
}
brick.alive = false;
game.bricksLeft = Math.max(0, game.bricksLeft - 1);
game.combo++;
if (game.combo > game.bestCombo) game.bestCombo = game.combo;
var multiplier = Math.min(1 + 0.1 * (game.combo - 1), 3);
var points = Math.round((50 + (brick.maxHp - 1) * 30) * multiplier);
var colors = HP_COLORS[brick.maxHp] || HP_COLORS[1];
addScore(
points,
brick.x + brick.w / 2,
brick.y + brick.offY + brick.h / 2,
colors.a,
game.combo >= 5 ? 20 : 16
);
Sound.brick(game.combo);
explodeBrick(brick);
game.trauma = Math.min(1.1, game.trauma + 0.12 + Math.min(game.combo, 10) * 0.012);
if (game.combo >= 6) game.hitStop = Math.max(game.hitStop, 0.035);
if (Math.random() < 0.135 + Math.min(game.combo, 10) * 0.004) {
spawnPowerup(brick.x + brick.w / 2, brick.y + brick.offY + brick.h / 2);
}
return true;
}
/* ============================== POWER-UPS ============================== */
function rollPowerup() {
var total = 0;
var i;
for (i = 0; i < POWER_TYPES.length; i++) total += POWER_TYPES[i].weight;
var roll = Math.random() * total;
for (i = 0; i < POWER_TYPES.length; i++) {
roll -= POWER_TYPES[i].weight;
if (roll <= 0) return POWER_TYPES[i];
}
return POWER_TYPES[0];
}
function spawnPowerup(x, y) {
powerups.push({
x: clamp(x, FL + 18, FR - 18), y: y,
vy: 95, w: 30, h: 26,
definition: rollPowerup(),
time: 0
});
}
function applyPowerup(powerup) {
var definition = powerup.definition;
switch (definition.key) {
case 'wide':
effects.small = 0;
effects.wide = definition.duration;
break;
case 'small':
effects.wide = 0;
effects.small = definition.duration;
break;
case 'slow':
effects.slow = definition.duration;
break;
case 'laser':
effects.laser = definition.duration;
break;
case 'sticky':
effects.sticky = definition.duration;
break;
case 'multi':
multiball();
break;
case 'life':
game.lives = Math.min(game.lives + 1, 6);
Sound.life();
break;
}
if (definition.key !== 'life') Sound.power();
addScore(75, powerup.x, powerup.y - 10, definition.color, 16);
addFloatingText(definition.label, powerup.x, powerup.y - 34, definition.color, 17);
shockwave(powerup.x, powerup.y, 6, 90, 0.4, definition.color);
for (var i = 0; i < 16; i++) {
spawnParticle(powerup.x, powerup.y, definition.color, rand(70, 260), { size: rand(2, 5) });
}
game.trauma = Math.min(1, game.trauma + 0.12);
paddle.flash = 1;
}
function updatePowerups(dt, canCollect) {
var left = paddle.cx - paddle.w / 2;
var right = paddle.cx + paddle.w / 2;
for (var i = powerups.length - 1; i >= 0; i--) {
var powerup = powerups[i];
powerup.vy = Math.min(powerup.vy + 130 * dt, 330);
powerup.y += powerup.vy * dt;
powerup.time += dt;
if (canCollect &&
powerup.y + powerup.h / 2 >= paddle.y &&
powerup.y - powerup.h / 2 <= paddle.y + paddle.h &&
powerup.x + powerup.w / 2 >= left &&
powerup.x - powerup.w / 2 <= right) {
applyPowerup(powerup);
powerups.splice(i, 1);
continue;
}
if (powerup.y - powerup.h > H) powerups.splice(i, 1);
}
}
function updateTimedEffects(dt) {
for (var i = 0; i < TIMED_EFFECTS.length; i++) {
var name = TIMED_EFFECTS[i];
if (effects[name] <= 0) continue;
effects[name] -= dt;
if (effects[name] <= 0) {
effects[name] = 0;
Sound.ui();
if (name === 'sticky') {
for (var j = 0; j < balls.length; j++) {
if (balls[j].stuck) releaseBall(balls[j]);
}
}
}
}
}
/* =============================== LASERS ================================ */
function updateBolts(dt) {
paddle.cooldown = Math.max(0, paddle.cooldown - dt);
var firing = fireHeld || keys.Space || keys.KeyW || keys.ArrowUp;
if (effects.laser > 0 && game.state === 'play' && firing && paddle.cooldown <= 0) {
paddle.cooldown = 0.19;
var startY = paddle.y - 4;
bolts.push({ x: paddle.cx - paddle.w / 2 + 9, y: startY, previousY: startY, vy: -980 });
bolts.push({ x: paddle.cx + paddle.w / 2 - 9, y: startY, previousY: startY, vy: -980 });
Sound.laser();
paddle.squash = Math.max(paddle.squash, 0.45);
}
for (var i = bolts.length - 1; i >= 0; i--) {
var bolt = bolts[i];
bolt.previousY = bolt.y;
bolt.y += bolt.vy * dt;
if (bolt.y < FT - 6) {
sparks(bolt.x, FT, '#ffd36b', 4, 120);
bolts.splice(i, 1);
continue;
}
var sweptTop = Math.min(bolt.y, bolt.previousY);
var sweptBottom = Math.max(bolt.y, bolt.previousY);
var hitBrick = null;
var hitY = -Infinity;
/* Bolts fly upward, so of every brick the swept segment touches, the one
with the lowest bottom edge is the first the bolt would actually reach. */
for (var j = 0; j < bricks.length; j++) {
var brick = bricks[j];
if (!brick.alive) continue;
var drawY = brick.y + brick.offY;
var lowerEdge = drawY + brick.h;
if (bolt.x >= brick.x && bolt.x <= brick.x + brick.w &&
sweptTop <= lowerEdge && sweptBottom >= drawY &&
lowerEdge > hitY) {
hitBrick = brick;
hitY = lowerEdge;
}
}
if (hitBrick) {
damageBrick(hitBrick, 1, bolt.x, hitY);
bolts.splice(i, 1);
}
}
}
/* =============================== PADDLE ================================ */
function updatePaddle(dt) {
var movingLeft = keys.ArrowLeft || keys.KeyA;
var movingRight = keys.ArrowRight || keys.KeyD;
if (movingLeft || movingRight) pointerMode = false;
var previousX = paddle.cx;
if (pointerMode) {
paddle.cx = lerp(paddle.cx, pointerX, 1 - Math.exp(-34 * dt));
} else {
var direction = (movingRight ? 1 : 0) - (movingLeft ? 1 : 0);
paddle.vx = lerp(paddle.vx, direction * 820, 1 - Math.exp(-22 * dt));
paddle.cx += paddle.vx * dt;
}
var targetWidth = effects.wide > 0
? PADDLE_WIDE
: (effects.small > 0 ? PADDLE_SMALL : PADDLE_BASE);
paddle.w = lerp(paddle.w, targetWidth, 1 - Math.exp(-12 * dt));
paddle.cx = clamp(paddle.cx, FL + paddle.w / 2, FR - paddle.w / 2);
paddle.vx = (paddle.cx - previousX) / Math.max(dt, 0.0001);
paddle.squash = Math.max(0, paddle.squash - dt * 4);
paddle.flash = Math.max(0, paddle.flash - dt * 3);
}
function onPaddleHit(ball) {
game.combo = 0;
paddle.squash = 1;
paddle.flash = 1;
Sound.paddle();
sparks(ball.x, paddle.y, '#9fe8ff', 8, 220);
shockwave(ball.x, paddle.y, 4, 46, 0.26, '#7fd8ff');
game.trauma = Math.min(1, game.trauma + 0.07);
ball.idle = 0;
}
/* ============================ BALL PHYSICS ============================= */
function stepBall(ball, dt) {
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
var wallHit = false;
if (ball.x - ball.r < FL) {
ball.x = FL + ball.r;
ball.vx = Math.abs(ball.vx);
wallHit = true;
} else if (ball.x + ball.r > FR) {
ball.x = FR - ball.r;
ball.vx = -Math.abs(ball.vx);
wallHit = true;
}
if (ball.y - ball.r < FT) {
ball.y = FT + ball.r;
ball.vy = Math.abs(ball.vy);
wallHit = true;
}
if (wallHit) {
if (game.time - lastWallSound > 0.03) {
Sound.wall();
lastWallSound = game.time;
}
sparks(ball.x, ball.y, '#9fd8ff', 3, 130);
game.trauma = Math.min(1, game.trauma + 0.025);
}
if (ball.y - ball.r > H) {
ball.dead = true;
return;
}
/* --- paddle --- */
var paddleLeft = paddle.cx - paddle.w / 2;
var paddleRight = paddle.cx + paddle.w / 2;
var paddleTop = paddle.y;
var paddleBottom = paddle.y + paddle.h;
if (ball.vy > 0 &&
ball.y + ball.r >= paddleTop && ball.y - ball.r <= paddleBottom &&
ball.x + ball.r >= paddleLeft && ball.x - ball.r <= paddleRight) {
if (ball.y <= paddleTop + paddle.h * 0.85) {
ball.y = paddleTop - ball.r - 0.01;
var relative = clamp((ball.x - paddle.cx) / Math.max(1, paddle.w / 2), -1, 1);
ball.speed = clamp(ball.speed * 1.012 + 2, baseSpeed() * 0.85, MAX_SPEED);
var angle = -Math.PI / 2 + relative * MAX_BOUNCE;
ball.vx = Math.cos(angle) * ball.speed + paddle.vx * 0.13;
ball.vy = Math.sin(angle) * ball.speed;
normalizeBall(ball);
if (effects.sticky > 0 && game.state === 'play') {
ball.stuck = true;
ball.offset = clamp(ball.x - paddle.cx, -paddle.w / 2 + ball.r, paddle.w / 2 - ball.r);
}
onPaddleHit(ball);
} else {
var direction = ball.x < paddle.cx ? -1 : 1;
ball.x = clamp(
direction < 0 ? paddleLeft - ball.r - 0.01 : paddleRight + ball.r + 0.01,
FL + ball.r, FR - ball.r
);
ball.vx = Math.abs(ball.vx) * direction;
normalizeBall(ball);
onPaddleHit(ball);
}
if (ball.stuck) return;
}
/* --- bricks (penetration-depth resolution, one brick per substep) --- */
for (var i = 0; i < bricks.length; i++) {
var brick = bricks[i];
if (!brick.alive) continue;
var drawY = brick.y + brick.offY;
var deltaX = ball.x - (brick.x + brick.w / 2);
var deltaY = ball.y - (drawY + brick.h / 2);
var penX = ball.r + brick.w / 2 - Math.abs(deltaX);
var penY = ball.r + brick.h / 2 - Math.abs(deltaY);
if (penX > 0 && penY > 0) {
if (penX < penY) {
ball.vx = (deltaX < 0 ? -1 : 1) * Math.abs(ball.vx);
ball.x += deltaX < 0 ? -penX : penX;
} else {
ball.vy = (deltaY < 0 ? -1 : 1) * Math.abs(ball.vy);
ball.y += deltaY < 0 ? -penY : penY;
}
normalizeBall(ball);
damageBrick(brick, 1, ball.x, ball.y);
ball.idle = 0;
break;
}
}
}
function updateBalls(dt) {
for (var i = balls.length - 1; i >= 0; i--) {
var ball = balls[i];
if (ball.stuck) {
ball.x = clamp(paddle.cx + ball.offset, FL + ball.r, FR - ball.r);
ball.y = paddle.y - ball.r - 1;
ball.trail.push({ x: ball.x, y: ball.y });
if (ball.trail.length > 12) ball.trail.shift();
continue;
}
normalizeBall(ball);
ball.idle += dt;
/* Anti-stall: nothing hit for a while, so nudge the angle and speed. */
if (ball.idle > 9) {
ball.idle = 0;
ball.speed = Math.min(ball.speed + 32, MAX_SPEED);
var angle = Math.atan2(ball.vy, ball.vx) + rand(-0.25, 0.25);
ball.vx = Math.cos(angle) * ball.speed;
ball.vy = Math.sin(angle) * ball.speed;
normalizeBall(ball);
addFloatingText('SPEED+', ball.x, ball.y - 18, '#ffd36b', 14);
}
var velocity = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
var steps = clamp(Math.ceil(velocity * dt / (ball.r * 0.6)), 1, 16);
var stepTime = dt / steps;
for (var step = 0; step < steps; step++) {
stepBall(ball, stepTime);
if (ball.dead) break;
}
ball.trail.push({ x: ball.x, y: ball.y });
if (ball.trail.length > 12) ball.trail.shift();
if (ball.dead) {
for (var p = 0; p < 14; p++) {
spawnParticle(
ball.x, Math.min(ball.y, H - 4),
p % 2 ? '#ff6b7a' : '#ffd0d6',
rand(80, 300), { size: rand(2, 5) }
);
}
shockwave(ball.x, H - 6, 6, 120, 0.4, '#ff6b7a');
balls.splice(i, 1);
}
}
if (balls.length === 0 && (game.state === 'play' || game.state === 'serve')) {
/* If the last brick and the last ball vanish on the same frame,
award the clear rather than charging a life. */
if (game.bricksLeft <= 0) return;
loseLife();
}
}
function loseLife() {
game.lives--;
game.combo = 0;
Sound.lose();
game.trauma = Math.min(1.3, game.trauma + 0.9);
game.flash = 1;
game.flashColor = '255,90,100';
game.hitStop = 0.12;
resetEffects();
paddle.w = PADDLE_BASE;
if (game.lives <= 0) {
game.lives = 0;
game.state = 'over';
saveHighScore();
Sound.over();
} else {
serveBall();
game.state = 'serve';
game.intro = 0.9;
}
}
function checkLevelClear() {
if (game.bricksLeft > 0) return;
game.state = 'clear';
game.timer = 2.4;
var bonus = 1000 + game.lives * 250 + game.level * 100 + game.loop * 500;
game.levelBonus = bonus;
game.score += bonus;
saveHighScore();
Sound.clear();
game.trauma = Math.min(1, game.trauma + 0.55);
game.flash = 0.85;
game.flashColor = '170,255,230';
for (var i = 0; i < 70; i++) {
spawnParticle(
rand(FL, FR), rand(FT, H * 0.72),
pick(['#5ff7e6', '#ff79dc', '#ffd36b', '#a6f759', '#ffffff']),
rand(60, 340), { size: rand(2, 6), life: rand(0.6, 1.4) }
);
}
for (var w = 0; w < 3; w++) {
shockwave(W / 2, H * 0.44, 10 + w * 20, 620, 1, '#9ef7ff');
}
}
/* ================================ INPUT ================================ */
function confirmAction() {
Sound.unlock();
if (game.error) { restartGame(); return; }
switch (game.state) {
case 'title': startGame(); break;
case 'serve':
case 'play': launchBalls(); break;
case 'paused': togglePause(); break;
case 'clear': game.timer = Math.min(game.timer, 0.05); break;
case 'over': startGame(); break;
case 'win': nextLoop(); break;
}
}
function togglePause() {
if (game.state === 'play' || game.state === 'serve') {
game.previousState = game.state;
game.state = 'paused';
Sound.ui();
} else if (game.state === 'paused') {
game.state = game.previousState || 'serve';
Sound.ui();
}
}
function toggleMute() {
muted = !muted;
Store.set('neon_breakout_muted', muted ? '1' : '0');
if (!muted) Sound.ui();
}
function restartGame() {
if (game.state === 'title' && !game.error) return;
if (game.error) { game.error = null; resize(); }
startGame();
}
window.addEventListener('keydown', function (event) {
var code = event.code;
var key = (event.key || '').toLowerCase();
if (code === 'Space' || code === 'ArrowLeft' || code === 'ArrowRight' ||
code === 'ArrowUp' || code === 'ArrowDown') {
event.preventDefault();
}
keys[code] = true;
Sound.unlock();
if (event.repeat) return;
if (code === 'Space' || code === 'Enter' || code === 'NumpadEnter') {
confirmAction();
} else if (key === 'p' || code === 'Escape') {
togglePause();
} else if (key === 'm') {
toggleMute();
} else if (key === 'r') {
restartGame();
}
}, { passive: false });
window.addEventListener('keyup', function (event) {
keys[event.code] = false;
});
function pointerToLocalX(event) {
var rect = canvas.getBoundingClientRect();
return clamp((event.clientX - rect.left) / Math.max(1, rect.width) * W, 0, W);
}
canvas.addEventListener('pointermove', function (event) {
pointerMode = true;
pointerX = pointerToLocalX(event);
});
canvas.addEventListener('pointerdown', function (event) {
event.preventDefault();
try { canvas.focus({ preventScroll: true }); } catch (e) {
try { canvas.focus(); } catch (e2) {}
}
try {
if (canvas.setPointerCapture) canvas.setPointerCapture(event.pointerId);
} catch (e) {}
pointerMode = true;
pointerX = pointerToLocalX(event);
fireHeld = true;
confirmAction();
});
window.addEventListener('pointerup', function () { fireHeld = false; });
window.addEventListener('pointercancel', function () { fireHeld = false; });
canvas.addEventListener('contextmenu', function (event) { event.preventDefault(); });
window.addEventListener('blur', function () {
fireHeld = false;
for (var key in keys) keys[key] = false;
if (game.state === 'play' || game.state === 'serve') togglePause();
});
document.addEventListener('visibilitychange', function () {
if (document.hidden && (game.state === 'play' || game.state === 'serve')) togglePause();
});
/* =============================== UPDATE ================================ */
function updateParticles(dt) {
for (var i = particles.length - 1; i >= 0; i--) {
var particle = particles[i];
particle.life -= dt;
if (particle.life <= 0) { particles.splice(i, 1); continue; }
particle.vy += particle.gravity * dt;
var drag = Math.pow(particle.drag, dt * 60);
particle.vx *= drag;
particle.vy *= drag;
particle.x += particle.vx * dt;
particle.y += particle.vy * dt;
particle.rotation += particle.rotationSpeed * dt;
if (particle.y > H + 30) particles.splice(i, 1);
}
}
function updateFloatingTexts(dt) {
for (var i = floatingTexts.length - 1; i >= 0; i--) {
var text = floatingTexts[i];
text.life -= dt;
text.y += text.vy * dt;
text.vy *= Math.pow(0.9, dt * 60);
if (text.life <= 0) floatingTexts.splice(i, 1);
}
}
function updateWaves(dt) {
for (var i = waves.length - 1; i >= 0; i--) {
waves[i].life -= dt;
if (waves[i].life <= 0) waves.splice(i, 1);
}
}
function updateBrickAnimations(dt) {
for (var i = 0; i < bricks.length; i++) {
var brick = bricks[i];
brick.flash = Math.max(0, brick.flash - dt * 4.5);
brick.shake = Math.max(0, brick.shake - dt * 5);
if (brick.offY !== 0) {
brick.offY = lerp(brick.offY, 0, 1 - Math.exp(-9 * dt));
if (Math.abs(brick.offY) < 0.4) brick.offY = 0;
}
}
}
function updateTitleBackground(dt) {
demoBall.x += demoBall.vx * dt;
demoBall.y += demoBall.vy * dt;
if (demoBall.x - demoBall.r < FL) { demoBall.x = FL + demoBall.r; demoBall.vx = Math.abs(demoBall.vx); }
if (demoBall.x + demoBall.r > FR) { demoBall.x = FR - demoBall.r; demoBall.vx = -Math.abs(demoBall.vx); }
if (demoBall.y - demoBall.r < FT) { demoBall.y = FT + demoBall.r; demoBall.vy = Math.abs(demoBall.vy); }
if (demoBall.y + demoBall.r > H - 20) { demoBall.y = H - 20 - demoBall.r; demoBall.vy = -Math.abs(demoBall.vy); }
demoBall.trail.push({ x: demoBall.x, y: demoBall.y });
if (demoBall.trail.length > 14) demoBall.trail.shift();
if (Math.random() < 0.5) {
spawnParticle(rand(FL, FR), H - 10, pick(['#5ff7e6', '#4ad7ff', '#ff79dc']), rand(20, 60), {
gravity: -40, life: rand(0.8, 1.6), size: rand(1.5, 3), shape: 'spark', drag: 0.99
});
}
}
function update(dt) {
game.time += dt;
game.trauma = Math.max(0, game.trauma - dt * 1.6);
game.flash = Math.max(0, game.flash - dt * 3.1);
updateParticles(dt);
updateFloatingTexts(dt);
updateWaves(dt);
updateBrickAnimations(dt);
if (game.state === 'title') {
updatePaddle(dt);
updateTitleBackground(dt);
return;
}
if (game.state === 'paused') return;
if (game.state === 'over' || game.state === 'win') {
updatePowerups(dt, false);
return;
}
if (game.state === 'clear') {
updatePowerups(dt, false);
game.timer -= dt;
if (game.timer <= 0) advanceLevel();
return;
}
if (game.intro > 0) game.intro = Math.max(0, game.intro - dt);
if (game.hitStop > 0) { game.hitStop -= dt; return; }
updatePaddle(dt);
updateTimedEffects(dt);
updateBalls(dt);
if (game.state !== 'play' && game.state !== 'serve') return;
updatePowerups(dt, true);
updateBolts(dt);
checkLevelClear();
}
/* =============================== RENDER ================================ */
var backgroundGradient = null;
var vignetteGradient = null;
function makeGradients() {
backgroundGradient = ctx.createLinearGradient(0, 0, 0, H);
backgroundGradient.addColorStop(0, '#0b1130');
backgroundGradient.addColorStop(0.55, '#070a1c');
backgroundGradient.addColorStop(1, '#04060f');
vignetteGradient = ctx.createRadialGradient(W / 2, H * 0.42, 80, W / 2, H * 0.5, H * 0.95);
vignetteGradient.addColorStop(0, 'rgba(0,0,0,0)');
vignetteGradient.addColorStop(1, 'rgba(0,0,0,0.65)');
}
function drawText(text, x, y, options) {
options = options || {};
ctx.save();
ctx.font = (options.weight || '700') + ' ' + (options.size || 16) + 'px ' +
(options.mono ? FONT_MONO : FONT_DISPLAY);
ctx.textBaseline = options.baseline || 'alphabetic';
ctx.globalAlpha = options.alpha == null ? 1 : options.alpha;
ctx.fillStyle = options.color || '#eaf6ff';
if (options.glow) {
ctx.shadowColor = options.glow;
ctx.shadowBlur = options.blur || 14;
}
if (options.letter) {
ctx.textAlign = 'left';
var characters = String(text).split('');
var totalWidth = 0;
var i;
for (i = 0; i < characters.length; i++) {
totalWidth += ctx.measureText(characters[i]).width + options.letter;
}
totalWidth -= options.letter;
var cursor = options.align === 'center'
? x - totalWidth / 2
: (options.align === 'right' ? x - totalWidth : x);
for (i = 0; i < characters.length; i++) {
ctx.fillText(characters[i], cursor, y);
cursor += ctx.measureText(characters[i]).width + options.letter;
}
} else {
ctx.textAlign = options.align || 'left';
ctx.fillText(String(text), x, y);
}
ctx.restore();
}
function drawBackground() {
ctx.fillStyle = backgroundGradient;
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.beginPath();
ctx.rect(FL, FT, FR - FL, H - FT);
ctx.clip();
ctx.strokeStyle = 'rgba(90,160,255,0.07)';
ctx.lineWidth = 1;
var offset = (game.time * 22) % 44;
var x, y;
for (x = FL - 44 + offset; x < FR + 44; x += 44) {
ctx.beginPath();
ctx.moveTo(x, FT);
ctx.lineTo(x, H);
ctx.stroke();
}
for (y = FT + offset; y < H; y += 44) {
ctx.beginPath();
ctx.moveTo(FL, y);
ctx.lineTo(FR, y);
ctx.stroke();
}
var pulse = 0.05 + Math.sin(game.time * 1.6) * 0.02;
ctx.fillStyle = 'rgba(80,150,255,' + pulse + ')';
ctx.fillRect(FL, FT, FR - FL, H - FT);
ctx.restore();
ctx.fillStyle = vignetteGradient;
ctx.fillRect(0, 0, W, H);
}
function drawField() {
ctx.save();
ctx.strokeStyle = 'rgba(120,200,255,.35)';
ctx.lineWidth = 2;
ctx.shadowColor = 'rgba(90,170,255,.8)';
ctx.shadowBlur = 16;
ctx.beginPath();
ctx.moveTo(FL - 1, H);
ctx.lineTo(FL - 1, FT - 1);
ctx.lineTo(FR + 1, FT - 1);
ctx.lineTo(FR + 1, H);
ctx.stroke();
ctx.restore();
var danger = ctx.createLinearGradient(0, H - 70, 0, H);
danger.addColorStop(0, 'rgba(255,80,110,0)');
danger.addColorStop(1, 'rgba(255,70,100,.16)');
ctx.fillStyle = danger;
ctx.fillRect(FL, H - 70, FR - FL, 70);
}
function drawBricks(dimmed) {
for (var i = 0; i < bricks.length; i++) {
var brick = bricks[i];
if (!brick.alive) continue;
var shakeX = brick.shake > 0 ? rand(-2, 2) * brick.shake : 0;
var shakeY = brick.shake > 0 ? rand(-2, 2) * brick.shake : 0;
var x = brick.x + shakeX;
var y = brick.y + brick.offY + shakeY;
var spriteKey = brick.steel ? 'S' : String(clamp(brick.hp, 1, 4));
ctx.save();
if (dimmed) ctx.globalAlpha = 0.32;
ctx.drawImage(brickSprites[spriteKey], x, y, brick.w, brick.h);
if (!brick.steel && brick.hp < brick.maxHp) {
ctx.globalAlpha = (dimmed ? 0.32 : 1) * (1 - brick.hp / brick.maxHp) * 0.3;
roundedRect(ctx, x, y, brick.w, brick.h, 6);
ctx.fillStyle = '#000000';
ctx.fill();
ctx.globalAlpha = dimmed ? 0.32 : 1;
}
if (brick.flash > 0) {
ctx.globalCompositeOperation = 'lighter';
ctx.globalAlpha = brick.flash * 0.75 * (dimmed ? 0.4 : 1);
roundedRect(ctx, x, y, brick.w, brick.h, 6);
ctx.fillStyle = '#ffffff';
ctx.fill();
}
ctx.restore();
}
}
function drawPaddle() {
var width = paddle.w * (1 + paddle.squash * 0.1);
var height = paddle.h * (1 - paddle.squash * 0.28);
var x = paddle.cx - width / 2;
var y = paddle.y + (paddle.h - height) / 2;
ctx.save();
ctx.shadowColor = 'rgba(90,200,255,.85)';
ctx.shadowBlur = 18 + paddle.flash * 22;
var gradient = ctx.createLinearGradient(0, y, 0, y + height);
gradient.addColorStop(0, '#ddf6ff');
gradient.addColorStop(0.45, '#51c6ff');
gradient.addColorStop(1, '#1763b8');
roundedRect(ctx, x, y, width, height, height / 2);
ctx.fillStyle = gradient;
ctx.fill();
ctx.shadowBlur = 0;
roundedRect(ctx, x + 3, y + 2.5, width - 6, height * 0.34, height * 0.2);
ctx.fillStyle = 'rgba(255,255,255,.5)';
ctx.fill();
if (effects.sticky > 0) {
ctx.fillStyle = 'rgba(201,163,255,.95)';
for (var dot = -width / 2 + 8; dot < width / 2 - 6; dot += 11) {
ctx.beginPath();
ctx.arc(paddle.cx + dot, y + 2.5, 1.8, 0, Math.PI * 2);
ctx.fill();
}
}
if (effects.laser > 0) {
ctx.fillStyle = '#ffd36b';
ctx.shadowColor = 'rgba(255,210,100,.9)';
ctx.shadowBlur = 12;
var cannons = [paddle.cx - width / 2 + 9, paddle.cx + width / 2 - 9];
for (var i = 0; i < cannons.length; i++) {
ctx.beginPath();
ctx.moveTo(cannons[i] - 4, y);
ctx.lineTo(cannons[i] + 4, y);
ctx.lineTo(cannons[i], y - 9);
ctx.closePath();
ctx.fill();
}
ctx.shadowBlur = 0;
}
if (paddle.flash > 0) {
ctx.globalCompositeOperation = 'lighter';
ctx.globalAlpha = paddle.flash * 0.6;
roundedRect(ctx, x, y, width, height, height / 2);
ctx.fillStyle = '#ffffff';
ctx.fill();
}
ctx.restore();
}
function drawBallShape(x, y, radius, lightColor, darkColor) {
var gradient = ctx.createRadialGradient(
x - radius * 0.35, y - radius * 0.35, radius * 0.15, x, y, radius
);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(0.45, lightColor);
gradient.addColorStop(1, darkColor);
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = gradient;
ctx.fill();
}
function drawBalls(list, trailColor) {
ctx.save();
for (var i = 0; i < list.length; i++) {
var ball = list[i];
for (var j = 0; j < ball.trail.length; j++) {
var amount = (j + 1) / ball.trail.length;
ctx.globalAlpha = amount * 0.33;
ctx.beginPath();
ctx.arc(ball.trail[j].x, ball.trail[j].y, ball.r * amount * 0.95, 0, Math.PI * 2);
ctx.fillStyle = trailColor || '#7fe9ff';
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.shadowColor = 'rgba(120,230,255,.95)';
ctx.shadowBlur = 18;
drawBallShape(ball.x, ball.y, ball.r, '#9ff2ff', '#2aa7e0');
ctx.shadowBlur = 0;
}
ctx.restore();
}
function drawBolts() {
ctx.save();
ctx.shadowColor = 'rgba(255,210,100,.9)';
ctx.shadowBlur = 14;
for (var i = 0; i < bolts.length; i++) {
var bolt = bolts[i];
var gradient = ctx.createLinearGradient(0, bolt.y - 14, 0, bolt.y + 6);
gradient.addColorStop(0, 'rgba(255,230,160,0)');
gradient.addColorStop(0.5, '#fff3c4');
gradient.addColorStop(1, '#ff9f43');
ctx.fillStyle = gradient;
ctx.fillRect(bolt.x - 2, bolt.y - 14, 4, 20);
}
ctx.restore();
}
function drawPowerups() {
for (var i = 0; i < powerups.length; i++) {
var powerup = powerups[i];
var definition = powerup.definition;
var bob = Math.sin(powerup.time * 7) * 2;
var scale = 1 + Math.sin(powerup.time * 9) * 0.05;
ctx.save();
ctx.translate(powerup.x, powerup.y + bob);
ctx.scale(scale, scale);
ctx.shadowColor = definition.color;
ctx.shadowBlur = 18;
roundedRect(ctx, -15, -11, 30, 22, 8);
var gradient = ctx.createLinearGradient(0, -11, 0, 11);
gradient.addColorStop(0, 'rgba(255,255,255,.95)');
gradient.addColorStop(0.35, definition.color);
gradient.addColorStop(1, 'rgba(0,0,0,.45)');
ctx.fillStyle = gradient;
ctx.fill();
ctx.shadowBlur = 0;
ctx.lineWidth = 1.5;
ctx.strokeStyle = 'rgba(255,255,255,.75)';
ctx.stroke();
ctx.fillStyle = '#07101f';
ctx.font = '900 15px ' + FONT_DISPLAY;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(definition.icon, 0, 1);
ctx.restore();
}
}
function drawParticles() {
ctx.save();
for (var i = 0; i < particles.length; i++) {
var particle = particles[i];
ctx.globalAlpha = clamp(particle.life / particle.maxLife, 0, 1);
ctx.fillStyle = particle.color;
if (particle.shape === 'spark') {
ctx.fillRect(
particle.x - particle.size / 2, particle.y - particle.size / 2,
particle.size, particle.size
);
} else {
ctx.save();
ctx.translate(particle.x, particle.y);
ctx.rotate(particle.rotation);
ctx.fillRect(-particle.size / 2, -particle.size / 2, particle.size, particle.size * 0.85);
ctx.restore();
}
}
ctx.restore();
}
function drawWaves() {
ctx.save();
for (var i = 0; i < waves.length; i++) {
var wave = waves[i];
var progress = 1 - wave.life / wave.maxLife;
var radius = lerp(wave.radius, wave.maxRadius, 1 - Math.pow(1 - progress, 2));
ctx.globalAlpha = (1 - progress) * 0.6;
ctx.strokeStyle = wave.color;
ctx.lineWidth = 1 + 3 * (1 - progress);
ctx.beginPath();
ctx.arc(wave.x, wave.y, radius, 0, Math.PI * 2);
ctx.stroke();
}
ctx.restore();
}
function drawFloatingTexts() {
for (var i = 0; i < floatingTexts.length; i++) {
var text = floatingTexts[i];
drawText(text.text, text.x, text.y, {
size: text.size,
align: 'center',
color: text.color,
alpha: clamp(text.life / text.maxLife, 0, 1),
glow: text.color,
blur: 10,
weight: '900'
});
}
}
function drawEffectIndicators() {
var x = FL + 4;
var y = H - 36;
for (var i = 0; i < TIMED_EFFECTS.length; i++) {
var name = TIMED_EFFECTS[i];
if (effects[name] <= 0) continue;
var definition = POWER_BY_KEY[name];
if (!definition) continue;
var width = 92;
var remaining = effects[name];
var fraction = clamp(remaining / Math.max(1, definition.duration), 0, 1);
ctx.save();
roundedRect(ctx, x, y, width, 20, 10);
ctx.fillStyle = 'rgba(8,14,32,.8)';
ctx.fill();
ctx.strokeStyle = definition.color;
ctx.globalAlpha = 0.85;
ctx.lineWidth = 1.2;
ctx.stroke();
ctx.globalAlpha = 1;
roundedRect(ctx, x + 2, y + 15, (width - 4) * fraction, 3, 1.5);
ctx.fillStyle = definition.color;
ctx.fill();
ctx.restore();
drawText(name.toUpperCase() + ' ' + remaining.toFixed(1), x + width / 2, y + 13, {
size: 11, align: 'center', color: definition.color, mono: true, weight: '700'
});
x += width + 8;
}
}
function drawHUD() {
ctx.save();
var gradient = ctx.createLinearGradient(0, 0, 0, HUD_H);
gradient.addColorStop(0, 'rgba(9,15,38,.96)');
gradient.addColorStop(1, 'rgba(9,15,38,.55)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, W, HUD_H);
ctx.strokeStyle = 'rgba(120,200,255,.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, HUD_H + 0.5);
ctx.lineTo(W, HUD_H + 0.5);
ctx.stroke();
ctx.restore();
drawText('SCORE', 24, 24, { size: 11, color: '#6f9fd0', letter: 2, weight: '700' });
drawText(pad(game.score, 6), 24, 52, {
size: 26, color: '#eaf6ff', mono: true, weight: '700',
glow: 'rgba(120,200,255,.6)', blur: 10
});
drawText('HIGH', 196, 24, { size: 11, color: '#6f9fd0', letter: 2, weight: '700' });
drawText(pad(Math.max(highScore, game.score), 6), 196, 52, {
size: 20, color: '#8fd3ff', mono: true, weight: '700'
});
var level = LEVELS[game.level % LEVELS.length];
var levelTitle = 'LEVEL ' + (game.level + 1) + '/' + LEVELS.length +
(game.loop > 0 ? (' ' + BULLET + ' LOOP ' + (game.loop + 1)) : '');
drawText(levelTitle, W / 2, 24, {
size: 11, color: '#6f9fd0', align: 'center', letter: 2, weight: '700'
});
drawText(level.name, W / 2, 46, {
size: 19, color: '#ffffff', align: 'center', weight: '900',
glow: 'rgba(120,200,255,.5)', blur: 10
});
var barWidth = 230;
var barX = W / 2 - barWidth / 2;
var barY = 55;
var progress = 1 - clamp(game.bricksLeft / game.bricksTotal, 0, 1);
ctx.save();
roundedRect(ctx, barX, barY, barWidth, 5, 2.5);
ctx.fillStyle = 'rgba(255,255,255,.12)';
ctx.fill();
var barGradient = ctx.createLinearGradient(barX, 0, barX + barWidth, 0);
barGradient.addColorStop(0, '#5ff7e6');
barGradient.addColorStop(1, '#ff79dc');
roundedRect(ctx, barX, barY, Math.max(2, barWidth * progress), 5, 2.5);
ctx.fillStyle = barGradient;
ctx.fill();
ctx.restore();
var livesX = W - 24;
drawText('LIVES', livesX, 24, {
size: 11, color: '#6f9fd0', align: 'right', letter: 2, weight: '700'
});
ctx.save();
for (var i = 0; i < game.lives; i++) {
ctx.shadowColor = 'rgba(120,230,255,.9)';
ctx.shadowBlur = 10;
drawBallShape(livesX - 9 - i * 22, 44, 7, '#9ff2ff', '#2aa7e0');
}
ctx.restore();
if (game.lives === 0) {
drawText('--', livesX, 50, { size: 18, align: 'right', color: '#ff6b7a', mono: true });
}
if (game.combo >= 2) {
var multiplier = Math.min(1 + 0.1 * (game.combo - 1), 3);
var pulse = 1 + Math.sin(game.time * 14) * 0.06;
ctx.save();
ctx.translate(396, 46);
ctx.scale(pulse, pulse);
drawText('x' + multiplier.toFixed(1) + ' COMBO', 0, 0, {
size: 17, color: '#ffd36b', weight: '900',
glow: 'rgba(255,200,80,.8)', blur: 14, mono: true
});
ctx.restore();
}
if (muted) {
drawText('MUTED (M)', W - 24, HUD_H - 4, {
size: 10, align: 'right', color: '#ff8a3d', mono: true
});
}
}
function drawHints() {
drawText(
ARROW_L + ' ' + ARROW_R + ' / A D / MOUSE ' + BULLET +
' SPACE or CLICK: launch & fire ' + BULLET +
' P pause ' + BULLET + ' M mute ' + BULLET + ' R restart',
W / 2, H - 10,
{ size: 11, align: 'center', color: 'rgba(150,190,235,.55)', mono: true, weight: '400' }
);
}
function drawPanel(x, y, width, height) {
ctx.save();
roundedRect(ctx, x, y, width, height, 18);
ctx.fillStyle = 'rgba(6,10,26,.9)';
ctx.fill();
ctx.lineWidth = 1.5;
ctx.strokeStyle = 'rgba(130,210,255,.45)';
ctx.shadowColor = 'rgba(80,170,255,.55)';
ctx.shadowBlur = 26;
ctx.stroke();
ctx.restore();
}
function drawBigTitle(text, y, size) {
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'alphabetic';
ctx.font = '900 ' + size + 'px ' + FONT_DISPLAY;
var gradient = ctx.createLinearGradient(0, y - size * 0.8, 0, y + size * 0.15);
gradient.addColorStop(0, '#aefcff');
gradient.addColorStop(0.5, '#59b6ff');
gradient.addColorStop(1, '#ff79dc');
ctx.fillStyle = gradient;
ctx.shadowColor = 'rgba(90,180,255,.9)';
ctx.shadowBlur = 34;
ctx.fillText(text, W / 2, y);
ctx.restore();
}
function drawOverlays() {
var pulse = 0.55 + 0.45 * Math.sin(game.time * 4);
var i;
if (game.state === 'title') {
drawPanel(W / 2 - 320, 118, 640, 430);
drawBigTitle('NEON BREAKOUT', 212, 62);
drawText(
LEVELS.length + ' LEVELS ' + BULLET + ' POWER-UPS ' + BULLET + ' COMBO SCORING',
W / 2, 244,
{ size: 13, align: 'center', color: '#8fd3ff', mono: true, letter: 1 }
);
var rows = [
['MOVE', ARROW_L + ' ' + ARROW_R + ' or A D or MOUSE'],
['LAUNCH / FIRE', 'SPACE or CLICK'],
['PAUSE', 'P or ESC'],
['MUTE / RESTART', 'M / R']
];
for (i = 0; i < rows.length; i++) {
drawText(rows[i][0], W / 2 - 250, 300 + i * 34, {
size: 13, color: '#6f9fd0', mono: true
});
drawText(rows[i][1], W / 2 + 250, 300 + i * 34, {
size: 14, color: '#eaf6ff', align: 'right', mono: true
});
}
drawText('HIGH SCORE ' + pad(highScore, 6), W / 2, 462, {
size: 16, align: 'center', color: '#ffd36b', mono: true, glow: 'rgba(255,200,80,.5)'
});
drawText('PRESS SPACE OR CLICK TO PLAY', W / 2, 514, {
size: 19, align: 'center', color: '#ffffff', weight: '900',
alpha: pulse, glow: 'rgba(140,220,255,.9)', blur: 18
});
} else if (game.state === 'paused') {
ctx.fillStyle = 'rgba(3,6,16,.68)';
ctx.fillRect(0, 0, W, H);
drawPanel(W / 2 - 220, H / 2 - 130, 440, 260);
drawBigTitle('PAUSED', H / 2 - 50, 52);
drawText('P / ESC / CLICK to resume', W / 2, H / 2 + 6, {
size: 15, align: 'center', color: '#8fd3ff', mono: true
});
drawText('M mute ' + BULLET + ' R restart', W / 2, H / 2 + 38, {
size: 13, align: 'center', color: '#6f9fd0', mono: true
});
drawText('SCORE ' + pad(game.score, 6), W / 2, H / 2 + 84, {
size: 16, align: 'center', color: '#eaf6ff', mono: true
});
} else if (game.state === 'clear') {
drawPanel(W / 2 - 250, H / 2 - 130, 500, 260);
drawBigTitle('LEVEL CLEAR', H / 2 - 52, 46);
drawText('BONUS +' + game.levelBonus, W / 2, H / 2 + 4, {
size: 22, align: 'center', color: '#5ff7e6', mono: true, weight: '900',
glow: 'rgba(90,250,230,.7)', blur: 16
});
drawText('SCORE ' + pad(game.score, 6), W / 2, H / 2 + 42, {
size: 16, align: 'center', color: '#eaf6ff', mono: true
});
drawText('BEST COMBO x' + game.bestCombo, W / 2, H / 2 + 68, {
size: 13, align: 'center', color: '#ffd36b', mono: true
});
drawText(
'NEXT LEVEL IN ' + Math.max(0, game.timer).toFixed(1) + 's (SPACE to skip)',
W / 2, H / 2 + 104,
{ size: 12, align: 'center', color: '#6f9fd0', mono: true, alpha: pulse }
);
} else if (game.state === 'over') {
ctx.fillStyle = 'rgba(30,4,10,.6)';
ctx.fillRect(0, 0, W, H);
drawPanel(W / 2 - 260, H / 2 - 150, 520, 300);
drawBigTitle('GAME OVER', H / 2 - 62, 54);
drawText('FINAL SCORE ' + pad(game.score, 6), W / 2, H / 2 - 4, {
size: 22, align: 'center', color: '#ffffff', mono: true, weight: '900'
});
drawText('HIGH SCORE ' + pad(highScore, 6), W / 2, H / 2 + 30, {
size: 15, align: 'center', color: '#8fd3ff', mono: true
});
drawText(
'REACHED LEVEL ' + (game.level + 1) + ' ' + BULLET + ' BEST COMBO x' + game.bestCombo,
W / 2, H / 2 + 56,
{ size: 13, align: 'center', color: '#6f9fd0', mono: true }
);
if (game.newRecord) {
drawText(STAR + ' NEW RECORD ' + STAR, W / 2, H / 2 + 90, {
size: 17, align: 'center', color: '#ffd36b', weight: '900',
alpha: pulse, glow: 'rgba(255,200,80,.9)', blur: 16
});
}
drawText('SPACE OR CLICK TO PLAY AGAIN', W / 2, H / 2 + 124, {
size: 16, align: 'center', color: '#ffffff', alpha: pulse, weight: '900'
});
} else if (game.state === 'win') {
drawPanel(W / 2 - 290, H / 2 - 160, 580, 320);
drawBigTitle('YOU WIN!', H / 2 - 74, 58);
drawText('ALL ' + LEVELS.length + ' LEVELS CLEARED', W / 2, H / 2 - 28, {
size: 16, align: 'center', color: '#5ff7e6', mono: true, letter: 1
});
drawText('SCORE ' + pad(game.score, 6), W / 2, H / 2 + 12, {
size: 24, align: 'center', color: '#ffffff', mono: true, weight: '900',
glow: 'rgba(140,220,255,.7)', blur: 16
});
drawText(
'HIGH ' + pad(highScore, 6) + ' BEST COMBO x' + game.bestCombo,
W / 2, H / 2 + 44,
{ size: 13, align: 'center', color: '#8fd3ff', mono: true }
);
drawText(
'SPACE: LOOP ' + (game.loop + 2) + ' (faster, score kept)',
W / 2, H / 2 + 88,
{ size: 15, align: 'center', color: '#ffd36b', mono: true, alpha: pulse }
);
drawText('R: fresh game', W / 2, H / 2 + 114, {
size: 13, align: 'center', color: '#6f9fd0', mono: true
});
} else {
if (game.intro > 0 && game.state === 'serve') {
var alpha = clamp(game.intro / 0.6, 0, 1);
var introLevel = LEVELS[game.level % LEVELS.length];
ctx.save();
ctx.globalAlpha = alpha;
drawBigTitle('LEVEL ' + (game.level + 1), H * 0.42, 56);
drawText(introLevel.name, W / 2, H * 0.42 + 38, {
size: 20, align: 'center', color: '#8fd3ff', mono: true, letter: 3, alpha: alpha
});
ctx.restore();
}
var hasHeldBall = false;
for (i = 0; i < balls.length; i++) {
if (balls[i].stuck) { hasHeldBall = true; break; }
}
if (hasHeldBall && game.intro <= 0.9) {
drawText('PRESS SPACE OR CLICK TO LAUNCH', W / 2, paddle.y - 52, {
size: 15, align: 'center', color: '#ffffff', alpha: pulse,
weight: '900', glow: 'rgba(140,220,255,.8)', blur: 14
});
}
}
}
function render() {
ctx.setTransform(canvas.width / W, 0, 0, canvas.height / H, 0, 0);
drawBackground();
ctx.save();
var shake = game.trauma * game.trauma;
if (shake > 0.001) {
ctx.translate((Math.random() * 2 - 1) * 15 * shake, (Math.random() * 2 - 1) * 15 * shake);
}
drawField();
drawBricks(game.state === 'title');
drawWaves();
drawPowerups();
if (game.state === 'title') {
drawBalls([demoBall], '#7fe9ff');
drawPaddle();
} else {
drawPaddle();
drawBolts();
drawBalls(balls, '#7fe9ff');
}
drawParticles();
drawFloatingTexts();
ctx.restore();
drawHUD();
if (game.state !== 'title') drawEffectIndicators();
drawHints();
drawOverlays();
if (game.flash > 0.001) {
ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = 'rgba(' + game.flashColor + ',' + (game.flash * 0.33) + ')';
ctx.fillRect(0, 0, W, H);
ctx.restore();
}
}
function drawErrorScreen() {
ctx.setTransform(canvas.width / W, 0, 0, canvas.height / H, 0, 0);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(4,6,16,.94)';
ctx.fillRect(0, 0, W, H);
drawText('SOMETHING WENT WRONG', W / 2, H / 2 - 26, {
size: 20, align: 'center', color: '#ff8a8a', weight: '900'
});
drawText(String(game.error).slice(0, 72), W / 2, H / 2 + 4, {
size: 13, align: 'center', color: '#ffd0d6', mono: true
});
drawText('Press R or click to restart', W / 2, H / 2 + 40, {
size: 14, align: 'center', color: '#8fd3ff', mono: true
});
}
/* ================================= BOOT ================================ */
function resize() {
var dpr = Math.min(window.devicePixelRatio || 1, 2);
var availableWidth = Math.max(1, window.innerWidth - 20);
var availableHeight = Math.max(1, window.innerHeight - 20);
var scale = Math.min(availableWidth / W, availableHeight / H);
canvas.style.width = (W * scale) + 'px';
canvas.style.height = (H * scale) + 'px';
canvas.width = Math.max(1, Math.round(W * scale * dpr));
canvas.height = Math.max(1, Math.round(H * scale * dpr));
ctx.setTransform(canvas.width / W, 0, 0, canvas.height / H, 0, 0);
ctx.imageSmoothingEnabled = true;
makeGradients();
}
window.addEventListener('resize', resize);
window.addEventListener('orientationchange', function () { setTimeout(resize, 150); });
resize();
loadLevel(0);
for (var initial = 0; initial < bricks.length; initial++) {
bricks[initial].offY = 0;
}
game.state = 'title';
var lastTime = (window.performance && performance.now) ? performance.now() : Date.now();
function frame(now) {
var dt = clamp((now - lastTime) / 1000, 0, MAX_DT);
lastTime = now;
if (game.error) {
try { drawErrorScreen(); } catch (e) {}
} else {
try {
update(dt);
render();
} catch (error) {
game.error = (error && error.message) ? error.message : String(error);
try { drawErrorScreen(); } catch (e) {}
}
}
var hidePointer = pointerMode && (game.state === 'play' || game.state === 'serve');
canvas.style.cursor = hidePointer ? 'none' : 'default';
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>