A lot of breakout clones written by AI. See the different branches for each AI. The prompt is always something like """ I want you to create a breakout clone for HTML. Breakout is an old 2D game where a ball set on a paddle on the bottom of the screen is shot up, bouncing off walls and bricks, where the walls are the containment of the playing field and the bricks are destroyed (or damaged) when hit. I want the clone to be strictly written in HTML, CSS and javascript, so it does not need to be deployed (except laying the source code in a directory that's served by a webserver or file://). Basically, if you have questions, try to just make it awesome. """
  • JavaScript 69.4%
  • CSS 12.5%
  • Python 11.1%
  • HTML 4.8%
  • Shell 2.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Stefan Huber 6812a16d46 Landscape layout: move HUD and toolbar into a side rail
Stacked vertically, a landscape phone sized the playfield from its SHORT edge,
so the canvas ended up tiny while ~500px of width sat unused. Worse, the
stacked chrome no longer fitted: `--chrome` under-reserved the real HUD +
toolbar height, so `#wrap` overflowed its `100dvh` and `overflow: hidden`
clipped the top of the HUD and the bottom of the toolbar.

Under `(orientation: landscape) and (max-height: 620px)` the layout is now a
grid -- playfield on the left, HUD and toolbar stacked in a right-hand rail:

  grid-template-areas: "stage hud"
                       "stage tools"

The HUD becomes vertical label/value rows (and gets back the Best and Combo
cells that the narrow-width breakpoint hides, since the rail has room), the
toolbar becomes a 3-column button pad, and the overlay panels are compacted so
they fit a short stage -- including restoring the key list that the
short-viewport rule drops. Rail padding respects safe-area insets, so the rail
is not tucked under a notch.

Stage width uses the same "min(what the width allows, what the height allows)"
formula as portrait, transposed. Measured playfield area, vs the old stacked
layout at the same viewport:

  915x412  389x292 -> 531x398   1.86x
  667x375  340x255 -> 481x361   2.00x
  568x320  267x200 -> 406x304   2.32x

Also threads viewport changes through one handler driven by a ResizeObserver
on the canvas -- the resize event can report the old size before the
orientation reflow has settled -- and makes Renderer.resize() idempotent, since
it re-bakes every brick sprite and now has five overlapping triggers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 11:58:29 +02:00
css Landscape layout: move HUD and toolbar into a side rail 2026-08-23 11:58:29 +02:00
js Landscape layout: move HUD and toolbar into a side rail 2026-08-23 11:58:29 +02:00
tools Neon Breakout: complete game in vanilla HTML/CSS/JS 2026-08-23 11:27:51 +02:00
.gitignore Neon Breakout: complete game in vanilla HTML/CSS/JS 2026-08-23 11:27:51 +02:00
Caddyfile Host Neon Breakout behind the edge proxy 2026-08-23 11:32:14 +02:00
CLAUDE.md Landscape layout: move HUD and toolbar into a side rail 2026-08-23 11:58:29 +02:00
docker-compose.yml Host Neon Breakout behind the edge proxy 2026-08-23 11:32:14 +02:00
index.html Fix catastrophic mobile performance; add performance mode 2026-08-23 11:46:41 +02:00
install.sh Host Neon Breakout behind the edge proxy 2026-08-23 11:32:14 +02:00
README.md Landscape layout: move HUD and toolbar into a side rail 2026-08-23 11:58:29 +02:00

Neon Breakout

A complete Breakout clone in plain HTML, CSS and JavaScript. No build step, no dependencies, no bundler, no assets — every sound is synthesised at runtime and every graphic is drawn on a <canvas>.

Running it

Just open the file:

xdg-open index.html          # or double-click it

file:// works because the scripts are classic <script> tags rather than ES modules. If you'd rather serve it:

python3 -m http.server 8000   # then browse to http://localhost:8000

Drop the directory into any webserver's document root and it will work as-is.

Controls

Input Action
Mouse / touch Move the paddle
← → or A D Move the paddle
Space / click / tap Launch the ball, fire lasers, dismiss screens
P or Esc Pause
M Mute everything
N Soundtrack on/off
G Performance mode
H Power-up and brick guide

What's in it

Ten hand-built levels, then endless. After Last Stand the game generates mirrored layouts forever, with the hit points, solid blocks and explosive density all climbing with depth.

Four brick types. Standard bricks (1–5 hit points, colour-coded, with cracks and pips showing damage), indestructible solids, explosive bricks that chain- detonate, and mystery bricks that always drop a capsule.

Eleven power-ups, including two traps that also drop: wide paddle, multiball, laser cannons, slow motion, catch, piercing ball, force field, extra life, bonus points — and narrow paddle and overdrive, which you want to dodge.

Combo scoring. Every brick destroyed without touching the paddle raises the multiplier, up to x10. Touch the paddle and it resets. Extra life every 25,000 points. The high score persists in localStorage.

Presentation. Parallax starfield and perspective grid, per-brick glow, particle shatter, shockwaves, screen shake, ball trails, CRT scanlines, and a full WebAudio sound board driven by oscillators and filtered noise.

An 80s synthwave soundtrack, generated live — see below.

A neon reticle replaces the arrow cursor over the playfield, tethered down to the paddle so you can always see which one the mouse is driving.

Two layouts. Portrait stacks the HUD above and the toolbar below the playfield; landscape on a phone moves both into a side rail so the playfield gets the whole height — roughly twice the play area of the stacked layout.

Performance mode for phones and weak GPUs — G, or the ⚡ button. It is picked automatically on mobile, and the game will also drop into it on its own if it measures a bad frame rate.

Layout

index.html          markup, HUD and overlay screens
css/style.css       all presentation, responsive down to phone widths
js/utils.js         maths, easing, safe localStorage
js/audio.js         WebAudio synthesiser — every sound effect
js/levels.js        level data, endless generator, reachability guard
js/powerups.js      capsule catalogue and effects
js/fx.js            particles, popups, shockwaves, shake, background
js/game.js          simulation: state machine, physics, collision, scoring
js/render.js        all canvas drawing
js/main.js          input, fixed-timestep loop, HUD and screens

Scripts load in that order; each is an IIFE that hangs one object off window.

The soundtrack

There is no audio file anywhere in this project. The music is a four-bar Am - F - C - G loop built out of oscillators at schedule time:

  • Bass — two detuned oscillators (saw + square) through a resonant lowpass that plucks open and shut on every note.
  • Drum machine — four-on-the-floor kick with a pitch drop, hats on the offbeats, and a gated-reverb snare: a noise crack followed by three tails that are cut off with a hard ramp to silence, which is the single most 80s sound ever committed to tape.
  • Arpeggio — plucky sixteenths through the chord, each one echoed one and a half steps later at a third of the volume to fake a tape delay.
  • Pad — a slow string-machine chord, three saws spread by a few cents.
  • Lead — a detuned triple-saw riff that only shows up on the deeper levels.

Layers switch in as an intensity value rises, so the track is a moody pad on the title screen, picks up drums and arpeggio once you're playing, and gets the screaming lead by the later levels. Intensity is eased rather than snapped, so nothing pops in mid-phrase.

Timing uses the standard WebAudio lookahead pattern — a coarse 30 ms setTimeout queues notes a fraction of a second ahead against the audio hardware clock — so the groove stays locked regardless of frame rate.

M mutes everything; N toggles just the music, since the soundtrack rides its own gain bus. Both preferences persist.

Notes on the implementation

Mouse tracking is bound to the window, not the playfield. Bound to the stage — the obvious place — the paddle froze the instant the pointer crossed into the HUD or the toolbar, which happens constantly because the playfield is short and you overshoot it vertically. Listening globally means only X matters and leaving the field vertically changes nothing. The canvas rect is cached and invalidated on relayout, since getBoundingClientRect() forces a layout flush and pointermove fires 120+ times a second.

Leaving the browser window entirely still stops mouse events — the browser sends none — but the reticle disappears when that happens, so the state is visible rather than mysterious, and the keyboard still works.

The canvas is sized from the display, not the logical field. The backing store is displayedCssWidth x devicePixelRatio, not 800 x dpr. Sizing it from the logical units meant a phone with a ~390 px-wide stage rendered 2000x1500 and downscaled it — roughly 4x more pixels than the screen could show, and the cause of single-digit frame rates on Firefox Android. It also means large desktop displays now get a properly sharp render instead of an upscaled 800x600 one.

Performance mode (js/quality.js) gates everything expensive behind a preset: canvas shadowBlur, blend modes, backdrop-filter, the full-canvas vignette, particle counts, the starfield and parallax grid, and the physics rate. mix-blend-mode over an animating canvas was removed outright — on Firefox Android it can drop the entire page to software compositing. On a Pixel-class phone the rendered pixel count goes 3.0M → 728k (high) → 117k (low).

Ball speed. The ball starts at 400 px/s, gains 14 px/s per level and 2 px/s per brick destroyed, and caps at 780 px/s (947 with the Overdrive trap active). The paddle tops out at 1050 px/s so it can still keep up.

Fixed timestep. Physics runs at a fixed 120 Hz with an accumulator, capped at 8 steps per frame so a backgrounded tab can't spiral. Rendering runs at display rate. Behaviour is identical at 60 Hz and 144 Hz.

No tunnelling. Each tick the ball is advanced in sub-steps no longer than 0.55 × its radius, so it can never skip past a brick regardless of speed. At the worst case the game allows — 947 px/s — that is a 7.9 px move split into three 2.6 px sub-steps, against a 22 px brick. Per sub-step only the single deepest overlap is resolved, which keeps corner hits predictable instead of double-reflecting.

No horizontal orbits. After every brick bounce the vertical component is forced to at least 18% of the speed, so the ball can never settle into a near-flat path that grinds along a row forever.

Levels are guaranteed clearable. Solid bricks are permanent walls; every other cell becomes passable once broken. So a breakable brick is reachable iff a 4-connected path of non-solid cells reaches it from below the field. levels.js flood-fills to check exactly that, and the endless generator carves escape routes until the layout passes — without it, roughly 8% of generated levels contained bricks that could never be hit.

Anti-stall. If eight or fewer bricks remain and none is destroyed for 18 seconds, the game air-drops a helpful capsule (piercing, multiball, laser, wide or slow). Awkward last bricks resolve themselves instead of grinding.

Brick sprite cache. Brick glow uses shadowBlur, which is far too expensive to pay ~130 times per frame. Each distinct brick appearance is painted once into an offscreen canvas and blitted thereafter; the cache is keyed on type, hue, and damage, and is rebuilt when the device pixel ratio changes.

Crisp on any display. The canvas backing store is scaled by devicePixelRatio while the game draws in fixed 800×600 logical units, so the same code is sharp on a phone and on a HiDPI monitor.