Files
oikos/web/vendor
dtoro 3cd4cf98c3 chore: vendor @joan/procedural-glyph-engine for portable builds
Move @joan/procedural-glyph-engine from absolute path dep
(/private/tmp/orby-pkg) to vendored local dep (web/vendor/) so the
SPA builds in Docker and on other machines without the temp dir.

- Vendor Orby v5.0.0 into web/vendor/
- Switch package.json dep to file:../vendor
- Update Dockerfile to COPY vendor into build context
- Use npm install instead of npm ci (file: deps need install)
- Fix missing trailing newline in Dockerfile
2026-08-05 17:27:04 +02:00
..

Orby — A procedural glyph engine by Joan Sterjo, shown as a deterministic pixel field

Orby

A procedural glyph engine by Joan Sterjo.
Design one visual identity, invoke semantic states from product code, and keep every transition responsive, reproducible, and alive.

Open the live Studio · Download v5.0.0 · Quick-start guide · Runnable examples

Version 5.0.0 Canvas 2D and native ESM Zero runtime dependencies TypeScript declarations included Source-available license: Apache 2.0 with Commons Clause

Orby turns product intent—ready, listening, thinking, using a tool, progressing, completed, failed—into a coherent live glyph. Each frame combines an analytic silhouette, a seeded field, a persistent pixel gate, and a spatial transition. The result is motion with identity, not a generic loading ornament.

Orby is distributed as @joan/procedural-glyph-engine. Its primary runtime class is JoanGlyphEngine, its browser-native element is <joan-glyph>, and portable Studio recipes use the .joan.json format.

The runtime has no third-party dependencies. It ships as native ES modules with TypeScript declarations, a browser-native web component, deterministic exports, and a complete offline Studio.

Important

Orby is source-available under the Apache License 2.0 subject to the Commons Clause License Condition v1.0. You may use, copy, modify, and redistribute it, including inside a larger value-added product. You may not sell Orby or a product or service whose value derives entirely or substantially from Orby's functionality.

Choose your path

I want to… Start here
Explore states and tune a recipe Open the live Studio
Download everything for offline use Get the complete integration kit
Integrate the runtime into a product Follow the focused quick-start guide
Try plain Canvas, web component, or timed sequences Run the included examples
Understand the engine deeply Architecture · state language · API
Operate it responsibly Accessibility · performance · development
Complete contents

Quick start

Install the extracted integration kit from a consuming project:

npm install ./joan-procedural-glyph-engine-5.0.0

Mount one long-lived engine instance, then drive it with real product state:

<canvas id="ai-glyph" width="160" height="160"></canvas>

<script type="module">
  import { createGlyph } from "@joan/procedural-glyph-engine";

  const glyph = createGlyph("#ai-glyph", {
    sprite: "ai.idle",
    seed: "conversation-42",
    gridSize: 68,
  });

  await glyph.transitionTo("ai.thinking");
  await glyph.transitionTo("status.success", {
    transition: "path-draw",
  });
</script>

For direct, unbundled browser use, replace the package import with ./src/joan-engine.js. For a declarative integration, use the included <joan-glyph> web component. For pre-recorded or timed flows, use playStateSequence().

Why Orby

Semantic by default Deterministic by design Portable by construction
Twenty-five states cover the real lifecycle of AI work, from ambient readiness to completion and recovery. A stable seed preserves visual identity across frames, products, previews, and exports. Use Canvas 2D, native ESM, a web component, serialized configurations, or a single offline HTML Studio.
25 semantic states 19 seeded fields 12 transitions 12 switch systems 9 pixel geometries 68×68 default grid
Presence → recovery Quiet → chaotic Morph → ignite Dither → trace Disc → composites Adaptive quality

What Orby includes

  • 25 immutable semantic sprite recipes covering presence, field-only ambient motion, AI activity, status, transfer, and handoff states.
  • 19 seeded scalar fields: fbm, ridged, domain-warp, curl, flow, worley, voronoi, plasma, interference, vortex, metaballs, caustics, strata, radar, constellation, liquid, electric, ripple, and kaleidoscope.
  • 12 pixel-switch strategies: ordered-dither, temporal-blue-noise, threshold-hysteresis, sdf-wavefront, contour-trace, curl-advect, neighbor-propagation, radial-cascade, path-draw, axis-flip, seeded-dissolve, and field-morph.
  • 12 transition strategies: field-morph, seeded-dissolve, radial-cascade, angular-sweep, scanline, contour-trace, path-draw, axis-flip, cluster-dissolve, neighbor-ignite, glitch-bands, and instant.
  • Nine Canvas 2D pixel shapes: disc, square, diamond, capsule, line, ring, cross, square-cross, and square-cross-ring. The composite shapes choose one primitive per cell from its sampled signal lightness.
  • Analytic glyph masks with progress, audio-energy, pointer, press, signal, and reduced-motion inputs.
  • Seeded ordered dithering, dwell time, hysteresis, afterglow, and spring response so pixels switch deliberately instead of flickering at a threshold.
  • Runtime registration of sampler functions and bitmap glyphs, plus browser image-file import.
  • PNG, JSON-safe configuration, static SVG, and deterministic frame-sampled animated SVG export without mutating the live engine.
  • A semantic timing helper for escalating long-running reasoning states.
  • Serializable named state sequences plus one-off timed playback with cancellation, pause, resume, and stop controls.

Self-service download kit

The website's Download section publishes a versioned ZIP assembled from the same runtime source used by the live Studio. The complete integration kit contains:

  • the complete native ESM runtime in src/;
  • all TypeScript declarations in types/;
  • this full API and integration reference;
  • a focused quick start in docs/QUICKSTART.md;
  • runnable canvas, web-component, and sequence examples;
  • the self-contained joan-engine-v5.standalone.html Studio;
  • package metadata and a runtime manifest;
  • the complete LICENSE and NOTICE; and
  • SHA256SUMS.txt covering every packaged payload file.

Beside the ZIP, the website publishes its .sha256 file and a machine-readable release manifest. After extracting the kit, install that local folder from a consuming project—the package is not currently registry-published:

npm install ./joan-procedural-glyph-engine-5.0.0

Alternatively, serve the extracted folder and import ./src/joan-engine.js directly, or open the standalone Studio without a build step. The archive grants the same source-available permissions—and carries the same no-sale condition—as the repository. See LICENSE for the complete terms.

Architecture

The renderer is intentionally layered. Each layer can be used independently or composed by JoanGlyphEngine.

Module Responsibility
src/fields.js Seed hashing, gradient/value/cellular noise, Bayer dithering, and the canonical scalar-field registry. Every named field samples to 0..1.
src/glyphs.js Resolution-aware analytic glyph coverage functions. Coordinates are normalized to -1..1; coverage is 0..1.
src/sprites.js Deep-frozen semantic recipes: glyph, field stack, palette, switching, timing, interaction, labels, and reduced-motion representation.
src/joan-engine.js Canvas lifecycle, state transitions, interaction impulses, pixel gating, spring dynamics, drawing, exports, and events.
src/web-component.js The <joan-glyph> custom element and its attribute-to-engine adapter.
src/state-director.js Optional, explicit orchestration for thinking, deep-thinking, still-working, completion, failure, and reset.
src/state-sequence.js Reusable, serializable timelines for timed state choreography, playback control, and cancellation.
src/studio.js Interactive demo/studio wiring. It is not required by the runtime.

The frame pipeline keeps meaning, identity, motion, and output as explicit layers:

flowchart LR
    A[Semantic state] --> B[Analytic glyph or field orb]
    A --> C[Seeded procedural field stack]
    D[Signals and interaction] --> B
    D --> C
    B --> F[Spatial transition]
    C --> F
    F --> E[Dither · hysteresis · dwell pixel gate]
    E --> G[Spring and afterglow]
    G --> H[Canvas · PNG · SVG]

Seeds affect the field permutation and per-cell decisions. The same seed, sprite, coordinates, time, and options produce the same field samples. Animation time and live interaction still intentionally change a rendered frame.

The 25 semantic sprites

These IDs are the stable built-in catalog. Short aliases such as thinking, success, error, upload, and handoff are accepted, but product code should prefer the canonical IDs.

View all 25 canonical state IDs
Canonical ID Default label Intended meaning
ai.idle Ready Ready and available
ai.ambient-idle Ambient ready Calm presence expressed only through a field
ai.ambient-thinking Ambient thinking Reasoning expressed only through a field
ai.ambient-thinking-symmetric Ambient thinking — symmetric Clean, symmetrical reasoning expressed only through a field
ai.ambient-speaking Ambient speaking Voice output expressed only through a field
ai.listening Listening Capturing voice or input
ai.thinking Thinking Reasoning
ai.thinking-deep Reasoning deeply Deliberate extended reasoning
ai.still-working Still working Work is taking longer than expected
ai.loading Loading Indeterminate startup or wait
ai.progress In progress Determinate completion progress
ai.generating Generating Producing content
ai.searching Searching Searching or retrieving information
ai.tool-use Using a tool Executing a tool or action
ai.speaking Speaking Producing voice output
ai.awaiting-input Your input is needed User action is required
status.success Completed Completed successfully
status.warning Warning Attention is needed for a nonfatal issue
status.error Error Operation failed
status.paused Paused Work is suspended and can resume
status.cancelled Cancelled Operation was stopped
status.offline Offline Disconnected or unavailable
transfer.active Transferring Uploading, downloading, or synchronizing data
workflow.handoff Handing off Passing work to another agent or person
status.celebration Milestone completed A milestone or high-value success

Use listSprites() to obtain the frozen ordered catalog and getSprite(id) to resolve either a canonical ID or alias.

Runtime API

Package entry points

Every JavaScript entry point is native ESM and carries TypeScript declarations.

Import Provides
@joan/procedural-glyph-engine JoanGlyphEngine, createGlyph(), core catalogs, and rendering utilities
@joan/procedural-glyph-engine/config Strict option inspection, validation, and frozen recipe helpers
@joan/procedural-glyph-engine/fields Seeded field samplers, noise helpers, and field registration
@joan/procedural-glyph-engine/glyphs Analytic glyph masks and glyph registration helpers
@joan/procedural-glyph-engine/sprites Immutable semantic sprite catalog and aliases
@joan/procedural-glyph-engine/state-director Escalation timing for long-running task presentation
@joan/procedural-glyph-engine/state-sequence Serializable timelines and controlled timed playback
@joan/procedural-glyph-engine/web-component <joan-glyph> element, definition helper, and automatic browser registration
@joan/procedural-glyph-engine/web-component/register Explicit side-effect registration for <joan-glyph>
@joan/procedural-glyph-engine/styles.css Default web-component presentation styles

Engine construction

Create one long-lived engine instance per surface. A more fully authored setup can override the active recipe while preserving the same semantic API:

import { createGlyph } from "@joan/procedural-glyph-engine";

const glyph = createGlyph("#ai-glyph", {
  sprite: "ai.idle",
  seed: "conversation-42",
  gridSize: 68,
  pixelShape: "disc",
  pixelSwitch: "threshold-hysteresis",
  transition: "field-morph",
  orbBoundary: "gestalt",
  orbBackgroundColor: "#14212b",
  orbBackgroundMode: "pixelated",
  speed: 1,
  density: 1,
});

await glyph.transitionTo("ai.thinking", {
  transition: "neighbor-ignite",
  duration: 0.5,
  preservePhase: true,
});

// Release observers, listeners, and the animation frame when unmounting.
glyph.destroy();

setSprite() is synchronous and chainable. transitionTo() resolves when the visual transition completes and accepts an AbortSignal; use it when product flow must wait for presentation. For a direct, unbundled demo import, replace the package import with ./src/joan-engine.js.

createGlyph(canvasOrSelector, options) is the preferred factory. createProceduralGlyph({ canvas, ...options }) and mountProceduralGlyph(target, options) provide equivalent construction forms.

The constructor form is equivalent:

import JoanGlyphEngine from "@joan/procedural-glyph-engine";

const glyph = new JoanGlyphEngine(canvas, {
  sprite: "ai.loading",
  autoplay: true,
});

Lifecycle and configuration

Useful lifecycle and configuration methods include play(), pause(), toggle(), activate(), resume(), renderOnce(), setSeed(), setResolution(), setField(), setPixelShape(), setPixelSwitch(), setNonErrorPalette(), setOptions(), transitionTo(), whenTransitionComplete(), configure(), inspect(), exportConfig(), toDataURL(), toBlob(), toSVG(), toAnimatedSVG(), downloadPNG(), downloadSVG(), downloadAnimatedSVG(), and destroy().

Built-in sprites use layered field stacks. setField("radar") deliberately replaces that stack with one field; call useRecipeFields() to restore the sprite's authored composition. exportConfig() records this distinction as fieldMode: "recipe" | "override", so configurations round-trip faithfully. Recipe-owned pixel geometry, switching, and transitions serialize as null; explicit global overrides serialize as their string or structured object. This keeps reconstructed engines on each future state's authored motion recipe.

Orb boundary and background

Set orbBoundary: "gestalt" to replace the continuous circular rim with an implied edge built from separated pixel clusters. The default is orbBoundary: "defined", which preserves the authored hard outline. The Gestalt treatment is available for ai.idle, ai.ambient-idle, ai.ambient-thinking, and ai.ambient-speaking; other sprites retain their authored silhouette. The option remains configured when moving between states, so one engine can carry the same boundary preference through a product flow. Custom circular recipes can opt in with composition: { gestaltBoundary: true, gestaltOpenness: 0.5 }.

glyph.setOptions({ orbBoundary: "gestalt" });
glyph.setOptions({ orbBoundary: "defined" }); // restore the continuous rim

orbBackgroundColor supplies the color for an independent fill behind the pixels of the same four supported presence orbs. Choose its treatment with orbBackgroundMode: "none" disables the layer, "solid" draws a smooth circle, and "pixelated" builds the circle from grid-aligned row runs. The pixelated mode follows the selected engine resolution, so its edge belongs to the same grid as the foreground glyph. The fill carries through other states without painting there, remains visible when the canvas background is disabled, and is included in static and animated SVG exports.

An initial orbBackgroundColor without an explicit mode selects "solid". Once "pixelated" is selected, later color-only patches preserve that treatment so changing the swatch does not reset the shape. Clearing the color with null or "transparent" disables the layer. Set the mode explicitly when you want to retain a color while temporarily hiding it.

glyph.setOptions({
  orbBackgroundColor: "#14212b",
  orbBackgroundMode: "pixelated",
});
glyph.setOptions({ orbBackgroundMode: "none" }); // retain the chosen color
glyph.setOptions({ orbBackgroundMode: "solid" });
glyph.setOptions({ orbBackgroundColor: null }); // disable and clear the color

Named options fail fast with a nearby-name suggestion instead of silently falling back. Use the config subpath to validate before constructing an engine, or to define and freeze a custom recipe:

import { getSprite } from "@joan/procedural-glyph-engine";
import {
  defineRecipe,
  inspectEngineOptions,
  validateEngineOptions,
} from "@joan/procedural-glyph-engine/config";

const options = validateEngineOptions({
  sprite: "thinking",       // canonicalized to ai.thinking
  field: "domain_warp",     // canonicalized to domain-warp
  gridSize: 68,
  quality: "auto",
});

const inspection = inspectEngineOptions(untrustedOptions);
if (!inspection.ok) console.table(inspection.issues);

const branded = defineRecipe({
  ...getSprite("ai.generating"),
  id: "product.generating",
  glyph: "product.mark",
});

validateEngineOptions() is strict and non-mutating. inspectEngineOptions() returns { ok, value, issues }, safely coerces ordinary HTML-style values, and omits invalid named options.

API shape

Operation Return Use it for
createGlyph(target, options) engine The preferred canvas-or-selector invocation
setSprite(sprite, options) engine Immediate, chainable state commands
transitionTo(sprite, options) Promise<TransitionDetail> Waiting for the visual handoff or cancelling it with signal
configure(patch) engine Strict, atomic runtime configuration with one consolidated event/render
setOptions(patch) engine Runtime configuration with safe coercion
signal(type, payload) engine Short-lived product events and semantic inputs
on(type, listener) unsubscribe function Typed event subscription with one-call cleanup
inspect() JSON-safe snapshot State, transition, signals, palette, quality, and performance debugging
renderOnce(time) stats Deterministic paused previews and test fixtures
exportConfig() JSON-safe object Reconstructing the authored runtime configuration
destroy() undefined Releasing frames, observers, and listeners

The package declarations expose literal unions for built-in sprites, fields, pixel shapes, switches, transitions, quality settings, event payloads, recipes, exports, the custom element, StateDirector, and StateSequencePlayer.

Color control

Use nonErrorPalette to override any combination of the background, off, ink, accent, and glow channels for every state except status.error. Unspecified channels continue to come from each states authored palette. Use variants when shared light or dark surfaces need to preserve the authored semantic color family. Variant keys match palette names such as info, success, warning, and celebration; explicit top-level channels still win.

const glyph = new JoanGlyphEngine(canvas, {
  sprite: "ai.thinking",
  nonErrorPalette: {
    background: "#e5e6e2",
    off: "#c7cbc8",
    ink: "#17191e",
    variants: {
      info: { accent: "#4788aa", glow: "#6aa6c4" },
      success: { accent: "#397c57", glow: "#62a27b" },
      warning: { accent: "#9a6817", glow: "#bd8b3c" },
    },
  },
});

glyph.setNonErrorPalette({ ink: "#ffffff", accent: "#53d6c7" });
glyph.setNonErrorPalette({ glow: "transparent" }); // disable the halo
glyph.setNonErrorPalette(null); // restore authored state colors

The error state keeps its danger palette so a product-level color choice cannot erase its failure semantics. The palette option provides an explicit global override—including for errors—when that behavior is needed. Hex and rgb() color values provide consistent Canvas and SVG output.

In the studio, turn Glow off to select no glow color. The previous chosen color is preserved and returns when Glow is enabled again.

Web component

Importing the web-component subpath registers <joan-glyph> in a browser and is safe to evaluate during SSR:

import "@joan/procedural-glyph-engine/web-component";

An explicit side-effect entry is also available for application bootstrap:

import "@joan/procedural-glyph-engine/web-component/register";
<joan-glyph
  sprite="ai.generating"
  seed="answer-108"
  resolution="68"
  speed="0.9"
  density="1"
  field="electric"
  pixel-shape="diamond"
  pixel-switch="curl-advect"
  orb-boundary="gestalt"
  orb-background-color="#14212b"
  orb-background-mode="pixelated"
  non-error-background="#10131a"
  non-error-off="#293140"
  non-error-ink="#f7f8fb"
  non-error-accent="#8f7cff"
  non-error-glow="#6554e8"
></joan-glyph>

Size the host with CSS; its default size is 68px × 68px.

joan-glyph {
  inline-size: 3rem;
  block-size: 3rem;
}

The five non-error-* color attributes are optional and may be added, changed, or removed at runtime. The boolean paused attribute disables autoplay. noninteractive disables pointer interaction and transparent disables the painted background; both react when added or removed. Removing field, pixel-shape, or pixel-switch restores the recipe-authored behavior. The optional orb-boundary attribute accepts gestalt; removing it (or using an unsupported value) restores the default defined boundary. The optional orb-background-color attribute adds the orb-only fill; removing it clears the fill without changing the canvas background. Use orb-background-mode="none", "solid", or "pixelated" to choose the treatment. Supplying only orb-background-color selects solid; removing the mode attribute returns to that color-driven behavior. The element forwards the engine's state, signal, palette, playback, configuration, and inspection methods and exposes the underlying instance as element.engine.

Custom glyphs

A sampler receives normalized x, y, animation time, and the live engine context. Return coverage from 0 (off) to 1 (fully covered). Keep the hot sampler pure and allocation-free.

import {
  createProceduralGlyph,
  getSprite,
} from "@joan/procedural-glyph-engine";

const glyph = createProceduralGlyph({ canvas, autoplay: true });

glyph.registerGlyph("product.spark", (x, y, time, context) => {
  const radius = Math.hypot(x, y);
  const spokes = Math.cos(Math.atan2(y, x) * 8 + time * 0.6);
  const pulse = 0.04 * Math.sin(time * 1.4 + context.energy * Math.PI);
  return radius < 0.42 + spokes * 0.08 + pulse ? 1 : 0;
});

const base = getSprite("ai.generating");
glyph.setSprite({
  ...base,
  id: "product.spark",
  label: "Generating with Product",
  glyph: "product.spark",
  semantic: {
    ...base.semantic,
    category: "custom",
    meaning: "generating branded output",
  },
  labels: {
    ...base.labels,
    default: "Generating with Product",
    aria: "Generating branded output",
  },
});

A two-dimensional numeric array is accepted as a bitmap and its dimensions are inferred:

glyph.registerGlyph("product.pixel-heart", [
  [0, 1, 0, 1, 0],
  [1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1],
  [0, 1, 1, 1, 0],
  [0, 0, 1, 0, 0],
]);

In the browser, loadGlyphFile(file, options) rasterizes an image file and activates it as a custom glyph:

await glyph.loadGlyphFile(fileInput.files[0], {
  id: "product.uploaded-mark",
  label: "Product mark",
  baseSprite: "ai.generating",
  resolution: 48,
});

Signals, audio, and progress

Signals add short-lived spatial energy without changing semantic state. Signal names are intentionally open-ended, so the host can mirror its own event model.

glyph.signal("token", { energy: 0.55 });
glyph.signal("search.hit", { x: 0.35, y: -0.2, energy: 0.9, life: 0.8 });
glyph.signal("tool.call", { energy: 1 });
glyph.signal("audio.level", { value: microphoneLevel });
glyph.signal("transfer.direction", { direction: "up" });
glyph.signal("handoff.accepted", { accepted: true });
glyph.signal("network.retry", { energy: 0.8 });
glyph.signal("resume", { value: 1 }); // restores the state held before status.paused

audio.level updates the smoothed listening/speaking input. progress is special-cased as determinate state:

glyph.setSprite("ai.progress");
glyph.setProgress(0.42); // clamped to 0..1

// Equivalent low-level form:
glyph.signal("progress", { value: 0.42 });

transfer.active also consumes progress. Register a direction-specific custom glyph if the product must distinguish upload from download visually.

The engine dispatches spritechange, transitionqueued, transitioncomplete, timelinecomplete, resume, configchange, signal, activate, play, pause, qualitychange, stats, and destroy events:

glyph.addEventListener("spritechange", ({ detail }) => {
  console.log(`${detail.from}${detail.to}`);
});

Export

downloadPNG() captures the current canvas. toSVG() / downloadSVG() create a vector snapshot using the selected pixel geometry. toAnimatedSVG() / downloadAnimatedSVG() simulate an isolated deterministic clone, sample its pixel gates, and encode shape, opacity, and scale frames without changing the live engine.

await glyph.downloadPNG("thinking.png");
await glyph.downloadSVG("thinking.svg");
await glyph.downloadAnimatedSVG("thinking.animated.svg", {
  duration: 2.4,
  fps: 12,
  maxGridSize: 48,
});

Animated export is intentionally capped and yields between frame batches to keep the studio responsive. Raise fps, duration, or maxGridSize only after checking file size and export time.

Timed and pre-recorded state sequences

StateSequencePlayer turns product-owned state choreography into a small, reusable API. Definitions are frozen and JSON-friendly, so a host can keep them beside an agent workflow, load them from its own configuration, or construct a one-off sequence at the point of use.

import {
  StateSequencePlayer,
  defineStateSequence,
} from "@joan/procedural-glyph-engine/state-sequence";

const idleThenDone = defineStateSequence("idle-then-done", [
  { sprite: "ai.idle", holdMs: 5_000 },
  { sprite: "status.success", transition: "path-draw" },
]);

const states = new StateSequencePlayer(glyph, {
  sequences: [idleThenDone],
  transition: "field-morph",
});

const controller = new AbortController();
const result = await states.play("idle-then-done", {
  signal: controller.signal,
});

The first step is entered immediately by default. Each later step waits for its visual transition to complete, then holds for holdMs before advancing. durationMs is accepted as an alias for holdMs; transition duration values continue to use the engine's seconds-based API.

For an inline command, playStateSequence() starts immediately and returns the playback controller:

import { playStateSequence } from
  "@joan/procedural-glyph-engine/state-sequence";

const playback = playStateSequence(glyph, [
  { sprite: "ai.ambient-idle", holdMs: 1_500 },
  { sprite: "ai.progress", transition: "contour-trace" },
]);

playback.pause();  // freezes the current hold clock
playback.resume();
await playback.finished;

Call stop() for an intentional early finish, or pass an AbortSignal when the surrounding task owns cancellation. Aborts reject with AbortError; stop() resolves with a stopped result. Starting another playback safely stops the active run and starts the new one. Pausing freezes the hold clock and step progression; an already-running visual transition continues to settle. sequencestart, stepstart, statechange, stepenter, stepcomplete, sequencepause, sequenceresume, sequencestop, sequencecancel, sequencecomplete, and sequenceerror events expose the full lifecycle. Completed, stopped, cancelled, and destroyed players clear their timeout and abort listeners.

StateDirector

StateDirector is an optional presentation timer. The host still owns the real task state; the director never guesses whether work started, succeeded, or failed.

import { StateDirector } from
  "@joan/procedural-glyph-engine/state-director";

const director = new StateDirector(glyph, {
  deepThinkingAfterMs: 7_000,
  stillWorkingAfterMs: 18_000,
  transition: "field-morph",
});

director.beginThinking();

try {
  await performWork();
  director.complete(); // status.success + a completion impulse
} catch (error) {
  director.fail(); // status.error
}

// Return to a clean phase for the next operation.
director.reset();

// Clear pending escalation timers when the owner unmounts.
director.destroy();
glyph.destroy();

beginThinking() moves from ai.thinking to ai.thinking-deep, then to ai.still-working at the configured thresholds. set(), complete(), fail(), or reset() cancels pending timers.

For the common one-task lifecycle, run() removes the surrounding state boilerplate and rethrows failures after presenting them:

const controller = new AbortController();

const result = await director.run(
  ({ signal }) => performWork({ signal }),
  {
    signal: controller.signal,
    deepThinkingAfterMs: 7_000,
    stillWorkingAfterMs: 18_000,
    successSprite: "status.success",
    errorSprite: "status.error",
    cancelledSprite: "status.cancelled",
  },
);

Starting another director operation prevents earlier asynchronous work from overwriting its state. Aborting moves to status.cancelled and rejects with an AbortError. Every manual, escalated, successful, failed, cancelled, and reset state dispatches statechange with { state, from, to, reason }.

Studio preset library

Save preset and Save current add the tuned recipe to the visible Saved presets tile library above the State Atlas. The Studio keeps up to 24 entries in this browser's local storage under joan.studio.recent-presets.v1; they do not sync to another browser or device. A tile restores its recipe, Delete has an immediate Undo action, and Prepare recipe creates a portable .joan.json file when a preset must move beyond browser-local Studio state.

Accessibility and reduced motion

  • The engine assigns the canvas role="img" and updates its accessible label when interaction is disabled. Interactive canvases use button semantics, support Enter and Space activation, and expose a visible focus treatment. Pass an ariaLive element when state changes should be announced.
  • Keep a visible text status beside the glyph. Do not communicate success, warning, failure, progress, or waiting through motion or color alone.
  • Avoid duplicate announcements: use ariaLive only if the surrounding product does not already announce the same state.
  • reducedMotion: "system" is the default. It follows prefers-reduced-motion, pauses continuous autoplay, renders representative glyph phases, and uses 100120 ms transitions for the built-in recipes; explicit reduced-motion durations remain capped at 200 ms.
  • Set reducedMotion: true to force the static behavior or false only when the product has a deliberate, user-controlled motion policy.
  • Semantic recipes include a reduced-motion representation and use palettes designed to remain understandable in monochrome, but application-level contrast and surrounding copy still need product accessibility review.
  • Call setProgress() even in reduced motion. Data changes remain meaningful when spatial animation is removed.

Development, test, and build

Requirements: Node.js 18 or newer; Node.js 20 is the tested handoff target.

Note

The commands in this section require a repository clone. The downloadable integration kit intentionally includes runtime sources, types, documentation, examples, and the offline Studio—not the project build scripts or test suite.

# Run the local studio at http://127.0.0.1:4173
npm run dev

# Run dependency-free node:test contract tests
npm test

# Recreate dist/, the standalone HTML build, and the website download kit
npm run build

# Run tests, build, then verify the complete download artifact
npm run check

# Validate, build, and create the installable .tgz package
npm pack

npm pack is suitable for local integration testing. The resulting package includes LICENSE and NOTICE and carries the same source-available terms as the repository.

Set JOAN_ENGINE_PORT to use a different development port:

JOAN_ENGINE_PORT=4400 npm run dev

The studio preview defaults to Fit. Switch it to 1:1 to center the glyph at its actual grid footprint, with one engine cell mapped to one CSS pixel. This is a studio-only inspection view and does not alter exported recipe configuration.

The build copies the native module sources and studio assets into dist/, creates the Sites-compatible dist/client and dist/server/index.js outputs, generates the versioned ZIP, checksums, and release manifest in downloads/, and creates joan-engine-v5.standalone.html. Commit source files rather than editing generated output.

Performance guidance

Rendering cost grows approximately with gridSize². The runtime defaults to 68×68 and uses adaptive quality to hold its frame budget. Override that with 2436 for compact product icons or dense multi-glyph surfaces.

  • Reuse an engine and call setSprite(); do not construct an engine for every state change.
  • For thumbnail grids, set autoplay: false, autoResize: false, interactive: false, quality: "low", dprMax: 1, and call renderOnce() only when a preview needs updating.
  • Use fps: 24 or 30 for ambient UI. Reserve 60 fps for close, interactive motion.
  • Set offPixels: false to remove the background-dot draw pass. Disable the painted background with background: false when the product surface already supplies one.
  • quality: "auto" adapts between high, balanced, and low from measured frame cost. Read the effective tier from stats.quality or subscribe to qualitychange. Balanced and low tiers time-slice field sampling across the grid, and low also skips the glow path. Use a fixed high tier or renderOnce() for full-grid deterministic visual fixtures.
  • Cap device-pixel work with dprMax; a value of 1 is often enough for pixel-art thumbnails and dense dashboards.
  • Disable pointer work with interactive: false for decorative or noninteractive instances.
  • Keep custom sampler functions allocation-free and avoid DOM reads, object creation, and network/state access inside them.
  • Let the built-in visibility observation cancel animation-frame scheduling for hidden and off-screen canvases; it resumes automatically when visible. Always call destroy() when removing an instance.
  • Watch the stats event (fps, frameMs, sampledPixels, activePixels, and resolution) in realistic multi-glyph screens, not just an isolated demo.

For deterministic visual regression fixtures, use autoplay: false, disable interaction and auto-resize, set a fixed canvas size and seed, then call renderOnce(fixedTime).

License

Orby is source-available under the Apache License 2.0 subject to the Commons Clause License Condition v1.0.

  • You may use, copy, modify, and redistribute Orby, and include it in a larger value-added product, subject to the full terms.
  • You may not sell Orby or offer a product or service whose value derives entirely or substantially from Orby's functionality.
  • Keep the required license and attribution notices when redistributing it.

Because it restricts selling, this is a source-available license rather than an OSI-approved open-source license. Read LICENSE for the complete terms and NOTICE for attribution.


Live Studio · Download kit · Back to top