013D / WebGL2026Solo — concept, physics, level design, 3D scene, page, tests
VYR — A Game Inside Its Own Landing Page
The landing page of a made-up indie game whose hero is the game: pull the probe back, let go, and let the planets bend its flight.
Private repo

The problem
A game's landing page usually has everything except the game: a trailer, a few screenshots, a wishlist button. I wanted the opposite — a page where the first thing a visitor does is play, with the hero as the live scene and one click turning it into the game in place, with nothing to install and no sign-up. That puts a real game's problems on a marketing page: physics that must come out the same on every machine, levels that someone designed rather than generated, and a first frame that cannot make a phone wait.
Decisions
01
A flight is computed, then replayed
The physics is plain TypeScript with no three.js in it: softened inverse-square gravity from planets and moons, velocity Verlet at a fixed 1/120 s step. The moment you let go, the whole flight is computed to its end, and the renderer plays the recorded path back. A flight is therefore identical at any frame rate on any machine, and the dotted hint, the real flight and the tests are all the same function. A unit test keeps a circular orbit within 0.2% of its radius for ten revolutions.
02
Levels found by search, not by eye
A script flies all 58,000 aims of a level, draws the result as an ASCII map and picks a path that bends a lot, skims a planet and has winning neighbours, so a person can actually find it. The shards go on that path, and the most forgiving three-star launch is recorded as the level's solution. The tests fly every recorded solution through the physics and expect three stars — also with the level turned 180°, because that is how the hero shows level 06 — and check that no level is won by its opening aim.
03
The grid is the gravity
The rubber sheet under the planets is sunk in a vertex shader by the same softened potential the physics uses, so the deepest, brightest dips are exactly where the pull is strongest. On the hero the cursor is one more mass. Nothing is drawn separately from the simulation, and nothing is loaded either: planets, rings, atmospheres, stars, the vortex and the trail are shaders and geometry, and the sound effects are synthesised with Web Audio.
04
A first frame that does not block
Before the game starts, the page loads 2.1 KB of JavaScript; three.js arrives as a lazy chunk after load, or on the first click on Play. Scene set-up is split into short tasks, and shaders are compiled with compileAsync against the composer's linear buffer rather than the canvas — compiled for the canvas, the first frame did all of it again, synchronously. Together that took mobile Total Blocking Time from 3.25 s to 0.46 s, and the Lighthouse mobile score from 69 to the high eighties.
05
Local green is not green
The end-to-end tests passed on my machine and failed in CI, where the runner has no GPU and WebGL falls back to SwiftShader: the scene took about forty seconds to prepare and the game tests timed out. A visitor without hardware acceleration would have had the same page. The renderer's name is now probed, and software rendering starts on a minimal tier — half the pixels, no bloom or antialiasing, simpler spheres and sheet. The ordinary tiers step down by themselves when frames are missed, measured against the display's own refresh, so the same code serves a 165 Hz monitor and a software renderer.
06
One aim for a mouse, a finger and a keyboard
On an upright phone the field turns a quarter and the camera backs off until all of it fits between the controls; the same pull-back works with a mouse, a finger or the arrow keys, and an end-to-end test wins level 1 by typing its recorded solution. A CI run caught a bug in the drag: it read 109.5° where only 0–90° is possible. The pull-back point was projected onto the field once, with the camera of that moment, while the current point was projected every time — and during the fly-in from the hero the two used different cameras. Both ends now go through the current camera on every move.
What I measured
2.1 KB
JavaScript before the game loads
gzipped; three.js is fetched after load or on the first click on Play
86 / 99
Lighthouse mobile / desktop
on the live site; 100 for accessibility, best practices and SEO in both
6.1 ms
median frame in flight
on a 165 Hz display; 6 of 380 frames ran over 1.6× the refresh interval
81 + 12
unit + end-to-end tests
every level's solution is flown for three stars; the browser tests also run against the live site



Built with
- three.js
- TypeScript
- Vite
- Playwright
- Cloudflare Workers
