API Reference
The “look it up” page for all public methods, options, and the curve definition schema.
createSarmal
Creates a canvas-based sarmal instance that start animating automatically. Returns SarmalInstance.
import { createSarmal, curves } from "@sarmal/core";
const canvas = document.getElementById("loading-indicator") as HTMLCanvasElement;
const sarmal = createSarmal(canvas, curves.rose3);
Options
| Option | Type | Default | Description |
|---|---|---|---|
trailLength | number | 120 | Number of points in trail |
skeletonColor | string | '#ffffff' | Hex color string for the skeleton path. Use "transparent" to hide |
trailColor | string | string[] | '#ffffff' | Hex color string for solid trails, or array of hex colors for gradients |
headColor | string | derived | Hex color string for the head dot. Omit to auto-follow trail color |
headRadius | number | 4 | Radius of the head dot in pixels |
trailStyle | TrailStyle | 'default' | 'default' | 'gradient-static' | 'gradient-animated' |
autoStart | boolean | true | Start the animation loop automatically on creation |
initialT | number | undefined | Initial position along the curve (t value). Calls seek(initialT) before first frame |
Auto-start and initial position
By default, sarmal instances start animating immediately from t=0. You can control this behavior:
// Start automatically from t=0 (default behavior)
const s1 = createSarmal(canvas, curves.rose3);
// Start automatically from t=Math.PI
const s2 = createSarmal(canvas, curves.rose5, { initialT: Math.PI });
// Create dormant at t=0; call play() when ready
const s3 = createSarmal(canvas, curves.deltoid, { autoStart: false });
s3.play();
// Create dormant at t=Math.PI
const s4 = createSarmal(canvas, curves.lissajous32, {
autoStart: false,
initialT: Math.PI
});
// Canvas shows skeleton + trail at t=π position
s4.play();
createSarmalSVG
Same API as createSarmal. First argument is any container element (typically a <div>). Outputs animated SVG elements.
import { createSarmalSVG, curves } from "@sarmal/core";
const container = document.getElementById("loading-indicator-container")!;
const sarmal = createSarmalSVG(container, curves.lissajous32);
// Animation starts automatically
trailStyle and palette work identically in both canvas and SVG renderers.
Instance Methods
play()
Starts the animation loop (requestAnimationFrame). If already running, does nothing.
sarmal.play();
pause()
Pauses the animation loop (cancelAnimationFrame) and cancels any speed transition. When paused, the state of t, trail, and speed are all preserved. Call play() to resume.
pause() is different from setSpeed(0). pause() stops the RAF loop entirely and saves CPU. setSpeed(0) freezes the animation but keeps the loop running.
sarmal.pause();
reset()
Resets the engine and clears the trail. The next frame will start fresh from the beginning of the curve. The animation loop keeps running.
sarmal.reset();
destroy()
Stops the animation and cleans up resources. Call in component unmount / cleanup handlers. The instance cannot be restarted after destruction.
// React example
useEffect(() => {
const sarmal = createSarmal(canvas, curves.rose3);
return () => {
sarmal.destroy(); // Cleanup on unmount
};
}, []);
Engine Control
jump(t)
Instantly moves the head to position t. Does not update actualTime. Trail is left untouched by default, but you can use clearTrail: true to wipe it.
Use jump when you need a raw position override: morphing mid-flight, scrubbing, or situations where the existing trail context should stay intact.
// Teleport to halfway through the curve
sarmal.jump(Math.PI);
// Teleport and clear the trail for a blank start
sarmal.jump(0, { clearTrail: true });
Options:
| Option | Type | Default | Description |
|---|---|---|---|
clearTrail | boolean | false | Clear the trail on jump |
seek(t)
Moves to t and reconstructs the trail as if the animation naturally arrived there from t=0. Also updates actualTime to match.
Use seek when you want the trail to look meaningful after the move, like making a jump where keeping the trail context matters.
// Seek to t=2 with a natural-looking trail
sarmal.seek(2);
// Wrap trail around period boundary for a full trail near t=0
sarmal.seek(0.5, { wrap: true });
// Custom step size (default: period / trailLength)
sarmal.seek(1, { step: 0.1 });
Options:
| Option | Type | Default | Description |
|---|---|---|---|
wrap | boolean | false | Trail wraps around period boundary for a full trail everywhere |
step | number | period / trailLength | Time gap between trail points (deterministic, FPS-independent) |
Animation Control
setSpeed(speed)
Overrides the animation speed. Returns void. The override persists until cleared with resetSpeed() or replaced with another call.
// Slow to half speed
sarmal.setSpeed(0.5);
// Speed up to twice the speed
sarmal.setSpeed(2.0);
// Freeze in place while keeping the loop (`t`) advancing
sarmal.setSpeed(0);
// Reverse direction
sarmal.setSpeed(-1);
Validation: speed must be a finite number. 0 and negative values are valid.
setSpeed(0) freezes the animation but does not stop the loop. Use pause() if you need to cancel the requestAnimationFrame entirely and save CPU.
getSpeed()
Returns the effective speed the engine is currently using. When setSpeed() has been called, returns that value. Otherwise, returns curve.speed, which is the default value from the CurveDef.
sarmal.setSpeed(0.5);
sarmal.getSpeed(); // 0.5
sarmal.resetSpeed();
sarmal.getSpeed(); // 1 (or whatever curve.speed is)
resetSpeed()
Clears the speed override, returning to the curve’s default speed. This is the correct way to “undo” a speed change without knowing the curve’s default.
sarmal.setSpeed(2.0);
sarmal.getSpeed(); // 2.0
sarmal.resetSpeed();
sarmal.getSpeed(); // curve.speed (e.g., 1)
resetSpeed() removes the override layer entirely. It defers to the curve dynamically, so future curve changes are reflected in getSpeed() automatically.
setSpeedOver(speed, duration)
Transitions to a new speed over a duration, returning a Promise<void> that resolves when complete. Uses linear interpolation between current and target speed.
// Decelerate to 0.2× over 500ms
await sarmal.setSpeedOver(0.2, 500);
// Then resume normal speed
await sarmal.setSpeedOver(1.0, 300);
Cancellation: Calling setSpeed(), setSpeedOver(), or pause() while a transition is in progress cancels it. The Promise rejects with Error('Speed transition cancelled'). A subsequent setSpeedOver() starts from the current interpolated speed.
To stop with a graceful deceleration, you can make use of two explicit steps:
await sarmal.setSpeedOver(0, 400); // decelerate over 400ms
sarmal.pause(); // then pause the loop
Validation:
speedmust be a finite numberdurationmust be a finite number greater than0
setRenderOptions(partial)
Changes colors and trail style on a live Sarmal instance without destroying and recreating it. Render options are independent of engine state. They do not affect and are not affected by morphing, seeking, jumping, speed changes, pause/play, or reset.
// Theme change: switch the whole palette
sarmal.setRenderOptions({ trailColor: palettes.sunset });
// Error state: flip to a solid red.
sarmal.setRenderOptions({ trailStyle: "default", trailColor: "#c0143c" });
// Override head color to make it independent of trailColor
sarmal.setRenderOptions({ headColor: "#ffffff" });
// Make head color auto inheirt from trailColor again
sarmal.setRenderOptions({ headColor: null });
// Hide skeleton
sarmal.setRenderOptions({ skeletonColor: "transparent" });
Options:
| Option | Type | Description |
|---|---|---|
trailColor | string | string[] | Single hex color for solid trails, or array of hex colors for gradients |
headColor | string | null | Hex color to orverride, or null to let it auto pick from trail color |
skeletonColor | string | Hex color, or "transparent" to hide |
trailStyle | TrailStyle | 'default' | 'gradient-static' | 'gradient-animated' |
Accepted color format is currently only 6-digit hex strings. Shorthand hex (#fff), named colors ("red"), rgb(), hsl(), or CSS keywords like (currentColor) are not yet accepted.
Validation: Throws an error if any field fails validation. In case of a validation error, no fields will be mutated, so the animation continues on the previous valid state.
Morphing
morphTo(target)
Smooth transition between curves. Returns Promise<void> that resolves when morph completes.
// Basic morph over default duration (300ms)
await sarmal.morphTo(curves.deltoid);
// Morph with custom duration
await sarmal.morphTo(curves.rose3, { duration: 500 });
// Use 'raw' strategy for different period handling
await sarmal.morphTo(curves.lissajous32, { morphStrategy: "raw" });
// Chain morphs
await sarmal.morphTo(curves.astroid);
await sarmal.morphTo(curves.rose5);
await sarmal.morphTo(curves.artemis2);
Options:
| Option | Type | Default | Description |
|---|---|---|---|
duration | number | 300 | Duration of the morph transition in milliseconds |
morphStrategy | 'normalized' | 'raw' | 'normalized' | Strategy for lerping between curves with different periods |
Morph strategies:
'normalized'(default):tB = (t / periodA) * periodBsmooth for all period ratios'raw': Sametfor both curves, which can produce chaotic results with mismatched periods
Behavior notes:
- Calling
morphTo()mid-morph resolves the previous Promise immediately. The current interpolated state becomescurveAfor the new morph while avoiding a visual jump - The transition flow for speed and morph run independently
Curve Definition Schema
interface CurveDef {
name: string; // Required: identifier for the curve
fn: (t, time, params) => Point; // Required: parametric function
period?: number; // Default: 2π
speed?: number; // Default: 1 (radians per second)
skeleton?: 'static' | 'live'; // Default: 'static'
skeletonFn?: (t: number) => Point; // Optional stable-shape override
}
fn(t, time, params)
t: Position along curve (0 -> period)time: Actual elapsed secondsparams: Animated parameters object
params is reserved for future implementations and currently always uses {} internally
const myCurve: CurveDef = {
name: "circle",
fn: (t, time, params) => ({
x: Math.cos(t),
y: Math.sin(t),
}),
period: Math.PI * 2,
speed: 1,
};
Skeleton modes
| Mode | Description |
|---|---|
'static' | Skeleton is computed once at initialization from fn(t, 0) and cached. Use for curves with fixed shapes. |
'live' | Skeleton is recomputed each frame using fn(t, actualTime). Use for curves whose shape drifts over time. |
skeletonFn | Override function for computing a skeleton independent of fn. |
See Concepts for detailed explanation of skeleton modes and when to use each.
Not in the schema: Color, trail style, rendering options, trailLength, and scale are renderer concerns, not curve concerns.
Trail Styles
| Style | Description |
|---|---|
'default' | Solid trail with opacity fade |
'gradient-static' | Head-to-tail color transition, fixed |
'gradient-animated' | Colors flow along trail continuously |
// Solid color trail (string)
const sarmal = createSarmal(canvas, curves.rose3, {
trailStyle: "default",
trailColor: "#3b82f6",
});
// Static gradient from head to tail (array)
const sarmal = createSarmal(canvas, curves.deltoid, {
trailStyle: "gradient-static",
trailColor: palettes.ice,
});
// Animated flowing colors (array)
const sarmal = createSarmal(canvas, curves.astroid, {
trailStyle: "gradient-animated",
trailColor: palettes.bard,
});
Mismatched combinations (e.g. an array of colors with "default" trail style, or a color string with gradient styles) still produce a valid render, but output a console warning to inform that there is a mismatch. The selected trailStyle determines whether to use a single color, or the whole array (if available).
Built-in Palettes
These color combinations are exposed as the palettes named export. When in gradient trail style mode, pass any palette to trailColor for gradient trails.
| Preset | Colors | Best For |
|---|---|---|
bard | Purple → blue → teal → pink | Animated gradient |
sunset | Orange → red → purple → pink | Warm, energetic |
ocean | Deep blue → cyan → aqua → white | Cool, calm |
ice | Deep blue → ice cyan | Static gradient |
fire | Deep red → amber | Bold, attention-grabbing |
forest | Deep green → mint | Natural, organic |
import { createSarmal, curves, palettes } from "@sarmal/core";
// Use a preset
const sarmal = createSarmal(canvas, curves.rose3, {
trailStyle: "gradient-animated",
trailColor: palettes.sunset,
});
// Custom palette
const sarmal = createSarmal(canvas, curves.lissajous32, {
trailStyle: "gradient-static",
trailColor: ["#ff6b6b", "#ffd93d", "#6bcb77"],
});
Gotchas
-
Mutable trail buffer: The internal trail array is reused each frame. If you need to keep previous frame values, copy with
[...trail]. -
jump()is position-only: Updatestbut NOTactualTime. Clock time only advances with the internal animation loop. Useseek()if you needactualTimeto match. -
seekstep: Default isperiod / trailLength, which is deterministic and FPS-independent. Pass explicitstepto match your actual render loop delta. -
Live skeleton drift: Curves with
skeleton: 'live'drift based onactualTime. At speed ≠ 1, skeleton and trail may misalign. This is a known limitation. -
Trail bunching/spreading: Speed changes affect point spacing immediately. Slowing down means points bunch up together; speed up means the points spread apart. This is the currently intended behavior, not a bug.
-
Canvas sizing is fixed at init:
createSarmal()captures canvas dimensions at creation. If the canvas is resized after init, you will need to calldestroy()and re-create. -
Negative speed: Reverses
ttraversal direction. Trail extends in the opposite direction. Supported but not polished. -
setSpeed(0)vspause():setSpeed(0)freezes the animation but the RAF loop keeps running, during which the sarmal remains reactive.pause()cancels the loop entirely and saves CPU. They look identical on screen but have different resource implications. -
pause()during a speed transition: The pendingsetSpeedOver()Promise rejects with'Speed transition cancelled', butuserSpeedOverrideis not cleared. Onplay(), the animation does not resume with curve’s default, but with the interpolated speed from when it had paused.- Call
resetSpeed()afterpause()to return to the natural rhythm.
- Call
TypeScript Types
// Core types
interface Point {
x: number;
y: number;
}
interface CurveDef {
name: string;
fn: (t: number, time: number, params: Record<string, number>) => Point;
period?: number;
speed?: number;
skeleton?: "static" | "live";
skeletonFn?: (t: number) => Point;
}
interface SarmalInstance {
play(): void;
pause(): void;
reset(): void;
destroy(): void;
jump(t: number, options?: { clearTrail?: boolean }): void;
seek(t: number, options?: { wrap?: boolean; step?: number }): void;
setSpeed(speed: number): void;
getSpeed(): number;
resetSpeed(): void;
setSpeedOver(speed: number, duration: number): Promise<void>;
morphTo(target: CurveDef, options?: {
duration?: number;
morphStrategy?: "normalized" | "raw";
}): Promise<void>;
setRenderOptions(partial: RuntimeRenderOptions): void;
}
type TrailStyle = "default" | "gradient-static" | "gradient-animated";
type TrailColor = string | string[];
interface RuntimeRenderOptions {
trailColor?: TrailColor;
headColor?: string | null;
skeletonColor?: string;
trailStyle?: TrailStyle;
}
const palettes: {
bard: string[];
sunset: string[];
ocean: string[];
ice: string[];
fire: string[];
forest: string[];
};