Menu

Changelog

Version history notes of the Core, React, and Svelte packages

Core

0.41.0 - 31.05.2026

morphTo accepts an easing option (default easeInOutCubic) that shapes the transition’s progress over time. The morph now eases in and out instead of moving at a constant rate. Pass (t) => t to restore the previous linear ramp, or any custom easing function.

morphTo also accepts align (default false). When true, the target curve starts from the point nearest the current head instead of its own phase 0, removing the visual “snap” at the start of the morph. It is off by default because it only reliably improves asymmetric transitions.

0.40.0 - 25.05.2026

New gridColor option for the dot matrix renderer. Controls the color of the background dot layer (all grid cells at 5% opacity). 'transparent' can be passed to hide the background grid entirely, or any color string to decouple the background hue from the trail color.

When omitted, background dots use the trail’s primary color (existing behavior unchanged).

0.39.0 - 25.05.2026

Breaking
The @sarmal/core/curves barrel subpath export has been removed.
// Before
import { rose3 } from "@sarmal/core/curves";

// After
// use the main entry (identical output, no duplication)
import { rose3 } from "@sarmal/core";

// Or a deep import if you need curves with no renderer dependency
import { rose3 } from "@sarmal/core/curves/rose3";

The barrel was producing larger bundles than the main entry in every practical scenario because both dist/index.js and dist/curves/index.js inline the same catmull-rom utility code, and consumer bundlers cannot deduplicate code across two separately pre-bundled files. Individual deep imports (@sarmal/core/curves/<name>) are unaffected and continue to work.

0.38.0 - 24.05.2026

destroy() now clears the visual output in all renderers. Previously, canvas and dot matrix renderers left the last painted frame frozen on the canvas after destruction, whereas the SVG renderer completely removed its elements.

Calling any instance method after destroy() now throws an error. Previously, calling play() after destroy() on a canvas or dot matrix instance would silently restart the animation loop. On an SVG instance it would start a ghost loop (engine ticking with nothing visible).

Calling destroy() multiple times is safe and subsequent calls do nothing.

For temporarily suspending an animation, pause() should be used, which preserves all state and can be followed by play() at any time.

0.37.2 - 24.05.2026

BaseInit, CanvasInit, DotMatrixInit, BaseSarmalOptions, and BaseDotMatrixOptions are now named exports from @sarmal/core. These types were previously defined independently in each framework package. They now have a single source of truth.

0.37.0 - 24.05.2026

Auto-init (@sarmal/core/auto) now supports the dot matrix renderer and several previously unmapped options.

New data attributes for all three renderers: data-trail-width, data-auto-start, data-pause-on-hidden, data-initial-phase.

Dot matrix renderer support: add data-renderer="dot-matrix" to a canvas element to use createSarmalDotMatrix. Dot-matrix-specific attributes data-cols, data-rows, and data-roundness are also supported. data-renderer="canvas" is accepted as an explicit but redundant value. Any unrecognised data-renderer value is now an error. The element with the unknown data-renderer is skipped and a console.error is printed.

0.36.4 - 23.05.2026

createSarmalDotMatrix now uses the same shared color utilities (resolveTrailMainColor, resolveTrailPalette, warnIfTrailColorMismatch) as the canvas and SVG renderers. Three observable changes:

  • Mismatched trailColor/trailStyle pairs now warn at construction time, not only on setRenderOptions.
  • The gradient-animated cycle period is now 2 seconds, matching the canvas and SVG renderers. It was previously 6 seconds.
  • The mismatch warning message is now identical across all three renderers.

0.36.3 - 23.05.2026

Fixed createSarmalDotMatrix ignoring trailStyle: "default" when trailColor is an array. The renderer was gating gradient sampling on whether gradientOklab was populated, not on the active trailStyle. So, in a scenario where user provides an array of colors, using a palette with trailStyle: "default" did not matter. It would still render a gradient instead of a solid color. The expectation is that it must use the first palette entry like canvas and svg renderers.

0.36.2 - 23.05.2026

Fixed createSarmalDotMatrix rendering skeleton dots dimmer than their resting opacity when the trail tail passed over them. The skeleton opacity now acts as a minimum brightness floor for any trail pixel that lands on a skeleton cell.

0.36.0 - 23.05.2026

Breaking
createSarmalDotMatrix now renders a skeleton by default (at 15% opacity). Pass skeletonColor: "transparent" to opt out.
// Before: no skeleton rendered
createSarmalDotMatrix(canvas, curve)

// After: skeleton at `#ffffff` by default.
// Pass `"transparent"` to disable
createSarmalDotMatrix(canvas, curve, { skeletonColor: "transparent" })

skeletonColor is now accepted by setRenderOptions on the dot matrix renderer and takes effect on the next frame without rebuilding any buffer. Accepts the same formats as trailColor: #rrggbb, #rgb, rgb(), rgba(), and oklch().

0.35.1 - 23.05.2026

Dot-matrix gradient colors are now interpolated in Oklab space. This will eliminate the gray midpoint that could appear with certain palettes. The gradient-animated style now cycles continuously through the palette instead of oscillating.

0.35.0 - 22.05.2026

Dot matrix renderer now uses ImageData/putImageData for drawing. Dot edges are antialiased through precomputed 4x4 supersampling, gradient colours are sampled per-dot for a smoother, band-free result. Frame cost is flat regardless of grid size.

0.34.0 - 20.05.2026

New createSarmalDotMatrix(canvas, curveDef, options?) canvas renderer. Maps the sarmal trail onto a grid of dots: the head is fully lit, the tail fades to dim, and off-trail dots appear as faint background points.

DotMatrixSarmalOptions is a named export from @sarmal/core.

Returns SarmalInstance: full interface parity with createSarmal and createSarmalSVG, including morphTo and setRenderOptions. Accepts cols, rows, roundness, trailLength, trailColor, and trailStyle options.

The renderer has no skeleton and no ribbon trail. setRenderOptions supports trailColor and trailStyle.

Warning
Passing headColor, skeletonColor, headRadius, or trailWidth throws.

0.33.0 - 17.05.2026

Added trailWidth option to createSarmal, createSarmalSVG, and setRenderOptions. A positive-number multiplier (default 1) that scales the trail’s width at both ends while preserving the taper shape. trailWidth: 2 doubles the width; trailWidth: 0.5 halves it.

0.32.0 - 17.05.2026

Breaking
The fire and forest palettes have been removed.

Six new color palettes added: sakura, neon, carnival, vaporwave, pastel, and rocketpop.

0.31.0 - 16.05.2026

oklch(L C H) and oklch(L C H / alpha) are now accepted everywhere a color string is expected: trailColor, headColor, skeletonColor, setRenderOptions.

oklch converts directly to OKLab, so floating-point precision is preserved end-to-end. Alpha is silently stripped. deg, rad, turn suffixes and percentage channels are not supported.

Breaking
lerpOklab and getPaletteColor signatures changed. hexToRgb removed from exports. Use parseColorToRgb instead. It accepts the same 6-digit hex plus all formats added in v0.30.0
// Before
lerpOklab(a: Rgb, b: Rgb, t: number): Rgb
getPaletteColor(palette: string[], position: number, timeOffset?: number): Rgb

// After
lerpOklab(a: Oklab, b: Oklab, t: number): Oklab
getPaletteColor(palette: Oklab[], position: number, timeOffset?: number): Oklab

Oklab and parseColorToRgb are now named exports.

0.30.0 - 16.05.2026

Color inputs now accept #rgb (3-digit hex), #rrggbbaa (8-digit hex, alpha stripped), rgb(), and rgba() in addition to the previous #rrggbb format. Any alpha channel in #rrggbbaa or rgba() is silently ignored. Sarmal applies its own opacity at render time. Out-of-range rgb() channels are also clamped to 0-255 range.

0.29.0 - 15.05.2026

Gradient trail colors are now interpolated in OKLab space instead of sRGB. Midpoints between saturated stops stay vivid rather than passing through a gray dead zone.

0.28.0 - 12.05.2026

Breaking
The --name CLI flag is renamed to --curve.
// Before
npx @sarmal/core --name deltoid --verbose

// After
npx @sarmal/core --curve deltoid --verbose
  • brailleBit(row, col) now returns 0 instead of throwing for out-of-range coordinates.
  • dimColor(hex, brightness) now clamps brightness to [0, 1] and each RGB channel to [0, 255].
  • Fixed detectColor() where a non-empty COLORTERM combined with TERM=linux incorrectly returned "256-color" instead of "monochrome".
  • Dropped the filled-block head indicator () in monochrome terminal mode The head now renders as the same braille dot pattern as the rest of the trail.
  • Both @sarmal/core and @sarmal/react now have a prepublishOnly script that rebuilds dist/ before every publish, preventing stale build artifacts from shipping.

    The breaking rename change was never supposed happen but I realized too late that I published a stale build…

0.27.0 - 11.05.2026

New terminalSarmal(stream, curveDef, options?) renders sarmal curves in the terminal using braille characters and ANSI colors. Import from @sarmal/core/terminal. It should be noted that this is a node-only subpath, which will be rejected by browser bundlers at build time.

import { terminalSarmal } from "@sarmal/core/terminal";
import { deltoid } from "@sarmal/core";

const stop = terminalSarmal(process.stdout, deltoid, { speed: 2.5 });
// Ctrl+C or stop() to clean up

Each character cell covers a 2x4 braille dot grid. At the default size of 16 characters wide, you get a 32x32 effective dot grid. Colors degrade through truecolor 256-color monochrome based on terminal capability. Trail fades from full brightness at the head to 15% at the tail.

New CLI: npx @sarmal/core runs a random curve. Pass --name <id> to pick a specific curve, --fps <num>, --speed <num>, --size <num>, --color <hex>, and --verbose.

0.26.0 - 10.05.2026

Breaking

CurveDef.fn parameter names changed from (t, time, params) to (phase, elapsed, params).

The initialT option on createSarmal, createSarmalSVG, and @sarmal/react is now initialPhase. The jump(t) and seek(t) parameter is now named phase (type unchanged).

// Before
const circle: CurveDef = {
  fn: (t, time, params) => ({ x: Math.cos(t), y: Math.sin(t) }),
  period: Math.PI * 2,
};
createSarmal(canvas, circle, { initialT: Math.PI });

// After
const circle: CurveDef = {
  fn: (phase, elapsed, params) => ({ x: Math.cos(phase), y: Math.sin(phase) }),
  period: Math.PI * 2,
};
createSarmal(canvas, circle, { initialPhase: Math.PI });

0.25.1 - 05.05.2026

drawCurve() now accepts an optional name parameter for custom display names.

Breaking
drawCurve() default name changed from "custom" to "drawn".
// Before
const curve = drawCurve(points)
curve.name  // "custom"

// After
const curve = drawCurve(points)
curve.name  // "drawn"

const named = drawCurve(points, { name: "Artemis II" })
named.name  // "Artemis II"

drawCurve() now copies the input points array at construction. Mutating the caller’s array after creation no longer affects the curve’s shape. It also outputs a console.warn text when all control points are identical or when points extend far outside [-1, 1].

The built-in Artemis II curve is now a hand-drawn Catmull-Rom spline through 21 control points, replacing the previous parametric approximation. It is the first built-in curve created with drawCurve.

0.24.0 - 03.05.2026

New pauseOnHidden option (default true) automatically pauses sarmal instances when the browser tab is hidden and resumes them when it becomes visible again. If the tab is already hidden at construction time, autoStart is suppressed and the animation waits for the tab to become visible. Set pauseOnHidden: false to opt out.

0.23.0 - 02.05.2026

The SVG renderer now calibrates trail width and skeleton stroke width to the container’s physical pixel size at construction, so strokes render at a consistent apparent thickness regardless of container dimensions.

The default headRadius for the SVG renderer changed from 1.5 to 0.5 viewBox units.

0.22.0 - 02.05.2026

headRadius is now a runtime option. setRenderOptions({ headRadius }) can be called to change the head dot radius live, without rebuilding the instance.

// Before
// headRadius was construction-only; changing it required reset
createSarmal(canvas, curve, { headRadius: 6 })

// After
const instance = createSarmal(canvas, curve)
instance.setRenderOptions({ headRadius: 6 })

setRenderOptions validates headRadius before applying it. Passing NaN, ±Infinity, zero, a negative number, or a non-number type (string, boolean, null) throws TypeError.

0.21.0 - 02.05.2026

Auto-init (data-sarmal) now supports <svg> elements. data-sarmal can be added to any <svg>. Then, the script picks the SVG renderer automatically. The API is identical to that of canvas. All data-* attributes work for both canvas and SVG.

<script src="https://unpkg.com/@sarmal/core/dist/auto-init.js"></script>

<canvas data-sarmal="rose3" width="200" height="200"></canvas>
<svg data-sarmal="rose3" style="width:200px; height:200px;"></svg>

SVG elements are sized with CSS. The renderer sets viewBox="0 0 100 100" automatically. data-head-radius uses viewBox units (0-100, default 1.5) for SVG and CSS pixels (default 4) for canvas.

0.20.0 - 02.05.2026

New drawCurve(points) export converts hand-drawn control points into a CurveDef that the engine can animate. evaluateCatmullRom(points, t) is also exported for evaluating the closed Catmull-Rom spline at arbitrary parametric positions.

import { createSarmal, drawCurve } from '@sarmal/core'

const curve = drawCurve([[-0.5, 0.3], [0.2, -0.8], [0.7, 0.4]])
createSarmal(canvas, curve)

Requires at least 3 control points in normalized [-1, 1] space. Throws if fewer are passed.

0.19.0 - 01.05.2026

Breaking
createSVGRenderer and createSarmalSVG now require an SVGSVGElement as the container, not a generic Element like a div. The renderer draws directly into the passed svg element. It no longer creates a nested child <svg>.

This change helps bring the initialization step to be identical to canvas renderer.

// Before
// <div id="spinner"></div> in HTML
const div = document.getElementById("spinner");
createSarmalSVG(div, curves.rose3);

// After (same pattern, different tag)
// <svg id="spinner"></svg> in HTML
const svg = document.getElementById("spinner");
createSarmalSVG(svg, curves.rose3);

The renderer takes ownership of the targeted DOM element.

The SVG renderer uses a fixed viewBox="0 0 100 100" and no longer reads pixel dimensions from the container. The default headRadius is now 2 (viewBox units). Override with headRadius if needed.

destroy() only cleans up the renderer’s own child content. It no longer removes the caller’s <svg> element.

0.18.0 - 28.04.2026

SVG renderer trail pool is now sized to trailLength at the time of construction instead of a hardcoded cap, which was 200. Before the change, trailLength values above 200 would silently clip in the SVG renderer while the canvas equivalent would have no problem rendering it fully.

Engine.trailLength is now exposed on Engine objects. It reflects the maximum trail capacity set at the time of engine creation.

0.17.3 - 24.04.2026

cyclePos in shared renderer where palette color is retrieved now uses the safe double-modulo form

0.17.0 - 24.04.2026

Four new built-in curves: rose52, star, star4, star7

0.16.0 - 23.04.2026

destroy() now rejects any pending morphTo Promise.

Previously, if a Sarmal instance was destroyed druing a morph, the Promise was never settled.

The rejection is swallowed by the .catch(() => {}) in @sarmal/react’s useSarmal hook. Callers who await morphTo directly should handle the rejection if they call destroy() before the morph completes.

0.15.1 - 19.04.2026

Replaced getBoundingClientRect() approach of getting container size with offsetWidth and offsetHeight.

The getBoundingClientRect() would cause unexpected width/height output, because it would return visually scaled size affected by CSS. So, if one were to apply transform: scale(0.99) to the canvas, but 280px for the actual canvas size, the end result would be 277.2px

The offset values now give the inherent layout box size, unaffected by CSS transforms

0.15.0 - 19.04.2026

Breaking
The palette option is removed. Colors are now unified under trailColor which accepts a single hex string OR an array of hex strings.
// Before
const sarmal = createSarmal(canvas, curves.astroid, {
  trailStyle: "gradient-animated",
  palette: "bard",
});

// After
import { palettes } from "@sarmal/core";
const sarmal = createSarmal(canvas, curves.rose3, {
  trailStyle: "gradient-animated",
  trailColor: palettes.sunset,
});

setRenderOptions(partial) is added to SarmalInstance. Change colors and trail style on a live instance without destroying and recreating it.

sarmal.setRenderOptions({ trailColor: palettes.sunset });

sarmal.setRenderOptions({ trailStyle: "default", trailColor: "#c0143c" });

sarmal.setRenderOptions({ headColor: "#ffffff" }); // override
sarmal.setRenderOptions({ headColor: null });      // inherited from trail color

Validation rules: The current setup requeires that all colors be 6-digit hex strings. skeletonColor also accepts "transparent". If any field fails validation, then no fields are mutated.


0.14.0 - 18.04.2026

Animated gradients are now supported in SVG renderer

trailStyle and palette options now work in createSarmalSVG with identical behavior to createSarmal. Previously these options were only available in canvas.

const sarmal = createSarmalSVG(container, curves.astroid, {
  trailStyle: 'gradient-animated',
  palette: 'bard',
});

0.13.0 - 16.04.2026

Breaking
SarmalInstance methods start() and stop() are renamed to play() and pause() to match similar libraries
  • start() play()
  • stop() pause()

autoStart added: createSarmal and createSarmalSVG now default to autoStart: true, so the animation begins immediately on creation. If you were calling .start() right after creation, you can remove that call. If you want to defer playback, pass autoStart: false.

// Before
const sarmal = createSarmal(canvas, curves.artemis2)
sarmal.start()

// After
const sarmal = createSarmal(canvas, curves.artemis2)
// No call needed here

Deferred start:

const sarmal = createSarmal(canvas, curves.artemis2, { autoStart: false })
// later...
sarmal.play()

A new initialT option lets you seek to a specific position before the first frame, replacing the common pattern of play() then seek(t).

// Start at a specific position
const sarmal = createSarmal(canvas, curves.artemis2, { initialT: Math.PI })

React

0.10.0 - 31.05.2026

<Sarmal>, <SarmalSVG>, and <SarmalDotMatrix> accept morphEasing and morphAlign props, forwarded to morphTo when the curve prop changes. morphEasing defaults to the core easeInOutCubic ease, so prop-driven morphs are eased automatically with no change required.

0.9.0 - 25.05.2026

<SarmalDotMatrix> now accepts a gridColor prop, passed through as a runtime option. Supports the same values as @sarmal/core.

0.8.0 - 24.05.2026

Added <SarmalDotMatrix> component, useSarmalDotMatrix hook, and SarmalDotMatrixProps type. It follows the <Sarmal> & <SarmalSVG> pattern.

CanvasInit is now exported from @sarmal/react. It was previously internal, leaving useSarmal callers without a named type for its init parameter.

0.7.0 - 24.05.2026

Added pauseOnHidden prop to <Sarmal> and <SarmalSVG>. Controls whether the animation automatically pauses when the browser tab is hidden and resumes when it becomes visible again. Defaults to true (existing behavior unchanged). Changing the prop after mount destroys and recreates the instance.

Added morphStrategy prop to <Sarmal> and <SarmalSVG>. Accepts 'normalized' (default) or 'raw'. Controls how the animation interpolates between curves with different periods. Also available as an option on useSarmal and useSarmalSVG hooks through the morphOptions argument.

0.6.0 - 17.05.2026

Canvas and SVG instances are now initialized with useLayoutEffect instead of useEffect, so the element is sized and the animation starts before the browser’s first paint. This eliminates the brief flash of an unsized canvas that could appear on first render.

Fixed a stale closure bug where changing morphDuration between renders had no effect on the next morph. The hook now always reads the latest morphOptions value.

0.5.0 - 17.05.2026

Added trailWidth prop to <Sarmal> and <SarmalSVG>. Changes after mount update the trail width smoothly using setRenderOptions without recreating the instance or resetting the trail.

0.4.1 - 15.05.2026

Fixed "use client" not appearing in the bundle output. Since esbuild strips RSC directives found in source files during bundling, the directive is now injected as a post-build step.

0.4.0 - 10.05.2026

Breaking
initialT init option renamed to initialPhase on both <Sarmal> and <SarmalSVG>. Also affects the BaseInit and BaseSarmalProps types.
// Before
<Sarmal curve={circle} initialT={Math.PI} />
// After
<Sarmal curve={circle} initialPhase={Math.PI} />

0.3.0 - 01.05.2026

Breaking
useSarmalSVG no longer sets viewBox on the <svg> element. The core SVG renderer (@sarmal/core@0.19.0) now owns viewBox. Upgrade both packages together.
  • Fixed useRenderOptions: Toggling skeletonColor or trailStyle from a value to undefined and back to the same value no longer silently drops the second change.

0.2.0 - 30.04.2026

Breaking
Init-time props (width, height, trailLength, headRadius, autoStart, initialT) now destroy and recreate the instance when changed after mount. Previously these were silently ignored on change.

Canvas sizing is fixed. <Sarmal> now reads width and height props and sets the canvas buffer dimensions before creating the instance. If omitted, it reads parentElement.clientWidth and parentElement.clientHeight. If the parent reports 0x0, it falls back to 300x300 and emits a warning.

// Before: width/height were not read at all, so canvas defaulted to 300x150
<Sarmal curve={rose3} />
// After: canvas is sized from explicit props, parent container, or 300x300 fallback
<Sarmal curve={rose3} width={200} height={200} />

trailLength and headRadius are now exposed as props on <Sarmal> and as init options on useSarmal. Note that changing them after mount destroys and recreates the instance, which resets the trail.


SVG output is now supported via SarmalSVG component and useSarmalSVG hook. Works identically to the canvas variants but renders to <svg> with viewBox="0 0 100 100".

Info
No width/height sizing props because SVG scales naturally with CSS
import { SarmalSVG } from "@sarmal/react";

<SarmalSVG curve={rose3} style={{ width: 200, height: 200 }} />

0.1.0 - 23.04.2026

@sarmal/react is a new package that provides a React wrapper over @sarmal/core. It exports a <Sarmal> component for drop-in usage and a useSarmal hook for imperative control.

Tip
Curve changes use morphTo under the hood
import { Sarmal } from "@sarmal/react";
import { curves } from "@sarmal/core";

<Sarmal curve={curves.rose3} />

Peer dependencies: react >= 18 and @sarmal/core. The package bundle emits "use client" for SSR compatibility.

Svelte

0.5.0 - 25.05.2026

<SarmalDotMatrix>, useSarmalDotMatrix, and sarmalDotMatrix now accept a gridColor option, passed through as a runtime option. Supports the same values as @sarmal/core.

0.4.1 - 24.05.2026

Breaking
Init type is renamed to BaseInit
// Before
import type { Init } from '@sarmal/svelte'

// After
import type { BaseInit } from '@sarmal/svelte'

0.4.0 - 24.05.2026

Added <SarmalDotMatrix> component, useSarmalDotMatrix composable, sarmalDotMatrix action, and SarmalDotMatrixProps, SarmalDotMatrixActionOptions, DotMatrixInit types. Follows the same <Sarmal> & <SarmalSVG> pattern.

cols, rows, roundness, trailLength, autoStart, initialPhase, pauseOnHidden, width, and height are init-time options. Changing any of them after mount destroys and recreates the instance. trailColor, trailStyle, and skeletonColor are runtime options that update without recreating.

headColor, headRadius, and trailWidth are absent: the dot matrix renderer has no head dot or ribbon trail.

0.3.0 - 24.05.2026

Added pauseOnHidden prop to <Sarmal> and <SarmalSVG> components, and to the sarmal and sarmalSVG actions. Controls whether the animation automatically pauses when the browser tab is hidden. Defaults to true. Changing the prop on a component after mount destroys and recreates the instance; the action handles it with destroy + recreate the same way other init options do.

Added morphStrategy prop to <Sarmal> and <SarmalSVG> components, and to the sarmal and sarmalSVG actions. Accepts 'normalized' (default) or 'raw'. Also available as a getMorphStrategy argument on the useSarmal and useSarmalSVG composables.

0.2.0 - 17.05.2026

Added trailWidth prop to <Sarmal> and <SarmalSVG> components, and to the sarmal and sarmalSVG actions. Changes update the trail width smoothly without recreating the instance or resetting the trail.

0.1.0 - 14.05.2026

@sarmal/svelte is a new package that provides a Svelte5 wrapper over @sarmal/core. It exports a <Sarmal> component for drop-in usage, a useSarmal composable for imperative control, and a sarmal action for idiomatic Svelte patterns. SVG is supported through the mirror exports <SarmalSVG>, useSarmalSVG, and sarmalSVG.

<script>
  import { Sarmal } from "@sarmal/svelte";
  import { curves } from "@sarmal/core";
</script>

<Sarmal curve={curves.rose3} />

Peer dependencies: svelte >= 5 and @sarmal/core. All composable files use the .svelte.ts extension. $effect never runs on the server, so no SSR guards are needed.