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
944 lines
38 KiB
Markdown
944 lines
38 KiB
Markdown
<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&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&labelColor=111111" />
|
||
<img alt="Zero runtime dependencies" src="https://img.shields.io/badge/dependencies-0-555555?style=flat-square&labelColor=111111" />
|
||
<img alt="TypeScript declarations included" src="https://img.shields.io/badge/types-TypeScript-555555?style=flat-square&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&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 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 `<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 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.
|
||
|
||
---
|
||
|
||
<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>
|