API Reference

Imperative control via FluidHandle.

Most of the time, you control <Fluid> with props. When application logic needs to inject splats, pause the simulation, or read engine state, use the FluidHandle interface.

Getting the Handle

Use Svelte's bind:this to capture a reference to the component instance. The handle property exposes the methods and state below.

<script lang="ts">
  import { Fluid } from 'svelte-fluid';
  import type { FluidHandle } from 'svelte-fluid';

  let fluidRef: { handle: FluidHandle };
</script>

<Fluid bind:this={fluidRef} />

<button onclick={() => fluidRef.handle.randomSplats(5)}>
  Add splats
</button>

The same pattern works with presets — every preset component exposes the same handle property:

<script lang="ts">
  import { LavaLamp } from 'svelte-fluid';
  import type { FluidHandle } from 'svelte-fluid';

  let lampRef: { handle: FluidHandle };
</script>

<LavaLamp bind:this={lampRef} />

FluidHandle Methods

splat(x, y, dx, dy, color)

Inject a single splat at the given position with velocity and color.

ParameterTypeDescription
xnumberX position in normalized coords (0 = left, 1 = right)
ynumberY position in normalized coords (0 = bottom, 1 = top)
dxnumberX velocity (raw value, not scaled by splatForce)
dynumberY velocity (raw value, not scaled by splatForce)
colorRGBColor in 0-1 linear range. Values above 1 are valid HDR and appear as bloom highlights.
// Inject a bright red splat at the center moving rightward
fluidRef.handle.splat(0.5, 0.5, 500, 0, { r: 1.5, g: 0.1, b: 0.1 });

Non-finite arguments (NaN, ±Infinity) in position, force, or color are ignored, with a one-time console warning.

Velocity values are in pixels per frame and written directly to the splat shader uniform. Unlike pointer-driven splats, these are not multiplied by splatForce. Pick the absolute magnitude you want. Typical values range from 100 (gentle) to 1000 (forceful).

randomSplats(count)

Push N additional random splats onto the splat queue. Colors are generated by the engine's internal generateColor function.

The queued backlog is capped at 64 and the engine consumes at most 16 per frame, so repeated calls saturate instead of growing without bound. Non-finite or negative counts are ignored.

ParameterTypeDescription
countnumberNumber of random splats to inject
// Add 10 random splats
fluidRef.handle.randomSplats(10);

pause()

Stop the animation loop. The WebGL context stays alive but no frames are requested. Call resume() to restart. Idempotent — calling pause() on an already-paused engine is a no-op.

fluidRef.handle.pause();

resume()

Restart the animation loop after a pause(). Idempotent — calling resume() on an already-running engine is a no-op.

fluidRef.handle.resume();

isPaused

Readonly boolean property indicating whether the engine's animation loop is currently paused.

if (fluidRef.handle.isPaused) {
  fluidRef.handle.resume();
}

getPerformanceState()

Return the current autoPerformance governor state. This is pull-based: no events are emitted, so call it when rendering diagnostics or controls.

const perf = fluidRef.handle.getPerformanceState();

if (perf.enabled && perf.tier !== 'none') {
  console.info(`Fluid quality shed: ${perf.tier}`);
}

Types

RGB

A simple color triple with r, g, b number fields. The unit convention depends on context:

ContextRangeNotes
backColor prop0-255CSS-style. Normalized internally before reaching the shader.
FluidHandle.splat() color0-1Linear. Values above 1 are valid HDR and create bloom highlights.
PresetSplat.color0-1Linear. Same convention as splat().
revealCoverColor, revealAccentColor, revealFringeColor0-1Linear.
interface RGB {
  r: number;
  g: number;
  b: number;
}

PerformanceState

Snapshot returned by FluidHandle.getPerformanceState().

type PerformanceTier = 'none' | 'pressure' | 'substeps';
type PerformanceAction = 'none' | 'shed-pressure' | 'shed-substeps';

interface PerformanceState {
  readonly enabled: boolean;
  readonly tier: PerformanceTier;
  readonly emaMs: number;
  readonly msSinceLastChange: number;
  readonly targetFrameMs: number;
  readonly pressureIterations: number;
  readonly substeps: number;
  readonly minPressureIterations: number;
  readonly minSubsteps: number;
  readonly lastAction: PerformanceAction;
}

PresetSplat

Initial splat object consumed once when the engine is created. Preset wrappers use these to paint a repeatable opening scene.

FieldTypeDescription
xnumberX position, 0-1 (left to right)
ynumberY position, 0-1 (bottom to top)
dxnumberX velocity (raw, not scaled by splatForce)
dynumberY velocity (raw, not scaled by splatForce)
colorRGBColor in 0-1 linear range (HDR values above 1 are valid)
interface PresetSplat {
  x: number;
  y: number;
  dx: number;
  dy: number;
  color: RGB;
}

Obstruction

An interior obstacle the fluid flows around, passed via the obstructions prop. Reuses the same path/text descriptor as the svgPath container variant — the filled region marks where fluid is blocked. All obstructions union into one combined mask; the allowed region is container × (1 − obstruction). Orthogonal to containerShape. See Container Shapes and ADR-0034.

FieldTypeDescription
dstring?SVG path data (path mode). Takes precedence over text.
textstring?Text to rasterize (text mode). Centered in the mask.
fontstring?CSS font for text mode. Default 'bold 72px sans-serif'.
viewBox[number, number, number, number]?viewBox for path mode. Default [0, 0, 100, 100].
fillRule'nonzero' | 'evenodd'?Fill rule for path mode. Default 'nonzero'.
offset{ x: number; y: number }?0-1 canvas-space translation after the fit transform. Default { x: 0, y: 0 }.
scalenumber?Multiplier on the fit scale. Default 1.
fit'contain' | 'fill'?viewBox→canvas mapping. 'contain' (default) uniform-fits + centers (letterboxes); 'fill' stretches to fill at any aspect (for maze/nozzle channels). Path mode only.
interface Obstruction {
  d?: string;
  text?: string;
  font?: string;
  viewBox?: [number, number, number, number];
  fillRule?: 'nonzero' | 'evenodd';
  offset?: { x: number; y: number };
  scale?: number;
  fit?: 'contain' | 'fill';
}

FlowConfig

Higher-level scene description for believable presets. It is additive: omitting flow keeps legacy splat behavior unchanged. FluidReveal and FluidDistortion ignore flow visualization because their display shaders are authoritative. When set, flow.boundary overrides individual canvas edges; legacy openBoundary is only the fallback for unspecified edges.

interface FlowConfig {
  mode?: 'live' | 'prescribed' | 'hybrid';
  boundary?: { left?: 'wall'|'open'; right?: 'wall'|'open'; top?: 'wall'|'open'; bottom?: 'wall'|'open' };
  sources?: FlowSource[];
  outlets?: FlowOutlet[];
  scalarFields?: FlowScalarField[];
  forces?: FlowForce[];
  prescribed?: PrescribedFlowField;
  visualization?: FlowVisualization;
}

Use FlowSource for persistent point/line/rect emitters, FlowOutlet for edge sponge/drain zones, FlowScalarField for temperature or ink-like quantities, FlowForce for gravity, pressure-gradient body force, or buoyancy, and PrescribedFlowField for custom uploaded flow grids. In mode: 'prescribed', source velocity and forces are ignored so the velocity remains exactly prescribed; use 'hybrid' when you want to add live velocity before projection. Outlets drain fields near an edge; use flow.boundary to decide which edges are actual pressure-open boundaries.

interface FlowScalarField {
  name: string;
  dissipation?: number;
  advection?: 'standard' | 'low-dissipation';
  color?: RGB;
	range?: [number, number];
}

type Vec2 = { x: number; y: number };

type FlowForce =
  | { kind: 'gravity'; vector: Vec2 }
  | { kind: 'pressureGradient'; vector: Vec2 }
  | { kind: 'buoyancy'; scalar: string; direction?: Vec2; strength: number; ambient?: number };

interface FlowVisualization {
  colorBy?: 'dye' | 'speed' | 'pressure' | 'temperature' | 'scalar';
  scalar?: string;
  glowBy?: 'speed' | 'scalar' | 'none';
  transfer?: 'fire' | 'water' | 'ink' | 'viridis' | 'cfd';
  range?: [number, number];
  scale?: number;
}

interface FlowGridField {
  width: number;
  height: number;
  data: ArrayLike<number>;
  scale?: number;
  version?: number | string;
}

low-dissipation reduces explicit scalar fading; it is not a MacCormack/BFECC advection correction. For uploaded grids, replace immutable data objects or bump version when values change so WebGL textures are re-uploaded. For velocity and pressure plots, FlowVisualization.range maps raw field values onto the selected transfer function; transfer: 'cfd' provides a blue→cyan→green→yellow→red engineering-style magnitude ramp.


GL-type-neutral API

As of 1.0 no root-exported declaration mentions a WebGL* DOM type. The internal types FBO, DoubleFBO, ExtInfo and ResolvedConfig are no longer exported (nothing in the public API returned them). isWebGLAvailable() accepts { failIfMajorPerformanceCaveat?: boolean }.

FluidEngine (Advanced)

For advanced users who need framework-agnostic fluid simulation, the FluidEngine class is exported from the package root. It is the WebGL engine that powers the <Fluid> component, but it has no Svelte dependency.

import { FluidEngine } from 'svelte-fluid';

The engine is constructed with a canvas element and a config object matching the FluidConfig interface. It exposes the same splat(), randomSplats(), pause(), resume(), isPaused, and getPerformanceState() API as FluidHandle, plus additional methods:

  • setConfig(config) — update configuration at runtime (respects the 4-bucket system)
  • dispose() — free all GPU resources (textures, framebuffers, programs). The WebGL context is left intact for potential reuse.

Experimental entry

svelte-fluid/experimental exposes engine surfaces built for the Verdigris puzzle game. No semver promise: any minor release may change or remove them.

  • LiquidEngine — a free-surface PIC/FLIP liquid on its own WebGL2 canvas, with terrain, three pigment species, exact sensor counts and drains (needs EXT_color_buffer_float and EXT_float_blend). Its scene, tick and sensor types are exported alongside.
  • createSteppedFluidEngine(options) — a FluidEngine driven by your clock: advance(steps, dt), renderOnce(), in-place resize(w, h), and nonblocking air velocity readback with queueVelocitySnapshot(tick, w, h) / pollVelocitySnapshot(). Pass a snapshot to LiquidEngine.step({ wind }) so air deflects falling liquid.
import { LiquidEngine, createSteppedFluidEngine } from 'svelte-fluid/experimental';

Architecture invariant: the engine never imports Svelte. It is fully framework-agnostic and could be used with React, vanilla JS, or any other framework. The Svelte component is a thin wrapper that handles lifecycle, sizing, and prop reactivity.