# 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.

<Aside type="note" title="Read Foundations first">
This guide builds on [Foundations](/guides/foundations) — the contract, the post-FX surface, and
the pure-module discipline.
</Aside>

## 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.

<Aside type="note" title="The N-body core now ships as a package">
As of 0.2.0 the integration core this guide builds by hand also ships as
**`@flare-engine/simulation`** (Layer 2): `createNBodySystem` (Plummer-softened n-body with optional
post-Newtonian precession), the velocity-Verlet / leapfrog integrators, and `createRipplePool` for
the wave packets. The warp lattice, perspective projection, and gravitational lensing remain your
own application code, and this site's hero still runs its local copy.
</Aside>

## 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:

```ts
// 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.

```ts
loop.onFixedUpdate.add((dt) => nbody.step(dt));   // deterministic integrator on the fixed step
```

## 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**:

```ts
lattice.reset();
for (const b of nbody.bodies) lattice.addWell(b.x, b.y, b.mass);  // curvature = summed potential
lattice.addRipples(ripples);
lattice.flattenBand(headlineRect);
lattice.computeCurvature();          // |∇z| per vertex → line weight + brightness
lattice.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

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

Two more pure modules carry the spectacle:

- `ripples.ts` — a `RipplePool` of 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

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:

```ts
this.surface.fadeTrail(BG, 0.12);
drawLensedStarfield(sc); drawLattice(sc, curvature);   // depth-dimmed segments
drawShadowVoid(sc); drawPhotonRing(sc); drawDisk(sc);  // the SrcOver shadow + rings
drawPlanets(sc); drawTrails(sc); drawMergerFx(sc);
this.surface.composite(sim.glowWithImpactSpike());     // engine bloom + aberration → present
```

Planet 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

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:

```ts
loop.onUpdate.add((dt) => { coro.update(dt); });
// pointer drag / slider → lattice.setCamera({ yaw, pitch })
```

## 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 `ParticlePool` burst, starting at `birthInrush` × the frame's
  short side, count scaled by `birthSparks`) 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 **`birthGlow`** lifts 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.

```ts
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 pop
```

Because 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.

## Recap

<Steps>

1. A symplectic velocity-Verlet + Plummer N-body core, energy-bounded and bun-tested.
2. A warp lattice whose height is the summed potential; project every vertex through a pitched camera.
3. An honest angular-momentum-only inspiral — the chirp falls out of Kepler's law.
4. Analytic GW ripples + 1/r lensing (Einstein ring, photon ring, disk) — pure modules.
5. Layered raw-CanvasKit render + the `SrcOver` shadow; merger spike routed into the engine post-FX.

</Steps>

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.

<Aside type="note">
See it live in the [hero](/) (pick "Spacetime"), and drag to rotate the lattice. Next, the
[Procedural map](/guides/map) — a seeded cartography piece on the same spine — or back to the
[Foundations](/guides/foundations) overview and the [package catalog](/reference/packages).
</Aside>
