API Reference

The “look it up” page for all public methods, options, and the curve definition schema.

Tip
This reference assumes familiarity with the core concepts. See Gotchas for common pitfalls

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

OptionTypeDefaultDescription
trailLengthnumber120Number of points in trail
skeletonColorstring'#ffffff'Hex color string for the skeleton path. Use "transparent" to hide
trailColorstring | string[]'#ffffff'Hex color string for solid trails, or array of hex colors for gradients
headColorstringderivedHex color string for the head dot. Omit to auto-follow trail color
headRadiusnumber4Radius of the head dot in pixels
trailStyleTrailStyle'default''default' | 'gradient-static' | 'gradient-animated'
autoStartbooleantrueStart the animation loop automatically on creation
initialTnumberundefinedInitial 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
Info

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();
state
playing

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.

Info

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();
playing
reset() clears the trail and resets t to 0. The loop keeps running.

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
  };
}, []);
playing
Stops the loop and removes the SVG. The instance cannot be restarted.

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:

OptionTypeDefaultDescription
clearTrailbooleanfalseClear 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:

OptionTypeDefaultDescription
wrapbooleanfalseTrail wraps around period boundary for a full trail everywhere
stepnumberperiod / trailLengthTime 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.

Warning

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)
Info

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:

  • speed must be a finite number
  • duration must be a finite number greater than 0

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:

OptionTypeDescription
trailColorstring | string[]Single hex color for solid trails, or array of hex colors for gradients
headColorstring | nullHex color to orverride, or null to let it auto pick from trail color
skeletonColorstringHex color, or "transparent" to hide
trailStyleTrailStyle'default' | 'gradient-static' | 'gradient-animated'
Warning

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:

OptionTypeDefaultDescription
durationnumber300Duration 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) * periodB smooth for all period ratios
  • 'raw': Same t for 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 becomes curveA for 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 seconds
  • params: Animated parameters object
Warning

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

ModeDescription
'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.
skeletonFnOverride function for computing a skeleton independent of fn.
Info

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

StyleDescription
'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,
});
Warning

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.

PresetColorsBest For
bardPurple → blue → teal → pinkAnimated gradient
sunsetOrange → red → purple → pinkWarm, energetic
oceanDeep blue → cyan → aqua → whiteCool, calm
iceDeep blue → ice cyanStatic gradient
fireDeep red → amberBold, attention-grabbing
forestDeep green → mintNatural, 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

  1. Mutable trail buffer: The internal trail array is reused each frame. If you need to keep previous frame values, copy with [...trail].

  2. jump() is position-only: Updates t but NOT actualTime. Clock time only advances with the internal animation loop. Use seek() if you need actualTime to match.

  3. seek step: Default is period / trailLength, which is deterministic and FPS-independent. Pass explicit step to match your actual render loop delta.

  4. Live skeleton drift: Curves with skeleton: 'live' drift based on actualTime. At speed ≠ 1, skeleton and trail may misalign. This is a known limitation.

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

  6. Canvas sizing is fixed at init: createSarmal() captures canvas dimensions at creation. If the canvas is resized after init, you will need to call destroy() and re-create.

  7. Negative speed: Reverses t traversal direction. Trail extends in the opposite direction. Supported but not polished.

  8. setSpeed(0) vs pause(): 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.

  9. pause() during a speed transition: The pending setSpeedOver() Promise rejects with 'Speed transition cancelled', but userSpeedOverride is not cleared. On play(), the animation does not resume with curve’s default, but with the interpolated speed from when it had paused.

    • Call resetSpeed() after pause() to return to the natural rhythm.

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[];
};