Getting Started
Install @sarmal/core and animate your first curve.
Installation
npm install @sarmal/core
Try it in your terminal
You can see an animated curve in your terminal just by running the code below, without installing as a dependency to any project:
npx @sarmal/core
A random sarmal curve animates in your terminal using braille characters and ANSI colors. Ctrl+C to stop. See the Terminal & CLI page for all options.
CDN Quick Start
No build step? Drop in the auto-init script and add data-sarmal to any canvas or svg:
<script src="https://unpkg.com/@sarmal/core/dist/auto-init.js"></script>
<canvas data-sarmal="artemis2" width="200" height="200"></canvas>
<svg data-sarmal="artemis2" style="width:200px; height:200px;"></svg>
The script scans for canvas[data-sarmal] and svg[data-sarmal] on page load and starts an animation for each one. Use any built-in curve name as the attribute value.
To use the dot matrix renderer, add data-renderer="dot-matrix" to a canvas:
<canvas data-sarmal="rose3" data-renderer="dot-matrix"
data-cols="32" data-rows="32" width="200" height="200"></canvas>
Data attributes
All three renderers (canvas, SVG, dot matrix) support these attributes:
| Attribute | Type | Description |
|---|---|---|
data-sarmal* | curve name | Required. The curve to animate. Any built-in curve name. |
data-trail-color | color or JSON array | Single color string or a JSON array of colors for gradient trails. Accepts #rrggbb, #rgb, rgb(), rgba(), oklch(). |
data-skeleton-color | color | Skeleton dot color. Pass "transparent" to hide. |
data-trail-style | "default" | "gradient-static" | "gradient-animated" | Trail rendering style. Gradient modes require an array in data-trail-color. |
data-trail-length | integer | Number of trail points to keep. |
data-speed | number | Speed multiplier, applied after creation via setSpeed(). |
data-auto-start | "false" | Set to "false" to prevent auto-play. Omit to auto-play (default). |
data-pause-on-hidden | "false" | Set to "false" to disable tab-visibility pausing. |
data-initial-phase | number | Seek to this phase before the first frame. |
The canvas and SVG renderers also support:
| Attribute | Type | Description |
|---|---|---|
data-head-color | color | Head dot color. |
data-head-radius | number | Head dot radius. |
data-trail-width | number | Trail width multiplier (1 = default, 2 = double width). |
The dot matrix renderer also supports:
| Attribute | Type | Description |
|---|---|---|
data-renderer | "dot-matrix" | Selects the dot matrix renderer on a canvas element. |
data-cols | integer | Number of dot columns. Default: 32. |
data-rows | integer | Number of dot rows. Default: 32. |
data-roundness | number | Corner rounding: 0 = square, 1 = circle. Default: 1. |
See CDN section in Framework Guides page for more details.
Your first curve
createSarmal is the fastest way to get an animated curve on screen. You point it to a canvas element and pick a curve. The animation starts automatically:
<canvas id="sarmal" width="200" height="200"></canvas>
import { createSarmal, curves } from "@sarmal/core";
const canvas = document.getElementById("sarmal") 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 and tree-shaking
@sarmal/core is built with preserveModules output and "sideEffects" properly marked, so modern bundlers (Vite, Rollup, webpack 5, esbuild) can tree-shake at the module level. Each source file is its own output module, not one flat bundle.
Named imports from the main entry (recommended)
import { createSarmal, rose3 } from "@sarmal/core";
const sarmal = createSarmal(canvas, rose3);
This is the best approach for almost every use case. Tree-shaking works correctly:
- Only the renderer you actually call (
createSarmal,createSarmalSVG, orcreateSarmalDotMatrix) is included. The other two are eliminated. - Only the named curves you import are included. Unused curves are dropped.
- Drawn curves (like artemis2) carry the catmull-rom library as a real module import. Pure math curves (like rose3) do not pull extra logic. So, catmull-rom is opt-in and only ships when you use a drawn curve.
Measured output: 5.69 KB gzip for
createSarmal+ one curve.
Using the curves collection
import { createSarmal, curves } from "@sarmal/core";
const sarmal = createSarmal(canvas, curves.rose3);
Convenient for runtime curve selection or when you want all curves available. The tradeoff: because curves is a plain object assembling all 14 built-in curves, no bundler can eliminate the unused ones at build time.
Measured output: 6.87 KB gzip, which is roughly +1.18 KB gzip over a named import.
If you pick one curve and it won’t change, prefer named imports. If you swap curves at runtime or render from user input, the collection is the right tool.
Deep imports: @sarmal/core/curves/<name>
import { rose3 } from "@sarmal/core/curves/rose3";
Each curve ships as a standalone file (~300 B gzip for rose3). The difference with named imports from the main entry is negligible when also importing a renderer. The only reason to reach for deep imports is when building a library that ships curve definitions with no renderer dependency at all. And I honestly don’t know why you would need my curves, but I don’t judge.
Engine only
If you only need the math for custom rendering, SSR pre-computation, or scrubbing without a display:
import { createEngine, rose3 } from "@sarmal/core";
The renderer and color utilities are tree-shaken away.
Measured output: 1.47 KB gzip, which is roughly 26% of the full canvas renderer bundle.
Terminal subpath: @sarmal/core/terminal
The terminal renderer (terminalSarmal) lives at @sarmal/core/terminal and is gated behind a "node" export condition. Modern browser bundlers (Vite, webpack) will not include it unless you explicitly override the condition, so it does not affect browser bundle size.
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: (phase, elapsed, params) => ({
x: Math.cos(phase),
y: Math.sin(phase),
}),
// period defaults to 2π, speed defaults to 1
};
const sarmal = createSarmal(canvas, myCurve);
Only name and fn are required.
fn receives (phase, elapsed, params). See Engine for details on each argument.
For behavior that varies independently of curve position (e.g. oscillating a shape parameter) use elapsed:
const wobble: CurveDef = {
name: "wobble",
fn: (phase, elapsed, _params) => {
const r = 1 + 0.3 * Math.sin(elapsed * 2); // radius pulses with real time
return { x: r * Math.cos(phase), y: r * Math.sin(phase) };
},
};
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 animation to scale freely (no fixed pixel dimensions), use the SVG renderer instead:
<svg id="sarmal" style="width: 200px; height: 200px;"></svg>
import { createSarmalSVG, curves } from "@sarmal/core";
const svg = document.getElementById("sarmal")!;
const sarmal = createSarmalSVG(svg, curves.lissajous32);
createSarmalSVG draws directly into the <svg> element. It accepts the same styling options as createSarmal.
Morphing
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 & 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("sarmal") 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.