Getting Started

Install @sarmal/core and animate your first spinner.

Installation

npm install @sarmal/core

CDN Quick Start

No build step? Drop in the auto-init script and add data-sarmal to any canvas:

<script src="https://unpkg.com/@sarmal/core/dist/auto-init.js"></script>
<canvas data-sarmal="artemis2" width="200" height="200"></canvas>

The script scans for canvas[data-sarmal] on page load and starts a spinner for each one. Use any built-in curve name as the attribute value.

Your first spinner

createSarmal is the fastest way to get a spinner on screen. You point it to a canvas element and pick a curve. The animation starts automatically:

<canvas id="spinner" width="200" height="200"></canvas>
import { createSarmal, curves } from "@sarmal/core";

const canvas = document.getElementById("spinner") as HTMLCanvasElement;
const sarmal = createSarmal(canvas, curves.artemis2);

The instance animates immediately by default. Call sarmal.pause() to freeze the animation in place and sarmal.play() to resume. Call sarmal.destroy() when you’re done (unmount, navigation, etc.).

Choosing a curve

While it is possible to create your very own custom curve, you might wish to skip the initial hassle. The curves export is an object with all the built-in curves, so you can pick from predefined curve definitions to get started right away:

import { curves } from "@sarmal/core";

curves.rose3; // 3-petal rose
curves.astroid; // 4-pointed star
// ...

Import paths

There are three ways to import curves, each with different bundle-size characteristics:

1. Convenience: @sarmal/core

import { createSarmal, curves } from "@sarmal/core";

const sarmal = createSarmal(canvas, curves.rose3);

All curves are available through the curves object. Modern bundlers will tree-shake unused individual curves, but the main entry point (index.js) is still resolved into the module graph first. This means that the engine and renderer modules are always processed by the bundler even if they end up unused.

2. Barrel subpath: @sarmal/core/curves

import { createSarmal } from "@sarmal/core";
import { rose3 } from "@sarmal/core/curves";

const sarmal = createSarmal(canvas, rose3);

Directly imports the curves file. You end up only pulling in the curve files you reference, so individual named imports are still tree-shakable.

This is the best balance of convenience and bundle size: you get all named exports without the engine/renderer module graph.

3. Deep import: @sarmal/core/curves/<name>

import { createSarmal } from "@sarmal/core";
import { rose3 } from "@sarmal/core/curves/rose3";

const sarmal = createSarmal(canvas, rose3);

Imports a single curve definition file, resulting in maximum tree-shaking. You can use this to guarantee the smallest possible bundle and already know exactly which curve you need.

Tip

If you need both engine/renderer and curves, option 2 or 3 won’t save much over option 1, because you’re already pulling the main modules. The subpath advantage matters most when you only need curve definitions, for whatever reason I won’t judge.

Custom curves

You’re not limited to the built-in curves! You can pass any object that matches CurveDef:

import type { CurveDef } from "@sarmal/core";
import { createSarmal } from "@sarmal/core";

const myCurve: CurveDef = {
  name: "circle",
  fn: (t, time, params) => ({
    x: Math.cos(t),
    y: Math.sin(t),
  }),
  // period defaults to 2π, speed defaults to 1
};

const sarmal = createSarmal(canvas, myCurve);

Only name and fn are required.

The fn receives three arguments:

  • t (position along the curve, 0 -> period),
  • time (elapsed seconds),
  • params (reserved for now; always {}).

For behavior that varies independently of curve position (e.g. oscillating a shape parameter) use time:

const wobble: CurveDef = {
  name: "wobble",
  fn: (t, time, _params) => {
    const r = 1 + 0.3 * Math.sin(time * 2); // radius pulses with real time
    return { x: r * Math.cos(t), y: r * Math.sin(t) };
  },
};

See the Curve Definition Schema for the full list of options. You can also prototype your curve ideas in the Playground page.

Styling options

Pass an optional custom object as the third argument to createSarmal:

const sarmal = createSarmal(canvas, curves.rose3, {
	trailColor: "#a78bfa", // trail color (default: '#ffffff')
	headColor: "#ffffff", // dot at the tip (default: '#ffffff')
	skeletonColor: "#ffffff", // ghost path of the full curve (default: '#ffffff')
	trailLength: 120 // number of trail points (default: 120)
});

Gradient trails

Set trailStyle to render a color-cycling gradient along the trail:

import { createSarmal, curves, palettes } from "@sarmal/core";

const sarmal = createSarmal(canvas, curves.astroid, {
	trailStyle: "gradient-animated",
	trailColor: palettes.bard // or a custom array: ['#ff6b6b', '#ffd93d', '#6bcb77']
});

SVG renderer

If you need the spinner to scale freely (no fixed pixel dimensions), use the SVG renderer instead:

<div id="spinner" style="width: 200px; height: 200px;"></div>
import { createSarmalSVG, curves } from "@sarmal/core";

const container = document.getElementById("spinner")!;
const sarmal = createSarmalSVG(container, curves.lissajous32);

createSarmalSVG appends an <svg> into the container and animates it. It accepts the same styling options as createSarmal.

Warning

trailStyle and palette properties are not yet supported, because it hasn’t been implemented yet.

Morphing between curves

Call morphTo to smoothly transition from one curve to another:

const sarmal = createSarmal(canvas, curves.astroid);

// at any point you want, transition to another curve over 500ms
await sarmal.morphTo(curves.deltoid, { duration: 500 });

The trail and skeleton crossfade during the transition. morphTo returns a Promise that resolves when the morph is complete, so you can chain them.

Frameworks and SSR

The engine is pure math and works anywhere, but the renderers need the DOM. If you’re using Next.js, Remix, or another SSR framework, you may need to guard the import and initialization:

import { createSarmal, curves } from "@sarmal/core";

if (typeof window !== "undefined") {
	const canvas = document.getElementById("spinner") as HTMLCanvasElement;
	const sarmal = createSarmal(canvas, curves.artemis2);
}

React

If you’re using React, install @sarmal/react for a drop-in <Sarmal> component and useSarmal hook:

npm install @sarmal/core @sarmal/react
"use client";

import { Sarmal } from "@sarmal/react";
import { curves } from "@sarmal/core";

export default function App() {
  return <Sarmal curve={curves.artemis2} />;
}

See the Framework Guides for the full React API and SSR notes.