Run details
- Model
- meta/muse-spark-open
- Route
- OpenRouter API, one user message
- Reasoning
- provider default, 3,378 reasoning tokens
- Limits
- 60 min · 141,176 output tokens
- Date
- 21 Oct 2026
- Time
- 2 min 40 s
- Size
- 39 kB
- Tokens
- 221 in, 16,089 out
- List price
- $0.069 ($1.25 in / $4.25 out per M)
- Paid
- $0.069
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
39 kB 1,061 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">
<title>BREAKOUT — Neon Shatter</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#070a14;
--panel:#12182b;
--line:rgba(255,255,255,.08);
--accent:#7a5cff;
--accent2:#00eaff;
--text:#eef2ff;
--muted:#9aa3c0;
}
html,body{height:100%}
body{
background:
radial-gradient(900px 600px at 15% -10%, #1a2b6b 0%, transparent 60%),
radial-gradient(1000px 700px at 90% 0%, #4a1b6b 0%, transparent 60%),
radial-gradient(800px 600px at 50% 120%, #0e3d5a 0%, transparent 70%),
var(--bg);
color:var(--text);
font-family: ui-sans-system, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
display:flex;align-items:center;justify-content:center;
padding:18px;
overflow-x:hidden;
}
.app{
width:min(980px, 100%);
display:flex;flex-direction:column;gap:14px;
}
header{
display:flex;align-items:center;justify-content:space-between;gap:12px;
background:linear-gradient(180deg, rgba(255,255,255,.07), rgba(255,255,255,.02));
border:1px solid var(--line);
border-radius:16px;
padding:12px 14px;
backdrop-filter: blur(8px);
box-shadow: 0 10px 30px rgba(0,0,0,.35), inset 0 1px 0 rgba(255,255,255,.06);
}
.brand{
display:flex;align-items:center;gap:12px;
}
.logo{
width:40px;height:40px;border-radius:10px;
background:
linear-gradient(135deg, #7a5cff, #00eaff);
display:grid;place-items:center;
box-shadow:0 6px 18px rgba(122,92,255,.45);
font-weight:900;letter-spacing:.04em;
}
.logo span{filter:drop-shadow(0 1px 4px rgba(0,0,0,.5));font-size:13px}
.brand h1{font-size:18px;line-height:1;letter-spacing:.14em;font-weight:900}
.brand p{font-size:12px;color:var(--muted);letter-spacing:.08em;margin-top:2px}
.stats{
display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end;
}
.stat{
background:rgba(0,0,0,.28);
border:1px solid var(--line);
border-radius:999px;
padding:8px 12px;
display:flex;align-items:center;gap:8px;
font-size:12px;letter-spacing:.08em;font-weight:700;
white-space:nowrap;
}
.stat b{font-size:14px;color:#fff}
.stat .dot{width:8px;height:8px;border-radius:50%}
.btn{
appearance:none;border:1px solid rgba(255,255,255,.14);
background:linear-gradient(180deg, rgba(255,255,255,.12), rgba(255,255,255,.04));
color:var(--text);font-weight:800;letter-spacing:.08em;
padding:10px 14px;border-radius:999px;cursor:pointer;
transition:transform .08s ease, filter .15s, border-color .15s;
font-size:12px;
}
.btn:hover{filter:brightness(1.15);border-color:rgba(255,255,255,.22)}
.btn:active{transform:scale(.97)}
.btn.primary{
background:linear-gradient(180deg, #7a5cff, #5a36ff);
border-color:rgba(122,92,255,.5);
box-shadow:0 8px 20px rgba(122,92,255,.4);
}
.btn.ghost{background:transparent}
.stage{
position:relative;
border-radius:18px;
overflow:hidden;
border:1px solid rgba(255,255,255,.10);
background:
linear-gradient(180deg, rgba(255,255,255,.04), transparent 30%),
linear-gradient(0deg, rgba(0,0,0,.45), transparent),
#0c1120;
box-shadow: 0 20px 60px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.03) inset;
}
canvas{display:block;width:100%;height:auto;aspect-ratio: 960/540;background:transparent}
.overlay{
position:absolute;inset:0;
display:flex;align-items:center;justify-content:center;
padding:22px;
background: radial-gradient(600px 400px at 50% 20%, rgba(122,92,255,.18), transparent 60%),
linear-gradient(180deg, rgba(7,10,20,.55), rgba(7,10,20,.78));
backdrop-filter: blur(6px);
}
.hidden{display:none !important}
.panel{
width:min(520px, 100%);
background: linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.04));
border:1px solid rgba(255,255,255,.12);
border-radius:18px;
padding:18px;
box-shadow: 0 20px 50px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.08);
text-align:center;
}
.kicker{font-size:11px;letter-spacing:.18em;color:#aab3d1;font-weight:800}
.panel h2{
font-size:32px;letter-spacing:.06em;line-height:1;margin:8px 0 6px;
text-shadow:0 6px 30px rgba(122,92,255,.55);
}
.panel p{color:#cbd2ea;font-size:13px;line-height:1.5}
.row{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:14px}
.badge{
display:inline-flex;align-items:center;gap:8px;
font-size:11px;letter-spacing:.08em;font-weight:800;
padding:8px 10px;border-radius:999px;
background:rgba(0,0,0,.35);border:1px solid rgba(255,255,255,.08);color:#d6dcf3
}
.gridKeys{
display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:14px 0;text-align:left;
}
.gridKeys div{
background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.06);
border-radius:12px;padding:10px 11px;display:flex;align-items:center;gap:10px;
}
.gridKeys i{
width:30px;height:30px;border-radius:8px;display:grid;place-items:center;
background:linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,.03));
border:1px solid rgba(255,255,255,.12);font-style:normal;font-weight:900;font-size:12px
}
.gridKeys span{font-size:12px;line-height:1.2;color:#dfe6ff}
.gridKeys small{color:var(--muted);font-size:11px}
.levelDots{display:flex;gap:6px;justify-content:center;margin-top:12px}
.levelDots s{
width:22px;height:6px;border-radius:999px;background:rgba(255,255,255,.15);display:block;
border:1px solid rgba(255,255,255,.06)
}
.levelDots s.on{background:linear-gradient(90deg,#7a5cff,#00eaff);box-shadow:0 4px 12px rgba(122,92,255,.45)}
.hudbar{
position:absolute;left:0;right:0;top:0;
display:flex;justify-content:space-between;align-items:center;
padding:10px 12px;pointer-events:none;
}
.hudchip{
background:rgba(0,0,0,.45);border:1px solid rgba(255,255,255,.08);
border-radius:999px;padding:6px 10px;font-size:11px;letter-spacing:.08em;font-weight:800;color:#dbe2ff;
backdrop-filter: blur(6px);
}
.progress{
position:absolute;left:12px;right:12px;bottom:10px;height:6px;
background:rgba(255,255,255,.08);border-radius:999px;overflow:hidden;
border:1px solid rgba(255,255,255,.06)
}
.progress i{display:block;height:100%;width:0%;background:linear-gradient(90deg,#7a5cff,#00eaff);box-shadow:0 0 12px rgba(0,234,255,.6)}
.controls{
display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;
color:var(--muted);font-size:11px;letter-spacing:.06em;
padding:0 4px;
}
.controls b{color:#dde4ff}
.toast{
position:absolute;left:50%;top:46%;transform:translate(-50%,-50%);
font-weight:1000;letter-spacing:.08em;text-align:center;
font-size:44px;line-height:1;
text-shadow: 0 10px 30px rgba(0,0,0,.6), 0 0 20px rgba(122,92,255,.6);
pointer-events:none;opacity:0;
}
.pulse{animation:pulse .6s ease}
@keyframes pulse{0%{transform:translate(-50%,-50%) scale(.9);opacity:0} 20%{opacity:1} 100%{transform:translate(-50%,-50%) scale(1.05);opacity:0}}
.shake{animation:shake .35s ease}
@keyframes shake{0%,100%{transform:translate(0,0)}20%{transform:translate(-6px,2px)}40%{transform:translate(5px,-2px)}60%{transform:translate(-3px,1px)}80%{transform:translate(4px,-1px)}}
.scorePop{position:absolute;pointer-events:none;font-weight:1000;font-size:14px;letter-spacing:.04em;text-shadow:0 4px 12px rgba(0,0,0,.6)}
@media (max-width:640px){
header{flex-direction:column;align-items:stretch}
.stats{justify-content:space-between}
.panel h2{font-size:26px}
.toast{font-size:32px}
}
</style>
</head>
<body>
<div class="app" id="app">
<header>
<div class="brand">
<div class="logo"><span>◈</span></div>
<div>
<h1>NEON SHATTER</h1>
<p>BREAKOUT • MOVE WITH MOUSE OR KEYS</p>
</div>
</div>
<div class="stats">
<div class="stat"><span class="dot" style="background:#42ff88;box-shadow:0 0 10px #42ff88"></span> SCORE <b id="score">0</b></div>
<div class="stat">LIVES <b id="lives">● ● ●</b></div>
<div class="stat">LEVEL <b id="level">1/6</b> <span id="mult" style="margin-left:6px;background:linear-gradient(90deg,#ffd23f,#ff8447);color:#1a1200;padding:3px 7px;border-radius:999px;font-size:10px;display:none">x1</span></div>
<button class="btn ghost" id="muteBtn" title="Mute">🔊</button>
<button class="btn" id="pauseBtn">⏸ PAUSE</button>
</div>
</header>
<div class="stage" id="stage">
<canvas id="c" width="960" height="540"></canvas>
<div class="hudbar">
<div class="hudchip" id="hudLeft">AIM • BREAK • SURVIVE</div>
<div class="hudchip" id="hudRight">SPACE TO LAUNCH • P TO PAUSE</div>
</div>
<div class="progress"><i id="prog"></i></div>
<div class="toast" id="toast"></div>
<!-- MENU -->
<div class="overlay" id="ovMenu">
<div class="panel">
<div class="kicker">ARCADE EDITION • 6 HANDCRAFTED LEVELS</div>
<h2 style="background:linear-gradient(90deg,#fff 20%, #a99cff 45%, #5af3ff 80%);-webkit-background-clip:text;background-clip:text;color:transparent;">BREAK THE NEON</h2>
<p>Move the paddle with <b>mouse</b> or <b>← → / A D</b>. Launch with <b>Space / Click / ↑</b>. Build combo streaks, catch power-ups, and shatter every brick. Don’t let the ball fall!</p>
<div class="gridKeys">
<div><i>🖱️</i><span>Move Mouse<br><small>Precision aiming</small></span></div>
<div><i>⌨️</i><span>← → or A D<br><small>Keyboard control</small></span></div>
<div><i>⎵</i><span>Space / Click / ↑<br><small>Launch ball</small></span></div>
<div><i>P</i><span>P to Pause<br><small>M to Mute</small></span></div>
</div>
<div class="row">
<button class="btn primary" id="playBtn">▶ PLAY NOW</button>
<button class="btn" id="howBtn">HOW TO JUICE?</button>
</div>
<div class="levelDots" id="dotsMenu"></div>
<p style="margin-top:10px;font-size:11px;color:#98a0bf;letter-spacing:.05em">Tip: Hit the edge of the paddle for sharp angles • Keep combos for 2× score</p>
</div>
</div>
<!-- PAUSED -->
<div class="overlay hidden" id="ovPause">
<div class="panel">
<div class="kicker">TAKE A BREATH</div>
<h2>PAUSED</h2>
<p>Game is frozen. Resume when ready.</p>
<div class="row">
<button class="btn primary" id="resumeBtn">▶ RESUME</button>
<button class="btn" id="restartBtn2">↻ RESTART</button>
</div>
</div>
</div>
<!-- LEVEL CLEAR -->
<div class="overlay hidden" id="ovClear">
<div class="panel">
<div class="kicker">LEVEL CLEARED</div>
<h2 id="clearTitle">NICE SHOT!</h2>
<p id="clearDesc">Preparing next wave…</p>
<div class="row">
<button class="btn primary" id="nextBtn">NEXT LEVEL →</button>
</div>
</div>
</div>
<!-- GAME OVER -->
<div class="overlay hidden" id="ovOver">
<div class="panel">
<div class="kicker" id="overKicker">GAME OVER</div>
<h2 id="overTitle">SHATTERED</h2>
<p id="overDesc">You fought well. Try again?</p>
<div class="row">
<button class="btn primary" id="againBtn">↻ PLAY AGAIN</button>
<button class="btn" id="menuBtn">MENU</button>
</div>
<div class="levelDots" id="dotsOver"></div>
</div>
</div>
</div>
<div class="controls">
<div><b>Mouse:</b> move to steer • <b>Click:</b> launch | <b>Keys:</b> ← → / A D to move • <b>Space/↑</b> launch • <b>P</b> pause</div>
<div>No installs • 60fps • Made with built-in sound</div>
</div>
</div>
<script>
(() => {
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const W = 960, H = 540;
const scoreEl = document.getElementById('score');
const livesEl = document.getElementById('lives');
const levelEl = document.getElementById('level');
const prog = document.getElementById('prog');
const multEl = document.getElementById('mult');
const toast = document.getElementById('toast');
const stage = document.getElementById('stage');
const ovMenu = document.getElementById('ovMenu');
const ovPause = document.getElementById('ovPause');
const ovClear = document.getElementById('ovClear');
const ovOver = document.getElementById('ovOver');
let muted = false;
let audioCtx = null;
function ensureAudio(){
if(muted) return;
if(!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)();
if(audioCtx.state==='suspended') audioCtx.resume();
}
function beep(f1,f2,dur,type='sine',vol=0.18){
if(muted) return;
try{
ensureAudio();
const t0 = audioCtx.currentTime;
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
const f = audioCtx.createBiquadFilter();
o.type = type;
o.frequency.setValueAtTime(f1, t0);
if(f2) o.frequency.exponentialRampToValueAtTime(Math.max(30,f2), t0+dur*0.9);
g.gain.setValueAtTime(vol, t0);
g.gain.exponentialRampToValueAtTime(0.001, t0+dur);
f.type='lowpass'; f.frequency.value=6000;
o.connect(f).connect(g).connect(audioCtx.destination);
o.start(t0); o.stop(t0+dur+0.02);
}catch(e){}
}
const SFX = {
paddle: ()=> beep(320,520,0.12,'square',0.16),
wall: ()=> beep(220,260,0.09,'square',0.12),
brick: (hi)=> {
const base = 420 + Math.random()*260;
beep(base, base+180, 0.12, 'square', 0.14);
if(hi) setTimeout(()=>beep(base*1.5, base*2, 0.1,'sine',0.08), 40);
},
brickHard: ()=> beep(180,90,0.18,'triangle',0.22),
launch: ()=> {beep(400,800,0.18,'sine',0.18); setTimeout(()=>beep(800,1200,0.12,'sine',0.12),90)},
lose: ()=> {beep(300,110,0.5,'sawtooth',0.15); setTimeout(()=>beep(180,60,0.5,'sine',0.12),160)},
power: ()=> {beep(600,900,0.2,'sine',0.16); setTimeout(()=>beep(900,1200,0.25,'sine',0.14),120)},
clear: ()=> { [0,2,4,7].forEach((n,i)=> setTimeout(()=>beep(300+n*80, 500+n*80,0.3,'sine',0.16), i*110))},
win: ()=> { [0,4,7,12,16].forEach((n,i)=> setTimeout(()=>beep(250+n*50, 400+n*50,0.45,'sine',0.18), i*130))},
};
// Levels as strings: ' '=empty, '1'=1hp, '2'=2hp, '3'=3hp, 'X'=unbreakable (metal)
const LEVELS = [
{ name:"WARM-UP GRID", data:[
"111111111111",
"111111111111",
"222211112222",
"111111111111",
"111111111111",
]},
{ name:"THE PYRAMID", data:[
"-----11-----",
"----1111----",
"---122221---",
"--11111111--",
"-1122222211-",
"111111111111",
]},
{ name:"NEON FORTRESS", data:[
"12X111111X21",
"111X1111X111",
"111111111111",
"11X111111X11",
"122111122211",
"11X 11 X11 ",
]},
{ name:"CHECKER VOID", data:[
"121212121212",
"212121212121",
"1-1-1--1-1-1",
"212121212121",
"1212XXXX1212",
"1-1-13311-1-1",
]},
{ name:"DIAMOND CORE", data:[
"-----XX-----",
"----1221----",
"---1XXXX1---",
"--12XXXX21--",
"-1XX1221XX1-",
"1111XXXX1111",
"-1XX1221XX1-",
]},
{ name:"FINAL HEX", data:[
"1X1122332211X1",
"X111222222111X",
"111XX111XX111",
"12211XXXX11221",
"X111XX11XX111X",
"111122221111",
]},
];
const PALETTE = ['#ff3b82','#ff7a3b','#ffd23f','#3be8ff','#7a5cff','#42ffa2','#ff9cf0'];
const PALETTE2 = ['#ff4d6d','#ff8c42','#ffe066','#5ef0ff','#8a6cff','#5cff9a'];
// Game state
let state = 'menu'; // menu, playing, paused, clear, over, win
let levelIndex = 0;
let score = 0;
let lives = 3;
let combo = 0;
let comboTimer = 0;
let mult = 1;
const paddle = { x: W/2 - 70, y: H - 38, w: 140, h: 16, tx: W/2, speed: 0, expandT:0 };
let balls = [];
let bricks = [];
let particles = [];
let texts = [];
let powerups = [];
let shake = 0;
let mouseActive = false;
let lastMouseMove = 0;
const keys = {};
const BRICK_W = 62, BRICK_H = 22, BRICK_GAP = 7;
function hideAll(){ ovMenu.classList.add('hidden'); ovPause.classList.add('hidden'); ovClear.classList.add('hidden'); ovOver.classList.add('hidden'); }
function showMenu(){
state='menu'; hideAll(); ovMenu.classList.remove('hidden');
buildDots();
}
function showPause(){ if(state==='playing'){ state='paused'; ovPause.classList.remove('hidden'); }}
function resume(){ if(state==='paused'){ state='playing'; ovPause.classList.add('hidden'); }}
function levelDotsHTML(idx){
let h='';
for(let i=0;i<LEVELS.length;i++) h+= `<s class="${i<=idx?'on':''}"></s>`;
return h;
}
function buildDots(){
document.getElementById('dotsMenu').innerHTML = levelDotsHTML(levelIndex-0.2);
document.getElementById('dotsOver').innerHTML = levelDotsHTML(levelIndex);
}
function resetGame(fullReset=true){
if(fullReset){ levelIndex=0; score=0; lives=3; combo=0; mult=1; }
paddle.w = 140; paddle.expandT=0;
balls=[]; particles=[]; texts=[]; powerups=[];
loadLevel(levelIndex);
spawnBall(true);
updateHUD();
}
function loadLevel(idx){
bricks=[];
const lvl = LEVELS[idx];
const rows = lvl.data.length;
let maxCols = 0; lvl.data.forEach(r=> maxCols = Math.max(maxCols, r.length));
const useW = BRICK_W, useH = BRICK_H, gap = BRICK_GAP;
const totalW = maxCols*useW + (maxCols-1)*gap;
const ox = (W - totalW)/2;
const oy = 74;
for(let r=0;r<rows;r++){
const rowStr = lvl.data[r];
for(let c=0;c<rowStr.length;c++){
const ch=rowStr[c];
if(ch===' '||ch==='-'||ch==='.' ) continue;
let hp=1, unbreak=false;
if(ch==='1') hp=1;
else if(ch==='2') hp=2;
else if(ch==='3') hp=3;
else if(ch==='X'||ch==='x'){ hp=999; unbreak=true; }
else continue;
const col = unbreak ? '#6e768e' : PALETTE[r % PALETTE.length];
const col2 = unbreak ? '#aab3cc' : PALETTE2[r % PALETTE2.length];
bricks.push({
x: ox + c*(useW+gap) + (maxCols - rowStr.length)*(useW+gap)/2,
y: oy + r*(useH+gap),
w: useW, h: useH,
hp, maxHp: hp, unbreak,
color: col, color2: col2,
alive:true, hitAnim:0
});
}
}
}
function spawnBall(stuck=false){
const b = {
x: paddle.x + paddle.w/2,
y: paddle.y - 12,
r: 8,
vx: (Math.random()<0.5?-1:1) * (260 + Math.random()*60),
vy: -360,
stuck: stuck,
trail: []
};
if(stuck && balls.length===0) balls=[b]; else if(!stuck) balls.push(b);
else if(stuck) balls[0]=b;
else balls.push(b);
}
function updateHUD(){
scoreEl.textContent = score.toLocaleString();
livesEl.innerHTML = Array.from({length:3},(_,i)=> i<lives? '●':'○').join(' ');
livesEl.style.color = lives===1 ? '#ff5a6a' : lives===2 ? '#ffcc66' : '#a6ffb0';
levelEl.textContent = `${levelIndex+1}/${LEVELS.length}`;
document.getElementById('hudLeft').textContent = LEVELS[levelIndex].name;
const total = bricks.filter(b=>!b.unbreak).length;
const alive = bricks.filter(b=>!b.unbreak && b.alive).length;
prog.style.width = (total? ((total-alive)/total*100):0) + '%';
if(combo>=2){ multEl.style.display='inline-block'; multEl.textContent='x'+mult+' COMBO'; multEl.style.transform=`scale(${1+Math.min(0.18, combo*0.02)})`; }
else multEl.style.display='none';
}
function triggerToast(txt, sub){
toast.innerHTML = txt + (sub? `<div style="font-size:13px;letter-spacing:.16em;margin-top:8px;color:#ffe9a6">${sub}</div>`:'');
toast.classList.remove('pulse'); void toast.offsetWidth; toast.classList.add('pulse');
toast.style.opacity=1;
setTimeout(()=> toast.style.opacity=0, 900);
stage.classList.remove('shake'); void stage.offsetWidth; stage.classList.add('shake');
shake = Math.min(10, shake+6);
}
function breakBrick(br){
br.hitAnim = 1;
if(br.unbreak){
SFX.brickHard(); shake=4; br.hitAnim=1;
spawnParticles(br.x+br.w/2, br.y+br.h/2, br.color2, 6);
spawnText(br.x+br.w/2, br.y, 'CLANK', '#aab3cc');
return; // not destroyed
}
br.hp--;
if(br.hp<=0){
br.alive=false;
const pts = 100 * (br.maxHp) * mult;
score += pts;
combo++; comboTimer=1.2; mult = combo>=8?3 : combo>=4?2 : 1;
SFX.brick(true); shake = 5 + mult*1.5;
spawnParticles(br.x+br.w/2, br.y+br.h/2, br.color, 14);
spawnText(br.x+br.w/2, br.y, '+'+pts, '#fff');
// power-up chance
if(Math.random()<0.12){
const type = Math.random()<0.6 ? 'expand' : 'multi';
powerups.push({x:br.x+br.w/2, y:br.y+br.h/2, vy:90, type, w:26,h:14, rot:0});
}
updateHUD();
} else {
SFX.brickHard();
spawnParticles(br.x+br.w/2, br.y+br.h/2, br.color2, 8);
spawnText(br.x+br.w/2, br.y+br.h/2, '−', br.color2);
// color fade to indicate damage
}
}
function spawnParticles(x,y,color,count){
for(let i=0;i<count;i++){
const ang = Math.random()*Math.PI*2;
const sp = 120 + Math.random()*260;
particles.push({
x,y, vx: Math.cos(ang)*sp, vy: Math.sin(ang)*sp - Math.random()*60,
life: 0.45+Math.random()*0.35, t:0, size: 2+Math.random()*3.5, color
});
}
}
function spawnText(x,y,txt,color){
texts.push({x,y, txt, color, life:0.7, t:0, vy:-40});
}
function collideBallBrick(ball, br){
// AABB circle collision
const closestX = Math.max(br.x, Math.min(ball.x, br.x+br.w));
const closestY = Math.max(br.y, Math.min(ball.y, br.y+br.h));
const dx = ball.x - closestX, dy = ball.y - closestY;
const dist2 = dx*dx+dy*dy;
if(dist2 <= ball.r*ball.r){
const dist = Math.sqrt(dist2) || 0.001;
// Determine normal
let nx=0, ny=0;
if(dist===0){ // inside
// choose smallest penetration
const overlapL = (ball.x - br.x);
const overlapR = (br.x+br.w - ball.x);
const overlapT = (ball.y - br.y);
const overlapB = (br.y+br.h - ball.y);
const min = Math.min(overlapL, overlapR, overlapT, overlapB);
if(min===overlapL){nx=-1;ny=0}
else if(min===overlapR){nx=1;ny=0}
else if(min===overlapT){nx=0;ny=-1}
else {nx=0;ny=1}
} else { nx=dx/dist; ny=dy/dist; }
// push out
const overlap = ball.r - dist;
ball.x += nx * (overlap+1);
ball.y += ny * (overlap+1);
// reflect
const dot = ball.vx*nx + ball.vy*ny;
ball.vx = ball.vx - 2*dot*nx;
ball.vy = ball.vy - 2*dot*ny;
// add a little speed per hit
const sp = Math.hypot(ball.vx, ball.vy);
const target = Math.min(620, sp*1.004);
const fac = target/sp;
ball.vx*=fac; ball.vy*=fac;
return true;
}
return false;
}
// Input
canvas.addEventListener('mousemove', e=>{
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left)/rect.width * W;
paddle.tx = x - paddle.w/2;
mouseActive=true; lastMouseMove=performance.now();
});
canvas.addEventListener('touchmove', e=>{
const t=e.touches[0]; if(!t) return;
const rect=canvas.getBoundingClientRect();
const x=(t.clientX-rect.left)/rect.width*W;
paddle.tx = x - paddle.w/2;
mouseActive=true; lastMouseMove=performance.now();
}, {passive:true});
canvas.addEventListener('click', ()=>{
if(state==='menu'){ startGame(); return; }
if(state==='playing') launchBalls();
if(state==='clear') nextLevel();
if(state==='over' || state==='win') resetAndMenu();
});
window.addEventListener('keydown', e=>{
const k=e.key.toLowerCase();
if(k===' ') e.preventDefault();
keys[k]=true;
if(k==='p'){ if(state==='playing') showPause(); else if(state==='paused') resume(); }
if(k==='m'){ muted=!muted; updateMute(); ensureAudio(); }
if(k===' ' || k==='arrowup' || k==='w'){
if(state==='menu') startGame();
else if(state==='playing') launchBalls();
else if(state==='clear') nextLevel();
else if(state==='over' || state==='win') resetAndMenu();
}
if(k==='r' && (state==='over'||state==='win')){ resetAndMenu(); startGame(); }
if(k==='escape'){ if(state==='paused') resume(); else if(state==='playing') showPause(); }
});
window.addEventListener('keyup', e=>{ keys[e.key.toLowerCase()]=false; });
document.getElementById('playBtn').onclick = startGame;
document.getElementById('howBtn').onclick = ()=> triggerToast('JUICE!', 'Particles • Shake • Trails • Slow-Mo • Combos');
document.getElementById('pauseBtn').onclick = ()=> (state==='playing'? showPause(): (state==='paused'?resume():null));
document.getElementById('resumeBtn').onclick = resume;
document.getElementById('restartBtn2').onclick = ()=>{ hideAll(); resetGame(true); state='playing'; };
document.getElementById('nextBtn').onclick = nextLevel;
document.getElementById('againBtn').onclick = ()=>{ hideAll(); resetGame(true); state='playing'; };
document.getElementById('menuBtn').onclick = ()=>{ resetAndMenu(); };
document.getElementById('muteBtn').onclick = ()=>{ muted=!muted; updateMute(); };
function updateMute(){
document.getElementById('muteBtn').textContent = muted? '🔇':'🔊';
document.getElementById('muteBtn').style.opacity = muted? .6:1;
}
function resetAndMenu(){ hideAll(); showMenu(); resetGame(true); }
function startGame(){
ensureAudio();
hideAll();
if(state==='menu'){ resetGame(true); }
else if(state==='paused'){ hideAll();}
state='playing';
updateHUD();
}
function launchBalls(){
let any=false;
balls.forEach(b=>{ if(b.stuck){ b.stuck=false; b.vx = (Math.random()-0.5)*120; b.vy=-380; any=true; }});
if(any) SFX.launch();
}
function nextLevel(){
levelIndex++;
if(levelIndex >= LEVELS.length){
state='win';
ovOver.classList.remove('hidden');
document.getElementById('overKicker').textContent='YOU DID IT';
document.getElementById('overTitle').textContent='NEON GOD';
document.getElementById('overDesc').textContent=`Final Score ${score.toLocaleString()} • All 6 levels shattered. Play again for perfection?`;
SFX.win(); triggerToast('YOU WIN!', `Score ${score.toLocaleString()}`);
buildDots();
return;
}
hideAll(); resetGame(false);
state='playing';
SFX.clear();
triggerToast(LEVELS[levelIndex].name, `Level ${levelIndex+1} • GO!`);
}
// Update Loop
let last = performance.now();
function frame(now){
requestAnimationFrame(frame);
const dt = Math.min(0.032, (now-last)/1000);
last = now;
update(dt);
render(dt);
}
function update(dt){
if(state==='playing'){
// combo timer
if(combo>0){
comboTimer -= dt;
if(comboTimer<=0){ combo=0; mult=1; updateHUD(); }
}
// paddle control blending: keyboard dominant if held, else mouse lerp
const kbLeft = keys['arrowleft']||keys['a'];
const kbRight = keys['arrowright']||keys['d'];
const kbMoving = kbLeft||kbRight;
const recentMouse = (performance.now() - lastMouseMove) < 1200;
if(kbMoving){
const dir = (kbRight?1:0) - (kbLeft?1:0);
paddle.x += dir * 520 * dt;
} else if(mouseActive && recentMouse){
// smooth follow
paddle.x += (paddle.tx - paddle.x) * Math.min(1, 14*dt);
} else {
// drift to center slowly if idle? keep position
}
// clamp
paddle.x = Math.max(4, Math.min(W - paddle.w -4, paddle.x));
// expand timer
if(paddle.expandT>0){
paddle.expandT -= dt;
if(paddle.expandT<=0){ paddle.w = 140; }
}
// balls
let aliveBricks = bricks.filter(b=>b.alive && !b.unbreak).length;
for(let i=balls.length-1;i>=0;i--){
const b=balls[i];
b.trail.unshift({x:b.x, y:b.y}); if(b.trail.length>10) b.trail.pop();
if(b.stuck){
b.x = paddle.x + paddle.w/2;
b.y = paddle.y - b.r -2;
continue;
}
// move
b.x += b.vx*dt;
b.y += b.vy*dt;
// walls
if(b.x - b.r <= 0){ b.x= b.r; b.vx *= -1; SFX.wall(); shake=2; spawnParticles(b.x,b.y,'#8ab4ff',4)}
if(b.x + b.r >= W){ b.x= W-b.r; b.vx*=-1; SFX.wall(); shake=2; spawnParticles(b.x,b.y,'#8ab4ff',4)}
if(b.y - b.r <= 0){ b.y= b.r; b.vy*=-1; SFX.wall(); shake=2; }
// paddle
if(b.vy>0 && b.y + b.r >= paddle.y && b.y - b.r <= paddle.y + paddle.h && b.x >= paddle.x -4 && b.x <= paddle.x + paddle.w +4){
// reposition to top
b.y = paddle.y - b.r -1;
const hitPos = (b.x - (paddle.x + paddle.w/2)) / (paddle.w/2); // -1..1
const angle = hitPos * 0.85; // radians factor
const speed = Math.hypot(b.vx,b.vy);
const newSpeed = Math.min(640, speed*1.015 + 6);
b.vx = Math.sin(angle*1.15) * newSpeed + paddle.speed*0.2;
b.vy = -Math.cos(angle*0.95) * newSpeed;
// add paddle velocity influence
SFX.paddle(); shake=3; spawnParticles(b.x, paddle.y, '#7a5cff', 6);
// stretch paddle visual? handled via anim
paddle.speed = b.vx*0.1;
comboTimer = Math.max(comboTimer, 0.6);
}
// bottom death
if(b.y - b.r > H+20){
balls.splice(i,1);
if(balls.length===0){
lives--; SFX.lose(); shake=9;
triggerToast('BALL LOST', `Lives ${lives}`);
combo=0; mult=1; updateHUD();
if(lives<=0){
state='over';
ovOver.classList.remove('hidden');
document.getElementById('overKicker').textContent='GAME OVER';
document.getElementById('overTitle').textContent='SHATTERED';
document.getElementById('overDesc').textContent=`Score ${score.toLocaleString()} • Reached Level ${levelIndex+1}. Try again?`;
buildDots();
} else {
// respawn single ball stuck after short delay
paddle.w = 140; // reset temporary?
spawnBall(true);
}
}
continue;
}
// bricks
for(let br of bricks){
if(!br.alive) continue;
if(collideBallBrick(b, br)){
breakBrick(br);
// if combo big, add time dilation illusion via particles?
break;
}
}
}
// check level cleared
if(bricks.filter(b=>!b.unbreak && b.alive).length===0){
score += 1000 * (levelIndex+1) + lives*250;
updateHUD();
state='clear';
ovClear.classList.remove('hidden');
document.getElementById('clearTitle').textContent = (levelIndex===LEVELS.length-1? 'FINAL CLEARED!' : 'CLEAN SWEEP!');
document.getElementById('clearDesc').textContent = `Bonus awarded • Score ${score.toLocaleString()}`;
SFX.clear();
triggerToast('LEVEL CLEAR!', '+1000 Bonus');
}
// powerups
for(let i=powerups.length-1;i>=0;i--){
const pu=powerups[i];
pu.y += pu.vy*dt;
pu.rot += dt*3;
if(pu.y + pu.h >= paddle.y && pu.y <= paddle.y+paddle.h && pu.x >= paddle.x && pu.x <= paddle.x+paddle.w){
// collect
if(pu.type==='expand'){
paddle.w = Math.min(220, paddle.w+42); paddle.expandT=10;
triggerToast('WIDE PADDLE!', '10s Expand');
} else if(pu.type==='multi'){
// spawn 2 more balls
for(let k=0;k<2;k++){
const nb = {x:paddle.x+paddle.w/2, y:paddle.y-14, r:8, vx:(Math.random()-0.5)*340, vy:-360-Math.random()*80, stuck:false, trail:[]};
balls.push(nb);
}
triggerToast('MULTIBALL!', '2 extra balls');
}
SFX.power();
spawnParticles(pu.x, pu.y, pu.type==='expand' ? '#ffd23f' : '#00eaff', 12);
powerups.splice(i,1);
} else if(pu.y > H+30) powerups.splice(i,1);
}
// particles
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];
p.t+=dt; p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+= 520*dt; p.vx*= (1 - 0.8*dt);
if(p.t > p.life) particles.splice(i,1);
}
for(let i=texts.length-1;i>=0;i--){
const t=texts[i];
t.t+=dt; t.y+=t.vy*dt; t.vy += 60*dt;
if(t.t>t.life) texts.splice(i,1);
}
if(shake>0) shake = Math.max(0, shake - 14*dt);
updateHUD();
} else if(state==='menu'){
// idle animate bricks? subtle
}
}
function roundRect(x,y,w,h,r){
ctx.beginPath();
ctx.moveTo(x+r,y);
ctx.arcTo(x+w,y,x+w,y+h,r);
ctx.arcTo(x+w,y+h,x,y+h,r);
ctx.arcTo(x,y+h,x,y,r);
ctx.arcTo(x,y,x+w,y,r);
ctx.closePath();
}
function render(dt){
// adapt dpr? canvas fixed.
ctx.save();
// shake transform
if(shake>0){
const sx = (Math.random()-0.5)*shake;
const sy = (Math.random()-0.5)*shake;
ctx.translate(sx,sy);
}
// clear
ctx.clearRect(0,0,W,H);
// background grid + vignette
ctx.fillStyle='#0a0f1f';
ctx.fillRect(0,0,W,H);
// subtle grid
ctx.strokeStyle='rgba(255,255,255,.035)';
ctx.lineWidth=1;
for(let x=0;x<W;x+=48){ ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); }
for(let y=0;y<H;y+=48){ ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }
// top glow
const g = ctx.createRadialGradient(W/2, -80, 40, W/2, -80, 680);
g.addColorStop(0,'rgba(122,92,255,.22)'); g.addColorStop(1,'transparent');
ctx.fillStyle=g; ctx.fillRect(0,0,W,220);
// bottom glow under paddle
const g2 = ctx.createLinearGradient(0,H-120,0,H);
g2.addColorStop(0,'transparent'); g2.addColorStop(1,'rgba(0,234,255,.12)');
ctx.fillStyle=g2; ctx.fillRect(0,H-120,W,120);
// bricks
for(let br of bricks){
if(!br.alive) continue;
// hit animation scale
let s = 1;
if(br.hitAnim>0){ br.hitAnim -= dt*6; s = 1 + Math.sin(br.hitAnim*Math.PI)*0.12; if(br.hitAnim<=0) br.hitAnim=0; }
ctx.save();
ctx.translate(br.x+br.w/2, br.y+br.h/2);
ctx.scale(s,s);
const x=-br.w/2, y=-br.h/2;
// shadow
ctx.fillStyle='rgba(0,0,0,.35)';
roundRect(x+2,y+3,br.w,br.h,6); ctx.fill();
// body
const grad = ctx.createLinearGradient(0,y,0,y+br.h);
if(br.unbreak){
grad.addColorStop(0,'#8e95ac'); grad.addColorStop(1,'#5a647c');
} else {
grad.addColorStop(0, br.color2); grad.addColorStop(1, br.color);
}
ctx.fillStyle=grad;
roundRect(x,y,br.w,br.h,6); ctx.fill();
// highlight
ctx.fillStyle='rgba(255,255,255,.18)';
roundRect(x,y,br.w,5,6); ctx.fill();
// hp indicator crack or dots
if(!br.unbreak && br.maxHp>1){
ctx.fillStyle='rgba(0,0,0,.35)';
for(let k=0;k<br.maxHp;k++){
const cx = x+10+k*14;
ctx.beginPath(); ctx.arc(cx+br.w/2-14, y+br.h/2, 3,0,Math.PI*2); ctx.fill();
}
// remaining hp glowing
ctx.fillStyle='#fff';
for(let k=0;k<br.hp;k++){
const cx = x+10+k*14;
ctx.beginPath(); ctx.arc(cx+br.w/2-14, y+br.h/2, 2.2,0,Math.PI*2); ctx.fill();
}
}
if(br.unbreak){
ctx.strokeStyle='rgba(255,255,255,.25)'; ctx.lineWidth=1.2;
roundRect(x+.6,y+.6,br.w-1.2,br.h-1.2,6); ctx.stroke();
// diagonal hatch
ctx.strokeStyle='rgba(255,255,255,.18)'; ctx.lineWidth=1;
ctx.beginPath();
ctx.moveTo(x+6,y+br.h-6); ctx.lineTo(x+br.w-6,y+6); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x+10,y+br.h-6); ctx.lineTo(x+br.w-6,y+10); ctx.stroke();
} else {
// outer glow for strong bricks
if(br.hp>1){
ctx.shadowColor=br.color; ctx.shadowBlur=12; roundRect(x,y,br.w,br.h,6); ctx.strokeStyle=br.color; ctx.lineWidth=1.4; ctx.stroke(); ctx.shadowBlur=0;
}
}
ctx.restore();
}
// powerups
for(let pu of powerups){
ctx.save();
ctx.translate(pu.x, pu.y);
ctx.rotate(pu.rot*0.2);
const isExp = pu.type==='expand';
ctx.fillStyle = isExp ? '#ffd23f' : '#00eaff';
ctx.shadowColor = ctx.fillStyle; ctx.shadowBlur=10;
roundRect(-13,-7,26,14,7); ctx.fill();
ctx.shadowBlur=0;
ctx.fillStyle='#0a0f1f'; ctx.font='700 10px system-ui'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(isExp? 'WIDE' : 'x3', 0,0);
// bob outline
ctx.strokeStyle='rgba(255,255,255,.9)'; ctx.lineWidth=1; roundRect(-13,-7,26,14,7); ctx.stroke();
ctx.restore();
}
// paddle
{
const px=paddle.x, py=paddle.y, pw=paddle.w, ph=paddle.h;
ctx.save();
// shadow
ctx.fillStyle='rgba(0,0,0,.45)';
roundRect(px, py+4, pw, ph, 8); ctx.fill();
// body gradient
const pg = ctx.createLinearGradient(0,py,0,py+ph);
pg.addColorStop(0,'#ffffff'); pg.addColorStop(0.5,'#dcd7ff'); pg.addColorStop(1,'#7a5cff');
ctx.fillStyle=pg;
roundRect(px,py,pw,ph,8); ctx.fill();
// inner highlight
ctx.fillStyle='rgba(255,255,255,.65)';
roundRect(px+6,py+2,pw-12,5,5); ctx.fill();
// side caps neon
ctx.fillStyle='#00eaff'; ctx.shadowColor='#00eaff'; ctx.shadowBlur=14;
ctx.fillRect(px-1, py+4, 2, ph-8);
ctx.fillRect(px+pw-1, py+4, 2, ph-8);
ctx.shadowBlur=0;
// expand timer ring
if(paddle.expandT>0){
ctx.strokeStyle='rgba(255,210,63,.9)'; ctx.lineWidth=2;
const pct = paddle.expandT/10;
roundRect(px-3, py-3, pw+6, ph+6, 10); ctx.globalAlpha=0.5*pct; ctx.stroke(); ctx.globalAlpha=1;
}
ctx.restore();
}
// balls
for(let b of balls){
// trail
for(let i=0;i<b.trail.length;i++){
const t=b.trail[i];
const a = 1 - i/b.trail.length;
ctx.fillStyle=`rgba(120,200,255,${a*0.22})`;
ctx.beginPath(); ctx.arc(t.x,t.y, b.r*(1 - i*0.05),0,Math.PI*2); ctx.fill();
}
// ball shadow
ctx.fillStyle='rgba(0,0,0,.35)';
ctx.beginPath(); ctx.arc(b.x+2,b.y+4,b.r,0,Math.PI*2); ctx.fill();
// ball body
const bg = ctx.createRadialGradient(b.x-3,b.y-3,1,b.x,b.y,b.r+4);
bg.addColorStop(0,'#ffffff'); bg.addColorStop(0.35,'#d9f5ff'); bg.addColorStop(1,'#3aa0ff');
ctx.fillStyle=bg;
ctx.shadowColor='#00eaff'; ctx.shadowBlur=12;
ctx.beginPath(); ctx.arc(b.x,b.y,b.r,0,Math.PI*2); ctx.fill();
ctx.shadowBlur=0;
// highlight dot
ctx.fillStyle='rgba(255,255,255,.95)';
ctx.beginPath(); ctx.arc(b.x-2.5,b.y-2.5,2,0,Math.PI*2); ctx.fill();
}
// particles
for(let p of particles){
const a = 1 - p.t/p.life;
ctx.globalAlpha = a;
ctx.fillStyle = p.color;
ctx.shadowColor=p.color; ctx.shadowBlur=8;
ctx.beginPath(); ctx.arc(p.x,p.y, p.size*a,0,Math.PI*2); ctx.fill();
ctx.shadowBlur=0;
}
ctx.globalAlpha=1;
// floating texts
for(let t of texts){
const a = 1 - t.t/t.life;
ctx.globalAlpha = Math.max(0,a);
ctx.fillStyle=t.color;
ctx.font='900 14px system-ui';
ctx.textAlign='center';
ctx.fillText(t.txt, t.x, t.y);
// glow
ctx.shadowColor=t.color; ctx.shadowBlur=8; ctx.fillText(t.txt, t.x, t.y); ctx.shadowBlur=0;
}
ctx.globalAlpha=1;
// vignette
const vig = ctx.createRadialGradient(W/2,H/2, Math.min(W,H)*0.5, W/2,H/2, Math.min(W,H));
vig.addColorStop(0,'transparent'); vig.addColorStop(1,'rgba(0,0,0,.45)');
ctx.fillStyle=vig; ctx.fillRect(0,0,W,H);
ctx.restore();
// update lives textual after render? already done
}
// init
buildDots();
updateHUD();
resetGame(true);
showMenu();
updateMute();
requestAnimationFrame(frame);
// auto resize observer to keep crisp? canvas already fixed logical, no need.
// prevent context menu on long press
canvas.addEventListener('contextmenu', e=>e.preventDefault());
})();
</script>
</body>
</html>

