Files
oikos/web/vendor/README.md
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

944 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<p align="center">
<img
src="./docs/assets/readme-hero.svg"
alt="Orby — A procedural glyph engine by Joan Sterjo, shown as a deterministic pixel field"
width="100%"
/>
</p>
<h1 align="center">Orby</h1>
<p align="center">
<strong>A procedural glyph engine by Joan Sterjo.</strong><br />
Design one visual identity, invoke semantic states from product code, and keep
every transition responsive, reproducible, and alive.
</p>
<p align="center">
<a href="https://joansterjo-celonis.github.io/Procedural-glyph-engine/"><strong>Open the live Studio</strong></a>
·
<a href="https://joansterjo-celonis.github.io/Procedural-glyph-engine/#download">Download v5.0.0</a>
·
<a href="./docs/QUICKSTART.md">Quick-start guide</a>
·
<a href="./examples/README.md">Runnable examples</a>
</p>
<p align="center">
<img alt="Version 5.0.0" src="https://img.shields.io/badge/version-5.0.0-555555?style=flat-square&amp;labelColor=111111" />
<img alt="Canvas 2D and native ESM" src="https://img.shields.io/badge/runtime-Canvas_2D_%C2%B7_ESM-555555?style=flat-square&amp;labelColor=111111" />
<img alt="Zero runtime dependencies" src="https://img.shields.io/badge/dependencies-0-555555?style=flat-square&amp;labelColor=111111" />
<img alt="TypeScript declarations included" src="https://img.shields.io/badge/types-TypeScript-555555?style=flat-square&amp;labelColor=111111" />
<img alt="Source-available license: Apache 2.0 with Commons Clause" src="https://img.shields.io/badge/license-Apache_2.0_%2B_Commons_Clause-555555?style=flat-square&amp;labelColor=111111" />
</p>
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](./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) |
<details>
<summary><strong>Complete contents</strong></summary>
- [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)
</details>
## 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
<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](#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 `<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:
```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.
<details>
<summary><strong>View all 25 canonical state IDs</strong></summary>
| 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 |
</details>
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:
```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<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.
```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 `<joan-glyph>` 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
<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`.
```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 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.
```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
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](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.
---
<p align="center">
<a href="https://joansterjo-celonis.github.io/Procedural-glyph-engine/">Live Studio</a>
·
<a href="https://joansterjo-celonis.github.io/Procedural-glyph-engine/#download">Download kit</a>
·
<a href="#orby">Back to top</a>
</p>