Compare commits
1 Commits
main
...
chore/vend
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cd4cf98c3 |
@@ -8,7 +8,8 @@ FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /build/web
|
||||
COPY web/package.json web/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY web/vendor /build/vendor
|
||||
RUN npm install --no-audit --no-fund
|
||||
COPY VERSION ./
|
||||
COPY web/ ./
|
||||
RUN npm run build
|
||||
|
||||
660
web/package-lock.json
generated
660
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -42,7 +42,7 @@
|
||||
"vitest": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joan/procedural-glyph-engine": "file:../../../../../private/tmp/orby-pkg",
|
||||
"@joan/procedural-glyph-engine": "file:../vendor",
|
||||
"@surdeddd/wmkit": "^0.3.0",
|
||||
"clsx": "^2.1.1",
|
||||
"d3-force": "^3.0.0",
|
||||
|
||||
224
web/vendor/LICENSE
vendored
Normal file
224
web/vendor/LICENSE
vendored
Normal file
@@ -0,0 +1,224 @@
|
||||
"Commons Clause" License Condition v1.0
|
||||
|
||||
The Software is provided to you by the Licensor under the License, as defined
|
||||
below, subject to the following condition.
|
||||
|
||||
Without limiting other conditions in the License, the grant of rights under the
|
||||
License will not include, and the License does not grant to you, the right to
|
||||
Sell the Software.
|
||||
|
||||
For purposes of the foregoing, "Sell" means practicing any or all of the rights
|
||||
granted to you under the License to provide to third parties, for a fee or other
|
||||
consideration (including without limitation fees for hosting or consulting/
|
||||
support services related to the Software), a product or service whose value
|
||||
derives, entirely or substantially, from the functionality of the Software. Any
|
||||
license notice or attribution required by the License must also include this
|
||||
Commons Clause License Condition notice.
|
||||
|
||||
Software: Orby
|
||||
License: Apache License 2.0
|
||||
Licensor: Joan Sterjo
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
7
web/vendor/NOTICE
vendored
Normal file
7
web/vendor/NOTICE
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
Orby
|
||||
Copyright 2026 Joan Sterjo
|
||||
|
||||
This product includes software developed by Joan Sterjo.
|
||||
|
||||
The Apache License 2.0 grant is subject to the Commons Clause License
|
||||
Condition v1.0. See LICENSE for the complete terms.
|
||||
943
web/vendor/README.md
vendored
Normal file
943
web/vendor/README.md
vendored
Normal file
@@ -0,0 +1,943 @@
|
||||
<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>
|
||||
36
web/vendor/SHA256SUMS.txt
vendored
Normal file
36
web/vendor/SHA256SUMS.txt
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
6dea5372df18fef1b3056ad6423552c7785a8a17e2f9fec705463f6028439b28 docs/assets/readme-hero.svg
|
||||
99b15d457eeba4e7414bc210d7040019a54f9fa838291bde1d3875c1bbca78e9 docs/QUICKSTART.md
|
||||
01b1bc8b37a39be3d8ae3be7da71668f0bcdfa0bcb07f847f91a59950ecebbc2 examples/canvas.html
|
||||
cc08f683e352db9566b5839bc918302b0e681937cdc035aadcfc7bcae588c291 examples/example.css
|
||||
0c46216df56ce372cafd26033a925f0462b47f510a014f1bc66a8b706b75d410 examples/README.md
|
||||
c0b22bf741c75cc025ab7717a6db44dbc5fd93fb28396767d369c2003a9d83af examples/state-sequence.html
|
||||
6802fde51637fe054cf870eb2c5f9a49907c474bc5ff347ea71cee7b3d636541 examples/web-component.html
|
||||
cd26e4164fa1fac69c93c45723a4412f68bd667be2c94337d99877a374a71418 joan-engine-v5.standalone.html
|
||||
31763c5e3583d9a5ad610f17456ad1e68f4fb2b73b7f53a735e418c3e1a7e81e LICENSE
|
||||
7a9441bdc68f23ada12bc4782fc3f7962d795f3738735638b754c195fdd6c21d manifest.json
|
||||
55cf377b8808993288f9aadfa309965eb0aaef8df3896fb487cc94c4f65c03b4 NOTICE
|
||||
77fc5e0a77ee82154d3a328827b4cc3e530729bdda8bc18db656f1aa1d916909 package.json
|
||||
a85a9d9b7f723672b630882d54a3c7e82675afb6d14ebcedf63f0e66e16f4bcd README.md
|
||||
324f45dfca15074ab65508e433cc20aa2124a196dad09d98d0a1ed7c27e7c503 src/config.js
|
||||
2bb9921205babd2204bdc03abc8a1bd2c916fa4ce4c203d8822e18289dd6f15d src/control-help.js
|
||||
547603442b1db4739f047833a3da8ea6ed518c49753260e2d488417004340fae src/fields.js
|
||||
a36015cbe69f7ce3b5c218189a4250a6baef7afe814492ed051643312bd34bf5 src/glyphs.js
|
||||
a759eaad654030e5b143f02641a2a4c41f997641e0add34ba03ee217861d2dc0 src/joan-engine.js
|
||||
1237112f05e55f46341e2b10e12f4b372d39cd7c34e05e62af8bb7ceb894250b src/preview-layout.js
|
||||
75c0be5f862135dac7b598974f99fb91e1b3a925c28d69815f374d4ff17ef86b src/preview-thumbnails.js
|
||||
c69b77f9c9592a0f9e417ee7be17f430139713945120d01478d43b7de587390d src/sprites.js
|
||||
175a7172707611fe456db159182350d0967665f673c231f443e69660ae09038c src/state-director.js
|
||||
ea45c075e503ee0dd4545556f61bbceff309dea722ceda96d5eb7baafa4f4e67 src/state-sequence.js
|
||||
55f4d43cf50928aed47d9233653ab971593995923005154f926814acecb43129 src/studio.js
|
||||
4372628fbca51ab2a19e4fbb0d2fd21bb0ef852fe17a61876188f1cacdab6aea src/styles.css
|
||||
7b61ac0803413c4c766db4f03c049dc24ee47688086abb85e7ddfdc9db0250b7 src/web-component-register.js
|
||||
4952d2ccdfdf98cc778840b464521983ac6e8ab215f90b1d865680258f808ac6 src/web-component.js
|
||||
f1d8fe2141b766da1b4bba4b88306a436f0f1971fce8caf26f18a562e0e65abb types/config.d.ts
|
||||
5d0c433ecf5356ed46a3edd318612ab76e818df1b4497a6c195b4c0c1f4c7cb5 types/fields.d.ts
|
||||
59aa75e1149319de9b191c44feddeeef8e1a12ce4e662b057c2ecc31a7ca6e3a types/glyphs.d.ts
|
||||
a62e4a17927b318a05580863141610b0767a00ee87031e17090e3597019572a3 types/index.d.ts
|
||||
99309aab1f114260456db816072039a7f1d65a8ec5bf3fb5d3d0ec4c4fcd5536 types/sprites.d.ts
|
||||
22a56518acd9626699d5bb39bb63a29aa285ef825b0b08bc4ec6bb6111cf4fb2 types/state-director.d.ts
|
||||
3a6bc92f0e3e774d8bbabac49b6b35209ff6995cde04d5f9e37b8cc7c4b4f5db types/state-sequence.d.ts
|
||||
59f77bee8bfe215a88d248f8ca06362905362154026f4868bbab58e733271a25 types/web-component-register.d.ts
|
||||
127ee9b7678fb4090207a4d8784643ab52f5ec5db2567bf61dd0fefee40c8b50 types/web-component.d.ts
|
||||
112
web/vendor/docs/QUICKSTART.md
vendored
Normal file
112
web/vendor/docs/QUICKSTART.md
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
# Orby — quick start
|
||||
|
||||
**A procedural glyph engine by Joan Sterjo.**
|
||||
|
||||
The download kit is self-contained. Start with the path that matches how you
|
||||
want to use the engine; no registry download or third-party runtime dependency
|
||||
is required.
|
||||
|
||||
## 1. Open the standalone Studio
|
||||
|
||||
Open `joan-engine-v5.standalone.html` in a modern browser. It contains the
|
||||
Studio, engine modules, and styles in one file, so it works without a build
|
||||
step or local server. Use this path to explore states, tune a recipe, and export
|
||||
PNG, SVG, animated SVG, or `.joan.json` output.
|
||||
|
||||
## 2. Import the native ES modules
|
||||
|
||||
Serve the extracted folder from a local HTTP server:
|
||||
|
||||
```sh
|
||||
cd joan-procedural-glyph-engine-5.0.0
|
||||
python3 -m http.server 4173
|
||||
```
|
||||
|
||||
Then import the engine directly from the included source:
|
||||
|
||||
```html
|
||||
<canvas id="ai-glyph" width="160" height="160"></canvas>
|
||||
<script type="module">
|
||||
import { createGlyph } from "./src/joan-engine.js";
|
||||
|
||||
const glyph = createGlyph("#ai-glyph", {
|
||||
sprite: "ai.idle",
|
||||
seed: "product-session",
|
||||
gridSize: 68,
|
||||
});
|
||||
|
||||
await glyph.transitionTo("ai.thinking");
|
||||
await glyph.transitionTo("status.success", {
|
||||
transition: "path-draw",
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
Open `examples/canvas.html` for a complete interactive version.
|
||||
|
||||
## 3. Install the extracted folder locally
|
||||
|
||||
From an application beside the extracted kit:
|
||||
|
||||
```sh
|
||||
npm install ./joan-procedural-glyph-engine-5.0.0
|
||||
```
|
||||
|
||||
Use the package and its TypeScript declarations through the included exports:
|
||||
|
||||
```js
|
||||
import { createGlyph } from "@joan/procedural-glyph-engine";
|
||||
import { playStateSequence } from
|
||||
"@joan/procedural-glyph-engine/state-sequence";
|
||||
|
||||
const glyph = createGlyph("#ai-glyph", {
|
||||
sprite: "ai.ambient-idle",
|
||||
seed: "conversation-42",
|
||||
gridSize: 68,
|
||||
});
|
||||
|
||||
const playback = playStateSequence(glyph, [
|
||||
{ sprite: "ai.ambient-idle", holdMs: 5_000 },
|
||||
{ sprite: "ai.progress", transition: "contour-trace" },
|
||||
{ sprite: "status.success", transition: "path-draw" },
|
||||
]);
|
||||
|
||||
await playback.finished;
|
||||
```
|
||||
|
||||
## Web component
|
||||
|
||||
Import `src/web-component-register.js`, then use `<joan-glyph>` anywhere in the
|
||||
page. Its intrinsic default is 68 × 68 CSS pixels.
|
||||
|
||||
```html
|
||||
<script type="module" src="./src/web-component-register.js"></script>
|
||||
<joan-glyph sprite="ai.thinking" seed="conversation-42"></joan-glyph>
|
||||
```
|
||||
|
||||
Open `examples/web-component.html` for a runnable example.
|
||||
|
||||
## Runtime essentials
|
||||
|
||||
- `glyph.transitionTo(sprite, options)` performs and awaits a visual handoff.
|
||||
- `glyph.setSprite(sprite)` changes state immediately and remains chainable.
|
||||
- `glyph.setOptions(patch)` updates the live engine safely.
|
||||
- `glyph.signal(type, payload)` injects short-lived product input.
|
||||
- `glyph.exportConfig()` returns a JSON-safe recipe.
|
||||
- `glyph.toSVG()` and `glyph.downloadPNG()` create portable output.
|
||||
- `glyph.destroy()` releases animation frames, observers, and listeners.
|
||||
|
||||
See `README.md` for the complete state catalog, configuration surface, API,
|
||||
events, custom fields and glyphs, orchestration, exports, performance guidance,
|
||||
accessibility, and testing.
|
||||
|
||||
## Verify the kit
|
||||
|
||||
`SHA256SUMS.txt` lists every payload file included in the archive. The website also
|
||||
publishes a checksum beside the ZIP so the archive itself can be verified
|
||||
before extraction.
|
||||
|
||||
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,
|
||||
but may not sell Orby or a product or service whose value derives entirely or
|
||||
substantially from Orby's functionality. See `LICENSE` and `NOTICE` in the kit.
|
||||
162
web/vendor/docs/assets/readme-hero.svg
vendored
Normal file
162
web/vendor/docs/assets/readme-hero.svg
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 760" role="img" aria-labelledby="title description" preserveAspectRatio="xMidYMid meet">
|
||||
<title id="title">Orby — A procedural glyph engine by Joan Sterjo.</title>
|
||||
<desc id="description">A monochrome editorial banner for Orby, a procedural glyph engine by Joan Sterjo, showing a procedural pixel orb and engine statistics: 25 semantic states, 19 fields, 12 transitions, and 9 pixel shapes.</desc>
|
||||
|
||||
<defs>
|
||||
<pattern id="page-grid" width="64" height="64" patternUnits="userSpaceOnUse">
|
||||
<path d="M64 0H0V64" fill="none" stroke="#ffffff" stroke-opacity="0.045" stroke-width="1"/>
|
||||
<circle cx="1" cy="1" r="1" fill="#ffffff" fill-opacity="0.09"/>
|
||||
</pattern>
|
||||
|
||||
<pattern id="micro-grid" width="12" height="12" patternUnits="userSpaceOnUse">
|
||||
<rect x="5.25" y="5.25" width="1.5" height="1.5" fill="#f2f1ee" fill-opacity="0.46"/>
|
||||
</pattern>
|
||||
|
||||
<pattern id="dither-a" width="32" height="32" patternUnits="userSpaceOnUse">
|
||||
<rect x="3" y="3" width="3" height="3" fill="#f2f1ee"/>
|
||||
<rect x="19" y="7" width="2" height="2" fill="#f2f1ee" fill-opacity="0.72"/>
|
||||
<rect x="10" y="22" width="4" height="4" fill="#f2f1ee" fill-opacity="0.9"/>
|
||||
<rect x="27" y="26" width="2" height="2" fill="#f2f1ee" fill-opacity="0.5"/>
|
||||
</pattern>
|
||||
|
||||
<pattern id="dither-b" width="48" height="48" patternUnits="userSpaceOnUse" patternTransform="rotate(9)">
|
||||
<rect x="5" y="8" width="6" height="6" fill="#f2f1ee"/>
|
||||
<rect x="29" y="3" width="3" height="3" fill="#f2f1ee" fill-opacity="0.7"/>
|
||||
<rect x="18" y="31" width="4" height="4" fill="#f2f1ee" fill-opacity="0.86"/>
|
||||
<rect x="41" y="38" width="2" height="2" fill="#f2f1ee" fill-opacity="0.48"/>
|
||||
</pattern>
|
||||
|
||||
<linearGradient id="field-tone" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#f2f1ee" stop-opacity="0.92"/>
|
||||
<stop offset="0.38" stop-color="#c9c8c4" stop-opacity="0.62"/>
|
||||
<stop offset="0.72" stop-color="#8b8d90" stop-opacity="0.25"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity="0.08"/>
|
||||
</linearGradient>
|
||||
|
||||
<radialGradient id="orb-ground" cx="35%" cy="28%" r="78%">
|
||||
<stop offset="0" stop-color="#252628"/>
|
||||
<stop offset="0.55" stop-color="#111214"/>
|
||||
<stop offset="1" stop-color="#080809"/>
|
||||
</radialGradient>
|
||||
|
||||
<linearGradient id="density-gradient" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="#ffffff"/>
|
||||
<stop offset="0.24" stop-color="#ffffff" stop-opacity="0.9"/>
|
||||
<stop offset="0.56" stop-color="#ffffff" stop-opacity="0.38"/>
|
||||
<stop offset="0.82" stop-color="#ffffff" stop-opacity="0.12"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity="0.02"/>
|
||||
</linearGradient>
|
||||
|
||||
<clipPath id="orb-clip">
|
||||
<circle cx="1232" cy="362" r="238"/>
|
||||
</clipPath>
|
||||
|
||||
<mask id="orb-density" maskUnits="userSpaceOnUse" x="975" y="105" width="514" height="514">
|
||||
<rect x="975" y="105" width="514" height="514" fill="url(#density-gradient)"/>
|
||||
</mask>
|
||||
|
||||
<mask id="crescent" maskUnits="userSpaceOnUse" x="975" y="105" width="514" height="514">
|
||||
<rect x="975" y="105" width="514" height="514" fill="#000000"/>
|
||||
<circle cx="1202" cy="362" r="222" fill="#ffffff"/>
|
||||
<circle cx="1280" cy="326" r="204" fill="#000000"/>
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<rect width="1600" height="760" fill="#050505"/>
|
||||
<rect width="1600" height="760" fill="url(#page-grid)"/>
|
||||
<path d="M0 80H1600M800 0V760M0 632H1600" fill="none" stroke="#ffffff" stroke-opacity="0.08"/>
|
||||
<rect x="24" y="24" width="1552" height="712" rx="24" fill="none" stroke="#ffffff" stroke-opacity="0.12"/>
|
||||
|
||||
<g fill="none" stroke="#f2f1ee" stroke-opacity="0.38">
|
||||
<path d="M48 72V48H72"/>
|
||||
<path d="M1528 48H1552V72"/>
|
||||
<path d="M48 688V712H72"/>
|
||||
<path d="M1528 712H1552V688"/>
|
||||
</g>
|
||||
|
||||
<g font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" fill="#8b8d90">
|
||||
<text x="88" y="84" font-size="13" letter-spacing="4">SYSTEM / 05</text>
|
||||
<text x="312" y="84" font-size="13" letter-spacing="4" fill="#c9c8c4">DETERMINISTIC · LIVE · PORTABLE</text>
|
||||
<text x="1512" y="84" text-anchor="end" font-size="13" letter-spacing="3">V5.0.0</text>
|
||||
</g>
|
||||
|
||||
<g font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif">
|
||||
<text x="82" y="238" fill="#f2f1ee" font-size="128" font-weight="650" letter-spacing="12">ORBY</text>
|
||||
<text x="88" y="292" fill="#c9c8c4" font-size="18" font-weight="600" letter-spacing="3">A PROCEDURAL GLYPH ENGINE BY JOAN STERJO.</text>
|
||||
|
||||
<text x="86" y="398" fill="#f2f1ee" font-size="43" font-weight="420" letter-spacing="-1.3">
|
||||
<tspan x="86" dy="0">A living signal for every</tspan>
|
||||
<tspan x="86" dy="51">state of intelligence.</tspan>
|
||||
</text>
|
||||
|
||||
<text x="88" y="514" fill="#8b8d90" font-size="18" font-weight="400">
|
||||
<tspan x="88" dy="0">Deterministic pixel motion for expressive AI states,</tspan>
|
||||
<tspan x="88" dy="29">responsive transitions, and coherent product presence.</tspan>
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<g aria-hidden="true">
|
||||
<path d="M941 363c-68-58-105-120-88-171 20-60 111-83 224-64 114 19 231 76 305 146 74 70 105 145 77 197-23 43-91 62-179 52" fill="none" stroke="#f2f1ee" stroke-opacity="0.11" stroke-dasharray="2 9"/>
|
||||
<path d="M1004 576c-78 23-137 12-153-29-22-57 47-151 161-221 114-70 249-102 333-72 69 25 76 87 27 158" fill="none" stroke="#c9c8c4" stroke-opacity="0.13" stroke-dasharray="1 8"/>
|
||||
|
||||
<circle cx="1232" cy="362" r="266" fill="none" stroke="#ffffff" stroke-opacity="0.07"/>
|
||||
<circle cx="1232" cy="362" r="250" fill="none" stroke="#c9c8c4" stroke-opacity="0.18" stroke-dasharray="2 7"/>
|
||||
<circle cx="1232" cy="362" r="238" fill="url(#orb-ground)" stroke="#f2f1ee" stroke-opacity="0.38"/>
|
||||
|
||||
<g clip-path="url(#orb-clip)">
|
||||
<rect x="990" y="120" width="484" height="484" fill="url(#micro-grid)" opacity="0.48"/>
|
||||
<rect x="984" y="114" width="496" height="496" fill="url(#dither-a)" mask="url(#orb-density)" opacity="0.82"/>
|
||||
<rect x="984" y="114" width="496" height="496" fill="url(#dither-b)" mask="url(#crescent)" opacity="0.95"/>
|
||||
|
||||
<path d="M1017 425c82-53 172-45 212 10 39 54 9 136-69 178" fill="none" stroke="#f2f1ee" stroke-opacity="0.42" stroke-width="2" stroke-dasharray="2 10"/>
|
||||
<path d="M1091 172c60 56 146 74 207 39 57-32 79-99 55-159" fill="none" stroke="#f2f1ee" stroke-opacity="0.24" stroke-dasharray="2 8"/>
|
||||
<path d="M1140 322c18-63 83-99 143-79 60 20 92 87 69 148-24 61-92 89-150 62-57-26-80-95-51-151" fill="none" stroke="#f2f1ee" stroke-opacity="0.36" stroke-dasharray="1 7"/>
|
||||
<path d="M1178 342c9-37 47-59 83-49 37 10 58 49 46 86-12 36-51 56-87 42-35-13-52-54-37-88" fill="none" stroke="#f2f1ee" stroke-opacity="0.28" stroke-dasharray="1 6"/>
|
||||
|
||||
<path d="M994 362H1470M1232 124V600" stroke="#ffffff" stroke-opacity="0.055" stroke-dasharray="1 11"/>
|
||||
</g>
|
||||
|
||||
<circle cx="1232" cy="362" r="176" fill="none" stroke="#ffffff" stroke-opacity="0.16" stroke-dasharray="1 8"/>
|
||||
<circle cx="1232" cy="362" r="72" fill="none" stroke="#f2f1ee" stroke-opacity="0.32" stroke-dasharray="3 8"/>
|
||||
<circle cx="1232" cy="362" r="20" fill="none" stroke="#f2f1ee" stroke-opacity="0.62"/>
|
||||
<rect x="1228" y="358" width="8" height="8" fill="#f2f1ee"/>
|
||||
|
||||
<g fill="none" stroke="#c9c8c4" stroke-opacity="0.5">
|
||||
<rect x="948" y="178" width="10" height="10"/>
|
||||
<rect x="1440" y="148" width="8" height="8"/>
|
||||
<rect x="1498" y="336" width="12" height="12"/>
|
||||
<rect x="1434" y="568" width="9" height="9"/>
|
||||
<rect x="968" y="552" width="12" height="12"/>
|
||||
</g>
|
||||
<g fill="#f2f1ee">
|
||||
<rect x="923" y="300" width="5" height="5" opacity="0.75"/>
|
||||
<rect x="1481" y="242" width="6" height="6" opacity="0.55"/>
|
||||
<rect x="1518" y="453" width="5" height="5" opacity="0.8"/>
|
||||
<rect x="1032" y="612" width="4" height="4" opacity="0.52"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">
|
||||
<text x="854" y="84" fill="#8b8d90" font-size="12" letter-spacing="3">LIVE FIELD / 68×68</text>
|
||||
<text x="1510" y="675" fill="#8b8d90" text-anchor="end" font-size="12" letter-spacing="3">STATE / AI.IDLE</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(88 664)" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">
|
||||
<g>
|
||||
<text y="0" fill="#f2f1ee" font-size="30">25</text>
|
||||
<text y="28" fill="#8b8d90" font-size="11" letter-spacing="2.4">SEMANTIC STATES</text>
|
||||
</g>
|
||||
<g transform="translate(190 0)">
|
||||
<text y="0" fill="#f2f1ee" font-size="30">19</text>
|
||||
<text y="28" fill="#8b8d90" font-size="11" letter-spacing="2.4">FIELDS</text>
|
||||
</g>
|
||||
<g transform="translate(330 0)">
|
||||
<text y="0" fill="#f2f1ee" font-size="30">12</text>
|
||||
<text y="28" fill="#8b8d90" font-size="11" letter-spacing="2.4">TRANSITIONS</text>
|
||||
</g>
|
||||
<g transform="translate(510 0)">
|
||||
<text y="0" fill="#f2f1ee" font-size="30">9</text>
|
||||
<text y="28" fill="#8b8d90" font-size="11" letter-spacing="2.4">PIXEL SHAPES</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.9 KiB |
18
web/vendor/examples/README.md
vendored
Normal file
18
web/vendor/examples/README.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Orby runnable examples
|
||||
|
||||
Serve the extracted download folder over HTTP, then open one of these files:
|
||||
|
||||
- `canvas.html` mounts the native canvas engine and invokes semantic states.
|
||||
- `web-component.html` uses the browser-native `<joan-glyph>` element.
|
||||
- `state-sequence.html` runs, pauses, resumes, and restarts a timed sequence.
|
||||
|
||||
For example:
|
||||
|
||||
```sh
|
||||
python3 -m http.server 4173
|
||||
```
|
||||
|
||||
Then visit `http://127.0.0.1:4173/examples/canvas.html`.
|
||||
|
||||
Every import is relative to the files included in the kit. No package registry,
|
||||
bundler, framework, or third-party dependency is required.
|
||||
50
web/vendor/examples/canvas.html
vendored
Normal file
50
web/vendor/examples/canvas.html
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Orby · Canvas example</title>
|
||||
<link rel="stylesheet" href="./example.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Native canvas</h1>
|
||||
<p>
|
||||
One long-lived engine instance moves between semantic states while
|
||||
preserving its seeded visual identity.
|
||||
</p>
|
||||
<div class="glyph-stage">
|
||||
<canvas id="aiGlyph" width="440" height="440" aria-label="Animated AI state"></canvas>
|
||||
</div>
|
||||
<div class="actions" role="group" aria-label="Choose a semantic state">
|
||||
<button type="button" data-sprite="ai.idle">Ready</button>
|
||||
<button type="button" data-sprite="ai.thinking">Thinking</button>
|
||||
<button type="button" data-sprite="ai.progress">Progress</button>
|
||||
<button type="button" data-sprite="status.success">Done</button>
|
||||
</div>
|
||||
<p id="stateStatus" role="status">Current state: ai.idle</p>
|
||||
</main>
|
||||
|
||||
<script type="module">
|
||||
import { createGlyph } from "../src/joan-engine.js";
|
||||
|
||||
const glyph = createGlyph("#aiGlyph", {
|
||||
sprite: "ai.idle",
|
||||
seed: "self-service-example",
|
||||
gridSize: 68,
|
||||
});
|
||||
const status = document.querySelector("#stateStatus");
|
||||
|
||||
document.querySelectorAll("[data-sprite]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
const sprite = button.dataset.sprite;
|
||||
status.textContent = `Transitioning to ${sprite}…`;
|
||||
await glyph.transitionTo(sprite);
|
||||
status.textContent = `Current state: ${sprite}`;
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("pagehide", () => glyph.destroy(), { once: true });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
90
web/vendor/examples/example.css
vendored
Normal file
90
web/vendor/examples/example.css
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
background: #050505;
|
||||
color: #f2f1ee;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background:
|
||||
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
||||
#050505;
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
main {
|
||||
width: min(680px, calc(100% - 32px));
|
||||
padding: 32px;
|
||||
border: 1px solid #2b2b2d;
|
||||
border-radius: 14px;
|
||||
background: #0b0b0c;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(2rem, 7vw, 4rem);
|
||||
font-weight: 300;
|
||||
letter-spacing: -0.055em;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 52ch;
|
||||
color: #9b9b9f;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.glyph-stage {
|
||||
min-height: 320px;
|
||||
margin-top: 28px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid #232426;
|
||||
border-radius: 10px;
|
||||
background: #070708;
|
||||
}
|
||||
|
||||
canvas,
|
||||
joan-glyph {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 42px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #35373a;
|
||||
border-radius: 7px;
|
||||
background: #0f1011;
|
||||
color: #e8e8e4;
|
||||
font: 600 0.72rem ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
button:focus-visible {
|
||||
border-color: #f2f1ee;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
[role="status"] {
|
||||
min-height: 1.4em;
|
||||
font: 0.78rem ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
84
web/vendor/examples/state-sequence.html
vendored
Normal file
84
web/vendor/examples/state-sequence.html
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Orby · State sequence example</title>
|
||||
<link rel="stylesheet" href="./example.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Timed sequence</h1>
|
||||
<p>
|
||||
Define a product-owned timeline, then pause, resume, stop, or replay it
|
||||
through one controller.
|
||||
</p>
|
||||
<div class="glyph-stage">
|
||||
<canvas id="aiGlyph" width="440" height="440" aria-label="Animated AI state sequence"></canvas>
|
||||
</div>
|
||||
<div class="actions" role="group" aria-label="Sequence controls">
|
||||
<button id="replay" type="button">Replay</button>
|
||||
<button id="pause" type="button">Pause</button>
|
||||
<button id="resume" type="button">Resume</button>
|
||||
<button id="stop" type="button">Stop</button>
|
||||
</div>
|
||||
<p id="stateStatus" role="status">Sequence ready</p>
|
||||
</main>
|
||||
|
||||
<script type="module">
|
||||
import { createGlyph } from "../src/joan-engine.js";
|
||||
import { playStateSequence } from "../src/state-sequence.js";
|
||||
|
||||
const glyph = createGlyph("#aiGlyph", {
|
||||
sprite: "ai.idle",
|
||||
seed: "self-service-sequence",
|
||||
gridSize: 68,
|
||||
});
|
||||
const status = document.querySelector("#stateStatus");
|
||||
let playback;
|
||||
|
||||
const play = () => {
|
||||
playback?.stop();
|
||||
const nextPlayback = playStateSequence(glyph, [
|
||||
{ sprite: "ai.idle", holdMs: 1_200 },
|
||||
{ sprite: "ai.thinking", holdMs: 2_000 },
|
||||
{ sprite: "ai.progress", holdMs: 1_200 },
|
||||
{ sprite: "status.success", transition: "path-draw" },
|
||||
]);
|
||||
playback = nextPlayback;
|
||||
status.textContent = "Sequence playing";
|
||||
nextPlayback.finished.then(
|
||||
() => {
|
||||
if (playback === nextPlayback) status.textContent = "Sequence complete";
|
||||
},
|
||||
(error) => {
|
||||
if (playback === nextPlayback) {
|
||||
status.textContent = `Sequence ended: ${error.name}`;
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
document.querySelector("#replay").addEventListener("click", play);
|
||||
document.querySelector("#pause").addEventListener("click", () => {
|
||||
playback?.pause();
|
||||
status.textContent = "Sequence paused";
|
||||
});
|
||||
document.querySelector("#resume").addEventListener("click", () => {
|
||||
playback?.resume();
|
||||
status.textContent = "Sequence playing";
|
||||
});
|
||||
document.querySelector("#stop").addEventListener("click", () => {
|
||||
playback?.stop();
|
||||
status.textContent = "Sequence stopped";
|
||||
});
|
||||
|
||||
window.addEventListener("pagehide", () => {
|
||||
playback?.stop();
|
||||
glyph.destroy();
|
||||
}, { once: true });
|
||||
|
||||
play();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
48
web/vendor/examples/web-component.html
vendored
Normal file
48
web/vendor/examples/web-component.html
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Orby · Web component example</title>
|
||||
<link rel="stylesheet" href="./example.css" />
|
||||
<script type="module" src="../src/web-component-register.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Web component</h1>
|
||||
<p>
|
||||
Drop the dependency-free custom element into a page, then drive its
|
||||
attributes or call the forwarded engine methods.
|
||||
</p>
|
||||
<div class="glyph-stage">
|
||||
<joan-glyph
|
||||
id="glyph"
|
||||
sprite="ai.ambient-thinking"
|
||||
seed="self-service-component"
|
||||
resolution="68"
|
||||
orb-boundary="gestalt"
|
||||
></joan-glyph>
|
||||
</div>
|
||||
<div class="actions" role="group" aria-label="Choose a semantic state">
|
||||
<button type="button" data-sprite="ai.ambient-idle">Ambient ready</button>
|
||||
<button type="button" data-sprite="ai.ambient-thinking">Thinking</button>
|
||||
<button type="button" data-sprite="status.success">Done</button>
|
||||
</div>
|
||||
<p id="stateStatus" role="status">Current state: ai.ambient-thinking</p>
|
||||
</main>
|
||||
|
||||
<script type="module">
|
||||
const glyph = document.querySelector("#glyph");
|
||||
const status = document.querySelector("#stateStatus");
|
||||
|
||||
document.querySelectorAll("[data-sprite]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
const sprite = button.dataset.sprite;
|
||||
status.textContent = `Transitioning to ${sprite}…`;
|
||||
await glyph.transitionTo(sprite);
|
||||
status.textContent = `Current state: ${sprite}`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
5592
web/vendor/joan-engine-v5.standalone.html
vendored
Normal file
5592
web/vendor/joan-engine-v5.standalone.html
vendored
Normal file
File diff suppressed because one or more lines are too long
11
web/vendor/manifest.json
vendored
Normal file
11
web/vendor/manifest.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@joan/procedural-glyph-engine",
|
||||
"version": "5.0.0",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"entry": "./src/joan-engine.js",
|
||||
"demo": "./index.html",
|
||||
"assets": [
|
||||
"./src/styles.css"
|
||||
],
|
||||
"format": "esm"
|
||||
}
|
||||
106
web/vendor/package.json
vendored
Normal file
106
web/vendor/package.json
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"name": "@joan/procedural-glyph-engine",
|
||||
"version": "5.0.0",
|
||||
"description": "Orby — A procedural glyph engine by Joan Sterjo.",
|
||||
"author": "Joan Sterjo",
|
||||
"homepage": "https://joansterjo-celonis.github.io/Procedural-glyph-engine/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/joansterjo-celonis/Procedural-glyph-engine.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/joansterjo-celonis/Procedural-glyph-engine/issues"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "./types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"import": "./src/joan-engine.js",
|
||||
"default": "./src/joan-engine.js"
|
||||
},
|
||||
"./config": {
|
||||
"types": "./types/config.d.ts",
|
||||
"import": "./src/config.js",
|
||||
"default": "./src/config.js"
|
||||
},
|
||||
"./fields": {
|
||||
"types": "./types/fields.d.ts",
|
||||
"import": "./src/fields.js",
|
||||
"default": "./src/fields.js"
|
||||
},
|
||||
"./glyphs": {
|
||||
"types": "./types/glyphs.d.ts",
|
||||
"import": "./src/glyphs.js",
|
||||
"default": "./src/glyphs.js"
|
||||
},
|
||||
"./sprites": {
|
||||
"types": "./types/sprites.d.ts",
|
||||
"import": "./src/sprites.js",
|
||||
"default": "./src/sprites.js"
|
||||
},
|
||||
"./state-director": {
|
||||
"types": "./types/state-director.d.ts",
|
||||
"import": "./src/state-director.js",
|
||||
"default": "./src/state-director.js"
|
||||
},
|
||||
"./state-sequence": {
|
||||
"types": "./types/state-sequence.d.ts",
|
||||
"import": "./src/state-sequence.js",
|
||||
"default": "./src/state-sequence.js"
|
||||
},
|
||||
"./web-component": {
|
||||
"types": "./types/web-component.d.ts",
|
||||
"import": "./src/web-component.js",
|
||||
"default": "./src/web-component.js"
|
||||
},
|
||||
"./web-component/register": {
|
||||
"types": "./types/web-component-register.d.ts",
|
||||
"import": "./src/web-component-register.js",
|
||||
"default": "./src/web-component-register.js"
|
||||
},
|
||||
"./styles.css": "./src/styles.css",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"sideEffects": [
|
||||
"./src/web-component.js",
|
||||
"./src/web-component-register.js",
|
||||
"./src/styles.css"
|
||||
],
|
||||
"files": [
|
||||
"src",
|
||||
"types",
|
||||
"docs",
|
||||
"examples",
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"NOTICE",
|
||||
"dist/manifest.json",
|
||||
"dist/joan-engine-v5.standalone.html"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.mjs",
|
||||
"build": "node scripts/build.mjs",
|
||||
"test": "node --test",
|
||||
"verify:download": "node scripts/verify-download.mjs",
|
||||
"check": "npm run test && npm run build && npm run verify:download",
|
||||
"prepack": "npm run check"
|
||||
},
|
||||
"keywords": [
|
||||
"orby",
|
||||
"canvas",
|
||||
"glyph",
|
||||
"pixel",
|
||||
"procedural-animation",
|
||||
"ai-status",
|
||||
"sprite",
|
||||
"dither"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
687
web/vendor/src/config.js
vendored
Normal file
687
web/vendor/src/config.js
vendored
Normal file
@@ -0,0 +1,687 @@
|
||||
import { FIELD_ALIASES, FIELD_IDS } from "./fields.js";
|
||||
import { SPRITE_ALIASES, SPRITE_IDS } from "./sprites.js";
|
||||
|
||||
export const TRANSITION_IDS = Object.freeze([
|
||||
"field-morph",
|
||||
"seeded-dissolve",
|
||||
"radial-cascade",
|
||||
"angular-sweep",
|
||||
"scanline",
|
||||
"contour-trace",
|
||||
"path-draw",
|
||||
"axis-flip",
|
||||
"cluster-dissolve",
|
||||
"neighbor-ignite",
|
||||
"glitch-bands",
|
||||
"instant",
|
||||
]);
|
||||
|
||||
export const PIXEL_SWITCH_IDS = Object.freeze([
|
||||
"ordered-dither",
|
||||
"temporal-blue-noise",
|
||||
"threshold-hysteresis",
|
||||
"sdf-wavefront",
|
||||
"contour-trace",
|
||||
"curl-advect",
|
||||
"neighbor-propagation",
|
||||
"radial-cascade",
|
||||
"path-draw",
|
||||
"axis-flip",
|
||||
"seeded-dissolve",
|
||||
"field-morph",
|
||||
]);
|
||||
|
||||
export const PIXEL_SHAPE_IDS = Object.freeze([
|
||||
"disc",
|
||||
"square",
|
||||
"diamond",
|
||||
"capsule",
|
||||
"line",
|
||||
"ring",
|
||||
"cross",
|
||||
"square-cross",
|
||||
"square-cross-ring",
|
||||
]);
|
||||
|
||||
const TRANSITION_ALIASES = Object.freeze({
|
||||
dissolve: "seeded-dissolve",
|
||||
radial: "radial-cascade",
|
||||
bloom: "radial-cascade",
|
||||
spiral: "angular-sweep",
|
||||
wave: "scanline",
|
||||
shutter: "axis-flip",
|
||||
glitch: "glitch-bands",
|
||||
"sdf-wavefront": "contour-trace",
|
||||
"neighbor-propagation": "neighbor-ignite",
|
||||
});
|
||||
|
||||
const PIXEL_SWITCH_ALIASES = Object.freeze({
|
||||
"cluster-dissolve": "seeded-dissolve",
|
||||
"neighbor-ignite": "neighbor-propagation",
|
||||
});
|
||||
|
||||
const PIXEL_SHAPE_ALIASES = Object.freeze({
|
||||
circle: "disc",
|
||||
"rounded-square": "square",
|
||||
dot: "disc",
|
||||
});
|
||||
|
||||
const ENGINE_OPTION_KEYS = Object.freeze([
|
||||
"ariaLive",
|
||||
"autoResize",
|
||||
"autoplay",
|
||||
"background",
|
||||
"canvas",
|
||||
"contrast",
|
||||
"density",
|
||||
"direction",
|
||||
"dprMax",
|
||||
"field",
|
||||
"fieldMode",
|
||||
"fps",
|
||||
"gridSize",
|
||||
"interactive",
|
||||
"nonErrorPalette",
|
||||
"offPixels",
|
||||
"orbBoundary",
|
||||
"orbBackgroundColor",
|
||||
"orbBackgroundMode",
|
||||
"package",
|
||||
"palette",
|
||||
"paletteOverride",
|
||||
"pixelShape",
|
||||
"pixelSwitch",
|
||||
"previewMode",
|
||||
"progress",
|
||||
"quality",
|
||||
"reducedMotion",
|
||||
"seed",
|
||||
"speed",
|
||||
"sprite",
|
||||
"targetGlyph",
|
||||
"transition",
|
||||
"version",
|
||||
]);
|
||||
|
||||
const NUMBER_OPTIONS = Object.freeze({
|
||||
contrast: { minimum: 0.25, maximum: 2 },
|
||||
density: { minimum: 0.35, maximum: 1.6 },
|
||||
dprMax: { minimum: 0.25, maximum: 8 },
|
||||
fps: { minimum: 1, maximum: 120, integer: true },
|
||||
gridSize: { minimum: 8, maximum: 96, integer: true },
|
||||
progress: { minimum: 0, maximum: 1 },
|
||||
speed: { minimum: 0.05, maximum: 5 },
|
||||
});
|
||||
|
||||
const BOOLEAN_OPTIONS = Object.freeze([
|
||||
"autoResize",
|
||||
"autoplay",
|
||||
"background",
|
||||
"interactive",
|
||||
"offPixels",
|
||||
]);
|
||||
|
||||
const normalizeKey = (value) =>
|
||||
String(value ?? "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\s_]+/g, "-");
|
||||
|
||||
function distanceBetween(left, right) {
|
||||
const a = [...String(left)];
|
||||
const b = [...String(right)];
|
||||
const previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
||||
const current = new Array(b.length + 1);
|
||||
|
||||
for (let row = 1; row <= a.length; row += 1) {
|
||||
current[0] = row;
|
||||
for (let column = 1; column <= b.length; column += 1) {
|
||||
current[column] = Math.min(
|
||||
current[column - 1] + 1,
|
||||
previous[column] + 1,
|
||||
previous[column - 1] + (a[row - 1] === b[column - 1] ? 0 : 1),
|
||||
);
|
||||
}
|
||||
for (let column = 0; column <= b.length; column += 1) {
|
||||
previous[column] = current[column];
|
||||
}
|
||||
}
|
||||
return previous[b.length];
|
||||
}
|
||||
|
||||
export function nearestName(value, candidates, options = {}) {
|
||||
const input = normalizeKey(value);
|
||||
if (!input || !Array.isArray(candidates) || candidates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let nearest = null;
|
||||
let nearestDistance = Infinity;
|
||||
for (const candidate of candidates) {
|
||||
const distance = distanceBetween(input, normalizeKey(candidate));
|
||||
if (distance < nearestDistance) {
|
||||
nearest = candidate;
|
||||
nearestDistance = distance;
|
||||
}
|
||||
}
|
||||
const maximum =
|
||||
options.maxDistance ?? Math.max(2, Math.floor(input.length * 0.34));
|
||||
return nearestDistance <= maximum ? nearest : null;
|
||||
}
|
||||
|
||||
export class JoanConfigurationError extends RangeError {
|
||||
constructor(message, details = {}) {
|
||||
super(message);
|
||||
this.name = "JoanConfigurationError";
|
||||
this.code = details.code || "invalid_configuration";
|
||||
this.path = details.path || null;
|
||||
this.value = details.value;
|
||||
this.suggestion = details.suggestion || null;
|
||||
this.allowed = details.allowed ? [...details.allowed] : null;
|
||||
}
|
||||
|
||||
toIssue() {
|
||||
return {
|
||||
code: this.code,
|
||||
message: this.message,
|
||||
path: this.path,
|
||||
value: this.value,
|
||||
suggestion: this.suggestion,
|
||||
severity: "error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function aliasLookup(aliases) {
|
||||
return new Map(
|
||||
Object.entries(aliases || {}).map(([alias, canonical]) => [
|
||||
normalizeKey(alias),
|
||||
canonical,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export function validateKnownValue(value, allowed, options = {}) {
|
||||
const label = options.label || "value";
|
||||
const path = options.path || label;
|
||||
const normalized = normalizeKey(value);
|
||||
const canonical = new Map(
|
||||
allowed.map((candidate) => [normalizeKey(candidate), candidate]),
|
||||
);
|
||||
if (canonical.has(normalized)) return canonical.get(normalized);
|
||||
|
||||
const aliases = aliasLookup(options.aliases);
|
||||
if (aliases.has(normalized)) return aliases.get(normalized);
|
||||
|
||||
const accepted = [...allowed, ...Object.keys(options.aliases || {})];
|
||||
const suggestion = nearestName(value, accepted);
|
||||
const suffix = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
||||
throw new JoanConfigurationError(
|
||||
`Unknown ${label} "${String(value)}".${suffix}`,
|
||||
{
|
||||
code: `unknown_${label.replaceAll(" ", "_")}`,
|
||||
path,
|
||||
value,
|
||||
suggestion,
|
||||
allowed,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const validateSpriteId = (value) =>
|
||||
validateKnownValue(value, SPRITE_IDS, {
|
||||
aliases: SPRITE_ALIASES,
|
||||
label: "sprite",
|
||||
path: "sprite",
|
||||
});
|
||||
|
||||
export const validateFieldId = (value) =>
|
||||
validateKnownValue(value, FIELD_IDS, {
|
||||
aliases: FIELD_ALIASES,
|
||||
label: "field",
|
||||
path: "field",
|
||||
});
|
||||
|
||||
export const validateTransition = (value) =>
|
||||
validateKnownValue(
|
||||
typeof value === "string"
|
||||
? value
|
||||
: value?.name || value?.type || value?.enter,
|
||||
TRANSITION_IDS,
|
||||
{
|
||||
aliases: TRANSITION_ALIASES,
|
||||
label: "transition",
|
||||
path: "transition",
|
||||
},
|
||||
);
|
||||
|
||||
export const validatePixelShape = (value) =>
|
||||
validateKnownValue(
|
||||
typeof value === "string" ? value : value?.shape || value?.name,
|
||||
PIXEL_SHAPE_IDS,
|
||||
{
|
||||
aliases: PIXEL_SHAPE_ALIASES,
|
||||
label: "pixel shape",
|
||||
path: "pixelShape",
|
||||
},
|
||||
);
|
||||
|
||||
export const validatePixelSwitch = (value) =>
|
||||
validateKnownValue(
|
||||
typeof value === "string"
|
||||
? value
|
||||
: value?.mode || value?.name || value?.type,
|
||||
PIXEL_SWITCH_IDS,
|
||||
{
|
||||
aliases: PIXEL_SWITCH_ALIASES,
|
||||
label: "pixel switch",
|
||||
path: "pixelSwitch",
|
||||
},
|
||||
);
|
||||
|
||||
function issueFrom(error, fallbackPath) {
|
||||
if (error instanceof JoanConfigurationError) return error.toIssue();
|
||||
return {
|
||||
code: "invalid_configuration",
|
||||
message: error?.message || "Invalid engine configuration.",
|
||||
path: fallbackPath || null,
|
||||
value: undefined,
|
||||
suggestion: null,
|
||||
severity: "error",
|
||||
};
|
||||
}
|
||||
|
||||
function inspectNumber(key, value, definition, coerce) {
|
||||
if (!coerce && typeof value !== "number") {
|
||||
throw new JoanConfigurationError(`${key} must be a number.`, {
|
||||
code: "invalid_type",
|
||||
path: key,
|
||||
value,
|
||||
});
|
||||
}
|
||||
let next = coerce ? Number(value) : value;
|
||||
if (!Number.isFinite(next)) {
|
||||
throw new JoanConfigurationError(`${key} must be a finite number.`, {
|
||||
code: "invalid_number",
|
||||
path: key,
|
||||
value,
|
||||
});
|
||||
}
|
||||
if (definition.integer && !coerce && !Number.isInteger(next)) {
|
||||
throw new JoanConfigurationError(`${key} must be an integer.`, {
|
||||
code: "invalid_number",
|
||||
path: key,
|
||||
value,
|
||||
});
|
||||
}
|
||||
if (definition.integer) next = Math.round(next);
|
||||
if (next < definition.minimum || next > definition.maximum) {
|
||||
if (!coerce) {
|
||||
throw new JoanConfigurationError(
|
||||
`${key} must be between ${definition.minimum} and ${definition.maximum}.`,
|
||||
{
|
||||
code: "out_of_range",
|
||||
path: key,
|
||||
value,
|
||||
},
|
||||
);
|
||||
}
|
||||
next = Math.min(Math.max(next, definition.minimum), definition.maximum);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function validatePalette(value, path, { allowVariants = true } = {}) {
|
||||
if (value === null) return null;
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new JoanConfigurationError(`${path} must be a palette object or null.`, {
|
||||
code: "invalid_type",
|
||||
path,
|
||||
value,
|
||||
});
|
||||
}
|
||||
const allowed = new Set([
|
||||
"name",
|
||||
"background",
|
||||
"shadow",
|
||||
"off",
|
||||
"ink",
|
||||
"accent",
|
||||
"glow",
|
||||
...(allowVariants ? ["variants"] : []),
|
||||
]);
|
||||
const next = {};
|
||||
for (const [key, color] of Object.entries(value)) {
|
||||
if (!allowed.has(key)) {
|
||||
const suggestion = nearestName(key, [...allowed]);
|
||||
throw new JoanConfigurationError(
|
||||
`Unknown ${path} channel "${key}".${
|
||||
suggestion ? ` Did you mean "${suggestion}"?` : ""
|
||||
}`,
|
||||
{
|
||||
code: "unknown_palette_channel",
|
||||
path: `${path}.${key}`,
|
||||
value: color,
|
||||
suggestion,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (key === "variants") {
|
||||
if (!color || typeof color !== "object" || Array.isArray(color)) {
|
||||
throw new JoanConfigurationError(`${path}.variants must be an object.`, {
|
||||
code: "invalid_type",
|
||||
path: `${path}.variants`,
|
||||
value: color,
|
||||
});
|
||||
}
|
||||
next.variants = Object.fromEntries(
|
||||
Object.entries(color).map(([name, palette]) => [
|
||||
name,
|
||||
validatePalette(palette, `${path}.variants.${name}`, {
|
||||
allowVariants: false,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (typeof color !== "string" || !color.trim()) {
|
||||
throw new JoanConfigurationError(`${path}.${key} must be a CSS color string.`, {
|
||||
code: "invalid_type",
|
||||
path: `${path}.${key}`,
|
||||
value: color,
|
||||
});
|
||||
}
|
||||
next[key] = color.trim();
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
const PORTABLE_COLOR_KEYWORDS = new Set([
|
||||
"aqua",
|
||||
"black",
|
||||
"blue",
|
||||
"fuchsia",
|
||||
"gray",
|
||||
"green",
|
||||
"grey",
|
||||
"lime",
|
||||
"maroon",
|
||||
"navy",
|
||||
"olive",
|
||||
"orange",
|
||||
"purple",
|
||||
"red",
|
||||
"silver",
|
||||
"teal",
|
||||
"transparent",
|
||||
"white",
|
||||
"yellow",
|
||||
]);
|
||||
|
||||
function validateOptionalColor(value, path) {
|
||||
if (value === null) return null;
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
throw new JoanConfigurationError(`${path} must be a CSS color string or null.`, {
|
||||
code: "invalid_type",
|
||||
path,
|
||||
value,
|
||||
});
|
||||
}
|
||||
const candidate = value.trim();
|
||||
if (candidate.toLowerCase() === "transparent") return null;
|
||||
const functionMatch =
|
||||
/^(rgb|rgba|hsl|hsla)\(([\d\s.,%+/-]+)\)$/i.exec(candidate);
|
||||
const portable =
|
||||
/^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i.test(candidate) ||
|
||||
PORTABLE_COLOR_KEYWORDS.has(candidate.toLowerCase()) ||
|
||||
(Boolean(functionMatch) && /\d/.test(functionMatch[2]));
|
||||
if (!portable) {
|
||||
throw new JoanConfigurationError(
|
||||
`${path} must be a portable CSS color (hex, rgb, hsl, a supported keyword) or null.`,
|
||||
{
|
||||
code: "invalid_color",
|
||||
path,
|
||||
value,
|
||||
},
|
||||
);
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function inspectEngineOptions(input, options = {}) {
|
||||
const coerce = options.coerce !== false;
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||
const error = new JoanConfigurationError(
|
||||
"Engine options must be an object.",
|
||||
{ code: "invalid_type", path: "options", value: input },
|
||||
);
|
||||
return { ok: false, value: {}, issues: [error.toIssue()] };
|
||||
}
|
||||
|
||||
const value = { ...input };
|
||||
const issues = [];
|
||||
const allowedKeys = new Set(ENGINE_OPTION_KEYS);
|
||||
if (options.allowUnknown !== true) {
|
||||
for (const key of Object.keys(value)) {
|
||||
if (allowedKeys.has(key)) continue;
|
||||
const suggestion = nearestName(key, ENGINE_OPTION_KEYS);
|
||||
issues.push({
|
||||
code: "unknown_option",
|
||||
message: `Unknown engine option "${key}".${
|
||||
suggestion ? ` Did you mean "${suggestion}"?` : ""
|
||||
}`,
|
||||
path: key,
|
||||
value: value[key],
|
||||
suggestion,
|
||||
severity: "error",
|
||||
});
|
||||
delete value[key];
|
||||
}
|
||||
}
|
||||
|
||||
const apply = (key, callback) => {
|
||||
if (!Object.hasOwn(value, key) || value[key] === undefined) return;
|
||||
try {
|
||||
value[key] = callback(value[key]);
|
||||
} catch (error) {
|
||||
issues.push(issueFrom(error, key));
|
||||
delete value[key];
|
||||
}
|
||||
};
|
||||
|
||||
apply("sprite", (sprite) =>
|
||||
sprite && typeof sprite === "object" ? validateRecipe(sprite) : validateSpriteId(sprite),
|
||||
);
|
||||
apply("field", (field) => (field === null ? null : validateFieldId(field)));
|
||||
apply("transition", (transition) =>
|
||||
transition === null
|
||||
? null
|
||||
: transition && typeof transition === "object"
|
||||
? {
|
||||
...transition,
|
||||
name: validateTransition(
|
||||
transition.name || transition.type || transition.enter,
|
||||
),
|
||||
}
|
||||
: validateTransition(transition),
|
||||
);
|
||||
apply("pixelShape", (shape) =>
|
||||
shape === null ? null : validatePixelShape(shape),
|
||||
);
|
||||
apply("pixelSwitch", (pixelSwitch) =>
|
||||
pixelSwitch === null
|
||||
? null
|
||||
: pixelSwitch && typeof pixelSwitch === "object"
|
||||
? {
|
||||
...pixelSwitch,
|
||||
mode: validatePixelSwitch(
|
||||
pixelSwitch.mode || pixelSwitch.name || pixelSwitch.type,
|
||||
),
|
||||
}
|
||||
: validatePixelSwitch(pixelSwitch),
|
||||
);
|
||||
apply("fieldMode", (mode) =>
|
||||
validateKnownValue(mode, ["recipe", "override"], {
|
||||
label: "field mode",
|
||||
path: "fieldMode",
|
||||
}),
|
||||
);
|
||||
apply("quality", (quality) =>
|
||||
validateKnownValue(quality, ["low", "balanced", "high", "auto"], {
|
||||
label: "quality",
|
||||
path: "quality",
|
||||
}),
|
||||
);
|
||||
apply("previewMode", (mode) =>
|
||||
validateKnownValue(mode, ["fit", "actual"], {
|
||||
label: "preview mode",
|
||||
path: "previewMode",
|
||||
}),
|
||||
);
|
||||
apply("orbBoundary", (boundary) =>
|
||||
validateKnownValue(boundary, ["defined", "gestalt"], {
|
||||
label: "orb boundary",
|
||||
path: "orbBoundary",
|
||||
}),
|
||||
);
|
||||
apply("orbBackgroundColor", (color) =>
|
||||
validateOptionalColor(color, "orbBackgroundColor"),
|
||||
);
|
||||
apply("orbBackgroundMode", (mode) =>
|
||||
validateKnownValue(mode, ["none", "solid", "pixelated"], {
|
||||
label: "orb background mode",
|
||||
path: "orbBackgroundMode",
|
||||
}),
|
||||
);
|
||||
apply("reducedMotion", (mode) => {
|
||||
if (typeof mode === "boolean" || mode === "system") return mode;
|
||||
throw new JoanConfigurationError(
|
||||
'reducedMotion must be true, false, or "system".',
|
||||
{ code: "invalid_type", path: "reducedMotion", value: mode },
|
||||
);
|
||||
});
|
||||
|
||||
for (const [key, definition] of Object.entries(NUMBER_OPTIONS)) {
|
||||
apply(key, (number) => inspectNumber(key, number, definition, coerce));
|
||||
}
|
||||
for (const key of BOOLEAN_OPTIONS) {
|
||||
apply(key, (boolean) => {
|
||||
if (typeof boolean === "boolean") return boolean;
|
||||
if (coerce && (boolean === "true" || boolean === "false")) {
|
||||
return boolean === "true";
|
||||
}
|
||||
throw new JoanConfigurationError(`${key} must be a boolean.`, {
|
||||
code: "invalid_type",
|
||||
path: key,
|
||||
value: boolean,
|
||||
});
|
||||
});
|
||||
}
|
||||
for (const key of ["palette", "paletteOverride", "nonErrorPalette"]) {
|
||||
apply(key, (palette) => validatePalette(palette, key));
|
||||
}
|
||||
|
||||
return { ok: issues.length === 0, value, issues };
|
||||
}
|
||||
|
||||
export function validateEngineOptions(input, options = {}) {
|
||||
const strict = options.strict !== false;
|
||||
const result = inspectEngineOptions(input, {
|
||||
...options,
|
||||
coerce: options.coerce ?? !strict,
|
||||
});
|
||||
if (!result.ok && strict) {
|
||||
const issue = result.issues[0];
|
||||
throw new JoanConfigurationError(issue.message, issue);
|
||||
}
|
||||
return result.value;
|
||||
}
|
||||
|
||||
export function validateRecipe(input) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||
throw new JoanConfigurationError("A recipe must be an object.", {
|
||||
code: "invalid_type",
|
||||
path: "recipe",
|
||||
value: input,
|
||||
});
|
||||
}
|
||||
if (typeof input.id !== "string" || !input.id.trim()) {
|
||||
throw new JoanConfigurationError("A recipe requires a non-empty id.", {
|
||||
code: "missing_recipe_id",
|
||||
path: "recipe.id",
|
||||
value: input.id,
|
||||
});
|
||||
}
|
||||
if (typeof input.glyph !== "string" || !input.glyph.trim()) {
|
||||
throw new JoanConfigurationError("A recipe requires a glyph id.", {
|
||||
code: "missing_recipe_glyph",
|
||||
path: "recipe.glyph",
|
||||
value: input.glyph,
|
||||
});
|
||||
}
|
||||
|
||||
const recipe = { ...input, id: input.id.trim(), glyph: input.glyph.trim() };
|
||||
if (recipe.field !== undefined) recipe.field = validateFieldId(recipe.field);
|
||||
if (Array.isArray(recipe.fieldMix)) {
|
||||
recipe.fieldMix = recipe.fieldMix.map((layer, index) => {
|
||||
if (!layer || typeof layer !== "object") {
|
||||
throw new JoanConfigurationError(`recipe.fieldMix[${index}] must be an object.`, {
|
||||
code: "invalid_type",
|
||||
path: `recipe.fieldMix[${index}]`,
|
||||
value: layer,
|
||||
});
|
||||
}
|
||||
return { ...layer, field: validateFieldId(layer.field) };
|
||||
});
|
||||
} else if (!recipe.field) {
|
||||
throw new JoanConfigurationError("A recipe requires field or fieldMix.", {
|
||||
code: "missing_recipe_field",
|
||||
path: "recipe.field",
|
||||
value: recipe.field,
|
||||
});
|
||||
}
|
||||
|
||||
if (recipe.pixelShape !== undefined) {
|
||||
recipe.pixelShape = validatePixelShape(recipe.pixelShape);
|
||||
}
|
||||
if (recipe.pixel?.shape !== undefined) {
|
||||
recipe.pixel = {
|
||||
...recipe.pixel,
|
||||
shape: validatePixelShape(recipe.pixel.shape),
|
||||
};
|
||||
}
|
||||
if (recipe.pixelSwitch !== undefined) {
|
||||
if (typeof recipe.pixelSwitch === "string") {
|
||||
recipe.pixelSwitch = validatePixelSwitch(recipe.pixelSwitch);
|
||||
} else if (recipe.pixelSwitch?.mode !== undefined) {
|
||||
recipe.pixelSwitch = {
|
||||
...recipe.pixelSwitch,
|
||||
mode: validatePixelSwitch(recipe.pixelSwitch.mode),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (recipe.transition !== undefined) {
|
||||
if (typeof recipe.transition === "string") {
|
||||
recipe.transition = validateTransition(recipe.transition);
|
||||
} else if (recipe.transition && typeof recipe.transition === "object") {
|
||||
const transition = { ...recipe.transition };
|
||||
for (const key of ["name", "type", "enter", "exit"]) {
|
||||
if (transition[key] !== undefined) {
|
||||
transition[key] = validateTransition(transition[key]);
|
||||
}
|
||||
}
|
||||
recipe.transition = transition;
|
||||
}
|
||||
}
|
||||
return recipe;
|
||||
}
|
||||
|
||||
function deepFreeze(value, seen = new WeakSet()) {
|
||||
if (!value || typeof value !== "object" || seen.has(value)) return value;
|
||||
seen.add(value);
|
||||
for (const nested of Object.values(value)) deepFreeze(nested, seen);
|
||||
return Object.freeze(value);
|
||||
}
|
||||
|
||||
export function defineRecipe(recipe) {
|
||||
return deepFreeze(validateRecipe(recipe));
|
||||
}
|
||||
207
web/vendor/src/control-help.js
vendored
Normal file
207
web/vendor/src/control-help.js
vendored
Normal file
@@ -0,0 +1,207 @@
|
||||
export const FIELD_HELP = Object.freeze({
|
||||
__recipe__:
|
||||
"Uses the selected state’s authored blend of fields, preserving its intended visual character.",
|
||||
fbm: "Builds soft, organic variation by layering several scales of smooth gradient noise.",
|
||||
ridged:
|
||||
"Folds layered noise into sharp ridges and filaments for a more etched texture.",
|
||||
"domain-warp":
|
||||
"Distorts layered noise with two moving noise fields, creating fluid folds and swirls.",
|
||||
curl: "Turns the curl magnitude of animated noise into turbulent, smoke-like density.",
|
||||
flow: "Carries animated ribbons along a curl-noise direction for a flowing texture.",
|
||||
worley:
|
||||
"Creates bright cellular islands around moving feature points, separated by darker gaps.",
|
||||
voronoi:
|
||||
"Smoothly morphs between seeded cellular layouts while keeping their moving division lines crisp.",
|
||||
plasma:
|
||||
"Combines layered sine waves with noise-driven phase shifts for an energetic plasma pattern.",
|
||||
interference:
|
||||
"Overlaps circular waves from moving emitters to create traveling beats and bands.",
|
||||
vortex:
|
||||
"Spins noisy spiral arms around the center for a rotating whirlpool-like field.",
|
||||
metaballs:
|
||||
"Blends several seeded moving blobs so nearby forms merge into one liquid mass.",
|
||||
caustics:
|
||||
"Produces sharp cellular highlights that resemble refracted light moving across water.",
|
||||
strata:
|
||||
"Stacks noise-warped sediment-like bands with a directional slope and sharp edges.",
|
||||
radar:
|
||||
"Combines a rotating beam, concentric rings, and seeded target blips.",
|
||||
constellation:
|
||||
"Connects twinkling cellular stars with faint Voronoi filaments.",
|
||||
liquid:
|
||||
"Layers domain-warped waves and highlights into a gently moving water surface.",
|
||||
electric:
|
||||
"Generates repeating branching lightning channels with pulses and traveling sparks.",
|
||||
ripple: "Sends noise-distorted concentric rings outward from the center.",
|
||||
kaleidoscope:
|
||||
"Folds rotating polar noise into mirrored, mandala-like symmetry.",
|
||||
});
|
||||
|
||||
export const TRANSITION_HELP = Object.freeze({
|
||||
"field-morph":
|
||||
"Morphs cells on a soft schedule combining seeded randomness, radius, and vertical position.",
|
||||
"seeded-dissolve":
|
||||
"Changes pixels in a stable, seed-determined random order.",
|
||||
"radial-cascade":
|
||||
"Moves the new state outward from the origin with slight seeded variation.",
|
||||
"angular-sweep":
|
||||
"Rotates the new state around the origin with a subtle outward offset.",
|
||||
scanline:
|
||||
"Reveals the new state from top to bottom in a lightly rippled scan.",
|
||||
"contour-trace":
|
||||
"Starts on a circular mid-radius contour, then spreads inward and outward.",
|
||||
"path-draw":
|
||||
"Draws the new state diagonally from the upper left toward the lower right.",
|
||||
"axis-flip":
|
||||
"Staggers alternating columns while advancing top to bottom, producing a shutter-like flip.",
|
||||
"cluster-dissolve":
|
||||
"Changes coarse pixel clusters in a seeded random order for a chunkier dissolve.",
|
||||
"neighbor-ignite":
|
||||
"Spreads the new state in a connected-looking wave from the origin.",
|
||||
"glitch-bands":
|
||||
"Switches horizontal bands in a scrambled order for a brief glitch effect.",
|
||||
instant: "Applies the new state immediately with no spatial interpolation.",
|
||||
});
|
||||
|
||||
export const PIXEL_SHAPE_HELP = Object.freeze({
|
||||
disc: "Draws each cell as a filled round dot.",
|
||||
square: "Draws each cell as a filled square for a crisp grid texture.",
|
||||
diamond: "Draws each cell as a compact square rotated 45 degrees.",
|
||||
capsule: "Draws each cell as a rounded horizontal pill.",
|
||||
line: "Draws each cell as a thin bar aligned tangentially around the center.",
|
||||
ring: "Draws each cell as a hollow circular outline.",
|
||||
cross: "Draws each cell as a compact plus sign.",
|
||||
"square-cross":
|
||||
"Uses filled squares for dim sampled signal cells and crosses for bright cells.",
|
||||
"square-cross-ring":
|
||||
"Uses filled squares, crosses, then rings as sampled signal lightness rises.",
|
||||
});
|
||||
|
||||
export const PIXEL_SWITCH_HELP = Object.freeze({
|
||||
"ordered-dither":
|
||||
"Uses a fixed Bayer pattern for stable, evenly distributed pixel activation.",
|
||||
"temporal-blue-noise":
|
||||
"Blends an ordered grid with time-varying noise for lively, fine-grained switching.",
|
||||
"threshold-hysteresis":
|
||||
"Uses a uniform midpoint threshold with hysteresis for calm, stable switching.",
|
||||
"sdf-wavefront":
|
||||
"Adds a moving radial wavefront to ordered thresholds so activation travels in rings.",
|
||||
"contour-trace":
|
||||
"Modulates thresholds along repeating field-value contours so similar intensity bands switch together.",
|
||||
"curl-advect":
|
||||
"Offsets ordered thresholds with a traveling wave so activation appears to drift diagonally.",
|
||||
"neighbor-propagation":
|
||||
"Lowers a cell’s threshold when adjacent cells are on, encouraging activation to spread locally.",
|
||||
"radial-cascade":
|
||||
"Biases ordered activation from the center outward.",
|
||||
"path-draw":
|
||||
"Biases ordered activation along a top-left-to-bottom-right path.",
|
||||
"axis-flip":
|
||||
"Staggers alternating columns and compresses cells vertically as they turn on, creating a flip.",
|
||||
"seeded-dissolve":
|
||||
"Gives each cell a stable, seed-determined random threshold for a repeatable dissolve texture.",
|
||||
"field-morph":
|
||||
"Blends ordered and seed-random thresholds for both structure and organic variation.",
|
||||
});
|
||||
|
||||
export const SELECT_CONTROL_HELP = Object.freeze({
|
||||
fieldSelect: Object.freeze({
|
||||
summary: "Chooses the procedural texture sampled inside the glyph.",
|
||||
options: FIELD_HELP,
|
||||
}),
|
||||
transitionSelect: Object.freeze({
|
||||
summary:
|
||||
"Controls the spatial order used when moving between semantic states.",
|
||||
options: TRANSITION_HELP,
|
||||
}),
|
||||
pixelShapeSelect: Object.freeze({
|
||||
summary: "Changes the silhouette used to draw each active cell.",
|
||||
options: PIXEL_SHAPE_HELP,
|
||||
}),
|
||||
pixelSwitchSelect: Object.freeze({
|
||||
summary:
|
||||
"Controls how continuous field intensity becomes persistent on/off pixels.",
|
||||
options: PIXEL_SWITCH_HELP,
|
||||
}),
|
||||
orbBackgroundModeSelect: Object.freeze({
|
||||
summary: "Chooses how the optional orb background disk is constructed.",
|
||||
options: Object.freeze({
|
||||
solid:
|
||||
"Fills one smooth circle behind the orb while preserving the grid above it.",
|
||||
pixelated:
|
||||
"Builds the disk from full grid-aligned cells so its edge matches the selected resolution.",
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const VALUE_CONTROL_HELP = Object.freeze({
|
||||
resolutionRange:
|
||||
"Sets the number of cells per side. Higher values add detail but increase rendering work roughly with the square.",
|
||||
speedRange:
|
||||
"Multiplies the procedural animation clock, making textures and sprite motion run slower or faster.",
|
||||
densityRange:
|
||||
"Biases activation so higher values keep more pixels on and lower values make the signal sparser.",
|
||||
seedInput:
|
||||
"A stable text key that determines noise and per-cell randomness. Reuse it with the same settings to reproduce the signal.",
|
||||
randomizeButton:
|
||||
"Creates a new seed and a new deterministic variation of the current signal.",
|
||||
backgroundColorInput:
|
||||
"Sets the canvas fill behind the grid across non-error states.",
|
||||
offColorInput:
|
||||
"Sets the faint color of grid cells that are currently off across non-error states.",
|
||||
inkColorInput:
|
||||
"Sets the base color of active pixels across non-error states.",
|
||||
accentColorInput:
|
||||
"Sets the color active pixels move toward as sampled luminance rises.",
|
||||
glowEnabledInput:
|
||||
"Turns the halo around active pixels on or off across non-error states.",
|
||||
orbBoundaryInput:
|
||||
"Defined draws the authored rim. Gestalt removes the continuous outline and lets clustered edge pixels suggest the circle through proximity, closure, and continuing arcs.",
|
||||
orbBackgroundEnabledInput:
|
||||
"Adds or removes the optional orb background layer in supported presence orbs. It is separate from the canvas background.",
|
||||
orbBackgroundColorInput:
|
||||
"Sets the optional color behind supported orb pixels. The color carries across states but stays visually inert for non-orb recipes.",
|
||||
glowColorInput:
|
||||
"Sets the halo around active pixels. Glow renders in high quality at resolutions up to 48×48.",
|
||||
resetPaletteButton:
|
||||
"Clears custom color overrides and returns non-error states to the current theme’s defaults.",
|
||||
});
|
||||
|
||||
const clamp = (value, minimum, maximum) =>
|
||||
Math.min(Math.max(value, minimum), maximum);
|
||||
|
||||
export function placeControlTooltip(
|
||||
anchor,
|
||||
tooltip,
|
||||
viewport,
|
||||
options = {},
|
||||
) {
|
||||
const gap = Number(options.gap) || 8;
|
||||
const gutter = Number(options.gutter) || 12;
|
||||
const maximumLeft = Math.max(
|
||||
gutter,
|
||||
Number(viewport.width) - Number(tooltip.width) - gutter,
|
||||
);
|
||||
const centeredLeft =
|
||||
Number(anchor.left) +
|
||||
Number(anchor.width) / 2 -
|
||||
Number(tooltip.width) / 2;
|
||||
const left = clamp(centeredLeft, gutter, maximumLeft);
|
||||
const below = Number(anchor.bottom) + gap;
|
||||
const above = Number(anchor.top) - Number(tooltip.height) - gap;
|
||||
const fitsBelow =
|
||||
below + Number(tooltip.height) <= Number(viewport.height) - gutter;
|
||||
const fitsAbove = above >= gutter;
|
||||
const maximumTop = Math.max(
|
||||
gutter,
|
||||
Number(viewport.height) - Number(tooltip.height) - gutter,
|
||||
);
|
||||
const placement = !fitsBelow && fitsAbove ? "top" : "bottom";
|
||||
const preferredTop = placement === "top" ? above : below;
|
||||
|
||||
return {
|
||||
left: Math.round(left),
|
||||
top: Math.round(clamp(preferredTop, gutter, maximumTop)),
|
||||
placement,
|
||||
};
|
||||
}
|
||||
1323
web/vendor/src/fields.js
vendored
Normal file
1323
web/vendor/src/fields.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1127
web/vendor/src/glyphs.js
vendored
Normal file
1127
web/vendor/src/glyphs.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4309
web/vendor/src/joan-engine.js
vendored
Normal file
4309
web/vendor/src/joan-engine.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
63
web/vendor/src/preview-layout.js
vendored
Normal file
63
web/vendor/src/preview-layout.js
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
export const PREVIEW_MODES = Object.freeze(["fit", "actual"]);
|
||||
|
||||
const DEFAULT_RESOLUTION = 68;
|
||||
const MIN_RESOLUTION = 8;
|
||||
const MAX_RESOLUTION = 96;
|
||||
const FIT_INSET = 0.075;
|
||||
|
||||
function finite(value, fallback) {
|
||||
return Number.isFinite(Number(value)) ? Number(value) : fallback;
|
||||
}
|
||||
|
||||
export function normalizePreviewMode(mode) {
|
||||
return mode === "actual" ? "actual" : "fit";
|
||||
}
|
||||
|
||||
export function normalizePreviewResolution(resolution) {
|
||||
return Math.min(
|
||||
MAX_RESOLUTION,
|
||||
Math.max(
|
||||
MIN_RESOLUTION,
|
||||
Math.round(finite(resolution, DEFAULT_RESOLUTION)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function previewModeDetails(mode, resolution) {
|
||||
const normalizedMode = normalizePreviewMode(mode);
|
||||
const normalizedResolution = normalizePreviewResolution(resolution);
|
||||
return {
|
||||
mode: normalizedMode,
|
||||
resolution: normalizedResolution,
|
||||
cellSize: normalizedMode === "actual" ? 1 : null,
|
||||
footprint: normalizedMode === "actual" ? normalizedResolution : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function canvasGridLayout(mode, resolution, width, height) {
|
||||
const details = previewModeDetails(mode, resolution);
|
||||
const safeWidth = Math.max(1, finite(width, details.resolution));
|
||||
const safeHeight = Math.max(1, finite(height, details.resolution));
|
||||
|
||||
if (details.mode === "actual") {
|
||||
const stageSize = details.resolution;
|
||||
return {
|
||||
...details,
|
||||
stageSize,
|
||||
cellSize: 1,
|
||||
offsetX: Math.round((safeWidth - stageSize) / 2),
|
||||
offsetY: Math.round((safeHeight - stageSize) / 2),
|
||||
};
|
||||
}
|
||||
|
||||
const shortSide = Math.min(safeWidth, safeHeight);
|
||||
const margin = shortSide * FIT_INSET;
|
||||
const stageSize = shortSide - margin * 2;
|
||||
return {
|
||||
...details,
|
||||
stageSize,
|
||||
cellSize: stageSize / details.resolution,
|
||||
offsetX: (safeWidth - stageSize) / 2,
|
||||
offsetY: (safeHeight - stageSize) / 2,
|
||||
};
|
||||
}
|
||||
82
web/vendor/src/preview-thumbnails.js
vendored
Normal file
82
web/vendor/src/preview-thumbnails.js
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
const DEFAULT_PREVIEW_PROGRESS = 0.48;
|
||||
|
||||
function clamp01(value) {
|
||||
return Math.min(1, Math.max(0, Number(value) || 0));
|
||||
}
|
||||
|
||||
function representativeTime(recipe) {
|
||||
const phase = clamp01(recipe?.reducedMotion?.phase ?? 0.25);
|
||||
const cycleMs =
|
||||
Number(recipe?.timeline?.macroCycleMs) ||
|
||||
Number(recipe?.timeline?.durationMs) ||
|
||||
4000;
|
||||
return (Math.max(1, cycleMs) / 1000) * phase;
|
||||
}
|
||||
|
||||
/**
|
||||
* One-shot recipes may advance to a different state while their card is active.
|
||||
* Atlas previews retain the requested recipe so their identity and resting frame
|
||||
* always match the card label.
|
||||
*/
|
||||
export function createThumbnailRecipe(recipe) {
|
||||
if (!recipe?.timeline?.next) return recipe;
|
||||
return {
|
||||
...recipe,
|
||||
timeline: {
|
||||
...recipe.timeline,
|
||||
next: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldAnimateThumbnail(
|
||||
id,
|
||||
{
|
||||
selectedId = null,
|
||||
mainRunning = false,
|
||||
hoveredIds = new Set(),
|
||||
focusedIds = new Set(),
|
||||
} = {},
|
||||
) {
|
||||
return (
|
||||
(id === selectedId && mainRunning) ||
|
||||
hoveredIds.has(id) ||
|
||||
focusedIds.has(id)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a deterministic, authored representative phase without leaving the
|
||||
* preview in reduced-motion mode. JoanGlyphEngine maps time zero to each
|
||||
* recipe's reducedMotion.phase while reduced motion is enabled.
|
||||
*/
|
||||
export function renderThumbnailRestFrame(
|
||||
preview,
|
||||
{ progress = DEFAULT_PREVIEW_PROGRESS } = {},
|
||||
) {
|
||||
if (!preview || preview.destroyed) return null;
|
||||
|
||||
const nextProgress = clamp01(progress);
|
||||
if (Math.abs(preview.progress - nextProgress) > 1e-6) {
|
||||
preview.setProgress(nextProgress);
|
||||
}
|
||||
|
||||
// Neighbor-propagation previews otherwise retain their previous gate buffer,
|
||||
// making the supposedly static pose depend on how long the card was hovered.
|
||||
preview.stateElapsed = representativeTime(preview.currentRecipe);
|
||||
preview.gates?.fill(0);
|
||||
preview.neighborBuffer?.fill(0);
|
||||
preview.dwell?.fill(0);
|
||||
|
||||
const previousReducedMotion = preview.reducedMotion;
|
||||
preview.reducedMotion = true;
|
||||
let stats;
|
||||
try {
|
||||
stats = preview.renderOnce(0);
|
||||
} finally {
|
||||
preview.reducedMotion = previousReducedMotion;
|
||||
}
|
||||
|
||||
if (preview.canvas?.dataset) preview.canvas.dataset.previewState = "ready";
|
||||
return stats;
|
||||
}
|
||||
1766
web/vendor/src/sprites.js
vendored
Normal file
1766
web/vendor/src/sprites.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
287
web/vendor/src/state-director.js
vendored
Normal file
287
web/vendor/src/state-director.js
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
function detailEvent(type, detail) {
|
||||
if (typeof CustomEvent === "function") {
|
||||
return new CustomEvent(type, { detail });
|
||||
}
|
||||
const event = new Event(type);
|
||||
Object.defineProperty(event, "detail", { value: detail });
|
||||
return event;
|
||||
}
|
||||
|
||||
function abortError(signal) {
|
||||
if (signal?.reason instanceof Error) return signal.reason;
|
||||
const message = signal?.reason ? String(signal.reason) : "The operation was aborted.";
|
||||
if (typeof DOMException === "function") {
|
||||
return new DOMException(message, "AbortError");
|
||||
}
|
||||
const error = new Error(message);
|
||||
error.name = "AbortError";
|
||||
return error;
|
||||
}
|
||||
|
||||
function stateId(sprite) {
|
||||
return typeof sprite === "string" ? sprite : sprite?.id || String(sprite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional semantic timing helper. The host explicitly begins and completes work;
|
||||
* the director only handles presentation thresholds and never infers AI state.
|
||||
*/
|
||||
export class StateDirector extends EventTarget {
|
||||
constructor(engine, options = {}) {
|
||||
super();
|
||||
if (!engine?.setSprite) throw new TypeError("StateDirector requires an engine.");
|
||||
this.engine = engine;
|
||||
this.options = {
|
||||
deepThinkingAfterMs: 7000,
|
||||
stillWorkingAfterMs: 18000,
|
||||
transition: "field-morph",
|
||||
...options,
|
||||
};
|
||||
this.timers = new Set();
|
||||
this.state = engine.currentRecipe?.id || "ai.idle";
|
||||
this.operation = 0;
|
||||
this.destroyed = false;
|
||||
}
|
||||
|
||||
clearTimers() {
|
||||
for (const timer of this.timers) clearTimeout(timer);
|
||||
this.timers.clear();
|
||||
}
|
||||
|
||||
schedule(callback, delay) {
|
||||
const timer = setTimeout(() => {
|
||||
this.timers.delete(timer);
|
||||
callback();
|
||||
}, Math.max(0, Number(delay) || 0));
|
||||
this.timers.add(timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
changeState(sprite, engineOptions = {}, metadata = {}) {
|
||||
if (metadata.clearTimers !== false) this.clearTimers();
|
||||
const from = this.state;
|
||||
const to = stateId(sprite);
|
||||
this.state = to;
|
||||
this.engine.setSprite(sprite, engineOptions);
|
||||
this.dispatchEvent(
|
||||
detailEvent("statechange", {
|
||||
state: to,
|
||||
from,
|
||||
to,
|
||||
reason: metadata.reason || "set",
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
set(sprite, options = {}) {
|
||||
this.operation += 1;
|
||||
return this.changeState(
|
||||
sprite,
|
||||
{
|
||||
transition: options.transition || this.options.transition,
|
||||
preservePhase: options.preservePhase ?? true,
|
||||
duration: options.duration,
|
||||
},
|
||||
{ reason: options.reason || "set" },
|
||||
);
|
||||
}
|
||||
|
||||
startThinking(options, operation) {
|
||||
this.changeState(
|
||||
"ai.thinking",
|
||||
{
|
||||
transition: options.transition || this.options.transition,
|
||||
preservePhase: options.preservePhase ?? true,
|
||||
duration: options.duration,
|
||||
},
|
||||
{ reason: options.reason || "thinking-start" },
|
||||
);
|
||||
this.schedule(() => {
|
||||
if (this.operation !== operation || this.state !== "ai.thinking") return;
|
||||
this.changeState(
|
||||
"ai.thinking-deep",
|
||||
{ transition: "neighbor-ignite", preservePhase: true },
|
||||
{ clearTimers: false, reason: "thinking-escalation" },
|
||||
);
|
||||
}, options.deepThinkingAfterMs ?? this.options.deepThinkingAfterMs);
|
||||
this.schedule(() => {
|
||||
if (
|
||||
this.operation !== operation ||
|
||||
!["ai.thinking", "ai.thinking-deep"].includes(this.state)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.changeState(
|
||||
"ai.still-working",
|
||||
{ transition: "radial-cascade", preservePhase: true },
|
||||
{ clearTimers: false, reason: "thinking-escalation" },
|
||||
);
|
||||
}, options.stillWorkingAfterMs ?? this.options.stillWorkingAfterMs);
|
||||
return this;
|
||||
}
|
||||
|
||||
beginThinking(options = {}) {
|
||||
const operation = ++this.operation;
|
||||
return this.startThinking(options, operation);
|
||||
}
|
||||
|
||||
finish(options = {}, reason = "complete") {
|
||||
const sprite = options.sprite || "status.success";
|
||||
this.changeState(
|
||||
sprite,
|
||||
{
|
||||
transition: options.transition || "path-draw",
|
||||
preservePhase: options.preservePhase ?? true,
|
||||
duration: options.duration,
|
||||
},
|
||||
{ reason },
|
||||
);
|
||||
this.engine.signal?.("complete", { energy: 1 });
|
||||
return this;
|
||||
}
|
||||
|
||||
complete(options = {}) {
|
||||
this.operation += 1;
|
||||
return this.finish(options, options.reason || "complete");
|
||||
}
|
||||
|
||||
reject(options = {}, reason = "failure") {
|
||||
return this.changeState(
|
||||
options.sprite || "status.error",
|
||||
{
|
||||
transition: options.transition || "glitch-bands",
|
||||
preservePhase: options.preservePhase ?? true,
|
||||
duration: options.duration,
|
||||
},
|
||||
{ reason },
|
||||
);
|
||||
}
|
||||
|
||||
fail(options = {}) {
|
||||
this.operation += 1;
|
||||
return this.reject(options, options.reason || "failure");
|
||||
}
|
||||
|
||||
cancel(options = {}) {
|
||||
this.operation += 1;
|
||||
return this.changeState(
|
||||
options.sprite || "status.cancelled",
|
||||
{
|
||||
transition: options.transition || "seeded-dissolve",
|
||||
preservePhase: options.preservePhase ?? true,
|
||||
duration: options.duration,
|
||||
},
|
||||
{ reason: options.reason || "cancelled" },
|
||||
);
|
||||
}
|
||||
|
||||
reset(options = {}) {
|
||||
this.operation += 1;
|
||||
return this.changeState(
|
||||
options.sprite || "ai.idle",
|
||||
{
|
||||
transition: options.transition || "field-morph",
|
||||
preservePhase: options.preservePhase ?? false,
|
||||
duration: options.duration,
|
||||
},
|
||||
{ reason: options.reason || "reset" },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Present one asynchronous task through thinking escalation and a terminal
|
||||
* state. The work receives `{ signal, director, engine }`; existing zero-arg
|
||||
* functions remain valid because JavaScript ignores extra arguments.
|
||||
*/
|
||||
async run(work, options = {}) {
|
||||
if (this.destroyed) throw new Error("StateDirector has been destroyed.");
|
||||
if (typeof work !== "function" && !work?.then) {
|
||||
throw new TypeError("StateDirector.run requires a function or Promise.");
|
||||
}
|
||||
|
||||
const signal = options.signal;
|
||||
const operation = ++this.operation;
|
||||
if (signal?.aborted) {
|
||||
if (this.operation === operation) {
|
||||
this.changeState(
|
||||
options.cancel?.sprite || options.cancelledSprite || "status.cancelled",
|
||||
{
|
||||
transition: options.cancel?.transition || "seeded-dissolve",
|
||||
preservePhase: options.cancel?.preservePhase ?? true,
|
||||
duration: options.cancel?.duration,
|
||||
},
|
||||
{ reason: "run-aborted" },
|
||||
);
|
||||
}
|
||||
throw abortError(signal);
|
||||
}
|
||||
|
||||
this.startThinking(options.thinking || options, operation);
|
||||
let removeAbortListener = () => {};
|
||||
let task;
|
||||
try {
|
||||
task =
|
||||
typeof work === "function"
|
||||
? Promise.resolve().then(() =>
|
||||
work({ signal, director: this, engine: this.engine }),
|
||||
)
|
||||
: Promise.resolve(work);
|
||||
|
||||
if (signal) {
|
||||
const aborted = new Promise((_, reject) => {
|
||||
const onAbort = () => reject(abortError(signal));
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
||||
});
|
||||
task = Promise.race([task, aborted]);
|
||||
}
|
||||
|
||||
const result = await task;
|
||||
if (this.operation === operation) {
|
||||
this.finish(
|
||||
{
|
||||
...(options.complete || {}),
|
||||
sprite: options.complete?.sprite || options.successSprite,
|
||||
},
|
||||
"run-complete",
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (this.operation === operation) {
|
||||
if (signal?.aborted || error?.name === "AbortError") {
|
||||
this.changeState(
|
||||
options.cancel?.sprite || options.cancelledSprite || "status.cancelled",
|
||||
{
|
||||
transition: options.cancel?.transition || "seeded-dissolve",
|
||||
preservePhase: options.cancel?.preservePhase ?? true,
|
||||
duration: options.cancel?.duration,
|
||||
},
|
||||
{ reason: "run-aborted" },
|
||||
);
|
||||
} else {
|
||||
this.reject(
|
||||
{
|
||||
...(options.fail || {}),
|
||||
sprite: options.fail?.sprite || options.errorSprite,
|
||||
},
|
||||
"run-failed",
|
||||
);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
removeAbortListener();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.destroyed) return;
|
||||
this.destroyed = true;
|
||||
this.operation += 1;
|
||||
this.clearTimers();
|
||||
}
|
||||
}
|
||||
|
||||
export default StateDirector;
|
||||
649
web/vendor/src/state-sequence.js
vendored
Normal file
649
web/vendor/src/state-sequence.js
vendored
Normal file
@@ -0,0 +1,649 @@
|
||||
export const STATE_SEQUENCE_SCHEMA_VERSION = 1;
|
||||
|
||||
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
||||
|
||||
function detailEvent(type, detail) {
|
||||
if (typeof CustomEvent === "function") {
|
||||
return new CustomEvent(type, { detail });
|
||||
}
|
||||
const event = new Event(type);
|
||||
Object.defineProperty(event, "detail", { value: detail });
|
||||
return event;
|
||||
}
|
||||
|
||||
function abortError(source) {
|
||||
if (source?.reason instanceof Error) return source.reason;
|
||||
const reason = source?.reason ?? source;
|
||||
const message = reason ? String(reason) : "The state sequence was aborted.";
|
||||
if (typeof DOMException === "function") {
|
||||
return new DOMException(message, "AbortError");
|
||||
}
|
||||
const error = new Error(message);
|
||||
error.name = "AbortError";
|
||||
return error;
|
||||
}
|
||||
|
||||
function stateId(sprite) {
|
||||
return typeof sprite === "string" ? sprite : sprite?.id || String(sprite);
|
||||
}
|
||||
|
||||
function normalizeTransition(value, path) {
|
||||
if (value === undefined || value === false || typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new TypeError(`${path} must be false, a transition name, or an options object.`);
|
||||
}
|
||||
return Object.freeze({ ...value });
|
||||
}
|
||||
|
||||
function normalizeOptions(value, path) {
|
||||
if (value === undefined) return undefined;
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new TypeError(`${path} must be an options object.`);
|
||||
}
|
||||
return Object.freeze({ ...value });
|
||||
}
|
||||
|
||||
function normalizeStep(input, index) {
|
||||
const source =
|
||||
typeof input === "string"
|
||||
? { sprite: input }
|
||||
: input && typeof input === "object"
|
||||
? input
|
||||
: null;
|
||||
const sprite = source?.sprite;
|
||||
if (
|
||||
!source ||
|
||||
(typeof sprite === "string"
|
||||
? !sprite.trim()
|
||||
: !sprite || typeof sprite !== "object")
|
||||
) {
|
||||
throw new TypeError(`State sequence step ${index} requires a sprite.`);
|
||||
}
|
||||
|
||||
const rawHold = source.holdMs ?? source.durationMs ?? 0;
|
||||
const holdMs = Number(rawHold);
|
||||
if (!Number.isFinite(holdMs) || holdMs < 0) {
|
||||
throw new RangeError(`State sequence step ${index} holdMs must be a non-negative number.`);
|
||||
}
|
||||
|
||||
const step = {
|
||||
sprite: source.sprite,
|
||||
holdMs,
|
||||
};
|
||||
if (hasOwn(source, "transition")) {
|
||||
step.transition = normalizeTransition(
|
||||
source.transition,
|
||||
`State sequence step ${index} transition`,
|
||||
);
|
||||
}
|
||||
if (hasOwn(source, "options")) {
|
||||
step.options = normalizeOptions(
|
||||
source.options,
|
||||
`State sequence step ${index} options`,
|
||||
);
|
||||
}
|
||||
if (hasOwn(source, "label")) step.label = String(source.label);
|
||||
if (hasOwn(source, "metadata")) {
|
||||
step.metadata = normalizeOptions(
|
||||
source.metadata,
|
||||
`State sequence step ${index} metadata`,
|
||||
);
|
||||
}
|
||||
return Object.freeze(step);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a frozen named sequence definition. Definitions that use sprite IDs
|
||||
* and data-only metadata/options can be serialized directly as JSON.
|
||||
*
|
||||
* @example
|
||||
* const readyThenDone = defineStateSequence("ready-then-done", [
|
||||
* { sprite: "ai.idle", holdMs: 5000 },
|
||||
* { sprite: "status.success", transition: "path-draw" },
|
||||
* ]);
|
||||
*/
|
||||
export function defineStateSequence(nameOrDefinition, steps, options = {}) {
|
||||
const source =
|
||||
nameOrDefinition && typeof nameOrDefinition === "object" && !Array.isArray(nameOrDefinition)
|
||||
? nameOrDefinition
|
||||
: { ...options, name: nameOrDefinition, steps };
|
||||
const name = String(source.name || "").trim();
|
||||
if (!name) throw new TypeError("A state sequence requires a non-empty name.");
|
||||
if (
|
||||
source.schemaVersion !== undefined &&
|
||||
Number(source.schemaVersion) !== STATE_SEQUENCE_SCHEMA_VERSION
|
||||
) {
|
||||
throw new RangeError(
|
||||
`Unsupported state sequence schema version "${String(source.schemaVersion)}".`,
|
||||
);
|
||||
}
|
||||
if (!Array.isArray(source.steps) || source.steps.length === 0) {
|
||||
throw new TypeError(`State sequence "${name}" requires at least one step.`);
|
||||
}
|
||||
|
||||
const sequence = {
|
||||
schemaVersion: STATE_SEQUENCE_SCHEMA_VERSION,
|
||||
name,
|
||||
steps: Object.freeze(source.steps.map(normalizeStep)),
|
||||
transitionFirst: source.transitionFirst === true,
|
||||
};
|
||||
if (hasOwn(source, "transition")) {
|
||||
sequence.transition = normalizeTransition(
|
||||
source.transition,
|
||||
`State sequence "${name}" transition`,
|
||||
);
|
||||
}
|
||||
if (hasOwn(source, "metadata")) {
|
||||
sequence.metadata = normalizeOptions(
|
||||
source.metadata,
|
||||
`State sequence "${name}" metadata`,
|
||||
);
|
||||
}
|
||||
return Object.freeze(sequence);
|
||||
}
|
||||
|
||||
function createClock(clock) {
|
||||
if (
|
||||
clock &&
|
||||
(typeof clock.now !== "function" ||
|
||||
typeof clock.setTimeout !== "function" ||
|
||||
typeof clock.clearTimeout !== "function")
|
||||
) {
|
||||
throw new TypeError(
|
||||
"A custom sequence clock requires now(), setTimeout(), and clearTimeout().",
|
||||
);
|
||||
}
|
||||
const now =
|
||||
clock
|
||||
? () => clock.now()
|
||||
: () =>
|
||||
typeof performance !== "undefined" && typeof performance.now === "function"
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
const schedule =
|
||||
clock
|
||||
? (callback, delay) => clock.setTimeout(callback, delay)
|
||||
: (callback, delay) => setTimeout(callback, delay);
|
||||
const cancel =
|
||||
clock
|
||||
? (timer) => clock.clearTimeout(timer)
|
||||
: (timer) => clearTimeout(timer);
|
||||
return Object.freeze({ now, setTimeout: schedule, clearTimeout: cancel });
|
||||
}
|
||||
|
||||
function transitionOptions(specification, preservePhase, stepOptions, signal) {
|
||||
const options = { preservePhase };
|
||||
if (typeof specification === "string") {
|
||||
options.transition = specification;
|
||||
} else if (specification && typeof specification === "object") {
|
||||
Object.assign(options, specification);
|
||||
if (!options.transition && typeof options.name === "string") {
|
||||
options.transition = options.name;
|
||||
delete options.name;
|
||||
}
|
||||
}
|
||||
Object.assign(options, stepOptions || {});
|
||||
options.signal = signal;
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays serial state definitions against any target implementing setSprite().
|
||||
* transitionTo() is awaited when available; otherwise setSprite() is used.
|
||||
*/
|
||||
export class StateSequencePlayer extends EventTarget {
|
||||
constructor(target, options = {}) {
|
||||
super();
|
||||
if (!target || typeof target.setSprite !== "function") {
|
||||
throw new TypeError("StateSequencePlayer requires a setSprite-capable target.");
|
||||
}
|
||||
this.target = target;
|
||||
this.clock = createClock(options.clock);
|
||||
this.options = Object.freeze({
|
||||
transition: hasOwn(options, "transition")
|
||||
? normalizeTransition(options.transition, "Default transition")
|
||||
: "field-morph",
|
||||
transitionFirst: options.transitionFirst === true,
|
||||
preservePhase: options.preservePhase !== false,
|
||||
});
|
||||
this.sequences = new Map();
|
||||
this.status = "idle";
|
||||
this.state = target.currentRecipe?.id || null;
|
||||
this.destroyed = false;
|
||||
this.finished = Promise.resolve(null);
|
||||
this._active = null;
|
||||
this._serial = 0;
|
||||
|
||||
if (Array.isArray(options.sequences)) {
|
||||
for (const sequence of options.sequences) this.register(sequence);
|
||||
} else if (options.sequences && typeof options.sequences === "object") {
|
||||
for (const [name, sequence] of Object.entries(options.sequences)) {
|
||||
this.register(
|
||||
Array.isArray(sequence)
|
||||
? defineStateSequence(name, sequence)
|
||||
: defineStateSequence({ ...sequence, name: sequence.name || name }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get currentSequence() {
|
||||
return this._active?.sequence || null;
|
||||
}
|
||||
|
||||
get currentStepIndex() {
|
||||
return this._active?.index ?? -1;
|
||||
}
|
||||
|
||||
get isPlaying() {
|
||||
return this.status === "running" || this.status === "paused";
|
||||
}
|
||||
|
||||
get isPaused() {
|
||||
return this.status === "paused";
|
||||
}
|
||||
|
||||
register(nameOrDefinition, steps, options) {
|
||||
if (this.destroyed) throw new Error("StateSequencePlayer has been destroyed.");
|
||||
const sequence = defineStateSequence(nameOrDefinition, steps, options);
|
||||
this.sequences.set(sequence.name, sequence);
|
||||
return sequence;
|
||||
}
|
||||
|
||||
unregister(name) {
|
||||
return this.sequences.delete(String(name));
|
||||
}
|
||||
|
||||
getSequence(name) {
|
||||
return this.sequences.get(String(name)) || null;
|
||||
}
|
||||
|
||||
listSequences() {
|
||||
return Object.freeze([...this.sequences.values()]);
|
||||
}
|
||||
|
||||
_resolveSequence(input, options) {
|
||||
if (typeof input === "string") {
|
||||
const sequence = this.getSequence(input);
|
||||
if (!sequence) throw new RangeError(`Unknown state sequence "${input}".`);
|
||||
return sequence;
|
||||
}
|
||||
if (Array.isArray(input)) {
|
||||
return defineStateSequence(options.name || `sequence-${this._serial + 1}`, input);
|
||||
}
|
||||
return defineStateSequence(input);
|
||||
}
|
||||
|
||||
_normalizePlayOptions(options) {
|
||||
const normalized = {
|
||||
signal: options.signal,
|
||||
preservePhase: options.preservePhase ?? this.options.preservePhase,
|
||||
transitionFirst: options.transitionFirst,
|
||||
};
|
||||
if (hasOwn(options, "transition")) {
|
||||
normalized.transition = normalizeTransition(options.transition, "Play transition");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
play(sequenceOrName, options = {}) {
|
||||
if (this.destroyed) {
|
||||
return Promise.reject(new Error("StateSequencePlayer has been destroyed."));
|
||||
}
|
||||
|
||||
let sequence;
|
||||
let playOptions;
|
||||
try {
|
||||
sequence = this._resolveSequence(sequenceOrName, options);
|
||||
playOptions = this._normalizePlayOptions(options);
|
||||
if (playOptions.signal?.aborted) throw abortError(playOptions.signal);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (this._active) this._stopRun(this._active, "superseded");
|
||||
|
||||
const run = {
|
||||
id: ++this._serial,
|
||||
sequence,
|
||||
options: playOptions,
|
||||
controller: new AbortController(),
|
||||
index: -1,
|
||||
completedSteps: 0,
|
||||
paused: false,
|
||||
delay: null,
|
||||
gates: new Set(),
|
||||
terminal: null,
|
||||
removeExternalAbort: () => {},
|
||||
};
|
||||
this._active = run;
|
||||
this.status = "running";
|
||||
|
||||
if (playOptions.signal) {
|
||||
const onAbort = () => this._cancelRun(run, playOptions.signal);
|
||||
playOptions.signal.addEventListener("abort", onAbort, { once: true });
|
||||
run.removeExternalAbort = () =>
|
||||
playOptions.signal.removeEventListener("abort", onAbort);
|
||||
if (playOptions.signal.aborted) this._cancelRun(run, playOptions.signal);
|
||||
}
|
||||
|
||||
this._emit("sequencestart", run, { totalSteps: sequence.steps.length });
|
||||
const finished = this._execute(run).then(
|
||||
() => {
|
||||
if (!run.terminal) this._completeRun(run);
|
||||
return this._result(run);
|
||||
},
|
||||
(error) => {
|
||||
if (run.terminal?.status === "stopped") return this._result(run);
|
||||
if (run.terminal?.status === "cancelled") throw run.terminal.error;
|
||||
this._errorRun(run, error);
|
||||
throw error;
|
||||
},
|
||||
).finally(() => this._finalizeRun(run));
|
||||
run.finished = finished;
|
||||
this.finished = finished;
|
||||
return finished;
|
||||
}
|
||||
|
||||
run(sequenceOrName, options = {}) {
|
||||
return this.play(sequenceOrName, options);
|
||||
}
|
||||
|
||||
async _execute(run) {
|
||||
for (let index = 0; index < run.sequence.steps.length; index += 1) {
|
||||
if (run.controller.signal.aborted) throw abortError(run.controller.signal);
|
||||
const step = run.sequence.steps[index];
|
||||
run.index = index;
|
||||
this._emit("stepstart", run, {
|
||||
step,
|
||||
sprite: stateId(step.sprite),
|
||||
});
|
||||
|
||||
await this._applyStep(run, step, index);
|
||||
await this._pauseGate(run);
|
||||
if (run.controller.signal.aborted) throw abortError(run.controller.signal);
|
||||
|
||||
const from = this.state;
|
||||
const to = stateId(step.sprite);
|
||||
this.state = to;
|
||||
this._emit("statechange", run, { from, to, state: to, step });
|
||||
this._emit("stepenter", run, { from, to, step, sprite: to });
|
||||
|
||||
if (step.holdMs > 0) await this._delay(run, step.holdMs);
|
||||
await this._pauseGate(run);
|
||||
if (run.controller.signal.aborted) throw abortError(run.controller.signal);
|
||||
|
||||
run.completedSteps = index + 1;
|
||||
this._emit("stepcomplete", run, { step, sprite: to });
|
||||
}
|
||||
}
|
||||
|
||||
_transitionFor(run, step, index) {
|
||||
if (hasOwn(step, "transition")) return step.transition;
|
||||
const transitionFirst =
|
||||
run.options.transitionFirst ??
|
||||
(run.sequence.transitionFirst || this.options.transitionFirst);
|
||||
if (index === 0 && !transitionFirst) return false;
|
||||
if (hasOwn(run.options, "transition")) return run.options.transition;
|
||||
if (hasOwn(run.sequence, "transition")) return run.sequence.transition;
|
||||
return this.options.transition;
|
||||
}
|
||||
|
||||
async _applyStep(run, step, index) {
|
||||
const specification = this._transitionFor(run, step, index);
|
||||
if (specification === false) {
|
||||
const options = { immediate: true, ...(step.options || {}) };
|
||||
this.target.setSprite(step.sprite, options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.target.transitionTo !== "function") {
|
||||
const options = transitionOptions(
|
||||
specification,
|
||||
run.options.preservePhase,
|
||||
step.options,
|
||||
run.controller.signal,
|
||||
);
|
||||
delete options.signal;
|
||||
this.target.setSprite(step.sprite, options);
|
||||
return;
|
||||
}
|
||||
|
||||
const options = transitionOptions(
|
||||
specification,
|
||||
run.options.preservePhase,
|
||||
step.options,
|
||||
run.controller.signal,
|
||||
);
|
||||
let transition;
|
||||
try {
|
||||
transition = this.target.transitionTo(step.sprite, options);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
await this._raceAbort(transition, run);
|
||||
}
|
||||
|
||||
_raceAbort(value, run) {
|
||||
if (run.controller.signal.aborted) {
|
||||
return Promise.reject(abortError(run.controller.signal));
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
const finish = (callback, result) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
run.controller.signal.removeEventListener("abort", onAbort);
|
||||
callback(result);
|
||||
};
|
||||
const onAbort = () => finish(reject, abortError(run.controller.signal));
|
||||
run.controller.signal.addEventListener("abort", onAbort, { once: true });
|
||||
Promise.resolve(value).then(
|
||||
(result) => finish(resolve, result),
|
||||
(error) => finish(reject, error),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_pauseGate(run) {
|
||||
if (!run.paused) return Promise.resolve();
|
||||
if (run.controller.signal.aborted) {
|
||||
return Promise.reject(abortError(run.controller.signal));
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
const gate = {
|
||||
resolve: () => finish(resolve),
|
||||
};
|
||||
const finish = (callback, value) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
run.gates.delete(gate);
|
||||
run.controller.signal.removeEventListener("abort", onAbort);
|
||||
callback(value);
|
||||
};
|
||||
const onAbort = () => finish(reject, abortError(run.controller.signal));
|
||||
run.gates.add(gate);
|
||||
run.controller.signal.addEventListener("abort", onAbort, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
_delay(run, durationMs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
const delay = {
|
||||
timer: null,
|
||||
remainingMs: durationMs,
|
||||
startedAt: 0,
|
||||
pause: () => {
|
||||
if (delay.timer === null) return;
|
||||
const elapsed = Math.max(0, this.clock.now() - delay.startedAt);
|
||||
delay.remainingMs = Math.max(0, delay.remainingMs - elapsed);
|
||||
this.clock.clearTimeout(delay.timer);
|
||||
delay.timer = null;
|
||||
},
|
||||
resume: () => {
|
||||
if (settled || run.paused || delay.timer !== null) return;
|
||||
delay.startedAt = this.clock.now();
|
||||
delay.timer = this.clock.setTimeout(finish, delay.remainingMs);
|
||||
},
|
||||
};
|
||||
const cleanup = () => {
|
||||
if (delay.timer !== null) this.clock.clearTimeout(delay.timer);
|
||||
delay.timer = null;
|
||||
if (run.delay === delay) run.delay = null;
|
||||
run.controller.signal.removeEventListener("abort", onAbort);
|
||||
};
|
||||
const finish = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const onAbort = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
reject(abortError(run.controller.signal));
|
||||
};
|
||||
run.delay = delay;
|
||||
run.controller.signal.addEventListener("abort", onAbort, { once: true });
|
||||
delay.resume();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause sequence progression and the active hold clock. A visual transition
|
||||
* already running in the target may finish, but no step is entered or
|
||||
* advanced until resume() is called.
|
||||
*/
|
||||
pause() {
|
||||
const run = this._active;
|
||||
if (!run || run.terminal || run.paused) return this;
|
||||
run.paused = true;
|
||||
run.delay?.pause();
|
||||
this.status = "paused";
|
||||
this._emit("sequencepause", run);
|
||||
return this;
|
||||
}
|
||||
|
||||
resume() {
|
||||
const run = this._active;
|
||||
if (!run || run.terminal || !run.paused) return this;
|
||||
run.paused = false;
|
||||
this.status = "running";
|
||||
this._emit("sequenceresume", run);
|
||||
run.delay?.resume();
|
||||
for (const gate of [...run.gates]) gate.resolve();
|
||||
return this;
|
||||
}
|
||||
|
||||
stop(reason = "stopped") {
|
||||
if (this._active) this._stopRun(this._active, reason);
|
||||
return this;
|
||||
}
|
||||
|
||||
_stopRun(run, reason) {
|
||||
if (run.terminal) return;
|
||||
run.terminal = { status: "stopped", reason: String(reason || "stopped") };
|
||||
run.controller.abort(abortError(reason));
|
||||
if (this._active === run) this.status = "idle";
|
||||
this._emit("sequencestop", run, { reason: run.terminal.reason });
|
||||
}
|
||||
|
||||
_cancelRun(run, signal) {
|
||||
if (run.terminal) return;
|
||||
const error = abortError(signal);
|
||||
run.terminal = { status: "cancelled", reason: signal?.reason, error };
|
||||
run.controller.abort(error);
|
||||
if (this._active === run) this.status = "idle";
|
||||
this._emit("sequencecancel", run, { reason: signal?.reason, error });
|
||||
}
|
||||
|
||||
_completeRun(run) {
|
||||
run.terminal = { status: "completed", reason: "completed" };
|
||||
if (this._active === run) this.status = "idle";
|
||||
this._emit("sequencecomplete", run, {
|
||||
completedSteps: run.completedSteps,
|
||||
});
|
||||
}
|
||||
|
||||
_errorRun(run, error) {
|
||||
if (run.terminal) return;
|
||||
run.terminal = { status: "error", reason: "error", error };
|
||||
if (this._active === run) this.status = "idle";
|
||||
this._emit("sequenceerror", run, { error });
|
||||
}
|
||||
|
||||
_result(run) {
|
||||
return Object.freeze({
|
||||
runId: run.id,
|
||||
name: run.sequence.name,
|
||||
sequence: run.sequence,
|
||||
status: run.terminal?.status || "completed",
|
||||
reason: run.terminal?.reason,
|
||||
completedSteps: run.completedSteps,
|
||||
});
|
||||
}
|
||||
|
||||
_emit(type, run, extra = {}) {
|
||||
const detail = Object.freeze({
|
||||
runId: run.id,
|
||||
name: run.sequence.name,
|
||||
sequence: run.sequence,
|
||||
index: run.index,
|
||||
...extra,
|
||||
});
|
||||
this.dispatchEvent(detailEvent(type, detail));
|
||||
return detail;
|
||||
}
|
||||
|
||||
_finalizeRun(run) {
|
||||
run.removeExternalAbort();
|
||||
run.delay?.pause();
|
||||
run.delay = null;
|
||||
for (const gate of [...run.gates]) gate.resolve();
|
||||
run.gates.clear();
|
||||
if (this._active === run) {
|
||||
this._active = null;
|
||||
if (!this.destroyed) this.status = "idle";
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.destroyed) return;
|
||||
if (this._active) this._stopRun(this._active, "destroyed");
|
||||
this.destroyed = true;
|
||||
this.status = "destroyed";
|
||||
this.sequences.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ergonomic one-off playback. The returned player is also the pause/resume/stop
|
||||
* handle; await player.finished for completion.
|
||||
*
|
||||
* @example
|
||||
* const playback = playStateSequence(engine, [
|
||||
* { sprite: "ai.ambient-idle", holdMs: 1200 },
|
||||
* { sprite: "ai.progress", transition: "contour-trace" },
|
||||
* ], { signal });
|
||||
* await playback.finished;
|
||||
*/
|
||||
export function playStateSequence(target, steps, options = {}) {
|
||||
const player = new StateSequencePlayer(target, {
|
||||
clock: options.clock,
|
||||
transition: hasOwn(options, "transition") ? options.transition : "field-morph",
|
||||
transitionFirst: options.transitionFirst,
|
||||
preservePhase: options.preservePhase,
|
||||
});
|
||||
const sequence = defineStateSequence(options.name || "one-off", steps, {
|
||||
metadata: options.metadata,
|
||||
});
|
||||
player.finished = player.play(sequence, { signal: options.signal });
|
||||
return player;
|
||||
}
|
||||
|
||||
export default StateSequencePlayer;
|
||||
3033
web/vendor/src/studio.js
vendored
Normal file
3033
web/vendor/src/studio.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4273
web/vendor/src/styles.css
vendored
Normal file
4273
web/vendor/src/styles.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9
web/vendor/src/web-component-register.js
vendored
Normal file
9
web/vendor/src/web-component-register.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import JoanGlyphElement, {
|
||||
JOAN_GLYPH_TAG_NAME,
|
||||
defineJoanGlyph,
|
||||
} from "./web-component.js";
|
||||
|
||||
defineJoanGlyph(JOAN_GLYPH_TAG_NAME);
|
||||
|
||||
export { JOAN_GLYPH_TAG_NAME, JoanGlyphElement, defineJoanGlyph };
|
||||
export default JoanGlyphElement;
|
||||
308
web/vendor/src/web-component.js
vendored
Normal file
308
web/vendor/src/web-component.js
vendored
Normal file
@@ -0,0 +1,308 @@
|
||||
import JoanGlyphEngine from "./joan-engine.js";
|
||||
|
||||
export const JOAN_GLYPH_TAG_NAME = "joan-glyph";
|
||||
|
||||
const observed = [
|
||||
"sprite",
|
||||
"seed",
|
||||
"resolution",
|
||||
"speed",
|
||||
"density",
|
||||
"field",
|
||||
"pixel-shape",
|
||||
"pixel-switch",
|
||||
"orb-boundary",
|
||||
"orb-background-color",
|
||||
"orb-background-mode",
|
||||
"non-error-background",
|
||||
"non-error-off",
|
||||
"non-error-ink",
|
||||
"non-error-accent",
|
||||
"non-error-glow",
|
||||
"noninteractive",
|
||||
"paused",
|
||||
"transparent",
|
||||
];
|
||||
|
||||
const paletteAttributes = {
|
||||
"non-error-background": "background",
|
||||
"non-error-off": "off",
|
||||
"non-error-ink": "ink",
|
||||
"non-error-accent": "accent",
|
||||
"non-error-glow": "glow",
|
||||
};
|
||||
|
||||
// Keeping the module evaluable without DOM globals lets SSR builds inspect and
|
||||
// bundle it. Instances are still browser elements and should only be created
|
||||
// after a real HTMLElement implementation exists.
|
||||
const HTMLElementBase =
|
||||
typeof globalThis.HTMLElement === "undefined"
|
||||
? class JoanGlyphSsrBase extends EventTarget {}
|
||||
: globalThis.HTMLElement;
|
||||
|
||||
export class JoanGlyphElement extends HTMLElementBase {
|
||||
static observedAttributes = observed;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const root = this.attachShadow({ mode: "open" });
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
:host {
|
||||
display: inline-block;
|
||||
inline-size: 68px;
|
||||
block-size: 68px;
|
||||
aspect-ratio: 1;
|
||||
contain: layout paint style;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
image-rendering: pixelated;
|
||||
touch-action: none;
|
||||
}
|
||||
`;
|
||||
this.canvas = document.createElement("canvas");
|
||||
root.append(style, this.canvas);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this._engine) return;
|
||||
this._engine = new JoanGlyphEngine(this.canvas, this.readOptions());
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this._engine?.destroy();
|
||||
this._engine = null;
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (!this._engine || oldValue === newValue) return;
|
||||
switch (name) {
|
||||
case "sprite":
|
||||
this._engine.setSprite(newValue || "ai.idle");
|
||||
break;
|
||||
case "seed":
|
||||
this._engine.setSeed(newValue || "joan-v5");
|
||||
break;
|
||||
case "resolution":
|
||||
this._engine.setResolution(Number(newValue) || 68);
|
||||
break;
|
||||
case "field":
|
||||
if (newValue === null) this._engine.useRecipeFields();
|
||||
else this._engine.setField(newValue);
|
||||
break;
|
||||
case "pixel-shape":
|
||||
if (newValue === null) this._engine.setOptions({ pixelShape: null });
|
||||
else this._engine.setPixelShape(newValue);
|
||||
break;
|
||||
case "pixel-switch":
|
||||
if (newValue === null) this._engine.setOptions({ pixelSwitch: null });
|
||||
else this._engine.setPixelSwitch(newValue);
|
||||
break;
|
||||
case "orb-boundary":
|
||||
this._engine.setOptions({
|
||||
orbBoundary:
|
||||
String(newValue || "").trim().toLowerCase() === "gestalt"
|
||||
? "gestalt"
|
||||
: "defined",
|
||||
});
|
||||
break;
|
||||
case "orb-background-color":
|
||||
case "orb-background-mode":
|
||||
this._engine.setOptions(this.readOrbBackground());
|
||||
break;
|
||||
case "non-error-background":
|
||||
case "non-error-off":
|
||||
case "non-error-ink":
|
||||
case "non-error-accent":
|
||||
case "non-error-glow":
|
||||
this._engine.setNonErrorPalette(this.readNonErrorPalette());
|
||||
break;
|
||||
case "paused":
|
||||
if (newValue === null) this._engine.play();
|
||||
else this._engine.pause();
|
||||
break;
|
||||
case "noninteractive":
|
||||
this._engine.setOptions({ interactive: newValue === null });
|
||||
break;
|
||||
case "transparent":
|
||||
this._engine.setOptions({ background: newValue === null });
|
||||
break;
|
||||
case "speed":
|
||||
this._engine.setOptions({ speed: Number(newValue) || 1 });
|
||||
break;
|
||||
case "density":
|
||||
this._engine.setOptions({ density: Number(newValue) || 1 });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
readOptions() {
|
||||
return {
|
||||
sprite: this.getAttribute("sprite") || "ai.idle",
|
||||
seed: this.getAttribute("seed") || "joan-v5",
|
||||
gridSize: Number(this.getAttribute("resolution")) || 68,
|
||||
speed: Number(this.getAttribute("speed")) || 1,
|
||||
density: Number(this.getAttribute("density")) || 1,
|
||||
field: this.getAttribute("field") || undefined,
|
||||
pixelShape: this.getAttribute("pixel-shape") || undefined,
|
||||
pixelSwitch: this.getAttribute("pixel-switch") || undefined,
|
||||
orbBoundary:
|
||||
String(this.getAttribute("orb-boundary") || "").trim().toLowerCase() ===
|
||||
"gestalt"
|
||||
? "gestalt"
|
||||
: "defined",
|
||||
...this.readOrbBackground(),
|
||||
nonErrorPalette: this.readNonErrorPalette(),
|
||||
autoplay: !this.hasAttribute("paused"),
|
||||
interactive: !this.hasAttribute("noninteractive"),
|
||||
background: !this.hasAttribute("transparent"),
|
||||
};
|
||||
}
|
||||
|
||||
readOrbBackground() {
|
||||
const colorAttribute = this.getAttribute("orb-background-color")?.trim();
|
||||
const orbBackgroundColor =
|
||||
colorAttribute && colorAttribute.toLowerCase() !== "transparent"
|
||||
? colorAttribute
|
||||
: null;
|
||||
const modeAttribute = this.getAttribute("orb-background-mode");
|
||||
const requestedMode = String(modeAttribute || "").trim().toLowerCase();
|
||||
const orbBackgroundMode =
|
||||
modeAttribute === null
|
||||
? orbBackgroundColor
|
||||
? "solid"
|
||||
: "none"
|
||||
: ["none", "solid", "pixelated"].includes(requestedMode)
|
||||
? requestedMode
|
||||
: "none";
|
||||
|
||||
return { orbBackgroundColor, orbBackgroundMode };
|
||||
}
|
||||
|
||||
readNonErrorPalette() {
|
||||
const palette = {};
|
||||
for (const [attribute, key] of Object.entries(paletteAttributes)) {
|
||||
const value = this.getAttribute(attribute)?.trim();
|
||||
if (value) palette[key] = value;
|
||||
}
|
||||
return Object.keys(palette).length ? palette : null;
|
||||
}
|
||||
|
||||
get engine() {
|
||||
return this._engine;
|
||||
}
|
||||
|
||||
setSprite(sprite, options) {
|
||||
return this._engine?.setSprite(sprite, options);
|
||||
}
|
||||
|
||||
setProgress(value) {
|
||||
return this._engine?.setProgress(value);
|
||||
}
|
||||
|
||||
setNonErrorPalette(palette) {
|
||||
return this._engine?.setNonErrorPalette(palette);
|
||||
}
|
||||
|
||||
setSeed(seed) {
|
||||
return this._engine?.setSeed(seed);
|
||||
}
|
||||
|
||||
setResolution(size) {
|
||||
return this._engine?.setResolution(size);
|
||||
}
|
||||
|
||||
setField(field) {
|
||||
return this._engine?.setField(field);
|
||||
}
|
||||
|
||||
useRecipeFields() {
|
||||
return this._engine?.useRecipeFields();
|
||||
}
|
||||
|
||||
setPixelShape(shape) {
|
||||
return this._engine?.setPixelShape(shape);
|
||||
}
|
||||
|
||||
setPixelSwitch(pixelSwitch) {
|
||||
return this._engine?.setPixelSwitch(pixelSwitch);
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
return this._engine?.setOptions(options);
|
||||
}
|
||||
|
||||
configure(options, configureOptions) {
|
||||
return this._engine?.configure(options, configureOptions);
|
||||
}
|
||||
|
||||
setAudioLevel(value) {
|
||||
return this._engine?.setAudioLevel(value);
|
||||
}
|
||||
|
||||
activate(options) {
|
||||
return this._engine?.activate(options);
|
||||
}
|
||||
|
||||
resume(options) {
|
||||
return this._engine?.resume(options);
|
||||
}
|
||||
|
||||
play() {
|
||||
return this._engine?.play();
|
||||
}
|
||||
|
||||
pause() {
|
||||
return this._engine?.pause();
|
||||
}
|
||||
|
||||
toggle() {
|
||||
return this._engine?.toggle();
|
||||
}
|
||||
|
||||
renderOnce(time) {
|
||||
return this._engine?.renderOnce(time);
|
||||
}
|
||||
|
||||
transitionTo(sprite, options) {
|
||||
return this._engine?.transitionTo(sprite, options);
|
||||
}
|
||||
|
||||
whenTransitionComplete(options) {
|
||||
return this._engine?.whenTransitionComplete(options);
|
||||
}
|
||||
|
||||
inspect() {
|
||||
return this._engine?.inspect();
|
||||
}
|
||||
|
||||
on(type, listener, options) {
|
||||
return this._engine?.on(type, listener, options);
|
||||
}
|
||||
|
||||
exportConfig() {
|
||||
return this._engine?.exportConfig();
|
||||
}
|
||||
|
||||
signal(type, payload) {
|
||||
return this._engine?.signal(type, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export function defineJoanGlyph(tagName = JOAN_GLYPH_TAG_NAME) {
|
||||
const registry = globalThis.customElements;
|
||||
if (!registry) return JoanGlyphElement;
|
||||
if (!registry.get(tagName)) registry.define(tagName, JoanGlyphElement);
|
||||
return JoanGlyphElement;
|
||||
}
|
||||
|
||||
// Preserve the original import-and-register behavior. Applications that prefer
|
||||
// an explicit side-effect entry can import `web-component/register` instead.
|
||||
if (typeof globalThis.customElements !== "undefined") defineJoanGlyph();
|
||||
|
||||
export default JoanGlyphElement;
|
||||
86
web/vendor/types/config.d.ts
vendored
Normal file
86
web/vendor/types/config.d.ts
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
import type {
|
||||
BuiltInSpriteId,
|
||||
FieldId,
|
||||
JoanEngineOptions,
|
||||
PixelShape,
|
||||
PixelSwitch,
|
||||
SpriteRecipe,
|
||||
Transition,
|
||||
} from "./index.js";
|
||||
|
||||
export interface ConfigurationIssue {
|
||||
code: string;
|
||||
message: string;
|
||||
path: string | null;
|
||||
value: unknown;
|
||||
suggestion: string | null;
|
||||
severity: "error";
|
||||
}
|
||||
|
||||
export interface InspectEngineOptionsResult {
|
||||
ok: boolean;
|
||||
value: Partial<JoanEngineOptions> & Record<string, unknown>;
|
||||
issues: ConfigurationIssue[];
|
||||
}
|
||||
|
||||
export interface InspectEngineOptionsSettings {
|
||||
coerce?: boolean;
|
||||
allowUnknown?: boolean;
|
||||
}
|
||||
|
||||
export interface ValidateEngineOptionsSettings
|
||||
extends InspectEngineOptionsSettings {
|
||||
strict?: boolean;
|
||||
}
|
||||
|
||||
export class JoanConfigurationError extends RangeError {
|
||||
readonly name: "JoanConfigurationError";
|
||||
readonly code: string;
|
||||
readonly path: string | null;
|
||||
readonly value: unknown;
|
||||
readonly suggestion: string | null;
|
||||
readonly allowed: unknown[] | null;
|
||||
constructor(
|
||||
message: string,
|
||||
details?: Partial<ConfigurationIssue> & { allowed?: readonly unknown[] },
|
||||
);
|
||||
toIssue(): ConfigurationIssue;
|
||||
}
|
||||
|
||||
export const TRANSITION_IDS: readonly Transition[];
|
||||
export const PIXEL_SWITCH_IDS: readonly PixelSwitch[];
|
||||
export const PIXEL_SHAPE_IDS: readonly PixelShape[];
|
||||
|
||||
export function nearestName<T extends string>(
|
||||
value: unknown,
|
||||
candidates: readonly T[],
|
||||
options?: { maxDistance?: number },
|
||||
): T | null;
|
||||
|
||||
export function validateKnownValue<T extends string>(
|
||||
value: unknown,
|
||||
allowed: readonly T[],
|
||||
options?: {
|
||||
aliases?: Readonly<Record<string, T>>;
|
||||
label?: string;
|
||||
path?: string;
|
||||
},
|
||||
): T;
|
||||
|
||||
export function validateSpriteId(value: unknown): BuiltInSpriteId;
|
||||
export function validateFieldId(value: unknown): FieldId;
|
||||
export function validateTransition(value: unknown): Transition;
|
||||
export function validatePixelShape(value: unknown): PixelShape;
|
||||
export function validatePixelSwitch(value: unknown): PixelSwitch;
|
||||
export function inspectEngineOptions(
|
||||
input: unknown,
|
||||
options?: InspectEngineOptionsSettings,
|
||||
): InspectEngineOptionsResult;
|
||||
export function validateEngineOptions<T extends Record<string, unknown>>(
|
||||
input: T,
|
||||
options?: ValidateEngineOptionsSettings,
|
||||
): T & JoanEngineOptions;
|
||||
export function validateRecipe(input: unknown): SpriteRecipe;
|
||||
export function defineRecipe<T extends SpriteRecipe>(
|
||||
recipe: T,
|
||||
): Readonly<T & SpriteRecipe>;
|
||||
110
web/vendor/types/fields.d.ts
vendored
Normal file
110
web/vendor/types/fields.d.ts
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { FieldAlias, FieldId, FieldName } from "./index.js";
|
||||
|
||||
export type FieldOptions = Readonly<Record<string, number | string | boolean>>;
|
||||
export type FieldSampler = (
|
||||
x: number,
|
||||
y: number,
|
||||
time?: number,
|
||||
options?: FieldOptions,
|
||||
kit?: FieldKit,
|
||||
) => number;
|
||||
|
||||
export const TAU: number;
|
||||
export const BAYER8_SIZE: 8;
|
||||
export const BAYER8: readonly number[];
|
||||
export function clamp(value: number, min?: number, max?: number): number;
|
||||
export function lerp(a: number, b: number, amount: number): number;
|
||||
export function smoothstep(edge0: number, edge1: number, value: number): number;
|
||||
export function smootherstep(edge0: number, edge1: number, value: number): number;
|
||||
export function fract(value: number): number;
|
||||
export function bayer8(x: number, y: number): number;
|
||||
export const bayerThreshold: typeof bayer8;
|
||||
export function orderedDither(
|
||||
value: number,
|
||||
x: number,
|
||||
y: number,
|
||||
levels?: number,
|
||||
): number;
|
||||
export function hashSeed(seed?: unknown): number;
|
||||
|
||||
export class FieldKit {
|
||||
constructor(seed?: unknown | { seed: unknown });
|
||||
seed: number;
|
||||
readonly permutation: Uint8Array;
|
||||
reseed(seed?: unknown): this;
|
||||
setSeed(seed?: unknown): this;
|
||||
clone(): FieldKit;
|
||||
list(): readonly FieldId[];
|
||||
has(id: unknown): boolean;
|
||||
get(id: FieldName): FieldSampler | undefined;
|
||||
sample(
|
||||
id: FieldName,
|
||||
x: number,
|
||||
y: number,
|
||||
time?: number,
|
||||
options?: FieldOptions,
|
||||
): number;
|
||||
hash2(x: number, y: number, salt?: number): number;
|
||||
signedNoise2(x: number, y: number): number;
|
||||
noise2(x: number, y: number): number;
|
||||
signedNoise3(x: number, y: number, z: number): number;
|
||||
noise3(x: number, y: number, z: number): number;
|
||||
value2(x: number, y: number): number;
|
||||
fbmNoise(x: number, y: number, z?: number, options?: FieldOptions): number;
|
||||
ridgedNoise(x: number, y: number, z?: number, options?: FieldOptions): number;
|
||||
curl2(
|
||||
x: number,
|
||||
y: number,
|
||||
z?: number,
|
||||
epsilon?: number,
|
||||
out?: Float32Array | number[],
|
||||
): Float32Array | number[];
|
||||
cellular2(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
voronoi2(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
fbm(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
ridged(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
domainWarp(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
curl(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
flow(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
worley(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
voronoi(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
plasma(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
interference(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
vortex(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
metaballs(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
caustics(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
strata(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
radar(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
constellation(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
liquid(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
electric(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
ripple(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
kaleidoscope(x: number, y: number, time?: number, options?: FieldOptions): number;
|
||||
}
|
||||
|
||||
export const fbm: FieldSampler;
|
||||
export const ridged: FieldSampler;
|
||||
export const domainWarp: FieldSampler;
|
||||
export const curl: FieldSampler;
|
||||
export const flow: FieldSampler;
|
||||
export const worley: FieldSampler;
|
||||
export const voronoi: FieldSampler;
|
||||
export const plasma: FieldSampler;
|
||||
export const interference: FieldSampler;
|
||||
export const vortex: FieldSampler;
|
||||
export const metaballs: FieldSampler;
|
||||
export const caustics: FieldSampler;
|
||||
export const strata: FieldSampler;
|
||||
export const radar: FieldSampler;
|
||||
export const constellation: FieldSampler;
|
||||
export const liquid: FieldSampler;
|
||||
export const electric: FieldSampler;
|
||||
export const ripple: FieldSampler;
|
||||
export const kaleidoscope: FieldSampler;
|
||||
export const FIELDS: Readonly<Record<FieldId, FieldSampler>>;
|
||||
export const FIELD_IDS: readonly FieldId[];
|
||||
export const FIELD_ALIASES: Readonly<Record<FieldAlias, FieldId>>;
|
||||
export const DEFAULT_FIELD_KIT: FieldKit;
|
||||
export function createFieldKit(seed?: unknown): FieldKit;
|
||||
|
||||
export default FieldKit;
|
||||
46
web/vendor/types/glyphs.d.ts
vendored
Normal file
46
web/vendor/types/glyphs.d.ts
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { GlyphSampleContext, GlyphSampler } from "./index.js";
|
||||
|
||||
export type GlyphId =
|
||||
| "idle"
|
||||
| "listening"
|
||||
| "thinking"
|
||||
| "thinking-deep"
|
||||
| "still-working"
|
||||
| "loading"
|
||||
| "progress"
|
||||
| "generating"
|
||||
| "searching"
|
||||
| "tool-use"
|
||||
| "speaking"
|
||||
| "awaiting-input"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "error"
|
||||
| "paused"
|
||||
| "cancelled"
|
||||
| "offline"
|
||||
| "transfer"
|
||||
| "handoff"
|
||||
| "celebration"
|
||||
| "custom";
|
||||
|
||||
export const GLYPHS: Readonly<Record<GlyphId, GlyphSampler>>;
|
||||
export const GLYPH_IDS: readonly GlyphId[];
|
||||
export const GLYPH_ALIASES: Readonly<Record<string, GlyphId>>;
|
||||
export function resolveGlyphId(id: unknown): GlyphId;
|
||||
export function getGlyph(id: unknown): GlyphSampler;
|
||||
export function listGlyphs(): readonly GlyphId[];
|
||||
export function gestaltArcSupport(
|
||||
angle: number,
|
||||
context?: GlyphSampleContext,
|
||||
openness?: number,
|
||||
): number;
|
||||
export function sampleGlyph(
|
||||
id: unknown,
|
||||
x: number,
|
||||
y: number,
|
||||
time?: number,
|
||||
context?: GlyphSampleContext,
|
||||
): number;
|
||||
|
||||
export default GLYPHS;
|
||||
636
web/vendor/types/index.d.ts
vendored
Normal file
636
web/vendor/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,636 @@
|
||||
export type BuiltInSpriteId =
|
||||
| "ai.idle"
|
||||
| "ai.ambient-idle"
|
||||
| "ai.ambient-thinking"
|
||||
| "ai.ambient-thinking-symmetric"
|
||||
| "ai.ambient-speaking"
|
||||
| "ai.listening"
|
||||
| "ai.thinking"
|
||||
| "ai.thinking-deep"
|
||||
| "ai.still-working"
|
||||
| "ai.loading"
|
||||
| "ai.progress"
|
||||
| "ai.generating"
|
||||
| "ai.searching"
|
||||
| "ai.tool-use"
|
||||
| "ai.speaking"
|
||||
| "ai.awaiting-input"
|
||||
| "status.success"
|
||||
| "status.warning"
|
||||
| "status.error"
|
||||
| "status.paused"
|
||||
| "status.cancelled"
|
||||
| "status.offline"
|
||||
| "transfer.active"
|
||||
| "workflow.handoff"
|
||||
| "status.celebration";
|
||||
|
||||
export type SpriteAlias =
|
||||
| "idle"
|
||||
| "ready"
|
||||
| "ambient"
|
||||
| "ambient-idle"
|
||||
| "field-idle"
|
||||
| "ambient-thinking"
|
||||
| "field-thinking"
|
||||
| "ambient-thinking-symmetric"
|
||||
| "field-thinking-symmetric"
|
||||
| "symmetric-thinking"
|
||||
| "ambient-speaking"
|
||||
| "field-speaking"
|
||||
| "listening"
|
||||
| "thinking"
|
||||
| "thinking-deep"
|
||||
| "deep-thinking"
|
||||
| "still-working"
|
||||
| "working"
|
||||
| "loading"
|
||||
| "progress"
|
||||
| "generating"
|
||||
| "searching"
|
||||
| "tool-use"
|
||||
| "tool"
|
||||
| "speaking"
|
||||
| "awaiting-input"
|
||||
| "prompt"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "error"
|
||||
| "paused"
|
||||
| "cancelled"
|
||||
| "offline"
|
||||
| "transfer"
|
||||
| "upload"
|
||||
| "download"
|
||||
| "sync"
|
||||
| "handoff"
|
||||
| "celebration";
|
||||
|
||||
export type SpriteName = BuiltInSpriteId | SpriteAlias;
|
||||
|
||||
export type FieldId =
|
||||
| "fbm"
|
||||
| "ridged"
|
||||
| "domain-warp"
|
||||
| "curl"
|
||||
| "flow"
|
||||
| "worley"
|
||||
| "voronoi"
|
||||
| "plasma"
|
||||
| "interference"
|
||||
| "vortex"
|
||||
| "metaballs"
|
||||
| "caustics"
|
||||
| "strata"
|
||||
| "radar"
|
||||
| "constellation"
|
||||
| "liquid"
|
||||
| "electric"
|
||||
| "ripple"
|
||||
| "kaleidoscope";
|
||||
|
||||
export type FieldAlias =
|
||||
| "noise"
|
||||
| "turbulence"
|
||||
| "ridge"
|
||||
| "domainWarp"
|
||||
| "domain_warp"
|
||||
| "warp"
|
||||
| "curl-flow"
|
||||
| "curlFlow"
|
||||
| "cellular"
|
||||
| "cells"
|
||||
| "cell"
|
||||
| "water"
|
||||
| "lightning"
|
||||
| "waves"
|
||||
| "mandala";
|
||||
|
||||
export type FieldName = FieldId | FieldAlias;
|
||||
|
||||
export type PixelShape =
|
||||
| "disc"
|
||||
| "square"
|
||||
| "diamond"
|
||||
| "capsule"
|
||||
| "line"
|
||||
| "ring"
|
||||
| "cross"
|
||||
| "square-cross"
|
||||
| "square-cross-ring";
|
||||
export type PixelShapeAlias = "circle" | "rounded-square" | "dot";
|
||||
export type PixelShapeName = PixelShape | PixelShapeAlias;
|
||||
|
||||
export type PixelSwitch =
|
||||
| "ordered-dither"
|
||||
| "temporal-blue-noise"
|
||||
| "threshold-hysteresis"
|
||||
| "sdf-wavefront"
|
||||
| "contour-trace"
|
||||
| "curl-advect"
|
||||
| "neighbor-propagation"
|
||||
| "radial-cascade"
|
||||
| "path-draw"
|
||||
| "axis-flip"
|
||||
| "seeded-dissolve"
|
||||
| "field-morph";
|
||||
export type PixelSwitchAlias = "cluster-dissolve" | "neighbor-ignite";
|
||||
export type PixelSwitchName = PixelSwitch | PixelSwitchAlias;
|
||||
|
||||
export type Transition =
|
||||
| "field-morph"
|
||||
| "seeded-dissolve"
|
||||
| "radial-cascade"
|
||||
| "angular-sweep"
|
||||
| "scanline"
|
||||
| "contour-trace"
|
||||
| "path-draw"
|
||||
| "axis-flip"
|
||||
| "cluster-dissolve"
|
||||
| "neighbor-ignite"
|
||||
| "glitch-bands"
|
||||
| "instant";
|
||||
export type TransitionAlias =
|
||||
| "dissolve"
|
||||
| "radial"
|
||||
| "bloom"
|
||||
| "spiral"
|
||||
| "wave"
|
||||
| "shutter"
|
||||
| "glitch"
|
||||
| "sdf-wavefront"
|
||||
| "neighbor-propagation";
|
||||
export type TransitionName = Transition | TransitionAlias;
|
||||
|
||||
export type Quality = "low" | "balanced" | "high" | "auto";
|
||||
export type EffectiveQuality = Exclude<Quality, "auto">;
|
||||
export type ReducedMotion = boolean | "system";
|
||||
export type PreviewMode = "fit" | "actual";
|
||||
export type OrbBoundary = "defined" | "gestalt";
|
||||
export type OrbBackgroundMode = "none" | "solid" | "pixelated";
|
||||
export type CSSColor = string;
|
||||
|
||||
export interface Palette {
|
||||
/** Optional authored palette identifier preserved by exported configs. */
|
||||
name?: string;
|
||||
background?: CSSColor;
|
||||
/** Authored inactive color alias; effective palettes also expose `off`. */
|
||||
shadow?: CSSColor;
|
||||
off?: CSSColor;
|
||||
ink?: CSSColor;
|
||||
accent?: CSSColor;
|
||||
/** Use `transparent` to disable glow. */
|
||||
glow?: CSSColor;
|
||||
/** Optional overrides selected by a recipe's authored palette name. */
|
||||
variants?: Readonly<Record<string, Palette>>;
|
||||
}
|
||||
|
||||
export interface FieldLayer {
|
||||
field: FieldName;
|
||||
weight?: number;
|
||||
scale?: number;
|
||||
blend?: string;
|
||||
phase?: number;
|
||||
speed?: number;
|
||||
contrast?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface TransitionRecipe {
|
||||
name?: TransitionName;
|
||||
type?: TransitionName;
|
||||
enter?: TransitionName;
|
||||
exit?: TransitionName;
|
||||
duration?: number;
|
||||
durationMs?: number;
|
||||
exitDurationMs?: number;
|
||||
preservePhase?: boolean;
|
||||
preserveBuffer?: boolean;
|
||||
interruptible?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface PixelSwitchRecipe {
|
||||
mode: PixelSwitchName;
|
||||
dither?: string;
|
||||
hysteresis?: number;
|
||||
temporalRate?: number;
|
||||
neighborhood?: number;
|
||||
direction?: string | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SpriteComposition {
|
||||
mode?: "glyph" | "field-orb" | string;
|
||||
/** Opts a circular recipe into the global Gestalt boundary preference. */
|
||||
gestaltBoundary?: boolean;
|
||||
/** Controls how much open space remains between implied boundary arcs. */
|
||||
gestaltOpenness?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SpriteRecipe {
|
||||
schemaVersion?: number;
|
||||
id: string;
|
||||
glyph: string;
|
||||
/** A recipe can use one primary field, a field mix, or both. */
|
||||
field?: FieldName;
|
||||
fieldMix?: readonly FieldLayer[];
|
||||
palette?: Readonly<Palette>;
|
||||
semantic?: Readonly<Record<string, unknown>>;
|
||||
composition?: Readonly<SpriteComposition>;
|
||||
pixel?: Readonly<{
|
||||
shape?: PixelShapeName;
|
||||
scale?: number;
|
||||
gap?: number;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
pixelShape?: PixelShapeName;
|
||||
pixelSwitch?: PixelSwitchName | Readonly<PixelSwitchRecipe>;
|
||||
transition?: TransitionName | Readonly<TransitionRecipe>;
|
||||
interactions?: Readonly<Record<string, unknown>> | false;
|
||||
timeline?: Readonly<Record<string, unknown>>;
|
||||
reducedMotion?: Readonly<Record<string, unknown>>;
|
||||
labels?: Readonly<Record<string, string>>;
|
||||
threshold?: number;
|
||||
density?: number;
|
||||
speed?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface JoanEngineOptions {
|
||||
sprite?: SpriteName | SpriteRecipe;
|
||||
seed?: string | number;
|
||||
gridSize?: number;
|
||||
speed?: number;
|
||||
density?: number;
|
||||
contrast?: number;
|
||||
fps?: number;
|
||||
pixelShape?: PixelShapeName | null;
|
||||
pixelSwitch?: PixelSwitchName | PixelSwitchRecipe | null;
|
||||
transition?: TransitionName | TransitionRecipe | null;
|
||||
field?: FieldName | null;
|
||||
fieldMode?: "recipe" | "override";
|
||||
palette?: Palette | null;
|
||||
/** Compatibility alias for the global palette override. */
|
||||
paletteOverride?: Palette | null;
|
||||
nonErrorPalette?: Palette | null;
|
||||
previewMode?: PreviewMode;
|
||||
/** Use an implied, discontinuous edge for supported circular presence states. */
|
||||
orbBoundary?: OrbBoundary;
|
||||
/** Optional color used by the background layer inside supported presence orbs. */
|
||||
orbBackgroundColor?: CSSColor | null;
|
||||
/** Selects no disk, a smooth disk, or a grid-aligned pixel disk. */
|
||||
orbBackgroundMode?: OrbBackgroundMode;
|
||||
background?: boolean;
|
||||
offPixels?: boolean;
|
||||
interactive?: boolean;
|
||||
autoplay?: boolean;
|
||||
autoResize?: boolean;
|
||||
dprMax?: number;
|
||||
reducedMotion?: ReducedMotion;
|
||||
quality?: Quality;
|
||||
ariaLive?: HTMLElement | null;
|
||||
direction?: string;
|
||||
targetGlyph?: string | null;
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
export interface CreateProceduralGlyphOptions extends JoanEngineOptions {
|
||||
canvas: HTMLCanvasElement;
|
||||
}
|
||||
|
||||
export interface SetSpriteOptions {
|
||||
transition?: TransitionName | TransitionRecipe;
|
||||
duration?: number;
|
||||
preservePhase?: boolean;
|
||||
preserveBuffer?: boolean;
|
||||
immediate?: boolean;
|
||||
forceTransition?: boolean;
|
||||
useRecipeFields?: boolean;
|
||||
}
|
||||
|
||||
export interface TransitionToOptions extends SetSpriteOptions {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface ActivateOptions {
|
||||
x?: number;
|
||||
y?: number;
|
||||
energy?: number;
|
||||
life?: number;
|
||||
source?: string;
|
||||
key?: string | null;
|
||||
}
|
||||
|
||||
export interface ResumeOptions extends SetSpriteOptions {
|
||||
sprite?: SpriteName | SpriteRecipe;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export interface SignalPayload {
|
||||
value?: unknown;
|
||||
energy?: number;
|
||||
life?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
direction?: string;
|
||||
source?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface EngineStats {
|
||||
fps: number;
|
||||
activePixels: number;
|
||||
frameMs: number;
|
||||
resolution: number;
|
||||
simulationSteps: number;
|
||||
sampledPixels: number;
|
||||
quality: EffectiveQuality;
|
||||
}
|
||||
|
||||
export interface EngineInspection {
|
||||
sprite: string | null;
|
||||
recipeSource: string | null;
|
||||
previousSprite: string | null;
|
||||
fieldMode: "recipe" | "override";
|
||||
field: string | null;
|
||||
running: boolean;
|
||||
visible: boolean;
|
||||
destroyed: boolean;
|
||||
reducedMotion: boolean;
|
||||
transition: {
|
||||
active: boolean;
|
||||
name: Transition;
|
||||
progress: number;
|
||||
elapsed: number;
|
||||
duration: number;
|
||||
pending: string | null;
|
||||
};
|
||||
signals: Array<{ type: string; value: number; age: number; life: number }>;
|
||||
semanticSignals: Record<string, unknown>;
|
||||
palette: Palette;
|
||||
orbBoundary: OrbBoundary;
|
||||
orbBackgroundColor: CSSColor | null;
|
||||
orbBackgroundMode: OrbBackgroundMode;
|
||||
quality: { requested: Quality; effective: EffectiveQuality };
|
||||
stats: EngineStats;
|
||||
}
|
||||
|
||||
export interface TransitionDetail {
|
||||
from: string | null;
|
||||
to: string | null;
|
||||
duration: number;
|
||||
elapsed: number;
|
||||
transition: Transition;
|
||||
status: "completed" | "interrupted";
|
||||
reason: string;
|
||||
serial: number;
|
||||
}
|
||||
|
||||
export interface JoanGlyphEventDetailMap {
|
||||
activate: {
|
||||
sprite: string;
|
||||
x: number;
|
||||
y: number;
|
||||
source: string;
|
||||
key: string | null;
|
||||
};
|
||||
configchange: Partial<JoanEngineOptions> & Record<string, unknown>;
|
||||
destroy: Record<string, never>;
|
||||
pause: { sprite: string };
|
||||
play: { sprite: string };
|
||||
qualitychange: {
|
||||
from: EffectiveQuality;
|
||||
to: EffectiveQuality;
|
||||
frameMs: number;
|
||||
budgetMs: number;
|
||||
};
|
||||
resume: {
|
||||
from: string;
|
||||
to: string;
|
||||
restoredBuffer: boolean;
|
||||
source: string;
|
||||
};
|
||||
signal: { type: string; payload: SignalPayload };
|
||||
spritechange: { from?: string; to: string; label: string };
|
||||
stats: EngineStats;
|
||||
timelinecomplete: { from: string; to: string };
|
||||
transitioncomplete: TransitionDetail;
|
||||
transitionqueued: { from?: string; to?: string };
|
||||
}
|
||||
|
||||
export type JoanGlyphEventName = keyof JoanGlyphEventDetailMap;
|
||||
export type JoanGlyphEvent<K extends JoanGlyphEventName> = CustomEvent<
|
||||
JoanGlyphEventDetailMap[K]
|
||||
>;
|
||||
|
||||
export interface ExportedJoanConfig {
|
||||
package: "@joan/procedural-glyph-engine";
|
||||
version: string;
|
||||
sprite: string;
|
||||
seed: string;
|
||||
gridSize: number;
|
||||
speed: number;
|
||||
density: number;
|
||||
contrast: number;
|
||||
fps: number;
|
||||
field?: FieldName;
|
||||
fieldMode: "recipe" | "override";
|
||||
pixelShape: PixelShape | null;
|
||||
pixelSwitch: PixelSwitch | PixelSwitchRecipe | null;
|
||||
transition: Transition | TransitionRecipe | null;
|
||||
palette: Palette &
|
||||
Required<Pick<Palette, "background" | "off" | "ink" | "accent" | "glow">>;
|
||||
paletteOverride: Palette | null;
|
||||
nonErrorPalette: Palette | null;
|
||||
orbBoundary: OrbBoundary;
|
||||
orbBackgroundColor: CSSColor | null;
|
||||
orbBackgroundMode: OrbBackgroundMode;
|
||||
progress: number;
|
||||
background: boolean;
|
||||
offPixels: boolean;
|
||||
interactive: boolean;
|
||||
autoplay: boolean;
|
||||
autoResize: boolean;
|
||||
dprMax: number;
|
||||
quality: Quality;
|
||||
reducedMotion: ReducedMotion;
|
||||
direction: string | null;
|
||||
targetGlyph: string | null;
|
||||
}
|
||||
|
||||
export interface SvgExportOptions {
|
||||
size?: number;
|
||||
background?: boolean;
|
||||
offPixels?: boolean;
|
||||
}
|
||||
|
||||
export interface AnimatedSvgExportOptions extends SvgExportOptions {
|
||||
duration?: number;
|
||||
fps?: number;
|
||||
maxGridSize?: number;
|
||||
}
|
||||
|
||||
export interface GlyphSampleContext extends Record<string, unknown> {
|
||||
progress?: number;
|
||||
audioLevel?: number;
|
||||
energy?: number;
|
||||
elapsed?: number;
|
||||
resolution?: number;
|
||||
reducedMotion?: boolean;
|
||||
orbBoundary?: OrbBoundary;
|
||||
gestaltOpenness?: number;
|
||||
}
|
||||
|
||||
export type GlyphSampler = (
|
||||
x: number,
|
||||
y: number,
|
||||
time: number,
|
||||
context: GlyphSampleContext,
|
||||
) => number;
|
||||
export type GlyphBitmap =
|
||||
| ArrayLike<number>
|
||||
| readonly (readonly number[])[];
|
||||
|
||||
export interface RegisterGlyphDimensions {
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export interface LoadGlyphFileOptions {
|
||||
id?: string;
|
||||
label?: string;
|
||||
baseSprite?: SpriteName;
|
||||
resolution?: number;
|
||||
transition?: TransitionName | TransitionRecipe;
|
||||
}
|
||||
|
||||
export const TRANSITIONS: readonly Transition[];
|
||||
export const PIXEL_SWITCHES: readonly PixelSwitch[];
|
||||
export const PIXEL_SHAPES: readonly PixelShape[];
|
||||
export const ORB_BACKGROUND_MODES: readonly OrbBackgroundMode[];
|
||||
export const SPRITES: Readonly<Record<BuiltInSpriteId, Readonly<SpriteRecipe>>>;
|
||||
|
||||
export function getSprite(id: SpriteName): Readonly<SpriteRecipe> | undefined;
|
||||
export function listSprites(): readonly Readonly<SpriteRecipe>[];
|
||||
export function transitionDelay(
|
||||
transition: TransitionName,
|
||||
x: number,
|
||||
y: number,
|
||||
random?: number,
|
||||
origin?: { x: number; y: number },
|
||||
): number;
|
||||
|
||||
export class JoanGlyphEngine extends EventTarget {
|
||||
static readonly sprites: typeof SPRITES;
|
||||
static listSprites(): readonly Readonly<SpriteRecipe>[];
|
||||
|
||||
constructor(canvas: HTMLCanvasElement, options?: JoanEngineOptions);
|
||||
|
||||
readonly canvas: HTMLCanvasElement;
|
||||
readonly context: CanvasRenderingContext2D;
|
||||
options: JoanEngineOptions;
|
||||
currentRecipe: SpriteRecipe;
|
||||
recipeSource: SpriteRecipe;
|
||||
previousRecipe: SpriteRecipe;
|
||||
fieldOverride: FieldName | null;
|
||||
gridSize: number;
|
||||
seed: string;
|
||||
running: boolean;
|
||||
destroyed: boolean;
|
||||
visible: boolean;
|
||||
reducedMotion: boolean;
|
||||
progress: number;
|
||||
audioLevel: number;
|
||||
clock: number;
|
||||
stats: EngineStats;
|
||||
readonly values: Float32Array;
|
||||
readonly gates: Uint8Array;
|
||||
readonly luminance: Float32Array;
|
||||
|
||||
on<K extends JoanGlyphEventName>(
|
||||
type: K,
|
||||
listener: (event: JoanGlyphEvent<K>) => void,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): () => void;
|
||||
addEventListener<K extends JoanGlyphEventName>(
|
||||
type: K,
|
||||
listener: (this: JoanGlyphEngine, event: JoanGlyphEvent<K>) => unknown,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
addEventListener(
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject | null,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
|
||||
activate(options?: ActivateOptions): this;
|
||||
resume(options?: ResumeOptions): this;
|
||||
setSprite(sprite: SpriteName | SpriteRecipe, options?: SetSpriteOptions): this;
|
||||
transitionTo(
|
||||
sprite: SpriteName | SpriteRecipe,
|
||||
options?: TransitionToOptions,
|
||||
): Promise<TransitionDetail>;
|
||||
whenTransitionComplete(options?: { signal?: AbortSignal }): Promise<TransitionDetail>;
|
||||
setField(field: FieldName): this;
|
||||
useRecipeFields(): this;
|
||||
setPixelSwitch(pixelSwitch: PixelSwitchName | PixelSwitchRecipe): this;
|
||||
setPixelShape(pixelShape: PixelShapeName): this;
|
||||
setResolution(size: number): this;
|
||||
setSeed(seed: string | number): this;
|
||||
setProgress(value: number): this;
|
||||
setAudioLevel(value: number): this;
|
||||
setNonErrorPalette(
|
||||
palette: Palette | null,
|
||||
options?: { render?: boolean },
|
||||
): this;
|
||||
configure(
|
||||
options?: Partial<JoanEngineOptions>,
|
||||
configureOptions?: { strict?: boolean },
|
||||
): this;
|
||||
setOptions(options?: Partial<JoanEngineOptions>): this;
|
||||
signal(type: string, payload?: SignalPayload): this;
|
||||
registerGlyph(
|
||||
id: string,
|
||||
source: GlyphSampler | GlyphBitmap,
|
||||
dimensions?: RegisterGlyphDimensions,
|
||||
): this;
|
||||
loadGlyphFile(file: Blob, options?: LoadGlyphFileOptions): Promise<string>;
|
||||
play(): this;
|
||||
pause(): this;
|
||||
toggle(): this;
|
||||
renderFrame(time?: number, dt?: number, force?: boolean): EngineStats;
|
||||
renderOnce(time?: number): EngineStats;
|
||||
inspect(): EngineInspection;
|
||||
exportConfig(): ExportedJoanConfig;
|
||||
toSVG(options?: SvgExportOptions): string;
|
||||
toAnimatedSVG(options?: AnimatedSvgExportOptions): Promise<string>;
|
||||
downloadPNG(filename?: string): Promise<Blob>;
|
||||
downloadSVG(filename?: string, options?: SvgExportOptions): Promise<string>;
|
||||
downloadAnimatedSVG(
|
||||
filename?: string,
|
||||
options?: AnimatedSvgExportOptions,
|
||||
): Promise<string>;
|
||||
toDataURL(type?: string, quality?: number): string;
|
||||
toBlob(type?: string, quality?: number): Promise<Blob>;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export function createProceduralGlyph(
|
||||
options: CreateProceduralGlyphOptions,
|
||||
): JoanGlyphEngine;
|
||||
export function createProceduralGlyph(
|
||||
target: string | HTMLCanvasElement,
|
||||
options?: JoanEngineOptions,
|
||||
): JoanGlyphEngine;
|
||||
export function createGlyph(
|
||||
target: string | HTMLCanvasElement,
|
||||
options?: JoanEngineOptions,
|
||||
): JoanGlyphEngine;
|
||||
export function mountProceduralGlyph(
|
||||
target: string | HTMLCanvasElement,
|
||||
options?: JoanEngineOptions,
|
||||
): JoanGlyphEngine;
|
||||
|
||||
export default JoanGlyphEngine;
|
||||
23
web/vendor/types/sprites.d.ts
vendored
Normal file
23
web/vendor/types/sprites.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import type {
|
||||
BuiltInSpriteId,
|
||||
FieldId,
|
||||
Palette,
|
||||
SpriteAlias,
|
||||
SpriteName,
|
||||
SpriteRecipe,
|
||||
} from "./index.js";
|
||||
|
||||
export const FIELD_IDS: readonly FieldId[];
|
||||
export const PALETTES: Readonly<Record<string, Readonly<Palette>>>;
|
||||
export const SPRITES: Readonly<
|
||||
Record<BuiltInSpriteId, Readonly<SpriteRecipe>>
|
||||
>;
|
||||
export const SPRITE_IDS: readonly BuiltInSpriteId[];
|
||||
export const SPRITE_ALIASES: Readonly<Record<SpriteAlias, BuiltInSpriteId>>;
|
||||
|
||||
export function resolveSpriteId(id: unknown): BuiltInSpriteId | undefined;
|
||||
export function getSprite(id: SpriteName): Readonly<SpriteRecipe> | undefined;
|
||||
export function listSprites(): readonly Readonly<SpriteRecipe>[];
|
||||
export function hasSprite(id: unknown): boolean;
|
||||
|
||||
export default SPRITES;
|
||||
81
web/vendor/types/state-director.d.ts
vendored
Normal file
81
web/vendor/types/state-director.d.ts
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
import type {
|
||||
JoanGlyphEngine,
|
||||
SetSpriteOptions,
|
||||
SpriteName,
|
||||
SpriteRecipe,
|
||||
TransitionName,
|
||||
} from "./index.js";
|
||||
|
||||
export type DirectorSprite = SpriteName | SpriteRecipe;
|
||||
|
||||
export interface StateDirectorOptions {
|
||||
deepThinkingAfterMs?: number;
|
||||
stillWorkingAfterMs?: number;
|
||||
transition?: TransitionName;
|
||||
}
|
||||
|
||||
export interface DirectorSetOptions extends SetSpriteOptions {
|
||||
reason?: string;
|
||||
sprite?: DirectorSprite;
|
||||
}
|
||||
|
||||
export interface StateChangeDetail {
|
||||
state: string;
|
||||
from: string;
|
||||
to: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface DirectorRunContext {
|
||||
signal?: AbortSignal;
|
||||
director: StateDirector;
|
||||
engine: JoanGlyphEngine;
|
||||
}
|
||||
|
||||
export interface DirectorRunOptions extends StateDirectorOptions {
|
||||
signal?: AbortSignal;
|
||||
thinking?: StateDirectorOptions & DirectorSetOptions;
|
||||
complete?: DirectorSetOptions;
|
||||
fail?: DirectorSetOptions;
|
||||
cancel?: DirectorSetOptions;
|
||||
successSprite?: DirectorSprite;
|
||||
errorSprite?: DirectorSprite;
|
||||
cancelledSprite?: DirectorSprite;
|
||||
}
|
||||
|
||||
export class StateDirector extends EventTarget {
|
||||
constructor(engine: JoanGlyphEngine, options?: StateDirectorOptions);
|
||||
readonly engine: JoanGlyphEngine;
|
||||
readonly options: Required<StateDirectorOptions>;
|
||||
readonly timers: Set<ReturnType<typeof setTimeout>>;
|
||||
state: string;
|
||||
destroyed: boolean;
|
||||
|
||||
addEventListener(
|
||||
type: "statechange",
|
||||
listener: (this: StateDirector, event: CustomEvent<StateChangeDetail>) => unknown,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
addEventListener(
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject | null,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
|
||||
clearTimers(): void;
|
||||
set(sprite: DirectorSprite, options?: DirectorSetOptions): this;
|
||||
beginThinking(options?: StateDirectorOptions & DirectorSetOptions): this;
|
||||
complete(options?: DirectorSetOptions): this;
|
||||
fail(options?: DirectorSetOptions): this;
|
||||
cancel(options?: DirectorSetOptions): this;
|
||||
reset(options?: DirectorSetOptions): this;
|
||||
run<T>(
|
||||
work:
|
||||
| PromiseLike<T>
|
||||
| ((context: DirectorRunContext) => T | PromiseLike<T>),
|
||||
options?: DirectorRunOptions,
|
||||
): Promise<T>;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export default StateDirector;
|
||||
214
web/vendor/types/state-sequence.d.ts
vendored
Normal file
214
web/vendor/types/state-sequence.d.ts
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
import type {
|
||||
SetSpriteOptions,
|
||||
SpriteName,
|
||||
SpriteRecipe,
|
||||
TransitionDetail,
|
||||
TransitionName,
|
||||
TransitionToOptions,
|
||||
} from "./index.js";
|
||||
|
||||
export const STATE_SEQUENCE_SCHEMA_VERSION: 1;
|
||||
|
||||
export type StateSequenceSprite = SpriteName | SpriteRecipe;
|
||||
export type StateSequenceTransition =
|
||||
| false
|
||||
| TransitionName
|
||||
| (SetSpriteOptions & { name?: TransitionName });
|
||||
|
||||
export interface StateSequenceStep {
|
||||
sprite: StateSequenceSprite;
|
||||
/** Time to keep the entered state before advancing, in milliseconds. */
|
||||
holdMs?: number;
|
||||
/** Compatibility alias for holdMs; normalized definitions use holdMs. */
|
||||
durationMs?: number;
|
||||
transition?: StateSequenceTransition;
|
||||
options?: SetSpriteOptions;
|
||||
label?: string;
|
||||
metadata?: Readonly<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface DefinedStateSequenceStep
|
||||
extends Omit<StateSequenceStep, "durationMs" | "holdMs"> {
|
||||
readonly holdMs: number;
|
||||
}
|
||||
|
||||
export interface StateSequenceDefinition {
|
||||
schemaVersion?: number;
|
||||
name: string;
|
||||
steps: readonly (StateSequenceStep | SpriteName)[];
|
||||
transition?: StateSequenceTransition;
|
||||
transitionFirst?: boolean;
|
||||
metadata?: Readonly<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface DefinedStateSequence {
|
||||
readonly schemaVersion: 1;
|
||||
readonly name: string;
|
||||
readonly steps: readonly Readonly<DefinedStateSequenceStep>[];
|
||||
readonly transition?: StateSequenceTransition;
|
||||
readonly transitionFirst: boolean;
|
||||
readonly metadata?: Readonly<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface StateSequenceTarget {
|
||||
currentRecipe?: { id?: string } | null;
|
||||
setSprite(sprite: StateSequenceSprite, options?: SetSpriteOptions): unknown;
|
||||
transitionTo?(
|
||||
sprite: StateSequenceSprite,
|
||||
options?: TransitionToOptions,
|
||||
): PromiseLike<TransitionDetail | unknown> | TransitionDetail | unknown;
|
||||
}
|
||||
|
||||
export interface StateSequenceClock {
|
||||
now(): number;
|
||||
setTimeout(callback: () => void, delayMs: number): unknown;
|
||||
clearTimeout(timer: unknown): void;
|
||||
}
|
||||
|
||||
export interface StateSequencePlayerOptions {
|
||||
transition?: StateSequenceTransition;
|
||||
transitionFirst?: boolean;
|
||||
preservePhase?: boolean;
|
||||
clock?: StateSequenceClock;
|
||||
sequences?:
|
||||
| readonly (DefinedStateSequence | StateSequenceDefinition)[]
|
||||
| Readonly<
|
||||
Record<
|
||||
string,
|
||||
| readonly (StateSequenceStep | SpriteName)[]
|
||||
| StateSequenceDefinition
|
||||
>
|
||||
>;
|
||||
}
|
||||
|
||||
export interface StateSequencePlayOptions {
|
||||
name?: string;
|
||||
signal?: AbortSignal;
|
||||
transition?: StateSequenceTransition;
|
||||
transitionFirst?: boolean;
|
||||
preservePhase?: boolean;
|
||||
}
|
||||
|
||||
export interface StateSequenceResult {
|
||||
readonly runId: number;
|
||||
readonly name: string;
|
||||
readonly sequence: DefinedStateSequence;
|
||||
readonly status: "completed" | "stopped";
|
||||
readonly reason: unknown;
|
||||
readonly completedSteps: number;
|
||||
}
|
||||
|
||||
export interface StateSequenceEventDetail {
|
||||
readonly runId: number;
|
||||
readonly name: string;
|
||||
readonly sequence: DefinedStateSequence;
|
||||
readonly index: number;
|
||||
readonly step?: Readonly<DefinedStateSequenceStep>;
|
||||
readonly sprite?: string;
|
||||
readonly from?: string | null;
|
||||
readonly to?: string;
|
||||
readonly state?: string;
|
||||
readonly reason?: unknown;
|
||||
readonly error?: unknown;
|
||||
readonly totalSteps?: number;
|
||||
readonly completedSteps?: number;
|
||||
}
|
||||
|
||||
export type StateSequenceEventName =
|
||||
| "sequencestart"
|
||||
| "stepstart"
|
||||
| "statechange"
|
||||
| "stepenter"
|
||||
| "stepcomplete"
|
||||
| "sequencepause"
|
||||
| "sequenceresume"
|
||||
| "sequencestop"
|
||||
| "sequencecancel"
|
||||
| "sequencecomplete"
|
||||
| "sequenceerror";
|
||||
|
||||
export function defineStateSequence(
|
||||
name: string,
|
||||
steps: readonly (StateSequenceStep | SpriteName)[],
|
||||
options?: Omit<StateSequenceDefinition, "name" | "steps" | "schemaVersion">,
|
||||
): DefinedStateSequence;
|
||||
export function defineStateSequence(
|
||||
definition: StateSequenceDefinition,
|
||||
): DefinedStateSequence;
|
||||
|
||||
export class StateSequencePlayer extends EventTarget {
|
||||
constructor(target: StateSequenceTarget, options?: StateSequencePlayerOptions);
|
||||
readonly target: StateSequenceTarget;
|
||||
readonly sequences: Map<string, DefinedStateSequence>;
|
||||
readonly currentSequence: DefinedStateSequence | null;
|
||||
readonly currentStepIndex: number;
|
||||
readonly isPlaying: boolean;
|
||||
readonly isPaused: boolean;
|
||||
status: "idle" | "running" | "paused" | "destroyed";
|
||||
state: string | null;
|
||||
destroyed: boolean;
|
||||
finished: Promise<StateSequenceResult | null>;
|
||||
|
||||
addEventListener(
|
||||
type: StateSequenceEventName,
|
||||
listener: (
|
||||
this: StateSequencePlayer,
|
||||
event: CustomEvent<StateSequenceEventDetail>,
|
||||
) => unknown,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
addEventListener(
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject | null,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
|
||||
register(
|
||||
name: string,
|
||||
steps: readonly (StateSequenceStep | SpriteName)[],
|
||||
options?: Omit<StateSequenceDefinition, "name" | "steps" | "schemaVersion">,
|
||||
): DefinedStateSequence;
|
||||
register(
|
||||
definition: DefinedStateSequence | StateSequenceDefinition,
|
||||
): DefinedStateSequence;
|
||||
unregister(name: string): boolean;
|
||||
getSequence(name: string): DefinedStateSequence | null;
|
||||
listSequences(): readonly DefinedStateSequence[];
|
||||
play(
|
||||
sequence:
|
||||
| string
|
||||
| DefinedStateSequence
|
||||
| StateSequenceDefinition
|
||||
| readonly (StateSequenceStep | SpriteName)[],
|
||||
options?: StateSequencePlayOptions,
|
||||
): Promise<StateSequenceResult>;
|
||||
run(
|
||||
sequence:
|
||||
| string
|
||||
| DefinedStateSequence
|
||||
| StateSequenceDefinition
|
||||
| readonly (StateSequenceStep | SpriteName)[],
|
||||
options?: StateSequencePlayOptions,
|
||||
): Promise<StateSequenceResult>;
|
||||
/**
|
||||
* Pauses the hold clock and step progression. It does not rewind or suspend
|
||||
* a visual transition already running inside the target engine.
|
||||
*/
|
||||
pause(): this;
|
||||
resume(): this;
|
||||
stop(reason?: string): this;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export interface PlayStateSequenceOptions extends StateSequencePlayOptions {
|
||||
clock?: StateSequenceClock;
|
||||
metadata?: Readonly<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export function playStateSequence(
|
||||
target: StateSequenceTarget,
|
||||
steps: readonly (StateSequenceStep | SpriteName)[],
|
||||
options?: PlayStateSequenceOptions,
|
||||
): StateSequencePlayer;
|
||||
|
||||
export default StateSequencePlayer;
|
||||
6
web/vendor/types/web-component-register.d.ts
vendored
Normal file
6
web/vendor/types/web-component-register.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
JOAN_GLYPH_TAG_NAME,
|
||||
JoanGlyphElement,
|
||||
defineJoanGlyph,
|
||||
} from "./web-component.js";
|
||||
export { default } from "./web-component.js";
|
||||
93
web/vendor/types/web-component.d.ts
vendored
Normal file
93
web/vendor/types/web-component.d.ts
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
import type {
|
||||
ActivateOptions,
|
||||
EngineStats,
|
||||
EngineInspection,
|
||||
ExportedJoanConfig,
|
||||
FieldName,
|
||||
JoanEngineOptions,
|
||||
JoanGlyphEngine,
|
||||
Palette,
|
||||
PixelSwitchRecipe,
|
||||
PixelShapeName,
|
||||
PixelSwitchName,
|
||||
ResumeOptions,
|
||||
SetSpriteOptions,
|
||||
SignalPayload,
|
||||
SpriteName,
|
||||
SpriteRecipe,
|
||||
TransitionDetail,
|
||||
TransitionToOptions,
|
||||
JoanGlyphEventName,
|
||||
JoanGlyphEvent,
|
||||
} from "./index.js";
|
||||
|
||||
export const JOAN_GLYPH_TAG_NAME: "joan-glyph";
|
||||
|
||||
export class JoanGlyphElement extends HTMLElement {
|
||||
static readonly observedAttributes: readonly string[];
|
||||
readonly canvas: HTMLCanvasElement;
|
||||
readonly engine: JoanGlyphEngine | null | undefined;
|
||||
|
||||
connectedCallback(): void;
|
||||
disconnectedCallback(): void;
|
||||
attributeChangedCallback(
|
||||
name: string,
|
||||
oldValue: string | null,
|
||||
newValue: string | null,
|
||||
): void;
|
||||
readOptions(): JoanEngineOptions;
|
||||
readNonErrorPalette(): Palette | null;
|
||||
setSprite(
|
||||
sprite: SpriteName | SpriteRecipe,
|
||||
options?: SetSpriteOptions,
|
||||
): JoanGlyphEngine | undefined;
|
||||
setProgress(value: number): JoanGlyphEngine | undefined;
|
||||
setNonErrorPalette(palette: Palette | null): JoanGlyphEngine | undefined;
|
||||
setSeed(seed: string | number): JoanGlyphEngine | undefined;
|
||||
setResolution(size: number): JoanGlyphEngine | undefined;
|
||||
setField(field: FieldName): JoanGlyphEngine | undefined;
|
||||
useRecipeFields(): JoanGlyphEngine | undefined;
|
||||
setPixelShape(shape: PixelShapeName): JoanGlyphEngine | undefined;
|
||||
setPixelSwitch(
|
||||
pixelSwitch: PixelSwitchName | PixelSwitchRecipe,
|
||||
): JoanGlyphEngine | undefined;
|
||||
setOptions(options: Partial<JoanEngineOptions>): JoanGlyphEngine | undefined;
|
||||
configure(
|
||||
options?: Partial<JoanEngineOptions>,
|
||||
configureOptions?: { strict?: boolean },
|
||||
): JoanGlyphEngine | undefined;
|
||||
setAudioLevel(value: number): JoanGlyphEngine | undefined;
|
||||
activate(options?: ActivateOptions): JoanGlyphEngine | undefined;
|
||||
resume(options?: ResumeOptions): JoanGlyphEngine | undefined;
|
||||
play(): JoanGlyphEngine | undefined;
|
||||
pause(): JoanGlyphEngine | undefined;
|
||||
toggle(): JoanGlyphEngine | undefined;
|
||||
renderOnce(time?: number): EngineStats | undefined;
|
||||
transitionTo(
|
||||
sprite: SpriteName | SpriteRecipe,
|
||||
options?: TransitionToOptions,
|
||||
): Promise<TransitionDetail> | undefined;
|
||||
whenTransitionComplete(options?: {
|
||||
signal?: AbortSignal;
|
||||
}): Promise<TransitionDetail> | undefined;
|
||||
inspect(): EngineInspection | undefined;
|
||||
on<K extends JoanGlyphEventName>(
|
||||
type: K,
|
||||
listener: (event: JoanGlyphEvent<K>) => void,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): (() => void) | undefined;
|
||||
exportConfig(): ExportedJoanConfig | undefined;
|
||||
signal(type: string, payload?: SignalPayload): JoanGlyphEngine | undefined;
|
||||
}
|
||||
|
||||
export function defineJoanGlyph(
|
||||
tagName?: string,
|
||||
): typeof JoanGlyphElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"joan-glyph": JoanGlyphElement;
|
||||
}
|
||||
}
|
||||
|
||||
export default JoanGlyphElement;
|
||||
Reference in New Issue
Block a user