Skip to content

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/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), 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.)

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