Showcase: Spacetime lattice
A 2.5D gravity ballet: planets and a black hole orbit on a projected wireframe lattice whose curvature is the summed gravitational potential. A companion spirals in, the LIGO chirp emerges for free, and the merger flashes through the post-FX. It is the most code of the three simulation showcases — and the least engine-dependent — so this guide is all about the bespoke physics and geometry: by now the shared spine is second nature.
What the engine provides here
Section titled “What the engine provides here”The least of the three: GameLoop + CoroutineManager (@flare-engine/core), SeededRNG +
clamp/lerp (@flare-engine/math), a merger ParticlePool (@flare-engine/particles), and
the post-FX finish (@flare-engine/postfx).
Everything else is yours. Velocity-Verlet integration, Plummer softening, GR precession, the
inspiral drag, the warp lattice, perspective projection, gravitational-wave ripples, gravitational
lensing — all bespoke, zero-import, bun-tested TypeScript across
spacetime/{nbody,lattice,ripples,starfield}.ts. It runs the loop and adds the glow; the lattice,
projection, and lensing are pure application code. That this seam can carry a simulation this heavy
is the showcase.
A stable N-body core
Section titled “A stable N-body core”nbody.ts integrates up to a dozen bodies with velocity-Verlet (leapfrog) under
Plummer-softened gravity — softening removes the singularity at r → 0, and the symplectic
integrator keeps energy bounded over minutes instead of drifting:
// acceleration on body i from body j, softened: a ∝ 1 / (r² + ε²)^(3/2)function accel(dx: number, dy: number, mj: number, eps2: number): number { const r2 = dx * dx + dy * dy + eps2; return (G * mj) / (r2 * Math.sqrt(r2));}An optional position-only 1/r⁴ precession term adds the relativistic rosette without breaking
the symplectic property. It’s all pure and tested — nbody.test.ts asserts total energy stays
bounded, the kind of invariant you want under unit test, not eyeballing.
loop.onFixedUpdate.add((dt) => nbody.step(dt)); // deterministic integrator on the fixed stepThe warp lattice is the potential
Section titled “The warp lattice is the potential”lattice.ts is the geometry core. Each frame it zeroes a height field and adds the summed
Plummer potential wells (mass-referenced, so a heavy hole curves the sheet a deliberate,
art-directed amount), a broad funnel under the black hole, and the gravitational-wave ripple
height. Then it flattens a band behind the headline text, computes |∇z| for the curvature, and
projects every vertex through a real pitched-perspective camera:
lattice.reset();for (const b of nbody.bodies) lattice.addWell(b.x, b.y, b.mass); // curvature = summed potentiallattice.addRipples(ripples);lattice.flattenBand(headlineRect);lattice.computeCurvature(); // |∇z| per vertex → line weight + brightnesslattice.project(camera); // world → screen (unproject() inverts it for the ground plane)The curvature drives the look: where the sheet bends hardest, the renderer draws the lattice segments bolder and brighter; far rows are depth-dimmed.
The chirp, for free
Section titled “The chirp, for free”The companion spirals in via an honest drag that removes only angular momentum (a Peters
da/dt ∝ a⁻³ cadence), not energy by fiat. Because Keplerian ω ∝ r^-1.5, as the orbit tightens
the frequency rises on its own — the LIGO chirp emerges from the physics rather than being
scripted. At the throat, a momentum- and mass-conserving inelastic absorb merges the bodies, with
mass bleeding back so the hole never grows without bound.
Gravitational waves and lensing
Section titled “Gravitational waves and lensing”Two more pure modules carry the spectacle:
ripples.ts— aRipplePoolof analytic expanding wave packets (≈1/√r attenuation, decaying envelope), emitted faster and stronger as the binary tightens, summed into the lattice height.starfield.ts— gravitational lensing of a background star field by the point-mass map: deflection ∝ 1/r piles images into an Einstein ring, plus a Doppler-beamed photon ring, a lensed accretion-disk arc, and gravitational redshift on infall.
The black hole’s shadow is the one non-additive draw in the whole piece — a SrcOver fill,
because additive Plus blending can only lighten, and a shadow has to darken.
Rendering: layered, back-to-front
Section titled “Rendering: layered, back-to-front”Raw CanvasKit drawLine/drawOval/drawCircle, painted back to front onto the shared surface,
then composited. The merger pushes a windup-darken then an impact² spike into the post-FX so
the flash blows past the bloom threshold:
this.surface.fadeTrail(BG, 0.12);drawLensedStarfield(sc); drawLattice(sc, curvature); // depth-dimmed segmentsdrawShadowVoid(sc); drawPhotonRing(sc); drawDisk(sc); // the SrcOver shadow + ringsdrawPlanets(sc); drawTrails(sc); drawMergerFx(sc);this.surface.composite(sim.glowWithImpactSpike()); // engine bloom + aberration → presentPlanet identities (size, mass, color) live in planets.ts and are deliberately off the brand
palette — real planetary hues read better than the prism here.
Merger as choreography, and a live camera
Section titled “Merger as choreography, and a live camera”A CoroutineManager drives calm → inspiral → merger → ringdown, gated on orbital separation, and
fires the merger’s “DEVOURER” envelope stack (windup inhale, impact stab, a damped-cosine lattice
rebound, swell, gulp) plus a tangential debris burst from a ParticlePool. Drag-to-rotate and a
pitch slider feed lattice.setCamera live, so the whole projected sheet tilts in real time:
loop.onUpdate.add((dt) => { coro.update(dt); });// pointer drag / slider → lattice.setCamera({ yaw, pitch })Planet birth — the implosion
Section titled “Planet birth — the implosion”The cycle doesn’t end at the merger. After the DEVOURER eats a body, the count is held steady by
injecting a fresh companion far out — but it never just pops into frame. A freshly injected
planet starts with a birth envelope birth = 0 and condenses into being over birthTime (≈0.9 s):
it’s the gentle inverse of the violent absorption. Where the merger is an outward flash, birth is
an inward implosion —
- a bright shell collapses inward from
birthShell× the planet’s radius onto the core; - a converging debris cloud (a
ParticlePoolburst, starting atbirthInrush× the frame’s short side, count scaled bybirthSparks) rushes inward and fades — the mirror of the merger’s outward tangential spray; - over the final stretch a brief achromatic pop flashes as the body snaps solid with a soft eased overshoot, blooming through the post-FX;
- a summed
birthGlowlifts the global bloom a touch while any planet is forming, and the lattice takes an extra birth-splash dent as the new mass condenses into the sheet.
injectSatellite(); // spawn far out (INJECT_R_FRAC of the frame) with birth = 0// each frame: birth = min(1, birth + dt / birthTime) → shell contracts, debris converges, then popBecause the mass that the hole absorbs bleeds back into the injected body, the system conserves its
budget and loops seamlessly — a body leaves in a flash and a new one implodes into being, with no
visible teleport. All four birth knobs (birthTime, birthInrush, birthShell, birthSparks)
are live in the dev panel.
- A symplectic velocity-Verlet + Plummer N-body core, energy-bounded and bun-tested.
- A warp lattice whose height is the summed potential; project every vertex through a pitched camera.
- An honest angular-momentum-only inspiral — the chirp falls out of Kepler’s law.
- Analytic GW ripples + 1/r lensing (Einstein ring, photon ring, disk) — pure modules.
- Layered raw-CanvasKit render + the
SrcOvershadow; merger spike routed into the engine post-FX.
Five small pure modules, each unit-tested, wrapped by the engine’s loop and post-FX. That division — heavy custom simulation, thin high-quality engine seam — is the whole thesis of these showcases.