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 ``, 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](./LICENSE). 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](https://joansterjo-celonis.github.io/Procedural-glyph-engine/#playground) | | Download everything for offline use | [Get the complete integration kit](https://joansterjo-celonis.github.io/Procedural-glyph-engine/#download) | | Integrate the runtime into a product | [Follow the focused quick-start guide](./docs/QUICKSTART.md) | | Try plain Canvas, web component, or timed sequences | [Run the included examples](./examples/README.md) | | Understand the engine deeply | [Architecture](#architecture) · [state language](#the-25-semantic-sprites) · [API](#api-shape) | | Operate it responsibly | [Accessibility](#accessibility-and-reduced-motion) · [performance](#performance-guidance) · [development](#development-test-and-build) |
Complete contents - [Quick start](#quick-start) - [Why Orby](#why-orby) - [What Orby includes](#what-orby-includes) - [Self-service download kit](#self-service-download-kit) - [Architecture](#architecture) - [The 25 semantic sprites](#the-25-semantic-sprites) - [Runtime API](#runtime-api) - [Web component](#web-component) - [Custom glyphs](#custom-glyphs) - [Signals, audio, and progress](#signals-audio-and-progress) - [Export](#export) - [Timed state sequences](#timed-and-pre-recorded-state-sequences) - [StateDirector](#statedirector) - [Studio preset library](#studio-preset-library) - [Accessibility and reduced motion](#accessibility-and-reduced-motion) - [Development, test, and build](#development-test-and-build) - [Performance guidance](#performance-guidance) - [License](#license)
## Quick start Install the extracted integration kit from a consuming project: ```sh npm install ./joan-procedural-glyph-engine-5.0.0 ``` Mount one long-lived engine instance, then drive it with real product state: ```html ``` For direct, unbundled browser use, replace the package import with `./src/joan-engine.js`. For a declarative integration, use the included [`` web component](#web-component). For pre-recorded or timed flows, use [`playStateSequence()`](#timed-and-pre-recorded-state-sequences). ## 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: ```sh 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`](./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 `` 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: ```mermaid 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` | `` element, definition helper, and automatic browser registration | | `@joan/procedural-glyph-engine/web-component/register` | Explicit side-effect registration for `` | | `@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: ```js 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: ```js 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 }`. ```js 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. ```js 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: ```js 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` | 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 state’s 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. ```js 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 `` in a browser and is safe to evaluate during SSR: ```js import "@joan/procedural-glyph-engine/web-component"; ``` An explicit side-effect entry is also available for application bootstrap: ```js import "@joan/procedural-glyph-engine/web-component/register"; ``` ```html ``` Size the host with CSS; its default size is `68px × 68px`. ```css 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. ```js 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: ```js 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: ```js 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. ```js 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: ```js 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: ```js 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. ```js 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. ```js 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: ```js 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. ```js 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: ```js 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 100–120 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. ```sh # 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: ```sh 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 24–36 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](https://www.apache.org/licenses/LICENSE-2.0) subject to the [Commons Clause License Condition v1.0](https://commonsclause.com/). - 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`](./LICENSE) for the complete terms and [`NOTICE`](./NOTICE) for attribution. ---

Live Studio · Download kit · Back to top