Showcases: Foundations
Five self-contained Skia/CanvasKit animations run on this site — Physarum, a nuclear reactor, a spacetime lattice, a procedural map, and a logo reveal — and they share one spine. This page is that spine. Read it first; each showcase guide then covers only what is unique to it.
The honest seam: what the engine does, and what you write
Section titled “The honest seam: what the engine does, and what you write”These pieces look like the engine is doing the heavy lifting. It isn’t, and the guides are careful about this — because the interesting, sellable story is the real one. Across the showcases:
| Concern | Where it lives |
|---|---|
| Fixed-timestep loop, variable display step | @flare-engine/core — GameLoop |
| Timer-free choreography (phases, sequences) | @flare-engine/core — CoroutineManager |
clamp / lerp, seeded RNG |
@flare-engine/math |
| Pooled, zero-alloc particle bursts | @flare-engine/particles — ParticlePool |
| Bloom + chromatic aberration (real SkSL) | @flare-engine/postfx — EffectChain |
| Agents as entities, control-rod tweens | @flare-engine/ecs + @flare-engine/animation (reactor only) |
| The simulation itself — pheromone fields, reactor kinetics, N-body gravity, lensing, seeded map + road generation | Your application code |
| Every scene draw — circles, lines, the field image | Raw CanvasKit |
So the engine gives you a tight loop, math helpers, a particle pool, and a high-quality post-FX
finish — plus, for the reactor, an ECS and tweens. Everything domain-specific is ordinary code
you write against raw CanvasKit. None of these demos import @flare-engine/render or
@flare-engine/physics; the physics is bespoke, pure TypeScript. Since these guides were written,
the reusable primitives behind two of them — the N-body gravity, Verlet integration, and
wave-packet ripples of Spacetime, and the stigmergy field of Physarum — have upstreamed into
@flare-engine/simulation (shipped in 0.2.0), and the demos on this site now import them:
the local forks are deleted and their unit tests point at the package, so the showcases exercise
the published surface instead of a private copy that could quietly drift from it. (The reactor’s neutron
kinetics stay bespoke — no reactor package exists.)
The animation contract
Section titled “The animation contract”Every showcase implements one small interface, HeroAnimation
(src/components/demos/animations/types.ts). The generic host (AnimationStage.tsx, one instance
per showcase page) owns CanvasKit loading (streamed WASM with a progress bar), the
ResizeObserver / IntersectionObserver lifecycle, an FPS monitor and the adaptive-quality
governor, and the reduced-motion and WebGL-failure fallbacks. An animation only has to build an
instance against an already-loaded CanvasKit and a sized canvas:
export interface HeroAnimation { readonly id: string; readonly label: string; readonly ariaLabel: string; create( kit: CanvasKit, canvas: HTMLCanvasElement, overrides?: ParamValues, ): HeroAnimationInstance | null;}
export interface HeroAnimationInstance { start(): void; pause(): void; resume(): void; resize(width: number, height: number): void; status(): string; dispose(): void; // Optional: a tunable-parameter schema + live stats the dev panel feature-detects. params?(): readonly ParamDescriptor[]; getParams?(): ParamValues; setParam?(key: string, value: ParamValue): void; stats?(): readonly StatReadout[];}The five adapters (animations/physarum-network.ts, nuclear-reaction.ts,
spacetime-lattice.ts, map.ts, logo.ts) are near-identical thin merges of defaults +
overrides. The whole wiring is three lines — build a renderer, build a sim, hang the render call
off the loop (the Logo piece is the minimal case: a pure timeline clock and no sim at all):
create(kit, canvas, overrides) { const renderer = createPhysarumRenderer(kit, canvas); if (!renderer) return null; const sim = createPhysarumSim(canvas.width, canvas.height, configFrom(overrides)); const offRender = sim.loop.onPreRender.add(() => renderer.draw(sim)); return { start: () => sim.loop.start(), pause: () => sim.loop.pause(), resume: () => sim.loop.resume(), resize: (w, h) => { renderer.resize(w, h); sim.resize(w, h); }, status: () => sim.phase(), dispose: () => { offRender(); sim.dispose(); renderer.dispose(); }, };}The game loop
Section titled “The game loop”The sim owns a GameLoop from @flare-engine/core. It separates a fixed-rate simulation step
from a variable-rate display/choreography step, and emits a pre-render signal the renderer hangs
off of:
import { GameLoop } from "@flare-engine/core";
const loop = new GameLoop({ fixedDt: 1 / 60, maxDt: 0.1 });
loop.onFixedUpdate.add((dt) => stepSimulation(dt)); // deterministic, 1/60 s — physics/agentsloop.onUpdate.add((dt) => { coro.update(dt); smoothDisplay(dt); }); // variable — choreographyloop.onPreRender.add(() => renderer.draw(sim)); // the renderer reads sim statePhysics on the fixed step is what makes the behavior frame-rate-independent and reproducible; rendering and choreography ride the variable step. This is the engine’s documented per-frame order — see Concepts → Architecture.
Timer-free choreography with coroutines
Section titled “Timer-free choreography with coroutines”None of these animations use wall-clock timers. Each runs a CoroutineManager state machine
whose waits are gated on the live state of the simulation, so the show always paces itself to
what’s actually happening on screen and loops seamlessly:
import { CoroutineManager } from "@flare-engine/core";
function* waitUntil(pred: () => boolean) { while (!pred()) yield;}
function* cycle() { yield* waitUntil(() => population() >= growthTrigger()); // wait for the reaction to take off // ...drive the next phase...}
const coro = new CoroutineManager();coro.start(cycle());// driven from loop.onUpdate: coro.update(dt)The post-FX surface (the one place the engine renders)
Section titled “The post-FX surface (the one place the engine renders)”This is where the seam moved the furthest. It began as a 292-line local module and is now
@flare-engine/render-web’s createPostFxSurface: the package owns the CanvasKit WebGL
on-screen surface, the offscreen ping-pong pair, the RuntimeEffect adaptation to
@flare-engine/postfx’s SkiaPostFxModuleLike, and the resize / context-loss lifecycle. That
hardening was written here first and ported upstream for 0.3.0, so the fork is gone. What stays
local (src/components/demos/postfx/surface.ts, 40 lines) is only the frozen look — the
EffectChain every animation on this site composites through:
import { createBloom, createChromaticAberration, EffectChain } from "@flare-engine/postfx";
const chain = new EffectChain();chain.add(createBloom("bloom", { threshold: 0.5, intensity: 0.9 }));chain.add(createChromaticAberration("aberration", { amount: 0.0022 }));Those are the engine’s actual SkSL shaders, compiled against CanvasKit’s RuntimeEffect — the
same post-processing you’d get on native Skia, here on the web. The surface defines the universal
per-frame shape every animation follows:
surface.fadeTrail(BG, trailFade); // 1. wipe the previous frame — trailFade<1 = motion-blur, 1 = hard cleardrawScene(surface.sceneCanvas()); // 2. draw the additive scene (raw CanvasKit, BlendMode.Plus)surface.composite(globalIntensity); // 3. snapshot → bloom+aberration chain → on-screen surfaceThe trailFade alpha passed to fadeTrail is the trails knob, and it’s per-animation. Below 1,
each frame fades the prior scene toward the background instead of erasing it, so motion leaves a
glow — Physarum and Spacetime do this (≈0.12–0.18). At exactly 1, fadeTrail is a hard
clear: the reactor uses that on purpose, for crisp, trail-free entities that read as solid
objects rather than streaks. Either way, composite() runs the real bloom + aberration chain and
presents the frame to the on-screen surface — so the three-step shape is universal even though
the trail is opt-in.
The palette
Section titled “The palette”One frozen prism palette (src/components/demos/reactor/palette.ts) is the single source for
PALETTE, SPARK_COLORS, and the color4f helper, re-exported verbatim by the other animations.
Spacetime adds scene-only planet colors (spacetime/planets.ts) deliberately outside the brand
palette, because the solar system reads better with real planetary hues.
The pure-module discipline
Section titled “The pure-module discipline”The interesting math in each piece — reactor/physics.ts, spacetime/{lattice,starfield}.ts,
logo/timeline.ts, and map/grid.ts — is written as
zero-import, framework-free TypeScript with a co-located *.test.ts, “designed to upstream.”
That discipline paid off, and the round trip is now complete: the Spacetime N-body/Verlet/ripple math
and the Physarum field shipped as @flare-engine/simulation (0.2.0) and this site deleted its
local copies to import them back — the test files stayed, repointed at the package, so they now
guard the dependency instead of a fork. The Map’s seeded road generator + edge-mask
autotiler are the seed of a planned @flare-engine/procgen (its tile side already has a home in
the shipped @flare-engine/tilemap). That’s the convention worth copying: your simulation math is
your own pure code, unit-tested with bun test in isolation from CanvasKit and from the engine, so
it stays correct and portable — and, when it’s good, liftable into a package. The engine seam
(loop, pool, post-FX) wraps it; it doesn’t own it.