Skip to content

Showcases: Foundations

The hero on this site is a switchable showcase: five self-contained Skia/CanvasKit pieces — Physarum, a nuclear reactor, a spacetime lattice, a procedural map, and a logo reveal — that 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/coreGameLoop
Timer-free choreography (phases, sequences) @flare-engine/coreCoroutineManager
clamp / lerp, seeded RNG @flare-engine/math
Pooled, zero-alloc particle bursts @flare-engine/particlesParticlePool
Bloom + chromatic aberration (real SkSL) @flare-engine/postfxEffectChain
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). The demos on this site still run their own local copies, but you no longer have to write those from scratch. (The reactor’s neutron kinetics stay bespoke — no reactor package exists.)

Every showcase implements one small interface, HeroAnimation (src/components/demos/animations/types.ts). The generic host (HeroStage.tsx) owns CanvasKit loading (streamed WASM with a progress bar), the ResizeObserver / IntersectionObserver lifecycle, an FPS monitor, cross-fade switching, and the reduced-motion fallback. 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 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/agents
loop.onUpdate.add((dt) => { coro.update(dt); smoothDisplay(dt); }); // variable — choreography
loop.onPreRender.add(() => renderer.draw(sim)); // the renderer reads sim state

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

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 the most important shared module: src/components/demos/postfx/surface.ts. It wraps a CanvasKit WebGL on-screen surface plus an offscreen ping-pong pair, adapts CanvasKit’s RuntimeEffect to @flare-engine/postfx’s SkiaPostFxModuleLike, and runs a real EffectChain:

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 clear
drawScene(surface.sceneCanvas()); // 2. draw the additive scene (raw CanvasKit, BlendMode.Plus)
surface.composite(globalIntensity); // 3. snapshot → bloom+aberration chain → on-screen surface

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

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 interesting math in each piece — reactor/physics.ts, physarum/field.ts, spacetime/{nbody,lattice,ripples,starfield}.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: the Spacetime N-body/Verlet/ripple math and the Physarum field have since shipped as @flare-engine/simulation (0.2.0), and 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.