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
38 KiB
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
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
- Why Orby
- What Orby includes
- Self-service download kit
- Architecture
- The 25 semantic sprites
- Runtime API
- Web component
- Custom glyphs
- Signals, audio, and progress
- Export
- Timed state sequences
- StateDirector
- Studio preset library
- Accessibility and reduced motion
- Development, test, and build
- Performance guidance
- License
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, andkaleidoscope. - 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, andfield-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, andinstant. - Nine Canvas 2D pixel shapes:
disc,square,diamond,capsule,line,ring,cross,square-cross, andsquare-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.htmlStudio; - package metadata and a runtime manifest;
- the complete
LICENSEandNOTICE; and SHA256SUMS.txtcovering 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 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.
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 anariaLiveelement 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
ariaLiveonly if the surrounding product does not already announce the same state. reducedMotion: "system"is the default. It followsprefers-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: trueto force the static behavior orfalseonly 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
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 callrenderOnce()only when a preview needs updating. - Use
fps: 24or30for ambient UI. Reserve 60 fps for close, interactive motion. - Set
offPixels: falseto remove the background-dot draw pass. Disable the painted background withbackground: falsewhen the product surface already supplies one. quality: "auto"adapts betweenhigh,balanced, andlowfrom measured frame cost. Read the effective tier fromstats.qualityor subscribe toqualitychange. Balanced and low tiers time-slice field sampling across the grid, and low also skips the glow path. Use a fixed high tier orrenderOnce()for full-grid deterministic visual fixtures.- Cap device-pixel work with
dprMax; a value of1is often enough for pixel-art thumbnails and dense dashboards. - Disable pointer work with
interactive: falsefor 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
statsevent (fps,frameMs,sampledPixels,activePixels, andresolution) 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.