Files
oikos/web/vendor/joan-engine-v5.standalone.html
dtoro 3cd4cf98c3 chore: vendor @joan/procedural-glyph-engine for portable builds
Move @joan/procedural-glyph-engine from absolute path dep
(/private/tmp/orby-pkg) to vendored local dep (web/vendor/) so the
SPA builds in Docker and on other machines without the temp dir.

- Vendor Orby v5.0.0 into web/vendor/
- Switch package.json dep to file:../vendor
- Update Dockerfile to COPY vendor into build context
- Use npm install instead of npm ci (file: deps need install)
- Fix missing trailing newline in Dockerfile
2026-08-05 17:27:04 +02:00

5593 lines
738 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Orby — A procedural glyph engine by Joan Sterjo. Deterministic pixel motion for expressive AI states, transitions, and product interfaces."
/>
<meta name="author" content="Joan Sterjo" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Orby" />
<meta property="og:title" content="Orby" />
<meta
property="og:description"
content="A procedural glyph engine by Joan Sterjo. 25 semantic states, 19 deterministic fields, and 12 transition modes for expressive AI product glyphs."
/>
<meta id="ogImage" property="og:image" content="./public/og.png" />
<meta property="og:image:alt" content="Orby — A procedural glyph engine by Joan Sterjo." />
<meta property="og:image:width" content="1731" />
<meta property="og:image:height" content="909" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Orby" />
<meta
name="twitter:description"
content="A procedural glyph engine by Joan Sterjo. Deterministic pixel motion for every state an AI can inhabit."
/>
<meta id="twitterImage" name="twitter:image" content="./public/og.png" />
<meta name="twitter:image:alt" content="Orby — A procedural glyph engine by Joan Sterjo." />
<meta name="color-scheme" content="dark light" />
<meta name="theme-color" content="#050506" />
<link rel="icon" type="image/png" data-orby-logo />
<link rel="apple-touch-icon" data-orby-logo />
<title>Orby — A procedural glyph engine by Joan Sterjo</title>
<script>
(() => {
try {
const theme = localStorage.getItem("joan.studio.theme.v1");
if (theme !== "light" && theme !== "dark") return;
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
document.querySelector('meta[name="theme-color"]').content =
theme === "light" ? "#eeece6" : "#050506";
} catch {
// Keep the authored dark default when storage is unavailable.
}
})();
</script>
<style>
/* Orby — A procedural glyph engine by Joan Sterjo. */
:root {
color-scheme: dark;
--bg: #050505;
--bg-raised: #080808;
--surface: #0b0b0c;
--surface-raised: #0f1011;
--surface-hover: #141516;
--canvas: #070708;
--ink: #f2f1ee;
--ink-soft: #c9c8c4;
--muted: #8b8d90;
--faint: #787b7f;
--line: #232426;
--line-strong: #35373a;
--accent: #c9c8c4;
--accent-strong: #f2f1ee;
--accent-wash: rgba(232, 232, 228, 0.1);
--cool: #aeb4bb;
--success: #97d9b0;
--danger: #ff9f99;
--on-accent: #090a0b;
--control-line: #232426;
--body-accent-glow: rgba(238, 238, 232, 0.045);
--body-cool-glow: rgba(174, 180, 187, 0.025);
--ambient-grid: rgba(255, 255, 255, 0.018);
--surface-sheen: rgba(255, 255, 255, 0.018);
--canvas-glow: rgba(226, 228, 230, 0.05);
--canvas-grid: rgba(255, 255, 255, 0.025);
--canvas-scan: rgba(210, 214, 218, 0.035);
--shadow: 0 28px 80px rgba(0, 0, 0, 0.38);
--shadow-soft: 0 8px 24px rgba(0, 0, 0, 0.18);
--shadow-card: 0 12px 28px rgba(0, 0, 0, 0.18);
--shadow-card-selected: 0 10px 30px rgba(0, 0, 0, 0.2);
--shadow-float:
0 16px 42px rgba(0, 0, 0, 0.34),
0 0 0 1px rgba(255, 255, 255, 0.025) inset;
--shadow-dialog: 0 38px 110px rgba(0, 0, 0, 0.58);
--inset-edge: rgba(0, 0, 0, 0.25);
--backdrop: rgba(0, 0, 0, 0.74);
--checker-base: #0c0c0d;
--checker-tile: #191a1c;
--topbar-bg: color-mix(in srgb, var(--bg), transparent 10%);
--topbar-shadow: none;
--code-bg: #080808;
--code-hover: #121314;
--code-line: #242528;
--code-line-strong: #47494e;
--code-muted: #777a7f;
--code-control: #2c2e31;
--code-control-ink: #a4a6a9;
--code-ink: #c0c1c2;
--code-ink-strong: #f2f1ee;
--code-dot-1: #414244;
--code-dot-2: #66686c;
--code-dot-3: #aeb0b4;
--code-grid: rgba(255, 255, 255, 0.025);
--code-scrollbar: #34363a;
--scrollbar-thumb: #35373a;
--scrollbar-track: #080808;
--font-sans:
"SF Pro Display", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
--font-mono:
"SFMono-Regular", "Cascadia Code", "Roboto Mono", Consolas, monospace;
--content: min(1480px, calc(100vw - 56px));
--radius-lg: 18px;
--radius-md: 11px;
--ease: cubic-bezier(0.22, 1, 0.36, 1);
}
html[data-theme="light"] {
color-scheme: light;
--bg: #eeece6;
--bg-raised: #f4f2ec;
--surface: #faf9f5;
--surface-raised: #ffffff;
--surface-hover: #f1efe8;
--canvas: #e5e6e2;
--ink: #17191e;
--ink-soft: #30343b;
--muted: #565c65;
--faint: #626973;
--line: #cbc9c1;
--line-strong: #898b87;
--accent: #4d5054;
--accent-strong: #25272a;
--accent-wash: rgba(37, 39, 42, 0.09);
--cool: #505a64;
--success: #256b45;
--danger: #a43836;
--on-accent: #ffffff;
--control-line: #898b87;
--body-accent-glow: rgba(45, 47, 50, 0.045);
--body-cool-glow: rgba(80, 90, 100, 0.025);
--ambient-grid: rgba(30, 34, 45, 0.04);
--surface-sheen: rgba(20, 24, 32, 0.022);
--canvas-glow: rgba(45, 47, 50, 0.055);
--canvas-grid: rgba(25, 28, 35, 0.05);
--canvas-scan: rgba(63, 66, 70, 0.04);
--shadow:
0 1px 2px rgba(30, 34, 44, 0.08),
0 18px 48px rgba(30, 34, 44, 0.07);
--shadow-soft: 0 8px 24px rgba(34, 36, 42, 0.08);
--shadow-card: 0 12px 28px rgba(34, 36, 42, 0.1);
--shadow-card-selected: 0 12px 30px rgba(38, 40, 45, 0.12);
--shadow-float:
0 16px 42px rgba(34, 36, 42, 0.14),
0 0 0 1px rgba(20, 24, 32, 0.025) inset;
--shadow-dialog: 0 32px 90px rgba(24, 26, 32, 0.22);
--inset-edge: rgba(30, 34, 44, 0.14);
--backdrop: rgba(28, 31, 38, 0.34);
--checker-base: #f7f7f3;
--checker-tile: #deded8;
--topbar-bg: rgba(250, 249, 245, 0.9);
--topbar-shadow: 0 1px 0 rgba(30, 34, 44, 0.05), 0 8px 24px rgba(30, 34, 44, 0.045);
--code-bg: #f4f3ef;
--code-hover: #e9e9e6;
--code-line: #cbc9c1;
--code-line-strong: #898b87;
--code-muted: #5a616c;
--code-control: #b7b8b5;
--code-control-ink: #565d69;
--code-ink: #2a3039;
--code-ink-strong: #171b22;
--code-dot-1: #b7bac1;
--code-dot-2: #85888d;
--code-dot-3: #54575c;
--code-grid: rgba(65, 68, 72, 0.04);
--code-scrollbar: #b9bbb7;
--scrollbar-thumb: #a6a8a5;
--scrollbar-track: #e6e4de;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
background: var(--bg);
}
body {
min-height: 100vh;
margin: 0;
overflow-x: hidden;
background:
radial-gradient(circle at 76% 2%, var(--body-accent-glow), transparent 26rem),
radial-gradient(circle at 9% 32%, var(--body-cool-glow), transparent 30rem),
var(--bg);
color: var(--ink);
font-family: var(--font-sans);
font-size: 15px;
line-height: 1.5;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
button,
input,
select {
color: inherit;
font: inherit;
}
button,
select,
input[type="range"] {
cursor: pointer;
}
button {
border: 0;
}
a {
color: inherit;
}
::selection {
background: color-mix(in srgb, var(--accent), transparent 72%);
color: var(--ink);
}
html {
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-track {
background: var(--scrollbar-track);
}
*::-webkit-scrollbar-thumb {
border: 2px solid var(--scrollbar-track);
border-radius: 999px;
background: var(--scrollbar-thumb);
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
[hidden] {
display: none !important;
}
.visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
.skip-link {
position: fixed;
z-index: 100;
top: 12px;
left: 12px;
padding: 10px 14px;
border: 1px solid var(--accent);
border-radius: 7px;
background: var(--surface-raised);
color: var(--ink);
font-family: var(--font-mono);
font-size: 12px;
text-decoration: none;
transform: translateY(-180%);
}
.skip-link:focus {
transform: translateY(0);
}
.ambient-field {
position: fixed;
z-index: -1;
inset: 0;
pointer-events: none;
opacity: 0.5;
background-image: radial-gradient(
circle,
var(--ambient-grid) 0 1px,
transparent 1.25px
);
background-size: 12px 12px;
-webkit-mask-image: linear-gradient(to bottom, black, transparent 72%);
mask-image: linear-gradient(to bottom, black, transparent 72%);
}
html[data-theme="light"] .ambient-field {
opacity: 0.55;
}
.topbar {
position: sticky;
z-index: 40;
top: 0;
border-bottom: 1px solid color-mix(in srgb, var(--line), transparent 16%);
background: var(--topbar-bg);
box-shadow: var(--topbar-shadow);
backdrop-filter: blur(20px) saturate(130%);
}
.topbar__inner {
width: var(--content);
min-height: 68px;
margin-inline: auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.site-nav {
margin-left: auto;
display: flex;
align-items: center;
gap: 3px;
}
.site-nav a {
min-height: 34px;
padding: 0 10px;
display: inline-flex;
align-items: center;
border: 1px solid transparent;
border-radius: 5px;
color: var(--muted);
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.075em;
text-decoration: none;
text-transform: uppercase;
transition:
border-color 160ms ease,
background-color 160ms ease,
color 160ms ease;
}
.site-nav a::before {
margin-right: 5px;
color: var(--faint);
content: "/";
}
.site-nav a:hover,
.site-nav a:focus-visible {
border-color: var(--line);
background: var(--surface-hover);
color: var(--ink);
}
.brand {
display: inline-flex;
align-items: center;
gap: 11px;
min-width: 0;
color: var(--ink);
text-decoration: none;
}
.brand__mark {
width: 40px;
height: 40px;
display: block;
flex: 0 0 auto;
}
.brand__mark img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
html[data-theme="light"] .brand__mark img {
filter: invert(1);
}
.brand__copy {
display: grid;
line-height: 1.1;
}
.brand__copy strong {
font-size: 13px;
font-weight: 600;
letter-spacing: 0.01em;
}
.brand__copy small {
margin-top: 4px;
color: var(--muted);
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.version-pill,
.deterministic-badge {
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.08em;
line-height: 1;
text-transform: uppercase;
}
.version-pill {
padding: 5px 7px;
}
.topbar__actions {
margin-left: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.runtime-status {
display: inline-flex;
align-items: center;
gap: 7px;
margin-right: 7px;
color: var(--muted);
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.runtime-status__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--success);
box-shadow: 0 0 0 0 color-mix(in srgb, var(--success), transparent 65%);
animation: status-pulse 2.8s ease-out infinite;
}
.button {
min-height: 38px;
padding: 0 14px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px solid var(--control-line);
border-radius: 7px;
background: var(--surface);
color: var(--ink-soft);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 550;
letter-spacing: 0.055em;
line-height: 1;
text-transform: uppercase;
text-decoration: none;
transition:
border-color 180ms ease,
background-color 180ms ease,
color 180ms ease,
transform 180ms var(--ease);
}
.button:hover:not(:disabled) {
border-color: var(--line-strong);
background: var(--surface-hover);
color: var(--ink);
transform: translateY(-1px);
}
.button:active:not(:disabled) {
transform: translateY(0);
}
.button:disabled {
cursor: wait;
opacity: 0.55;
}
.button--quiet {
background: transparent;
}
.button--primary {
border-color: var(--ink);
background: var(--ink);
color: var(--bg);
}
.button--primary:hover:not(:disabled),
.button--primary.is-active {
border-color: var(--accent-strong);
background: var(--accent-strong);
color: var(--on-accent);
}
.button--square {
width: 42px;
min-width: 42px;
height: 42px;
padding: 0;
font-size: 17px;
}
.theme-button::before {
width: 10px;
height: 10px;
border: 1px solid currentColor;
border-radius: 50%;
background: linear-gradient(90deg, currentColor 50%, transparent 50%);
content: "";
}
main {
width: var(--content);
margin-inline: auto;
}
#overview,
#playground,
#states,
#api,
#download {
scroll-margin-top: 92px;
}
.hero {
min-height: 700px;
padding: clamp(70px, 8vw, 118px) 0 clamp(76px, 8vw, 112px);
display: grid;
grid-template-columns: minmax(0, 1.12fr) minmax(390px, 0.88fr);
align-items: center;
gap: clamp(52px, 7vw, 112px);
border-bottom: 1px solid var(--line);
}
.hero__copy {
max-width: 900px;
}
.eyebrow,
.section-index {
margin: 0;
color: var(--muted);
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.16em;
line-height: 1.4;
text-transform: uppercase;
}
.eyebrow {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.eyebrow span {
color: var(--accent-strong);
}
.eyebrow::after {
width: 42px;
height: 1px;
content: "";
background: var(--line-strong);
}
.hero h1 {
max-width: 940px;
margin: 0;
color: var(--ink);
font-size: clamp(3.8rem, 6.2vw, 7rem);
font-weight: 260;
letter-spacing: -0.065em;
line-height: 0.91;
}
.hero__lede {
max-width: 720px;
margin: 28px 0 0;
color: var(--muted);
font-size: clamp(15px, 1.3vw, 18px);
font-weight: 350;
line-height: 1.68;
}
.landing-hero__actions {
margin-top: 34px;
display: flex;
flex-wrap: wrap;
gap: 9px;
}
.landing-hero__actions .button {
min-height: 48px;
padding-inline: 18px;
}
.landing-hero__visual {
--hero-proof-height: 90px;
--hero-proof-offset: 45px;
position: relative;
min-height: clamp(500px, 40vw, 590px);
overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius-lg);
background:
linear-gradient(90deg, var(--ambient-grid) 1px, transparent 1px) 0 0 / 24px 24px,
linear-gradient(var(--ambient-grid) 1px, transparent 1px) 0 0 / 24px 24px,
radial-gradient(circle at 50% 43%, var(--body-accent-glow), transparent 48%),
color-mix(in srgb, var(--surface), transparent 24%);
box-shadow: var(--shadow);
isolation: isolate;
}
.landing-hero__visual::before,
.landing-hero__visual::after {
position: absolute;
z-index: 4;
pointer-events: none;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.landing-hero__visual::before {
top: 17px;
left: 18px;
content: "FIELD / SEMANTIC PRESENCE";
}
.landing-hero__visual::after {
top: 17px;
right: 18px;
content: "LIVE 68×68";
}
.hero-engine-output {
position: absolute;
z-index: 2;
top: calc(50% - var(--hero-proof-offset));
left: 50%;
width: min(78%, 450px);
aspect-ratio: 1;
pointer-events: none;
transform: translate(-50%, -50%);
contain: layout paint;
}
.hero-engine-output canvas {
width: 100%;
height: 100%;
display: block;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.hero-proof {
position: absolute;
z-index: 3;
right: 0;
bottom: 0;
left: 0;
min-height: 90px;
margin: 0;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
border-top: 1px solid var(--line);
background: color-mix(in srgb, var(--bg-raised), transparent 5%);
backdrop-filter: blur(14px);
}
.hero-proof div {
min-width: 0;
padding: 18px 20px;
display: grid;
align-content: center;
gap: 4px;
border-right: 1px solid var(--line);
}
.hero-proof div:last-child {
border-right: 0;
}
.hero-proof dt {
color: var(--ink);
font-family: var(--font-mono);
font-size: 18px;
line-height: 1;
}
.hero-proof dd {
margin: 0;
overflow: hidden;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.06em;
line-height: 1.35;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
}
.principles-section {
padding: clamp(88px, 9vw, 132px) 0;
border-bottom: 1px solid var(--line);
}
.principles-section__heading {
display: grid;
grid-template-columns: minmax(150px, 0.36fr) minmax(0, 1fr);
align-items: start;
gap: clamp(34px, 8vw, 120px);
}
.principles-section__heading h2,
.studio-intro h2,
.usage-section__heading h2 {
margin: 0;
color: var(--ink);
font-size: clamp(2.5rem, 4.8vw, 5.25rem);
font-weight: 270;
letter-spacing: -0.055em;
line-height: 0.98;
}
.principles-grid {
margin-top: clamp(46px, 6vw, 82px);
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
border: 1px solid var(--line);
background: var(--line);
gap: 1px;
}
.principle-card {
position: relative;
min-height: 260px;
padding: 28px;
overflow: hidden;
background:
radial-gradient(circle, var(--ambient-grid) 0 1px, transparent 1.25px) 0 0 / 12px 12px,
var(--bg-raised);
}
.principle-card::after {
position: absolute;
right: -38px;
bottom: -38px;
width: 116px;
height: 116px;
border: 1px dashed var(--line-strong);
border-radius: 50%;
content: "";
}
.principle-card__index {
color: var(--faint);
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.12em;
}
.principle-card h3 {
max-width: 270px;
margin: 72px 0 0;
color: var(--ink-soft);
font-size: 21px;
font-weight: 430;
letter-spacing: -0.025em;
}
.principle-card p {
max-width: 36ch;
margin: 12px 0 0;
color: var(--muted);
font-size: 12px;
line-height: 1.68;
}
.studio-intro {
padding: clamp(80px, 8vw, 116px) 0 36px;
display: grid;
grid-template-columns: minmax(0, 0.88fr) minmax(300px, 0.72fr);
align-items: end;
justify-content: space-between;
gap: clamp(40px, 8vw, 120px);
}
.studio-intro h2 {
max-width: 780px;
margin-top: 14px;
}
.studio-intro > p {
max-width: 520px;
margin: 0;
color: var(--muted);
font-size: 14px;
line-height: 1.72;
}
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 24px;
border-bottom: 1px solid var(--line);
}
.hero__specs dt {
color: var(--ink);
font-family: var(--font-mono);
font-size: 17px;
}
.hero__specs dd {
margin: 0;
color: var(--muted);
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.workbench {
padding: 28px 0 110px;
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
align-items: start;
gap: 18px;
}
.panel {
border: 1px solid var(--line);
border-radius: var(--radius-lg);
background:
linear-gradient(145deg, var(--surface-sheen), transparent 32%),
var(--surface);
box-shadow: var(--shadow);
}
.panel-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 28px;
}
.stage-panel {
min-width: 0;
padding: 26px;
}
.stage-heading {
min-height: 116px;
padding: 2px 2px 22px;
}
.state-heading {
margin-top: 11px;
display: flex;
align-items: center;
gap: 12px;
}
.state-heading h2,
.control-heading h2 {
margin: 0;
color: var(--ink);
font-size: 24px;
font-weight: 440;
letter-spacing: -0.025em;
}
.state-category {
padding: 5px 7px;
border: 1px solid var(--line-strong);
border-radius: 4px;
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.09em;
line-height: 1;
text-transform: uppercase;
}
.state-description {
max-width: 64ch;
min-height: 36px;
margin: 8px 0 0;
color: var(--muted);
font-size: 12px;
line-height: 1.55;
}
.signal-cluster {
flex: 0 0 auto;
text-align: right;
}
.signal-cluster__label {
display: block;
margin: 0 1px 8px;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.13em;
text-transform: uppercase;
}
.signal-cluster__buttons {
display: flex;
gap: 5px;
}
.signal-button {
min-width: 64px;
min-height: 35px;
padding: 0 10px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
border: 1px solid var(--control-line);
border-radius: 6px;
background: transparent;
color: var(--muted);
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.04em;
transition:
color 160ms ease,
border-color 160ms ease,
background-color 160ms ease;
}
.signal-button::before {
width: 6px;
height: 6px;
content: "";
background: currentColor;
box-shadow: 4px 0 0 color-mix(in srgb, currentColor, transparent 56%);
}
.signal-button--search::before {
border: 1px solid currentColor;
border-radius: 50%;
background: transparent;
box-shadow: 4px 4px 0 -2px currentColor;
}
.signal-button--audio::before {
width: 2px;
height: 8px;
box-shadow:
4px -2px 0 currentColor,
8px 1px 0 currentColor;
}
.signal-button:hover {
border-color: color-mix(in srgb, var(--accent), var(--line) 40%);
background: var(--accent-wash);
color: var(--accent-strong);
}
.workflow-bar {
margin: 0 0 14px;
padding: 11px;
display: grid;
grid-template-columns: minmax(300px, 0.9fr) minmax(360px, 1.1fr);
align-items: end;
gap: 12px;
border: 1px solid var(--line);
border-radius: 9px;
background: color-mix(in srgb, var(--bg-raised), transparent 14%);
}
.state-picker {
min-width: 0;
display: grid;
grid-template-columns: minmax(128px, 0.72fr) minmax(160px, 1fr);
gap: 7px;
}
.state-picker label,
.export-field {
min-width: 0;
display: grid;
gap: 6px;
}
.state-picker label > span:first-child,
.export-field > span:first-child {
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.state-picker input,
.state-picker select,
.recent-preset select,
.export-field input,
.export-field select {
width: 100%;
height: 36px;
border: 1px solid var(--control-line);
border-radius: 6px;
outline: 0;
background: var(--surface);
color: var(--ink-soft);
font-family: var(--font-mono);
font-size: 9px;
}
.state-picker input,
.export-field input {
padding: 0 10px;
}
.state-picker input::placeholder {
color: var(--faint);
}
.state-picker input:hover,
.state-picker select:hover,
.recent-preset select:hover,
.export-field input:hover,
.export-field select:hover {
border-color: var(--line-strong);
}
.state-picker input:focus-visible,
.state-picker select:focus-visible,
.recent-preset select:focus-visible,
.export-field input:focus-visible,
.export-field select:focus-visible {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-wash);
}
.recipe-workflow {
min-width: 0;
display: grid;
justify-items: end;
gap: 7px;
}
.recipe-workflow__status {
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
}
#workflowStatus {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#workflowStatus[data-tone="success"] {
color: var(--success);
}
#workflowStatus[data-tone="warning"] {
color: var(--accent-strong);
}
#workflowStatus[data-tone="danger"] {
color: var(--danger);
}
.recipe-workflow__actions {
min-width: 0;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 5px;
}
.recent-preset {
min-width: 120px;
max-width: 150px;
}
.recent-preset select {
height: 34px;
padding: 0 25px 0 8px;
}
.workflow-button {
min-height: 34px;
padding: 0 8px;
border: 1px solid var(--control-line);
border-radius: 5px;
background: transparent;
color: var(--muted);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.04em;
}
.workflow-button:hover:not(:disabled) {
border-color: var(--line-strong);
background: var(--surface-hover);
color: var(--ink-soft);
}
.workflow-button:disabled,
.recent-preset select:disabled {
cursor: not-allowed;
opacity: 0.42;
}
.workflow-export {
min-height: 34px;
padding-inline: 11px;
font-size: 8px;
}
.scope-badge {
width: max-content;
max-width: 100%;
padding: 3px 5px;
display: inline-flex;
align-items: center;
border: 1px solid var(--line);
border-radius: 4px;
color: var(--faint);
font-family: var(--font-mono);
font-size: 7px;
font-weight: 500;
letter-spacing: 0.06em;
line-height: 1;
text-transform: uppercase;
white-space: nowrap;
}
.scope-badge[data-scope="custom"] {
border-color: color-mix(in srgb, var(--accent), transparent 64%);
background: var(--accent-wash);
color: var(--accent-strong);
}
.scope-badge[data-scope="carry"] {
border-color: color-mix(in srgb, var(--success), transparent 72%);
color: var(--success);
}
.canvas-frame {
position: relative;
width: min(100%, 820px);
aspect-ratio: 1;
margin-inline: auto;
overflow: hidden;
border: 1px solid var(--line-strong);
border-radius: var(--radius-md);
background:
radial-gradient(circle at 50% 48%, var(--canvas-glow), transparent 35%),
linear-gradient(var(--canvas-grid) 1px, transparent 1px),
linear-gradient(90deg, var(--canvas-grid) 1px, transparent 1px),
var(--canvas);
background-size:
auto,
24px 24px,
24px 24px,
auto;
isolation: isolate;
transition:
border-color 180ms ease,
box-shadow 180ms ease;
}
.canvas-frame::before,
.canvas-frame::after {
position: absolute;
z-index: 4;
pointer-events: none;
content: "";
}
.canvas-frame::before {
inset: 9px;
border:
solid color-mix(in srgb, var(--line-strong), transparent 18%);
border-width: 1px 0;
opacity: 0.55;
clip-path: polygon(
0 0,
38px 0,
38px 1px,
calc(100% - 38px) 1px,
calc(100% - 38px) 0,
100% 0,
100% 100%,
calc(100% - 38px) 100%,
calc(100% - 38px) calc(100% - 1px),
38px calc(100% - 1px),
38px 100%,
0 100%
);
}
.canvas-frame::after {
top: -22%;
right: 0;
left: 0;
height: 18%;
background: linear-gradient(to bottom, transparent, var(--canvas-scan), transparent);
animation: field-scan 9s linear infinite;
}
.canvas-frame.is-actual-preview::before {
inset: 50% auto auto 50%;
width: calc(var(--preview-grid-size, 68px) + 10px);
height: calc(var(--preview-grid-size, 68px) + 10px);
border: 1px solid color-mix(in srgb, var(--accent), transparent 58%);
clip-path: none;
opacity: 1;
transform: translate(-50%, -50%);
box-shadow: 0 0 28px color-mix(in srgb, var(--accent), transparent 88%);
}
.canvas-frame.is-actual-preview::after {
display: none;
}
.canvas-frame.is-dragging {
border-color: var(--accent);
box-shadow:
0 0 0 4px var(--accent-wash),
inset 0 0 80px var(--accent-wash);
}
.canvas-frame.is-dragging::before {
inset: 22px;
display: grid;
place-items: center;
border: 1px dashed var(--accent);
clip-path: none;
content: "DROP GLYPH TO COMPILE";
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.16em;
}
#engineCanvas {
position: absolute;
z-index: 2;
inset: 0;
width: 100%;
height: 100%;
display: block;
touch-action: none;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
#engineCanvas:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -5px;
box-shadow: inset 0 0 0 6px color-mix(in srgb, var(--accent), transparent 78%);
}
.frame-badge {
position: absolute;
z-index: 5;
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.12em;
line-height: 1;
text-transform: uppercase;
pointer-events: none;
}
.frame-badge--top {
top: 18px;
left: 19px;
}
.frame-badge--top::before {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--success);
content: "";
box-shadow: 0 0 10px color-mix(in srgb, var(--success), transparent 30%);
}
.frame-badge--bottom {
right: 19px;
bottom: 18px;
}
.preview-scale-control {
position: absolute;
z-index: 6;
top: 12px;
right: 12px;
padding: 3px;
display: inline-grid;
grid-auto-flow: column;
gap: 2px;
border: 1px solid var(--line-strong);
border-radius: 7px;
background: color-mix(in srgb, var(--surface), transparent 8%);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(10px);
}
.preview-scale-control button {
min-width: 42px;
min-height: 32px;
padding: 0 9px;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.preview-scale-control button:hover {
background: var(--surface-hover);
color: var(--ink);
}
.preview-scale-control button[aria-pressed="true"] {
background: var(--accent-wash);
color: var(--accent-strong);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent), transparent 52%);
}
.progress-control {
margin-top: 14px;
padding: 13px 15px;
display: none;
grid-template-columns: minmax(150px, 0.35fr) minmax(180px, 1fr);
align-items: center;
gap: 22px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--bg-raised);
}
.progress-control.is-visible,
#progressControl.is-visible {
display: grid;
}
.progress-control label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
color: var(--muted);
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.progress-control output {
color: var(--accent-strong);
}
.stage-lower {
padding: 18px 2px 12px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 22px;
}
.quick-states > span {
display: block;
margin-bottom: 7px;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.quick-states > div {
display: flex;
gap: 4px;
}
.quick-states button {
min-height: 30px;
padding: 0 10px;
border: 1px solid var(--control-line);
border-radius: 5px;
background: transparent;
color: var(--muted);
font-family: var(--font-mono);
font-size: 9px;
}
.quick-states button:hover {
border-color: var(--line-strong);
background: var(--surface-hover);
color: var(--ink);
}
.quick-states button.is-selected,
.quick-states button[aria-pressed="true"] {
border-color: color-mix(in srgb, var(--accent), var(--line) 35%);
background: var(--accent-wash);
color: var(--accent-strong);
}
.stage-metrics {
margin: 0;
display: flex;
justify-content: flex-end;
gap: 24px;
text-align: right;
}
.stage-metrics div {
min-width: 52px;
}
.stage-metrics dt {
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.stage-metrics dd {
margin: 4px 0 0;
color: var(--ink-soft);
font-family: var(--font-mono);
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.stage-metrics__state {
min-width: 112px !important;
}
.stage-actions {
padding: 17px 2px 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
border-top: 1px solid var(--line);
}
.stage-actions p {
margin: 0;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.03em;
}
kbd {
min-width: 19px;
height: 19px;
margin: 0 3px;
padding: 0 5px;
display: inline-grid;
place-items: center;
border: 1px solid var(--line-strong);
border-bottom-width: 2px;
border-radius: 4px;
background: var(--bg-raised);
color: var(--muted);
font-family: var(--font-mono);
font-size: 8px;
box-shadow: inset 0 -1px var(--inset-edge);
}
.stage-actions > div {
display: flex;
gap: 7px;
}
.play-button::before {
width: 8px;
height: 8px;
content: "";
background:
linear-gradient(90deg, currentColor 0 2px, transparent 2px 5px, currentColor 5px);
}
.control-panel {
position: sticky;
top: 86px;
overflow: hidden;
}
.control-heading {
padding: 25px 24px 21px;
border-bottom: 1px solid var(--line);
}
.control-heading h2 {
margin-top: 10px;
font-size: 20px;
}
.control-heading__hint {
margin: 7px 0 0;
color: var(--faint);
font-size: 9px;
line-height: 1.45;
}
.deterministic-badge {
padding: 6px 8px;
color: var(--success);
}
.control-form {
margin: 0;
}
.control-section {
margin: 0;
padding: 22px 24px 23px;
display: grid;
gap: 18px;
border: 0;
border-bottom: 1px solid var(--line);
}
.control-section legend {
width: 100%;
padding: 18px 24px 0;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.15em;
text-transform: uppercase;
}
.control-section legend .scope-badge {
margin-left: 8px;
vertical-align: 1px;
}
.control-label-line {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.control-with-tooltip {
position: relative;
cursor: help;
}
button.control-with-tooltip {
cursor: pointer;
}
.control-tooltip {
position: fixed;
z-index: 90;
width: max-content;
max-width: min(300px, calc(100vw - 24px));
padding: 10px 11px;
border: 1px solid var(--control-line);
border-radius: 7px;
visibility: hidden;
background: color-mix(in srgb, var(--surface-raised), var(--bg) 18%);
box-shadow: var(--shadow-float);
color: var(--ink-soft);
font-family: var(--font-sans);
font-size: 10px;
font-weight: 400;
letter-spacing: 0;
line-height: 1.5;
opacity: 0;
pointer-events: none;
text-align: left;
text-transform: none;
transform: translateY(3px);
transition:
opacity 140ms ease,
transform 140ms ease,
visibility 140ms ease;
}
.control-tooltip::before {
position: absolute;
left: 50%;
width: 7px;
height: 7px;
border: solid var(--control-line);
background: color-mix(in srgb, var(--surface-raised), var(--bg) 18%);
content: "";
transform: translateX(-50%) rotate(45deg);
}
.control-tooltip[data-placement="bottom"]::before {
top: -4px;
border-width: 1px 0 0 1px;
}
.control-tooltip[data-placement="top"]::before {
bottom: -4px;
border-width: 0 1px 1px 0;
}
.control-tooltip.is-visible {
visibility: visible;
opacity: 1;
transform: translateY(0);
}
.control-section--palette {
gap: 14px;
}
.palette-intro {
margin: 0;
color: var(--muted);
font-size: 10px;
line-height: 1.5;
}
.palette-intro-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.palette-intro-row .palette-intro {
flex: 1;
}
.palette-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.color-control {
min-width: 0;
padding: 10px;
display: grid;
gap: 8px;
border: 1px solid var(--control-line);
border-radius: 6px;
background: var(--bg-raised);
color: var(--ink-soft);
font-size: 10px;
transition:
border-color 160ms ease,
background-color 160ms ease;
}
.color-control:hover {
border-color: var(--line-strong);
background: var(--surface);
}
.color-control__picker {
position: relative;
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
}
.color-control__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.glow-toggle {
min-height: 22px;
padding: 0 6px;
display: inline-flex;
align-items: center;
gap: 5px;
border: 1px solid var(--control-line);
border-radius: 4px;
background: transparent;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.06em;
line-height: 1;
text-transform: uppercase;
cursor: pointer;
}
.glow-toggle:hover,
.glow-toggle:focus-within {
border-color: var(--line-strong);
color: var(--ink-soft);
}
.glow-toggle input {
width: 12px;
height: 12px;
margin: 0;
accent-color: var(--accent);
cursor: pointer;
}
.color-control--glow.is-glow-disabled .color-control__picker {
opacity: 0.48;
}
.color-control--glow.is-glow-disabled .glow-toggle {
border-color: color-mix(in srgb, var(--accent), transparent 58%);
color: var(--accent-strong);
}
.color-control--glow.is-glow-disabled .color-control__picker::after {
position: absolute;
top: 50%;
left: 0;
width: 34px;
height: 1px;
background: var(--faint);
content: "";
pointer-events: none;
transform: rotate(-35deg);
}
.color-control--glow.is-glow-disabled input[type="color"] {
cursor: not-allowed;
filter: grayscale(1);
}
.color-control input[type="color"] {
width: 30px;
height: 24px;
flex: 0 0 auto;
padding: 2px;
border: 1px solid var(--control-line);
border-radius: 4px;
outline: 0;
background: transparent;
cursor: pointer;
}
.color-control input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-control input[type="color"]::-webkit-color-swatch {
border: 0;
border-radius: 2px;
}
.color-control input[type="color"]::-moz-color-swatch {
border: 0;
border-radius: 2px;
}
.color-control input[type="color"]:focus-visible {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-wash);
}
.color-control output {
min-width: 0;
overflow: hidden;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
font-variant-numeric: tabular-nums;
text-overflow: ellipsis;
}
.palette-reset {
min-height: 34px;
padding: 0 11px;
justify-self: start;
border: 1px solid var(--control-line);
border-radius: 6px;
background: transparent;
color: var(--muted);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.07em;
text-transform: uppercase;
transition:
border-color 160ms ease,
color 160ms ease,
background-color 160ms ease;
}
.palette-reset:hover {
border-color: var(--line-strong);
background: var(--surface);
color: var(--ink-soft);
}
.control-section--palette.is-error-state .palette-intro::after {
display: block;
margin-top: 6px;
color: var(--danger);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.06em;
content: "ERROR PREVIEW PROTECTED";
}
.select-control,
.orb-boundary-control,
.orb-background-control,
.seed-control {
display: grid;
grid-template-columns: minmax(0, 0.85fr) minmax(132px, 1.15fr);
align-items: center;
gap: 15px;
}
.select-control > span:first-child,
.orb-boundary-control > span:first-child,
.orb-background-control > span:first-child,
.seed-control > span:first-child {
color: var(--ink-soft);
font-size: 11px;
font-weight: 500;
}
.select-control small,
.orb-boundary-control small,
.orb-background-control small,
.seed-control small {
display: block;
margin-top: 2px;
color: var(--faint);
font-size: 9px;
font-weight: 400;
line-height: 1.35;
}
.orb-boundary-control {
cursor: pointer;
transition: opacity 160ms ease;
}
.orb-boundary-toggle {
position: relative;
display: block;
}
.orb-boundary-toggle input {
position: absolute;
width: 1px;
height: 1px;
margin: 0;
overflow: hidden;
opacity: 0;
}
.orb-boundary-toggle__track {
position: relative;
min-height: 40px;
padding: 3px;
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
border: 1px solid var(--control-line);
border-radius: 7px;
background: var(--bg-raised);
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--ink), transparent 96%);
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.06em;
text-align: center;
text-transform: uppercase;
cursor: pointer;
transition:
border-color 160ms ease,
background-color 160ms ease;
}
.orb-boundary-toggle__track::before {
position: absolute;
z-index: 0;
top: 3px;
bottom: 3px;
left: 3px;
width: calc(50% - 3px);
border: 1px solid color-mix(in srgb, var(--accent), transparent 56%);
border-radius: 4px;
background: var(--accent-wash);
content: "";
transition: left 180ms ease;
}
.orb-boundary-toggle__track > span {
position: relative;
z-index: 1;
transition: color 160ms ease;
}
.orb-boundary-toggle input:not(:checked) + .orb-boundary-toggle__track > span:first-child,
.orb-boundary-toggle input:checked + .orb-boundary-toggle__track > span:last-child {
color: var(--accent-strong);
}
.orb-boundary-toggle input:checked + .orb-boundary-toggle__track::before {
left: 50%;
}
.orb-boundary-control:hover .orb-boundary-toggle__track {
border-color: var(--line-strong);
background: var(--surface);
}
.orb-boundary-toggle input:focus-visible + .orb-boundary-toggle__track {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-wash);
}
.orb-boundary-control[data-available="false"] {
opacity: 0.5;
cursor: not-allowed;
}
.orb-boundary-control[data-available="false"] .orb-boundary-toggle__track {
cursor: not-allowed;
}
.orb-background-control {
transition: opacity 160ms ease;
}
.orb-background-control[data-available="false"] {
opacity: 0.5;
}
.orb-background-actions {
min-height: 40px;
padding: 6px 8px;
display: grid;
grid-template-columns: auto minmax(54px, 1fr) auto;
align-items: center;
gap: 6px;
border: 1px solid var(--control-line);
border-radius: 7px;
background: var(--bg-raised);
transition:
border-color 160ms ease,
background-color 160ms ease;
}
.orb-background-mode {
min-width: 0;
}
.orb-background-mode select {
width: 100%;
min-width: 0;
height: 24px;
padding: 0 18px 0 7px;
border: 1px solid var(--control-line);
border-radius: 4px;
outline: 0;
appearance: none;
background:
linear-gradient(45deg, transparent 50%, currentColor 50%)
calc(100% - 9px) 10px / 4px 4px no-repeat,
linear-gradient(135deg, currentColor 50%, transparent 50%)
calc(100% - 6px) 10px / 4px 4px no-repeat,
var(--bg-raised);
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.03em;
text-transform: uppercase;
cursor: pointer;
}
.orb-background-mode select:hover,
.orb-background-mode select:focus-visible {
border-color: var(--line-strong);
color: var(--ink-soft);
}
.orb-background-mode select:focus-visible {
box-shadow: 0 0 0 3px var(--accent-wash);
}
.orb-background-mode select:disabled {
cursor: not-allowed;
opacity: 0.48;
}
.orb-background-control[data-enabled="true"] .orb-background-actions,
.orb-background-control:hover .orb-background-actions {
border-color: var(--line-strong);
background: var(--surface);
}
.color-enable-toggle {
min-height: 22px;
padding: 0 6px;
display: inline-flex;
align-items: center;
gap: 5px;
border: 1px solid var(--control-line);
border-radius: 4px;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.06em;
line-height: 1;
text-transform: uppercase;
cursor: pointer;
}
.color-enable-toggle:hover,
.color-enable-toggle:focus-within {
border-color: var(--line-strong);
color: var(--ink-soft);
}
.color-enable-toggle input {
width: 12px;
height: 12px;
margin: 0;
accent-color: var(--accent);
cursor: pointer;
}
.orb-background-picker {
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
}
.orb-background-picker input[type="color"] {
width: 30px;
height: 24px;
padding: 2px;
border: 1px solid var(--control-line);
border-radius: 4px;
outline: 0;
background: transparent;
cursor: pointer;
}
.orb-background-picker input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.orb-background-picker input[type="color"]::-webkit-color-swatch {
border: 0;
border-radius: 2px;
}
.orb-background-picker input[type="color"]:focus-visible {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-wash);
}
.orb-background-picker input:disabled {
cursor: not-allowed;
filter: grayscale(1);
opacity: 0.48;
}
.orb-background-picker output {
display: none;
min-width: 48px;
overflow: hidden;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
font-variant-numeric: tabular-nums;
text-overflow: ellipsis;
}
.select-shell {
position: relative;
display: block;
}
.select-shell::after {
position: absolute;
top: 50%;
right: 12px;
color: var(--muted);
font-family: var(--font-mono);
font-size: 10px;
content: "⌄";
pointer-events: none;
transform: translateY(-58%);
}
select,
.seed-control input {
width: 100%;
height: 40px;
border: 1px solid var(--control-line);
border-radius: 6px;
outline: 0;
background: var(--bg-raised);
color: var(--ink-soft);
font-family: var(--font-mono);
font-size: 10px;
transition:
border-color 160ms ease,
background-color 160ms ease,
box-shadow 160ms ease;
}
select {
padding: 0 30px 0 11px;
appearance: none;
}
select:hover,
.seed-control input:hover {
border-color: var(--line-strong);
}
select:focus-visible,
.seed-control input:focus-visible {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-wash);
}
.range-control {
display: grid;
gap: 10px;
}
.range-control > span {
display: flex;
align-items: center;
justify-content: space-between;
color: var(--ink-soft);
font-size: 11px;
font-weight: 500;
}
.range-control output {
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: 9px;
font-variant-numeric: tabular-nums;
}
input[type="range"] {
width: 100%;
height: 16px;
margin: 0;
padding: 0;
appearance: none;
outline: 0;
background: transparent;
}
input[type="range"]::-webkit-slider-runnable-track {
height: 2px;
border-radius: 0;
background: linear-gradient(90deg, var(--accent), var(--control-line));
}
input[type="range"]::-webkit-slider-thumb {
width: 13px;
height: 13px;
margin-top: -5.5px;
border: 2px solid var(--surface);
border-radius: 2px;
appearance: none;
background: var(--ink);
box-shadow: 0 0 0 1px var(--line-strong);
}
input[type="range"]::-moz-range-track {
height: 2px;
border: 0;
background: var(--control-line);
}
input[type="range"]::-moz-range-progress {
height: 2px;
background: var(--accent);
}
input[type="range"]::-moz-range-thumb {
width: 11px;
height: 11px;
border: 2px solid var(--surface);
border-radius: 2px;
background: var(--ink);
box-shadow: 0 0 0 1px var(--line-strong);
}
input[type="range"]:focus-visible::-webkit-slider-thumb {
box-shadow:
0 0 0 1px var(--accent),
0 0 0 4px var(--accent-wash);
}
.seed-control__row {
display: flex;
gap: 6px;
}
.seed-control input {
min-width: 0;
padding: 0 11px;
cursor: text;
}
.control-note {
padding: 18px 24px 21px;
display: flex;
align-items: flex-start;
gap: 10px;
background: color-mix(in srgb, var(--accent-wash), transparent 38%);
}
.control-note__mark {
width: 7px;
height: 7px;
margin-top: 4px;
flex: 0 0 auto;
border: 1px solid var(--accent);
transform: rotate(45deg);
}
.control-note p {
margin: 0;
color: var(--muted);
font-size: 10px;
line-height: 1.55;
}
.usage-section {
margin: 0 0 clamp(92px, 10vw, 144px);
padding: clamp(88px, 9vw, 132px) 0;
border-block: 1px solid var(--line);
}
.usage-section__heading {
display: grid;
grid-template-columns: minmax(140px, 0.34fr) minmax(0, 0.9fr) minmax(280px, 0.58fr);
align-items: end;
gap: clamp(32px, 5vw, 76px);
}
.usage-section__heading > p:last-child {
max-width: 500px;
margin: 0;
color: var(--muted);
font-size: 13px;
line-height: 1.72;
}
.usage-steps {
margin: clamp(46px, 6vw, 78px) 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1px;
border: 1px solid var(--line);
background: var(--line);
list-style: none;
}
.usage-step {
min-width: 0;
min-height: 430px;
padding: clamp(22px, 2.4vw, 34px);
display: grid;
grid-template-rows: auto 1fr;
gap: 66px;
background:
linear-gradient(145deg, var(--surface-sheen), transparent 46%),
var(--bg-raised);
}
.usage-step > div {
min-width: 0;
}
.usage-step__number {
width: fit-content;
padding-bottom: 6px;
border-bottom: 1px solid var(--line-strong);
color: var(--faint);
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.12em;
}
.usage-step h3 {
margin: 0;
color: var(--ink-soft);
font-size: 21px;
font-weight: 430;
letter-spacing: -0.025em;
}
.usage-step p {
min-height: 42px;
margin: 10px 0 22px;
color: var(--muted);
font-size: 11px;
line-height: 1.62;
}
.usage-step pre {
max-width: 100%;
margin: 8px 0 0;
padding: 14px;
overflow: auto;
border: 1px solid var(--code-line);
border-radius: 6px;
background:
linear-gradient(90deg, var(--code-grid), transparent 1px) 0 0 / 48px 100%,
var(--code-bg);
color: var(--code-ink);
scrollbar-color: var(--code-scrollbar) var(--code-bg);
}
.usage-step pre:focus-visible {
border-color: var(--accent);
outline-offset: 2px;
}
.usage-step code {
font-family: var(--font-mono);
font-size: 9px;
line-height: 1.72;
tab-size: 2;
}
.library-section {
padding: 0 0 112px;
}
.section-heading {
padding: 0 0 30px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 42px;
}
.section-heading h2,
.api-copy h2 {
max-width: 760px;
margin: 13px 0 0;
color: var(--ink);
font-size: clamp(2rem, 4.2vw, 4.4rem);
font-weight: 280;
letter-spacing: -0.05em;
line-height: 1;
}
.section-heading p:not(.section-index) {
max-width: 610px;
margin: 17px 0 0;
color: var(--muted);
font-size: 13px;
line-height: 1.6;
}
.section-heading__aside {
min-width: 190px;
padding: 12px 0;
display: grid;
gap: 5px;
border-block: 1px solid var(--line);
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.06em;
text-align: right;
text-transform: uppercase;
}
.section-heading__aside b {
color: var(--accent-strong);
font-size: 11px;
}
.saved-presets {
margin: 0 0 24px;
padding: 17px;
border: 1px solid var(--line);
border-radius: 10px;
background:
linear-gradient(135deg, var(--surface-sheen), transparent 54%),
color-mix(in srgb, var(--bg-raised), transparent 12%);
}
.saved-presets__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.saved-presets__header > div:first-child {
min-width: 0;
}
.saved-presets__eyebrow {
margin: 0 0 5px;
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: 7px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.saved-presets h3 {
margin: 0;
color: var(--ink-soft);
font-size: 15px;
font-weight: 540;
letter-spacing: -0.015em;
}
.saved-presets__header p:not(.saved-presets__eyebrow) {
max-width: 650px;
margin: 5px 0 0;
color: var(--faint);
font-size: 9px;
line-height: 1.5;
}
.saved-presets__actions {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 8px;
}
.saved-presets__count {
min-width: 54px;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
text-align: right;
text-transform: uppercase;
}
.saved-presets__empty {
min-height: 58px;
margin: 14px 0 0;
padding: 14px;
display: grid;
place-items: center;
border: 1px dashed var(--control-line);
border-radius: 7px;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
line-height: 1.5;
text-align: center;
}
.saved-preset-gallery {
max-height: 286px;
margin-top: 14px;
padding: 1px 3px 3px 1px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 7px;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
.saved-preset-card {
position: relative;
min-width: 0;
overflow: hidden;
border: 1px solid var(--control-line);
border-radius: 7px;
background: var(--surface);
transition:
border-color 160ms ease,
background-color 160ms ease,
transform 180ms var(--ease);
}
.saved-preset-card:hover,
.saved-preset-card:focus-within {
border-color: var(--line-strong);
background: var(--surface-raised);
}
.saved-preset-card:hover {
transform: translateY(-1px);
}
.saved-preset-card__open {
width: 100%;
min-height: 76px;
padding: 11px 58px 11px 11px;
display: grid;
grid-template-columns: 52px minmax(0, 1fr);
align-items: center;
gap: 10px;
border: 0;
background: transparent;
color: inherit;
text-align: left;
}
.saved-preset-card__open:focus-visible {
outline: 0;
box-shadow: inset 0 0 0 2px var(--accent);
}
.saved-preset-card__preview {
width: 52px;
height: 52px;
display: grid;
place-items: center;
overflow: hidden;
border: 1px solid var(--control-line);
border-radius: 5px;
background:
linear-gradient(var(--canvas-grid) 1px, transparent 1px),
linear-gradient(90deg, var(--canvas-grid) 1px, transparent 1px),
var(--canvas);
background-size: 8px 8px;
}
.saved-preset-card__preview canvas {
width: 50px;
height: 50px;
display: block;
image-rendering: pixelated;
}
.saved-preset-card__copy {
min-width: 0;
display: grid;
gap: 3px;
}
.saved-preset-card__copy strong,
.saved-preset-card__copy small,
.saved-preset-card__metadata {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.saved-preset-card__copy strong {
color: var(--ink-soft);
font-size: 10px;
font-weight: 560;
}
.saved-preset-card__copy small,
.saved-preset-card__metadata {
color: var(--faint);
font-family: var(--font-mono);
font-size: 7px;
line-height: 1.35;
}
.saved-preset-card__metadata {
color: var(--muted);
}
.saved-preset-card__delete {
position: absolute;
top: 8px;
right: 8px;
min-height: 24px;
padding: 0 6px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: var(--faint);
font-family: var(--font-mono);
font-size: 7px;
text-transform: uppercase;
}
.saved-preset-card__delete:hover,
.saved-preset-card__delete:focus-visible {
border-color: color-mix(in srgb, var(--danger), transparent 62%);
background: color-mix(in srgb, var(--danger), transparent 92%);
color: var(--danger);
}
.saved-presets__undo {
margin-top: 9px;
padding: 8px 10px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
border-top: 1px solid var(--line);
color: var(--muted);
font-family: var(--font-mono);
font-size: 8px;
}
.saved-presets__undo-button {
min-height: 28px;
padding: 0 8px;
border: 1px solid var(--accent);
border-radius: 4px;
background: var(--accent-wash);
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: 8px;
}
.sprite-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.sprite-card {
contain: layout paint;
position: relative;
min-width: 0;
min-height: 96px;
padding: 14px;
display: grid;
grid-template-columns: 66px minmax(0, 1fr);
align-items: center;
gap: 13px;
overflow: hidden;
border: 1px solid var(--control-line);
border-radius: 9px;
background:
linear-gradient(135deg, var(--surface-sheen), transparent 55%),
var(--surface);
color: var(--ink-soft);
text-align: left;
transition:
border-color 180ms ease,
background-color 180ms ease,
transform 220ms var(--ease),
box-shadow 220ms ease;
}
.sprite-card::before {
position: absolute;
top: 9px;
right: 9px;
width: 5px;
height: 5px;
border: solid var(--line-strong);
border-width: 1px 1px 0 0;
content: "";
}
.sprite-card::after {
position: absolute;
right: 12px;
bottom: 10px;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
content: "+";
opacity: 0;
transition: opacity 160ms ease;
}
.sprite-card:hover {
z-index: 1;
border-color: var(--line-strong);
background: var(--surface-raised);
box-shadow: var(--shadow-card);
transform: translateY(-2px);
}
.sprite-card:hover::after {
opacity: 1;
}
.sprite-card.is-selected {
border-color: color-mix(in srgb, var(--accent), var(--line) 20%);
background:
linear-gradient(135deg, var(--accent-wash), transparent 58%),
var(--surface-raised);
box-shadow:
inset 2px 0 var(--accent),
var(--shadow-card-selected);
}
.sprite-card.is-selected::after {
color: var(--accent-strong);
content: "SELECTED";
opacity: 1;
}
.sprite-preview {
contain: paint;
position: relative;
width: 66px;
height: 66px;
display: grid;
place-items: center;
overflow: hidden;
border: 1px solid var(--control-line);
border-radius: 6px;
background:
linear-gradient(var(--canvas-grid) 1px, transparent 1px),
linear-gradient(90deg, var(--canvas-grid) 1px, transparent 1px),
var(--canvas);
background-size: 11px 11px;
}
.sprite-preview::before,
.sprite-preview::after {
position: absolute;
z-index: 2;
width: 8px;
height: 8px;
content: "";
pointer-events: none;
}
.sprite-preview::before {
top: 4px;
left: 4px;
border-top: 1px solid var(--line-strong);
border-left: 1px solid var(--line-strong);
}
.sprite-preview::after {
right: 4px;
bottom: 4px;
border-right: 1px solid var(--line-strong);
border-bottom: 1px solid var(--line-strong);
}
.sprite-preview canvas {
position: relative;
z-index: 1;
width: 64px;
height: 64px;
display: block;
image-rendering: pixelated;
}
.sprite-copy {
min-width: 0;
display: grid;
gap: 4px;
}
.sprite-copy strong {
overflow: hidden;
color: var(--ink-soft);
font-size: 11px;
font-weight: 540;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
.sprite-copy small {
overflow: hidden;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.025em;
line-height: 1.4;
text-overflow: ellipsis;
white-space: nowrap;
}
.sprite-card.is-selected .sprite-copy strong,
.sprite-card.is-selected .sprite-copy small {
color: var(--accent-strong);
}
.api-section {
margin-bottom: 100px;
display: grid;
grid-template-columns: minmax(310px, 0.75fr) minmax(0, 1.25fr);
overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius-lg);
background: var(--surface);
box-shadow: var(--shadow);
}
.api-copy {
padding: clamp(32px, 5vw, 72px);
display: flex;
flex-direction: column;
align-items: flex-start;
border-right: 1px solid var(--line);
}
.api-copy h2 {
max-width: 620px;
font-size: clamp(2.5rem, 4.2vw, 4.8rem);
}
.api-copy > p:not(.section-index) {
max-width: 590px;
margin: 23px 0 0;
color: var(--muted);
font-size: 13px;
line-height: 1.7;
}
.api-facts {
width: 100%;
margin: 34px 0 0;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1px;
border: 1px solid var(--line);
background: var(--line);
}
.api-facts div {
min-width: 0;
padding: 14px;
background: var(--bg-raised);
}
.api-facts dt {
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.api-facts dd {
margin: 7px 0 0;
color: var(--ink-soft);
font-family: var(--font-mono);
font-size: 9px;
line-height: 1.45;
}
.export-actions {
margin-top: auto;
padding-top: 38px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.code-panel {
min-width: 0;
min-height: 620px;
display: grid;
grid-template-rows: 50px minmax(0, 1fr);
background: var(--code-bg);
}
.code-panel__header {
padding: 0 17px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--code-line);
color: var(--code-muted);
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.06em;
}
.code-panel__header > div {
display: flex;
align-items: center;
gap: 12px;
}
.code-lights {
display: flex;
gap: 4px;
}
.code-lights i {
width: 5px;
height: 5px;
display: block;
border-radius: 50%;
background: var(--code-dot-1);
}
.code-lights i:nth-child(2) {
background: var(--code-dot-2);
}
.code-lights i:nth-child(3) {
background: var(--code-dot-3);
}
.code-copy {
min-height: 30px;
padding: 0 9px;
border: 1px solid var(--code-control);
border-radius: 5px;
background: transparent;
color: var(--code-control-ink);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.code-copy:hover {
border-color: var(--code-line-strong);
background: var(--code-hover);
color: var(--code-ink-strong);
}
.code-panel pre {
min-width: 0;
max-width: 100%;
margin: 0;
padding: clamp(24px, 4vw, 52px);
overflow: auto;
background:
linear-gradient(90deg, var(--code-grid), transparent 1px) 0 0 / 72px 100%,
transparent;
scrollbar-color: var(--code-scrollbar) var(--code-bg);
}
.code-panel code {
color: var(--code-ink);
font-family: var(--font-mono);
font-size: clamp(10px, 0.9vw, 13px);
line-height: 1.85;
tab-size: 2;
}
.download-section {
margin-bottom: 100px;
display: grid;
grid-template-columns: minmax(310px, 0.75fr) minmax(0, 1.25fr);
overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius-lg);
background:
linear-gradient(135deg, var(--surface-sheen), transparent 38%),
var(--surface);
box-shadow: var(--shadow);
}
.download-section__copy {
min-width: 0;
padding: clamp(32px, 5vw, 72px);
display: flex;
flex-direction: column;
align-items: flex-start;
border-right: 1px solid var(--line);
}
.download-section__copy h2 {
max-width: 620px;
margin: 13px 0 0;
color: var(--ink);
font-size: clamp(2.5rem, 4.2vw, 4.8rem);
font-weight: 280;
letter-spacing: -0.05em;
line-height: 1;
}
.download-section__copy > p:not(.section-index):not(.download-license-note) {
max-width: 590px;
margin: 23px 0 0;
color: var(--muted);
font-size: 13px;
line-height: 1.7;
}
.download-card__contents {
width: 100%;
margin: 32px 0 0;
padding: 0;
display: grid;
border-top: 1px solid var(--line);
list-style: none;
}
.download-card__contents li {
position: relative;
min-height: 38px;
padding: 11px 0 10px 18px;
border-bottom: 1px solid var(--line);
color: var(--ink-soft);
font-family: var(--font-mono);
font-size: 9px;
line-height: 1.55;
}
.download-card__contents li::before {
position: absolute;
top: 15px;
left: 1px;
width: 6px;
height: 6px;
border: 1px solid var(--faint);
content: "";
transform: rotate(45deg);
}
.download-license-note {
margin: auto 0 0;
padding-top: 32px;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
line-height: 1.6;
}
.download-license-note strong {
color: var(--muted);
font-weight: 600;
}
.download-options {
min-width: 0;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1px;
background: var(--line);
}
.download-card {
min-width: 0;
min-height: 310px;
padding: clamp(24px, 3.2vw, 42px);
display: flex;
flex-direction: column;
align-items: flex-start;
background:
linear-gradient(145deg, var(--surface-sheen), transparent 52%),
var(--bg-raised);
}
.download-card--primary {
min-height: 340px;
grid-column: 1 / -1;
background:
linear-gradient(145deg, var(--accent-wash), transparent 55%),
var(--bg-raised);
}
.download-card__heading {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.download-card__heading p {
margin: 0;
}
.download-card__heading span {
padding: 4px 6px;
border: 1px solid var(--line-strong);
border-radius: 999px;
color: var(--muted);
}
.download-card h3 {
margin: 38px 0 0;
color: var(--ink);
font-size: clamp(1.55rem, 2.5vw, 2.5rem);
font-weight: 340;
letter-spacing: -0.035em;
line-height: 1.05;
}
.download-card > p:not(.download-card__meta) {
max-width: 600px;
margin: 13px 0 0;
color: var(--muted);
font-size: 11px;
line-height: 1.65;
}
.download-card__meta {
margin: 21px 0 0;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.download-card__meta span {
padding: 5px 7px;
border: 1px solid var(--line);
border-radius: 4px;
color: var(--muted);
font-family: var(--font-mono);
font-size: 8px;
line-height: 1;
text-transform: uppercase;
}
.download-card__actions {
width: 100%;
margin-top: auto;
padding-top: 30px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.download-card__actions .button {
min-height: 44px;
}
.download-inline-link {
min-height: 44px;
padding-block: 8px;
display: inline-flex;
align-items: center;
border-bottom: 1px solid var(--line-strong);
color: var(--muted);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.06em;
text-decoration: none;
text-transform: uppercase;
transition:
border-color 160ms ease,
color 160ms ease;
}
.download-inline-link:hover,
.download-inline-link:focus-visible {
border-color: var(--ink);
color: var(--ink);
}
.site-footer {
width: var(--content);
min-height: 86px;
margin-inline: auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
border-top: 1px solid var(--line);
color: var(--faint);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.site-footer p {
margin: 0;
}
.site-footer p span {
margin-left: 12px;
color: var(--muted);
}
.site-footer a {
color: var(--muted);
text-decoration: none;
}
.site-footer a:hover {
color: var(--ink);
}
.site-footer__links {
display: flex;
align-items: center;
gap: 18px;
}
.export-dialog {
width: min(920px, calc(100vw - 32px));
max-height: min(760px, calc(100dvh - 32px));
padding: 0;
overflow: auto;
border: 1px solid var(--line-strong);
border-radius: 14px;
background: var(--surface);
box-shadow: var(--shadow-dialog);
color: var(--ink);
}
.export-dialog::backdrop {
background: var(--backdrop);
backdrop-filter: blur(8px);
}
.export-dialog__surface {
margin: 0;
}
.export-dialog__header {
position: sticky;
z-index: 2;
top: 0;
padding: 22px 24px 18px;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
border-bottom: 1px solid var(--line);
background: color-mix(in srgb, var(--surface), transparent 4%);
backdrop-filter: blur(18px);
}
.export-dialog__header h2 {
margin: 8px 0 0;
font-size: 24px;
font-weight: 440;
letter-spacing: -0.025em;
}
.dialog-close {
width: 38px;
height: 38px;
flex: 0 0 auto;
border: 1px solid var(--control-line);
border-radius: 7px;
background: transparent;
color: var(--muted);
font-size: 22px;
line-height: 1;
}
.dialog-close:hover {
border-color: var(--line-strong);
background: var(--surface-hover);
color: var(--ink);
}
.export-dialog__body {
padding: 24px;
display: grid;
grid-template-columns: minmax(250px, 0.88fr) minmax(300px, 1.12fr);
gap: 24px;
}
.export-preview {
min-width: 0;
display: grid;
align-content: start;
gap: 15px;
}
.export-preview__frame {
position: relative;
width: min(100%, 340px);
aspect-ratio: 1;
overflow: hidden;
border: 1px solid var(--line-strong);
border-radius: 9px;
background: var(--canvas);
}
.export-preview__frame.is-transparent {
background-color: var(--checker-base);
background-image:
linear-gradient(45deg, var(--checker-tile) 25%, transparent 25%),
linear-gradient(-45deg, var(--checker-tile) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--checker-tile) 75%),
linear-gradient(-45deg, transparent 75%, var(--checker-tile) 75%);
background-position:
0 0,
0 8px,
8px -8px,
-8px 0;
background-size: 16px 16px;
}
.export-preview canvas {
width: 100%;
height: 100%;
display: block;
image-rendering: pixelated;
}
.export-preview h3 {
margin: 0;
color: var(--ink-soft);
font-size: 12px;
font-weight: 550;
}
.export-preview p {
margin: 6px 0 0;
color: var(--muted);
font-size: 10px;
line-height: 1.55;
}
.export-settings {
min-width: 0;
display: grid;
align-content: start;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.export-field--filename,
.export-check,
.export-summary {
grid-column: 1 / -1;
}
.export-field input,
.export-field select {
height: 42px;
background: var(--bg-raised);
font-size: 10px;
}
.export-field select:disabled,
.export-check input:disabled + span {
cursor: not-allowed;
opacity: 0.5;
}
.export-check {
min-height: 42px;
padding: 0 12px;
display: flex;
align-items: center;
gap: 9px;
border: 1px solid var(--control-line);
border-radius: 6px;
background: var(--bg-raised);
color: var(--ink-soft);
font-size: 10px;
}
.export-check input {
width: 16px;
height: 16px;
margin: 0;
accent-color: var(--accent);
}
.export-summary {
margin: 2px 0 0;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 7px;
background: var(--line);
}
.export-summary div {
min-width: 0;
padding: 12px;
background: var(--bg-raised);
}
.export-summary dt {
color: var(--faint);
font-family: var(--font-mono);
font-size: 7px;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.export-summary dd {
margin: 5px 0 0;
overflow: hidden;
color: var(--ink-soft);
font-family: var(--font-mono);
font-size: 9px;
text-overflow: ellipsis;
white-space: nowrap;
}
.export-dialog__actions {
position: sticky;
z-index: 2;
bottom: 0;
padding: 16px 24px;
display: flex;
justify-content: flex-end;
gap: 8px;
border-top: 1px solid var(--line);
background: color-mix(in srgb, var(--surface), transparent 4%);
backdrop-filter: blur(18px);
}
.export-dialog__actions .button:disabled {
cursor: not-allowed;
}
@keyframes status-pulse {
0% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--success), transparent 68%);
}
45%,
100% {
box-shadow: 0 0 0 7px color-mix(in srgb, var(--success), transparent 100%);
}
}
@keyframes field-scan {
from {
transform: translateY(0);
}
to {
transform: translateY(760%);
}
}
@media (max-width: 1240px) {
:root {
--content: min(calc(100% - 40px), 1180px);
}
.site-nav {
display: none;
}
.workbench {
grid-template-columns: minmax(0, 1fr) 340px;
}
.sprite-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.saved-preset-gallery {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.stage-lower {
align-items: flex-start;
flex-direction: column;
}
.stage-metrics {
width: 100%;
justify-content: flex-start;
text-align: left;
}
.workflow-bar {
grid-template-columns: 1fr;
}
.recipe-workflow {
justify-items: stretch;
}
.recipe-workflow__status,
.recipe-workflow__actions {
justify-content: flex-start;
}
}
@media (min-width: 1021px) and (max-height: 1040px) {
.control-panel {
position: static;
}
}
@media (max-width: 1020px) {
.site-nav {
display: none;
}
.hero {
min-height: auto;
padding-top: 76px;
grid-template-columns: 1fr;
gap: 58px;
}
.landing-hero__copy {
max-width: 820px;
}
.landing-hero__visual {
width: min(100%, 720px);
min-height: 540px;
}
.principles-section__heading,
.usage-section__heading {
grid-template-columns: 1fr;
gap: 20px;
}
.principles-section__heading h2,
.usage-section__heading h2 {
max-width: 850px;
}
.usage-section__heading > p:last-child {
max-width: 650px;
}
.studio-intro {
grid-template-columns: 1fr;
gap: 24px;
}
.studio-intro > p {
max-width: 680px;
}
.workbench {
grid-template-columns: 1fr;
}
.control-panel {
position: static;
}
.control-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.control-section {
border-right: 1px solid var(--line);
}
.control-section:nth-of-type(even) {
border-right: 0;
}
.control-note {
border-top: 0;
}
.api-section {
grid-template-columns: 1fr;
}
.download-section {
grid-template-columns: 1fr;
}
.api-copy {
min-height: 480px;
border-right: 0;
border-bottom: 1px solid var(--line);
}
.download-section__copy {
min-height: 520px;
border-right: 0;
border-bottom: 1px solid var(--line);
}
.code-panel {
min-height: 560px;
}
}
@media (max-width: 760px) {
:root {
--content: calc(100% - 28px);
--radius-lg: 13px;
}
.topbar__inner {
min-height: 62px;
}
.brand__copy small,
.version-pill,
.runtime-status {
display: none;
}
.topbar__actions {
gap: 5px;
}
.button {
min-height: 40px;
padding-inline: 11px;
font-size: 9px;
}
.theme-button {
width: 40px;
padding: 0;
overflow: hidden;
color: transparent;
white-space: nowrap;
}
.theme-button::before {
flex: 0 0 auto;
color: var(--ink-soft);
}
.hero {
min-height: auto;
padding: 64px 0 60px;
gap: 44px;
}
.hero h1 {
font-size: clamp(3rem, 15vw, 5.5rem);
letter-spacing: -0.07em;
}
.hero__lede {
margin-top: 22px;
font-size: 14px;
}
.landing-hero__actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.landing-hero__actions .button {
width: 100%;
padding-inline: 10px;
}
.landing-hero__visual {
--hero-proof-height: 82px;
--hero-proof-offset: 41px;
min-height: 430px;
}
.hero-engine-output {
width: min(76%, 330px);
}
.hero-proof {
min-height: 82px;
}
.hero-proof div {
padding: 14px;
}
.principles-section,
.usage-section {
padding-block: 78px;
}
.principles-section__heading h2,
.studio-intro h2,
.usage-section__heading h2 {
font-size: clamp(2.6rem, 12vw, 4.2rem);
}
.principles-grid,
.usage-steps {
grid-template-columns: 1fr;
}
.principle-card {
min-height: 210px;
padding: 24px;
}
.principle-card h3 {
margin-top: 45px;
}
.studio-intro {
padding-top: 76px;
}
.usage-section {
margin-bottom: 82px;
}
.usage-step {
min-height: auto;
gap: 48px;
}
.usage-step p {
min-height: 0;
}
.workbench {
padding-top: 14px;
padding-bottom: 82px;
}
.stage-panel {
padding: 16px;
}
.stage-heading {
min-height: 0;
padding: 4px 1px 17px;
display: grid;
}
.state-description {
min-height: 0;
}
.signal-cluster {
width: 100%;
margin-top: 7px;
text-align: left;
}
.signal-cluster__buttons {
width: 100%;
}
.signal-button {
min-height: 40px;
flex: 1;
}
#openRecipeButton,
#importButton {
width: 40px;
padding: 0;
overflow: hidden;
color: transparent;
white-space: nowrap;
}
#openRecipeButton::before,
#importButton::before {
display: grid;
place-items: center;
color: var(--ink-soft);
font-size: 15px;
}
#openRecipeButton::before {
content: "↥";
}
#importButton::before {
content: "+";
}
.workflow-bar {
padding: 10px;
}
.saved-presets__header {
align-items: flex-start;
}
.saved-preset-gallery {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.recipe-workflow__actions {
display: grid;
grid-template-columns: minmax(0, 1fr) repeat(4, auto);
}
.recent-preset {
width: auto;
max-width: none;
}
.workflow-button,
.workflow-export,
.recent-preset select {
min-height: 40px;
}
.frame-badge--bottom {
display: none;
}
.progress-control,
.progress-control.is-visible {
grid-template-columns: 1fr;
gap: 10px;
}
.stage-lower {
padding-bottom: 16px;
}
.quick-states {
width: 100%;
}
.quick-states > div {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
.quick-states button {
min-height: 36px;
padding-inline: 6px;
}
.stage-metrics {
gap: 17px;
}
.stage-metrics__state {
margin-left: auto;
}
.stage-actions {
align-items: stretch;
flex-direction: column;
}
.stage-actions p {
display: none;
}
.stage-actions > div {
display: grid;
grid-template-columns: 0.8fr 1.2fr;
}
.stage-actions .button {
min-height: 44px;
}
.control-heading {
padding: 21px 18px;
}
.control-form {
display: block;
}
.control-section {
padding: 20px 18px 22px;
border-right: 0;
}
.control-section legend {
padding: 17px 18px 0;
}
.select-control,
.orb-boundary-control,
.orb-background-control,
.seed-control {
grid-template-columns: minmax(0, 0.85fr) minmax(140px, 1.15fr);
}
.control-note {
padding-inline: 18px;
}
.section-heading {
align-items: flex-start;
flex-direction: column;
gap: 22px;
}
.section-heading h2,
.api-copy h2,
.download-section__copy h2 {
font-size: clamp(2.6rem, 12vw, 4.2rem);
}
.section-heading__aside {
width: 100%;
min-width: 0;
text-align: left;
}
.sprite-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sprite-card {
min-height: 132px;
padding: 12px;
grid-template-columns: 1fr;
justify-items: start;
}
.sprite-preview,
.sprite-preview canvas {
width: 58px;
height: 58px;
}
.library-section {
padding-bottom: 82px;
}
.api-section {
margin-bottom: 72px;
}
.download-section {
margin-bottom: 72px;
}
.api-copy {
min-height: 510px;
padding: 34px 22px;
}
.api-facts {
grid-template-columns: 1fr;
}
.api-facts div {
display: flex;
justify-content: space-between;
gap: 18px;
}
.api-facts dd {
margin: 0;
text-align: right;
}
.export-actions {
width: 100%;
}
.export-actions .button {
min-height: 44px;
flex: 1;
}
.download-section__copy {
min-height: auto;
padding: 34px 22px;
}
.download-license-note {
margin-top: 34px;
}
.download-options {
grid-template-columns: 1fr;
}
.download-card,
.download-card--primary {
min-height: 300px;
grid-column: auto;
}
.download-card__actions .button {
width: 100%;
}
.export-dialog__body {
grid-template-columns: 1fr;
}
.export-preview {
grid-template-columns: minmax(150px, 0.55fr) minmax(180px, 1fr);
align-items: center;
}
.export-preview__frame {
width: 100%;
}
.code-panel {
min-height: 500px;
}
.site-footer {
min-height: 100px;
}
.site-footer p span {
display: none;
}
}
@media (max-width: 430px) {
.brand {
gap: 7px;
}
.brand__mark {
width: 34px;
height: 34px;
}
.topbar__actions .button {
padding-inline: 8px;
letter-spacing: 0;
}
.landing-hero__actions {
grid-template-columns: 1fr;
}
.landing-hero__visual {
--hero-proof-height: 76px;
--hero-proof-offset: 38px;
min-height: 350px;
}
.landing-hero__visual::before,
.landing-hero__visual::after {
top: 13px;
font-size: 7px;
}
.landing-hero__visual::before {
left: 13px;
}
.landing-hero__visual::after {
right: 13px;
}
.hero-engine-output {
width: min(74%, 250px);
}
.hero-proof {
min-height: 76px;
}
.hero-proof div {
padding: 11px 9px;
}
.hero-proof dt {
font-size: 15px;
}
.hero-proof dd {
font-size: 7px;
white-space: normal;
}
.state-heading {
align-items: flex-start;
flex-direction: column;
gap: 7px;
}
.canvas-frame {
border-radius: 7px;
}
.frame-badge--top {
top: 12px;
left: 13px;
}
.preview-scale-control {
top: 9px;
right: 9px;
}
.preview-scale-control button {
min-width: 40px;
min-height: 34px;
padding-inline: 7px;
}
.state-picker {
grid-template-columns: 1fr;
}
.saved-presets {
padding: 14px;
}
.saved-presets__header {
flex-direction: column;
gap: 12px;
}
.saved-presets__actions {
width: 100%;
justify-content: space-between;
}
.saved-preset-gallery {
grid-template-columns: 1fr;
}
.recipe-workflow__status {
align-items: flex-start;
flex-direction: column;
gap: 5px;
}
#workflowStatus {
max-width: 100%;
}
.recipe-workflow__actions {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.recent-preset,
.workflow-export {
grid-column: 1 / -1;
}
.workflow-button,
.workflow-export,
.recent-preset select {
width: 100%;
min-height: 44px;
}
.quick-states > div {
grid-template-columns: repeat(2, 1fr);
}
.stage-metrics {
display: grid;
grid-template-columns: minmax(0, 0.5fr) minmax(0, 0.5fr) minmax(0, 1.3fr);
}
.stage-metrics > div,
.stage-metrics__state {
min-width: 0 !important;
}
.stage-metrics__state {
margin-left: 0;
}
.stage-metrics dd {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.select-control,
.orb-boundary-control,
.orb-background-control,
.seed-control {
grid-template-columns: 1fr;
gap: 8px;
}
.sprite-grid {
grid-template-columns: 1fr;
}
.sprite-card {
min-height: 88px;
grid-template-columns: 62px minmax(0, 1fr);
}
.export-dialog {
width: calc(100vw - 18px);
max-height: calc(100dvh - 18px);
border-radius: 10px;
}
.export-dialog__header,
.export-dialog__body,
.export-dialog__actions {
padding-inline: 16px;
}
.export-preview {
grid-template-columns: 1fr;
}
.export-preview__frame {
max-width: 260px;
}
.export-settings,
.export-summary {
grid-template-columns: 1fr;
}
.export-summary div {
grid-column: 1;
}
.site-footer {
align-items: flex-start;
justify-content: center;
flex-direction: column;
gap: 7px;
}
.download-card {
min-height: 280px;
padding: 24px 20px;
}
.download-card__meta,
.download-card__actions {
align-items: flex-start;
flex-direction: column;
}
.site-footer__links {
width: 100%;
justify-content: space-between;
}
}
@media (pointer: coarse) {
.preview-scale-control button,
.quick-states button,
.orb-boundary-toggle__track,
.glow-toggle,
.color-enable-toggle,
.orb-background-mode select,
.orb-background-picker input[type="color"],
.color-control input[type="color"],
.workflow-button,
.state-picker input,
.state-picker select,
.recent-preset select {
min-height: 44px;
}
.saved-preset-card__open,
.saved-preset-card__delete,
.saved-presets__undo-button {
min-height: 44px;
}
.color-control input[type="color"],
.orb-background-picker input[type="color"] {
width: 44px;
}
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
.canvas-frame::after {
display: none;
}
}
</style>
<script>
(() => {
const image = new URL("./public/og.png", window.location.href).href;
document.getElementById("ogImage").content = image;
document.getElementById("twitterImage").content = image;
})();
</script>
</head>
<body>
<a class="skip-link" href="#playground">Skip to the live Studio</a>
<div class="ambient-field" aria-hidden="true"></div>
<header class="topbar" id="top">
<div class="topbar__inner">
<a class="brand" href="#top" aria-label="Orby home">
<span class="brand__mark" aria-hidden="true">
<img
data-orby-logo
alt=""
width="40"
height="40"
decoding="async"
/>
</span>
<span class="brand__copy">
<strong>Orby</strong>
<small>By Joan Sterjo</small>
</span>
<span class="version-pill">v5.0</span>
</a>
<nav class="site-nav" aria-label="Primary navigation">
<a href="#overview">Overview</a>
<a href="#playground">Studio</a>
<a href="#states">States</a>
<a href="#api">API</a>
<a href="#download">Download</a>
</nav>
<div class="topbar__actions">
<span class="runtime-status">
<span class="runtime-status__dot" aria-hidden="true"></span>
Engine ready
</span>
<button
class="button button--quiet"
id="openRecipeButton"
type="button"
aria-label="Open an Orby recipe"
title="Open a saved Orby recipe (.joan.json)"
>
Open recipe
</button>
<input
class="visually-hidden"
id="recipeFile"
type="file"
accept=".joan.json,.json,application/json"
tabindex="-1"
aria-label="Choose an Orby recipe file"
/>
<button
class="button button--quiet"
id="importButton"
type="button"
aria-label="Import glyph artwork"
title="Import SVG or raster artwork"
>
Import artwork
</button>
<input
class="visually-hidden"
id="glyphFile"
type="file"
accept=".svg,.png,.jpg,.jpeg,.webp,image/svg+xml,image/png,image/jpeg,image/webp"
tabindex="-1"
aria-label="Choose a glyph image to import"
/>
<button
class="button button--quiet theme-button"
id="themeButton"
type="button"
role="switch"
aria-checked="false"
aria-label="Switch to Light mode"
title="Switch to Light mode"
>
Light mode
</button>
</div>
</div>
</header>
<main id="studio">
<section
class="hero landing-hero"
id="overview"
aria-labelledby="heroTitle"
>
<div class="hero__copy landing-hero__copy">
<p class="eyebrow"><span>System 05</span> Semantic motion infrastructure</p>
<h1 id="heroTitle">A living signal for every state of intelligence.</h1>
<p class="hero__lede">
A procedural glyph engine by Joan Sterjo. Orby turns product intent into
expressive, deterministic pixel motion. Shape one visual identity, invoke it
anywhere, and keep every transition responsive, reproducible, and alive.
</p>
<div class="landing-hero__actions" aria-label="Get started">
<a class="button button--primary" href="#playground">Try the live Studio</a>
<a class="button button--quiet" href="#download">Download the engine</a>
</div>
</div>
<div class="landing-hero__visual">
<div class="hero-engine-output" aria-hidden="true">
<canvas
id="heroEngineCanvas"
width="450"
height="450"
data-engine-render="ai.idle"
></canvas>
</div>
<dl class="hero-proof" aria-label="Engine capabilities">
<div>
<dt>25</dt>
<dd>semantic states</dd>
</div>
<div>
<dt>12</dt>
<dd>transition modes</dd>
</div>
<div>
<dt>68×68</dt>
<dd>default pixel field</dd>
</div>
</dl>
</div>
</section>
<section class="principles-section" aria-labelledby="principlesTitle">
<header class="principles-section__heading">
<p class="section-index">Foundation / 01</p>
<h2 id="principlesTitle">Motion infrastructure, not a loading ornament.</h2>
</header>
<div class="principles-grid">
<article class="principle-card">
<span class="principle-card__index" aria-hidden="true">01</span>
<h3>Deterministic identity</h3>
<p>
A seed preserves the character of the field while signals and interaction
change its energy.
</p>
</article>
<article class="principle-card">
<span class="principle-card__index" aria-hidden="true">02</span>
<h3>Semantic states</h3>
<p>
Ready, thinking, tool use, completion, and recovery share one coherent
visual language.
</p>
</article>
<article class="principle-card">
<span class="principle-card__index" aria-hidden="true">03</span>
<h3>Portable invocation</h3>
<p>
Call one state or choreograph a timed sequence, then carry the same recipe
into any product surface.
</p>
</article>
</div>
</section>
<section class="studio-intro" aria-labelledby="studioIntroTitle">
<div>
<p class="section-index">Live Studio / 02</p>
<h2 id="studioIntroTitle">Design the signal, then run it for real.</h2>
</div>
<p>
Stage a semantic state, tune its field and motion, and export the result as a
reusable recipe. Every control below edits the live engine.
</p>
</section>
<section
class="workbench"
id="playground"
tabindex="-1"
aria-label="Interactive glyph workbench"
>
<article class="panel stage-panel" aria-labelledby="stateTitle">
<header class="panel-heading stage-heading">
<div>
<p class="section-index">Canvas / 01</p>
<div class="state-heading">
<span class="state-category" id="stateCategory">Core state</span>
<h2 id="stateTitle">Idle</h2>
</div>
<p class="state-description" id="stateDescription">
A calm, responsive presence with a low-frequency breathing field.
</p>
</div>
<div class="signal-cluster" aria-label="Inject a live engine signal">
<span class="signal-cluster__label">Inject signal</span>
<div class="signal-cluster__buttons">
<button
class="signal-button signal-button--token"
id="signalTokenButton"
type="button"
title="Simulate token generation"
>
Token
</button>
<button
class="signal-button signal-button--search"
id="signalSearchButton"
type="button"
title="Simulate a search hit"
>
Search
</button>
<button
class="signal-button signal-button--audio"
id="signalAudioButton"
type="button"
title="Simulate speaking audio"
>
Audio
</button>
</div>
</div>
</header>
<div class="workflow-bar" aria-label="Preset and recipe workflow">
<div class="state-picker">
<label class="state-picker__search" for="stateSearchInput">
<span>Find a state</span>
<input
id="stateSearchInput"
type="search"
placeholder="Search 25 states"
autocomplete="off"
aria-controls="statePickerSelect"
/>
</label>
<label class="state-picker__select" for="statePickerSelect">
<span>State preset</span>
<span class="select-shell">
<select id="statePickerSelect" aria-describedby="statePickerStatus">
<option>Loading states…</option>
</select>
</span>
</label>
<span class="visually-hidden" id="statePickerStatus" role="status">
25 states available
</span>
</div>
<div class="recipe-workflow">
<div class="recipe-workflow__status">
<span class="scope-badge" id="overrideSummary">Recipe base</span>
<span id="workflowStatus" role="status">Ready to customize</span>
</div>
<div class="recipe-workflow__actions">
<label class="recent-preset" for="recentPresetSelect">
<span class="visually-hidden">Recent saved presets</span>
<select id="recentPresetSelect" aria-label="Recent saved presets">
<option value="">Recent presets</option>
</select>
</label>
<button class="workflow-button" id="savePresetButton" type="button">
Save preset
</button>
<button
class="workflow-button"
id="undoStudioButton"
type="button"
disabled
>
Undo
</button>
<button
class="workflow-button"
id="resetOverridesButton"
type="button"
disabled
>
Reset
</button>
<button
class="button button--primary workflow-export"
id="openExportButton"
type="button"
>
Export…
</button>
</div>
</div>
</div>
<div class="canvas-frame" id="canvasFrame">
<canvas
id="engineCanvas"
width="860"
height="860"
aria-label="Interactive procedural AI glyph"
aria-describedby="canvasHint"
>
Your browser does not support the canvas required by Orby.
</canvas>
<span class="frame-badge frame-badge--top" id="frameRenderBadge">
Live render
</span>
<span class="frame-badge frame-badge--bottom" id="canvasHint">
Move · press · drop a glyph
</span>
<span class="visually-hidden" id="previewScaleDescription">
Fit fills the preview frame. 1:1 shows each engine grid cell as one
CSS pixel.
</span>
<div
class="preview-scale-control"
role="group"
aria-label="Preview scale"
aria-describedby="previewScaleDescription"
>
<button
id="previewFitButton"
type="button"
aria-label="Fit preview to frame"
aria-pressed="true"
title="Fit the preview to the available canvas frame."
>
Fit
</button>
<button
id="previewActualButton"
type="button"
aria-label="Show actual size — one grid cell per CSS pixel"
aria-pressed="false"
title="Show actual size with one CSS pixel per grid cell."
>
1:1
</button>
</div>
</div>
<div
class="progress-control"
id="progressControl"
aria-hidden="true"
>
<label for="progressRange">
<span>Semantic progress</span>
<output id="progressValue" for="progressRange">48%</output>
</label>
<input
id="progressRange"
type="range"
min="0"
max="100"
step="1"
value="48"
/>
</div>
<div class="stage-lower">
<div class="quick-states" role="group" aria-label="Quick state selection">
<span>Quick states</span>
<div>
<button type="button" data-quick-sprite="ai.idle">Idle</button>
<button type="button" data-quick-sprite="ai.ambient-thinking">Field</button>
<button type="button" data-quick-sprite="ai.ambient-thinking-symmetric">
Symmetry
</button>
<button type="button" data-quick-sprite="ai.thinking">Think</button>
<button type="button" data-quick-sprite="ai.tool-use">Tool</button>
<button type="button" data-quick-sprite="status.success">Done</button>
</div>
</div>
<dl class="stage-metrics" aria-label="Live render metrics">
<div>
<dt>FPS</dt>
<dd id="fpsValue"></dd>
</div>
<div>
<dt>Pixels</dt>
<dd id="activeValue"></dd>
</div>
<div class="stage-metrics__state">
<dt>State</dt>
<dd id="currentStateValue">ai.idle</dd>
</div>
</dl>
</div>
<footer class="stage-actions">
<p><kbd>Space</kbd> motion <kbd></kbd><kbd></kbd> states <kbd>R</kbd> reseed</p>
<div>
<button
class="button button--quiet play-button"
id="playButton"
type="button"
aria-pressed="true"
aria-keyshortcuts="Space"
>
Pause motion
</button>
<button
class="button button--primary"
id="autopilotButton"
type="button"
aria-pressed="false"
aria-keyshortcuts="A"
aria-describedby="stateAutoplayDescription"
title="Automatically cycles through 10 common AI states every 4 seconds. Click again to stop."
>
Auto-play AI states
</button>
<span class="visually-hidden" id="stateAutoplayDescription">
Automatically cycles through Ready, Listening, Thinking, Tool Use,
Generating, Speaking, and Completed every four seconds until stopped.
</span>
</div>
</footer>
</article>
<aside class="panel control-panel" aria-labelledby="fieldLabTitle">
<header class="panel-heading control-heading">
<div>
<p class="section-index">Field lab / 02</p>
<h2 id="fieldLabTitle">Shape the signal</h2>
<p class="control-heading__hint">
Hover or focus a control to learn what it changes.
</p>
</div>
<span class="deterministic-badge">Deterministic</span>
</header>
<div class="control-form" role="group" aria-label="Engine configuration">
<fieldset class="control-section">
<legend>Field composition</legend>
<label class="select-control" for="fieldSelect">
<span>
<span class="control-label-line">
<span>Noise field</span>
<span class="scope-badge" id="fieldScopeBadge">Recipe</span>
</span>
<small>Spatial activation texture</small>
</span>
<span class="select-shell">
<select id="fieldSelect" aria-label="Noise field">
<option>Loading fields…</option>
</select>
</span>
</label>
<label class="select-control" for="transitionSelect">
<span>
<span class="control-label-line">
<span>Transition</span>
<span class="scope-badge" id="transitionScopeBadge">Recipe</span>
</span>
<small>State-to-state topology</small>
</span>
<span class="select-shell">
<select id="transitionSelect" aria-label="Transition mode">
<option>Loading transitions…</option>
</select>
</span>
</label>
</fieldset>
<fieldset class="control-section">
<legend>Pixel behavior</legend>
<label class="select-control" for="pixelShapeSelect">
<span>
<span class="control-label-line">
<span>Pixel geometry</span>
<span class="scope-badge" id="pixelShapeScopeBadge">Carries</span>
</span>
<small>Rendered cell silhouette</small>
</span>
<span class="select-shell">
<select id="pixelShapeSelect" aria-label="Pixel geometry">
<option>Loading shapes…</option>
</select>
</span>
</label>
<label class="select-control" for="pixelSwitchSelect">
<span>
<span class="control-label-line">
<span>Switch system</span>
<span class="scope-badge" id="pixelSwitchScopeBadge">Recipe</span>
</span>
<small>Activation and persistence logic</small>
</span>
<span class="select-shell">
<select id="pixelSwitchSelect" aria-label="Pixel switch system">
<option>Loading switches…</option>
</select>
</span>
</label>
<label
class="orb-boundary-control"
id="orbBoundaryControl"
for="orbBoundaryInput"
>
<span>
<span class="control-label-line">
<span>Orb boundary</span>
<span class="scope-badge" id="orbBoundaryScopeBadge">Carries</span>
</span>
<small id="orbBoundaryAvailability">
How the circular edge is perceived
</small>
</span>
<span class="orb-boundary-toggle">
<input
id="orbBoundaryInput"
type="checkbox"
role="switch"
aria-label="Use implied Gestalt orb boundary"
aria-describedby="orbBoundaryAvailability"
/>
<span class="orb-boundary-toggle__track" aria-hidden="true">
<span>Defined</span>
<span>Gestalt</span>
</span>
</span>
</label>
<div
class="orb-background-control"
id="orbBackgroundControl"
role="group"
aria-labelledby="orbBackgroundLabel"
data-available="true"
>
<span>
<span class="control-label-line">
<span id="orbBackgroundLabel">Orb background</span>
<span class="scope-badge" id="orbBackgroundScopeBadge">None</span>
</span>
<small id="orbBackgroundAvailability">
Optional color behind the orb pixels
</small>
</span>
<span class="orb-background-actions">
<label class="color-enable-toggle" for="orbBackgroundEnabledInput">
<input
id="orbBackgroundEnabledInput"
type="checkbox"
aria-label="Enable orb background color"
/>
<span id="orbBackgroundEnabledLabel" aria-hidden="true">None</span>
</label>
<label class="orb-background-mode" for="orbBackgroundModeSelect">
<span class="visually-hidden">Orb background rendering</span>
<select
id="orbBackgroundModeSelect"
aria-label="Orb background rendering"
disabled
>
<option value="solid">Solid</option>
<option value="pixelated">Pixels</option>
</select>
</label>
<label class="orb-background-picker" for="orbBackgroundColorInput">
<input
id="orbBackgroundColorInput"
type="color"
value="#262626"
aria-label="Orb background color"
disabled
/>
<output for="orbBackgroundColorInput">NONE</output>
</label>
</span>
</div>
</fieldset>
<fieldset
class="control-section control-section--palette"
id="nonErrorPaletteSection"
>
<legend>Non-error palette</legend>
<div class="palette-intro-row">
<p class="palette-intro">
Override any color channel across AI and status states. Error keeps
its authored danger palette.
</p>
<span class="scope-badge" id="paletteScopeBadge">Recipe</span>
</div>
<div class="palette-grid">
<label class="color-control" for="backgroundColorInput">
<span>Background</span>
<span class="color-control__picker">
<input
id="backgroundColorInput"
type="color"
value="#070708"
data-palette-color="background"
aria-label="Non-error background color"
/>
<output for="backgroundColorInput">#070708</output>
</span>
</label>
<label class="color-control" for="offColorInput">
<span>Inactive</span>
<span class="color-control__picker">
<input
id="offColorInput"
type="color"
value="#1f2022"
data-palette-color="off"
aria-label="Non-error inactive pixel color"
/>
<output for="offColorInput">#1F2022</output>
</span>
</label>
<label class="color-control" for="inkColorInput">
<span>Primary</span>
<span class="color-control__picker">
<input
id="inkColorInput"
type="color"
value="#f2f1ee"
data-palette-color="ink"
aria-label="Non-error primary pixel color"
/>
<output for="inkColorInput">#F2F1EE</output>
</span>
</label>
<label class="color-control" for="accentColorInput">
<span>Accent</span>
<span class="color-control__picker">
<input
id="accentColorInput"
type="color"
value="#c9c8c4"
data-palette-color="accent"
aria-label="Non-error accent color"
/>
<output for="accentColorInput">#C9C8C4</output>
</span>
</label>
<div
class="color-control color-control--glow"
id="glowColorControl"
role="group"
aria-labelledby="glowColorLabel"
>
<span class="color-control__header">
<span id="glowColorLabel">Glow</span>
<label class="glow-toggle" for="glowEnabledInput">
<input
id="glowEnabledInput"
type="checkbox"
checked
aria-label="Enable non-error glow"
/>
<span id="glowEnabledLabel" aria-hidden="true">Color</span>
</label>
</span>
<label class="color-control__picker" for="glowColorInput">
<input
id="glowColorInput"
type="color"
value="#8b8d90"
data-palette-color="glow"
aria-label="Non-error glow color"
/>
<output for="glowColorInput">#8B8D90</output>
</label>
</div>
</div>
<button
class="palette-reset"
id="resetPaletteButton"
type="button"
>
Reset custom colors
</button>
</fieldset>
<fieldset class="control-section control-section--ranges">
<legend>
Motion envelope
<span class="scope-badge" id="motionScopeBadge">Recipe</span>
</legend>
<label class="range-control" for="resolutionRange">
<span>
Resolution
<output id="resolutionValue" for="resolutionRange">68×68</output>
</span>
<input
id="resolutionRange"
type="range"
min="12"
max="96"
step="2"
value="68"
/>
</label>
<label class="range-control" for="speedRange">
<span>
Velocity
<output id="speedValue" for="speedRange">1×</output>
</span>
<input
id="speedRange"
type="range"
min="0.1"
max="5"
step="0.05"
value="1"
/>
</label>
<label class="range-control" for="densityRange">
<span>
Density
<output id="densityValue" for="densityRange">1</output>
</span>
<input
id="densityRange"
type="range"
min="0.35"
max="1.6"
step="0.05"
value="1"
/>
</label>
</fieldset>
<fieldset class="control-section">
<legend>Reproducibility</legend>
<label class="seed-control" for="seedInput">
<span>
Seed
<small>Shareable, stable output</small>
</span>
<span class="seed-control__row">
<input
id="seedInput"
type="text"
value="world-class-ai"
autocomplete="off"
spellcheck="false"
aria-label="Deterministic seed"
/>
<button
class="button button--square"
id="randomizeButton"
type="button"
aria-label="Generate a new random seed"
aria-keyshortcuts="R"
>
<span aria-hidden="true"></span>
</button>
</span>
</label>
</fieldset>
</div>
<div class="control-note">
<span class="control-note__mark" aria-hidden="true"></span>
<p>
Every choice serializes into one portable recipe. Interaction changes
energy, never identity.
</p>
</div>
</aside>
</section>
<section class="usage-section" aria-labelledby="usageTitle">
<header class="usage-section__heading">
<p class="section-index">Integration / 03</p>
<h2 id="usageTitle">From a blank canvas to a product signal.</h2>
<p>
Create one long-lived glyph, drive it with product state, and carry the
same deterministic recipe wherever the interface needs presence.
</p>
</header>
<ol class="usage-steps">
<li class="usage-step">
<span class="usage-step__number" aria-hidden="true">01</span>
<div>
<h3>Install and create</h3>
<p>Download the kit, install the extracted folder, and mount one long-lived glyph.</p>
<pre tabindex="0" aria-label="Local package installation command"><code>npm install ./joan-procedural-glyph-engine-5.0.0</code></pre>
<pre tabindex="0" aria-label="Create a glyph example"><code>import { createGlyph } from "@joan/procedural-glyph-engine";
const glyph = createGlyph("#ai-glyph", {
sprite: "ai.idle",
seed: "product-session",
gridSize: 68
});</code></pre>
</div>
</li>
<li class="usage-step">
<span class="usage-step__number" aria-hidden="true">02</span>
<div>
<h3>Invoke intent</h3>
<p>Move directly to a state or play a timed sequence as product work unfolds.</p>
<pre tabindex="0" aria-label="State sequence example"><code>import { playStateSequence } from "@joan/procedural-glyph-engine/state-sequence";
await glyph.transitionTo("ai.thinking");
const playback = playStateSequence(glyph, [
{ sprite: "ai.idle", holdMs: 5_000 },
{ sprite: "status.success" }
]);
await playback.finished;</code></pre>
</div>
</li>
<li class="usage-step">
<span class="usage-step__number" aria-hidden="true">03</span>
<div>
<h3>Export or embed</h3>
<p>Keep the live canvas in your UI, or package its current identity for delivery.</p>
<pre tabindex="0" aria-label="Export example"><code>const recipe = glyph.exportConfig();
const svg = glyph.toSVG();
await glyph.downloadPNG("ai-state.png");</code></pre>
</div>
</li>
</ol>
</section>
<section class="library-section" id="states" aria-labelledby="libraryTitle">
<header class="section-heading">
<div>
<p class="section-index">State atlas / 04</p>
<h2 id="libraryTitle">A complete language for AI presence.</h2>
<p>
Twenty-five semantic sprites cover attention, ambient presence,
latency, tool work, transfer, completion, and recovery.
</p>
</div>
<div class="section-heading__aside">
<span><b>25</b> motion recipes</span>
<span>Select any state to stage it above</span>
</div>
</header>
<section
class="saved-presets"
id="savedPresetPanel"
aria-labelledby="savedPresetTitle"
>
<header class="saved-presets__header">
<div>
<p class="saved-presets__eyebrow">Local collection</p>
<h3 id="savedPresetTitle">Saved presets</h3>
<p>
Reopen your tuned states here. Presets are stored locally in this
browser and do not sync between devices.
</p>
</div>
<div class="saved-presets__actions">
<span class="saved-presets__count" id="savedPresetCount" aria-live="polite">
0 presets
</span>
<button class="workflow-button" id="savePresetGalleryButton" type="button">
Save current
</button>
</div>
</header>
<p class="saved-presets__empty" id="savedPresetEmpty">
No presets saved yet. Tune a state, then choose Save current. It will stay
available locally in this browser.
</p>
<div
class="saved-preset-gallery"
id="savedPresetGallery"
role="list"
aria-label="Saved local presets"
hidden
></div>
<div
class="saved-presets__undo"
id="deletedPresetNotice"
role="status"
hidden
>
<span id="deletedPresetMessage">Preset deleted.</span>
<button class="saved-presets__undo-button" id="undoDeletePresetButton" type="button">
Undo delete
</button>
</div>
</section>
<div
class="sprite-grid"
id="spriteGrid"
role="radiogroup"
aria-label="Semantic sprite library"
></div>
</section>
<section class="api-section" id="api" aria-labelledby="apiTitle">
<div class="api-copy">
<p class="section-index">API / 05</p>
<h2 id="apiTitle">From intent to pixels through one small runtime.</h2>
<p>
Ship the same state language across copilots, agents, system trays,
notifications, and ambient interfaces. The runtime is dependency-free and
keeps every parameter inspectable. Named sequences can be saved as data or
invoked inline with explicit timing.
</p>
<dl class="api-facts">
<div>
<dt>Runtime</dt>
<dd>Canvas 2D · ESM</dd>
</div>
<div>
<dt>Control</dt>
<dd>Sequences · signals · progress</dd>
</div>
<div>
<dt>Output</dt>
<dd>PNG · SVG · JSON recipe</dd>
</div>
</dl>
<div class="export-actions">
<button class="button button--primary" id="exportPngButton" type="button">
Prepare PNG
</button>
<button class="button button--quiet" id="exportSvgButton" type="button">
Prepare SVG
</button>
<button
class="button button--quiet"
id="exportAnimatedSvgButton"
type="button"
>
Prepare animated SVG
</button>
<button class="button button--quiet" id="exportConfigButton" type="button">
Prepare recipe
</button>
</div>
</div>
<div class="code-panel">
<header class="code-panel__header">
<div>
<span class="code-lights" aria-hidden="true"><i></i><i></i><i></i></span>
<span>glyph-sequence.js</span>
</div>
<button class="code-copy" id="copyCodeButton" type="button">
Copy code
</button>
</header>
<pre
tabindex="0"
aria-label="Engine integration example"
><code id="codeSnippet">import { createGlyph } from "@joan/procedural-glyph-engine";
import { playStateSequence } from "@joan/procedural-glyph-engine/state-sequence";
const glyph = createGlyph("#ai-glyph", {
sprite: "ai.idle",
seed: "world-class-ai",
gridSize: 68
});
const playback = playStateSequence(glyph, [
{ sprite: "ai.idle", holdMs: 5_000 },
{ sprite: "status.success", transition: "path-draw" }
]);
await playback.finished;</code></pre>
</div>
</section>
<section class="download-section" id="download" aria-labelledby="downloadTitle">
<div class="download-section__copy">
<p class="section-index">Self service / 06</p>
<h2 id="downloadTitle">Everything you need to run Orby.</h2>
<p>
Take a versioned integration kit with the native ESM runtime,
TypeScript declarations, complete integration documentation, runnable
examples, and the offline Studio.
</p>
<ul class="download-card__contents" aria-label="Complete kit contents">
<li>Native dependency-free runtime modules</li>
<li>TypeScript declarations for every export</li>
<li>Complete API and integration reference</li>
<li>Canvas, web component, and sequence examples</li>
<li>Single-file offline Studio</li>
<li>License and attribution notices (LICENSE and NOTICE)</li>
</ul>
<p class="download-license-note">
Distribution status: <strong>SOURCE AVAILABLE</strong>. Orby is licensed
under
<a href="./LICENSE">Apache License 2.0 subject to Commons Clause 1.0</a>.
You may use, copy, modify, and redistribute Orby, but may not sell Orby
or a product or service whose value derives entirely or substantially
from Orby's functionality.
</p>
</div>
<div class="download-options" role="list" aria-label="Download options">
<article class="download-card download-card--primary" role="listitem">
<div class="download-card__heading">
<p>Complete integration kit</p>
<span>Recommended</span>
</div>
<h3>Integration kit</h3>
<p>
Install from the extracted folder or import the source directly.
Checksums make the complete package independently verifiable.
</p>
<p class="download-card__meta" id="developerKitMeta">
<span>v5.0.0</span>
<span>ZIP</span>
<span>Documentation included</span>
<span>SHA-256 verified</span>
</p>
<div
class="download-card__actions"
role="group"
aria-label="Integration kit downloads"
>
<a
class="button button--primary"
href="./downloads/joan-procedural-glyph-engine-5.0.0.zip"
download="joan-procedural-glyph-engine-5.0.0.zip"
aria-describedby="developerKitMeta"
>Download v5.0.0 kit</a>
<a
class="download-inline-link"
href="./downloads/joan-procedural-glyph-engine-5.0.0.zip.sha256"
download="joan-procedural-glyph-engine-5.0.0.zip.sha256"
aria-label="Download v5.0.0 SHA-256 checksum"
>Checksum</a>
<a
class="download-inline-link"
href="./downloads/manifest.json"
download="joan-procedural-glyph-engine-5.0.0.manifest.json"
aria-label="Download v5.0.0 release manifest"
>Release manifest</a>
</div>
</article>
<article class="download-card" role="listitem">
<div class="download-card__heading">
<p>No setup</p>
<span>HTML</span>
</div>
<h3>Standalone Studio</h3>
<p>
Open one self-contained HTML file to explore, tune, and export
glyphs without installing anything.
</p>
<div
class="download-card__actions"
role="group"
aria-label="Standalone Studio download"
>
<a
class="button button--quiet"
href="./joan-engine-v5.standalone.html"
download="joan-engine-v5.standalone.html"
>Download standalone HTML</a>
</div>
</article>
<article class="download-card" role="listitem">
<div class="download-card__heading">
<p>Full reference</p>
<span>Markdown</span>
</div>
<h3>Documentation</h3>
<p>
API, web component, states, signals, sequences, exports,
accessibility, performance, and testing guidance.
</p>
<div
class="download-card__actions"
role="group"
aria-label="Documentation download"
>
<a
class="button button--quiet"
href="./README.md"
download="Orby-Documentation.md"
>Download documentation</a>
</div>
</article>
</div>
</section>
</main>
<footer class="site-footer">
<p>Orby <span>A procedural glyph engine by Joan Sterjo.</span></p>
<nav class="site-footer__links" aria-label="Footer navigation">
<a href="#playground">Return to canvas</a>
<a href="#download">Download</a>
</nav>
</footer>
<dialog class="export-dialog" id="exportDialog" aria-labelledby="exportDialogTitle">
<form class="export-dialog__surface" id="exportForm">
<header class="export-dialog__header">
<div>
<p class="section-index">Export preflight</p>
<h2 id="exportDialogTitle">Prepare the exact output.</h2>
</div>
<button
class="dialog-close"
id="closeExportButton"
type="button"
aria-label="Close export preflight"
>
×
</button>
</header>
<div class="export-dialog__body">
<section class="export-preview" aria-labelledby="exportPreviewTitle">
<div class="export-preview__frame">
<canvas
id="exportPreviewCanvas"
width="320"
height="320"
aria-label="Preview of the exported glyph composition"
></canvas>
</div>
<div>
<h3 id="exportPreviewTitle">Output preview</h3>
<p id="exportPreviewNote">
Export uses the fitted composition, independently of the Fit or 1:1
inspection view.
</p>
</div>
</section>
<div class="export-settings">
<label class="export-field" for="exportFormatSelect">
<span>Format</span>
<select id="exportFormatSelect">
<option value="png">PNG · current frame</option>
<option value="svg">SVG · current frame</option>
<option value="animated-svg">Animated SVG · 2.4 seconds</option>
<option value="config">Orby recipe · JSON</option>
</select>
</label>
<label class="export-field" for="exportSizeSelect">
<span>Output size</span>
<select id="exportSizeSelect">
<option value="64">64×64</option>
<option value="128">128×128</option>
<option value="256">256×256</option>
<option value="512" selected>512×512</option>
<option value="1024">1024×1024</option>
</select>
</label>
<label class="export-field export-field--filename" for="exportFilenameInput">
<span>Filename</span>
<input id="exportFilenameInput" type="text" autocomplete="off" />
</label>
<label class="export-check" for="exportBackgroundInput">
<input id="exportBackgroundInput" type="checkbox" checked />
<span>Include background</span>
</label>
<dl class="export-summary" aria-label="Export behavior summary">
<div>
<dt>Dimensions</dt>
<dd id="exportDimensionsValue">512×512 px</dd>
</div>
<div>
<dt>Composition</dt>
<dd id="exportCompositionValue">Fit · full frame</dd>
</div>
<div>
<dt>Motion</dt>
<dd id="exportMotionValue">Current frame</dd>
</div>
<div>
<dt>Background</dt>
<dd id="exportBackgroundValue">Included</dd>
</div>
</dl>
</div>
</div>
<footer class="export-dialog__actions">
<button class="button button--quiet" id="cancelExportButton" type="button">
Cancel
</button>
<button class="button button--primary" id="confirmExportButton" type="submit">
Export PNG
</button>
</footer>
</form>
</dialog>
<p class="visually-hidden" id="liveRegion" aria-live="polite" aria-atomic="true"></p>
<script>
(() => {
const logo = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABCgAAAQoCAYAAADIaO/8AAAQAElEQVR4Aey9C7wk51neWd3nMvcZy7asS2LZMD09MyeyBRK3rI20SrwYyyGYizFO2DiQJUCyBpwENiFOuASSYLJAbELs7EJWSYjNj0ucNRYGkhVy7B/JDyxH1nBm5swZsOVYkiVsazT3c+ne9/911+nqPl31vVXnnJ7uPo/ST1fV9z1PfW/9v6+r+5QjUU/0jwiIgAiIgAiIgAiIgAiIgAiIgAiIwLQTGPvr0wOKsZ8iFSgCIiACIiACIiACIiACIiACIjD+BFThVgnoAcVWCSovAiIgAiIgAiIgAiIgAiIgAiKw8wQ0wtQT0AOKqZ9iXaAIiIAIiIAIiIAIiIAIiIAIxAnIIQI3moAeUNzoGdD4IiACIiACIiACIiACIiACu4GArlEERCBCQA8oIoDULQIiIAIiIAIiIAIiIAIiMAkEVKMIiMCkE9ADikmfQdUvAiIgAiIgAiIgAiIgAqMgoDFEQAREYIcJ6AHFDgPW6UVABERABERABERABETAQ0AeERABEdjtBPSAYrevAF2/CIiACIiACIiACOwOArpKERABERCBMSegBxRjPkEqTwREQAREQAREQAQmg4CqFAEREAEREIGtEdADiq3xU1oEREAEREAEREAERkNAo4iACIiACIjAlBPQA4opn2BdngiIgAiIgAiIgI+AXCIgAiIgAiIgAjeWgB5Q3Fj+Gl0EREAEREAEdgsBXacIiIAIiIAIiIAIFBLQA4pCPOoUAREQAREQgUkhoDpFQAREQAREQAREYLIJ6AHFZM+fqhcBERABERgVAY0jAiIgAiIgAiIgAiKwowT0gGJH8erkIiACIiACXgLyiYAIiIAIiIAIiIAI7G4CekCxu+dfVy8CIrB7COhKRUAEREAEREAEREAERGCsCegBxVhPj4oTARGYHAKqVAREQAREQAREQAREQAREYCsE9IBiK/SUFQERGB0BjSQCIiACIiACIiACIiACIjDVBPSAYqqnVxcnAn4CcoqACIiACIiACIiACIiACIjAjSSgBxQ3kr7G3k0EdK0iIAIiIAIiIAIiIAIiIAIiIAIFBPSAogCOuiaJgGoVAREQAREQAREQAREQAREQARGYZAJ6QDHJszfK2jWWCIiACIiACIiACIiACIiACIiACOwgAT2g2EG4ZU4trwiIgAiIgAiIgAiIgAiIgAiIgAjsZgK75QHFbp5jXbsIiIAIiIAIiIAIiIAIiIAIiIAIjD2BbXpAMfbXqQJFQAREQAREQAREQAREQAREQAREQAS2TGDnTqAHFDvHVmcWAREQAREQAREQAREQAREQAREQgXIEdrFbDyh28eTr0kVABERABERABERABERABERgtxHQ9Y4vAT2gGN+5UWUiIAIiIAIiIAIiIAIiIAIiMGkEVK8IVCagBxSV0SkoAiIgAiIgAiIgAiIgAiIgAqMmoPFEYHoJ6AHF9M6trkwEREAEREAEREAEREAERKAsAflFQARuGAE9oLhh6DWwCIiACIiACIiACIiACOw+ArpiERABEcgjoAcUeWTULgIiIAIiIAIiIAIiIAKTR0AVi4AIiMDEEtADiomdOhUuAiIgAiIgAiIgAiIwegIaUQREQAREYKcI6AHFTpHVeUVABERABERABERABMoTUEIEREAERGDXEtADil079bpwERABERABERCB3UhA1ywCIiACIiAC40pADyjGdWZUlwiIgAiIgAiIwCQSUM0iIAIiIAIiIAIVCegBRUVwiomACIiACIiACNwIAhpTBERABERABERgWgnoAcW0zqyuSwREQAREQASqEFBGBERABERABERABG4QAT2guEHgNawIiIAIiMDuJKCrFgEREAEREAEREAERGE5ADyiGc1GrCIiACIjAZBJQ1SIgAiIgAiIgAiIgAhNKQA8oJnTiVLYIiIAI3BgCGlUEREAEREAEREAEREAEdoaAHlDsDFedVQREQASqEVBKBERABERABERABERABHYpAT2g2KUTr8sWgd1KQNctAiIgAiIgAiIgAiIgAiIwngT0gGI850VVicCkElDdIiACIiACIiACIiACIiACIlCJgB5QVMKmkAjcKAIaVwREQAREQAREQAREQAREQASmk4AeUEznvOqqqhJQTgREQAREQAREQAREQAREQARE4IYQ0AOKG4J99w6qKxcBERABERABERABERABERABERCBYQT0gGIYlcltU+UiIAIiIAIiIAIiIAIiIAIiIAIiMJEE9ICi1LTJLAIiIAIiIAIiIAIiIAIiIAIiIAIisBMExusBxU5coc4pAiIgAiIgAiIgAiIgAiIgAiIgAiIwXgSGVKMHFEOgqEkEREAEREAEREAEREAEREAEREAEJpnAJNauBxSTOGuqWQREQAREQAREQAREQAREQARE4EYS0Ng7QEAPKHYAqk4pAiIgAiIgAiIgAiIgAiIgAiKwFQLK7kYCekCxG2dd1ywCIiACIiACIiACIiACIrC7CejqRWAMCegBxRhOikoSAREQAREQAREQAREQARGYbAKqXgREoDwBPaAoz0wJERABERABERABERABERCBG0tAo4uACEwhAT2gmMJJ1SWJgAiIgAiIgAiIgAiIwNYIKC0CIiACoyegBxSjZ64RRUAEREAEREAEREAEdjsBXb8IiIAIiMAmAnpAsQmJGkRABERABERABERABCadgOoXAREQARGYPAJ6QDF5c6aKRUAEREAEREAEROBGE9D4IiACIiACIrDtBPSAYtuR6oQiIAIiIAIiIAIisFUCyouACIiACIjA7iOgBxS7b851xSIgAiIgAiIgAiIgAiIgAiIgAiIwdgT0gGLspkQFiYAIiIAIiMDkE9AViIAIiIAIiIAIiEBZAnpAUZaY/CIgAiIgAiJw4wmoAhEQAREQAREQARGYOgJ6QDF1U6oLEgEREAER2DoBnUEEREAEREAEREAERGDUBPSAYtTENZ4IiIAIiECSiIEIiIAIiIAIiIAIiIAIDBDQA4oBIDoUAREQgWkgoGsQAREQAREQAREQAREQgUkjoAcUkzZjqlcERGAcCKgGERABERABERABERABERCBbSagBxTbDFSnEwER2A4COocIiIAIiIAIiIAIiIAIiMBuI6AHFLttxnW9IgABSQREQAREQAREQAREQAREQATGjIAeUIzZhKic6SCgqxABERABERABERABERABERABEShHQA8oyvGSezwIqAoREAEREAEREAEREAEREAEREIEpI6AHFFM2odtzOTqLCIiACIiACIiACIiACIiACIiACIyWgB5QjJZ3ZzS9i4AIiIAIiIAIiIAIiIAIiIAIiIAI9BGYygcUfVeoAxEQAREQAREQAREQAREQAREQAREQgbEnUOUBxdhflAoUAREQAREQAREQAREQAREQAREQARHYMoGRnkAPKEaKW4OJgAiIgAiIgAiIgAiIgAiIgAiIQEpA2ywBPaDI0tC+CIiACIiACIiACIiACIiACIjA9BDQlUwUAT2gmKjpUrEiIAIiIAIiIAIiIAIiIAIiMD4EVIkIbCcBPaDYTpo6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psmzW2lNeE4sT6L/BoUWgQjsPAGNIAIiIAIiIAIiIAIiIAIiIAIiECOg/x8UMULqH38CqlAEREAEREAEREAEREAEREAERGDiCegBxcRP4c5fgEYQAREQgd1CoN1uP23X+vNOmW1Lr//H0h79ivm2+volO4FHZqv+qtVqHzb9O4+qj9JJ2lz9W49ardbpTkLvIiACIiACIiAC405ADyhu/AypAhEQAREQgTEhcNttt/2wV7fffvuvVS37ta997eV77733p7yqOg65Bx544JdMP+sVmap63ete9zNevf71r/9U1XFe/epX/8F99933f3pVdRzlREAEREAEREAERktgFzygGC1QjSYCIiACIrC9BE6dOnXwmWeeaXiVjn7hwoVjXlXJXLx48SXknnrqqZu9teEjg5aWlr7YK/xV9fDDD//p3/u933u5V4zzIz/yI/VHHnnkix5xiswkyHs9+CbhelSjCIiACIiACEwbga0/oJg2IroeERABERCBsSJw880337W+vv7fnPrptPiZmZmH6/X673vUbrcPkqvVaj9k/kc9Mv99psSy91pt/9WjtbW1HyTz9NNPH7Ax/j+PrKYPkKmqubm5P7+6uvobTr2Rce6///6X2fYhj6w+/pUYs47/y2rlX5dxXddv/dZvHRj/K1KFIiACIiACIjBiAjs8nB5Q7DBgnV4EREAEREAEREAEREAEREAEREAEPAR2u0cPKHb7CtD1i4AIiIAIiIAIiIAIiIAIiMDuIKCrHHMCekAx5hOk8kRABERABERABERABERABERgMgioShHYGgE9oNgav6HpZvPkG5rNE285evz4/UMNQxobjTuPkkFDunOb8KOFhYX5XNNAB37UaJy8e6Ar9xAvGZRrGuigJvxooKvwED+CSaEx0wlrMk1jn2ku3G00Gjc3bZ5QoXGgEz8iP9CVe9i0upo2FnXmmgY6uH4yaKCr8BA/gn+hMdOJHzHPmebCXbxkUKEx00lN+FGmObqLH8Ekau4aYE2maey7TdENc9q0eUJRc8aAH5HPNBfuNq2upo1FnYXGTCfXTwZlmqO7+BH8o+auAT9inrtN0Q1eMihq7hqoCT/qNrk2q6ure1Cr1XJ/j62vr89aZn5tbW3ONYiZ2u12jRyyQ/fLxgn1kfeGrl69uv/KlasHr1+/vtebqbomLl++/AJk49RMrpfVdwQZ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";
for (const element of document.querySelectorAll("[data-orby-logo]")) {
element.setAttribute(element.localName === "img" ? "src" : "href", logo);
element.removeAttribute("data-orby-logo");
}
})();
</script>
<script type="module">
const createModuleUrl = (source) =>
URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
const fieldsUrl = createModuleUrl("/**\n * Deterministic scalar fields for Orby.\n *\n * Coordinate convention\n * ---------------------\n * `x` and `y` are continuous field-space coordinates (normally normalized to\n * -1..1 across a centered glyph), and `time` is an arbitrary monotonically\n * increasing value, conventionally seconds. Every named field returns a finite\n * value in 0..1. Options are optional plain objects and are never mutated.\n *\n * The hot sampling path is allocation-free. `FieldKit` owns its permutation\n * table and a few scalar scratch values; pass an output array to `curl2` when\n * calling that vector helper in an animation loop.\n */\n\nexport const TAU = Math.PI * 2;\nexport const BAYER8_SIZE = 8;\n\n/** Clamp `value` to an inclusive range. Non-finite values become `min`. */\nexport function clamp(value, min = 0, max = 1) {\n if (!Number.isFinite(value)) return Number.isFinite(min) ? min : 0;\n if (value < min) return min;\n if (value > max) return max;\n return value;\n}\n\n/** Linear interpolation without implicit clamping. */\nexport function lerp(a, b, amount) {\n return a + (b - a) * amount;\n}\n\n/** Hermite interpolation from 0 to 1 between `edge0` and `edge1`. */\nexport function smoothstep(edge0, edge1, value) {\n if (!Number.isFinite(value)) return 0;\n if (edge0 === edge1) return value < edge0 ? 0 : 1;\n const t = clamp((value - edge0) / (edge1 - edge0));\n return t * t * (3 - 2 * t);\n}\n\n/** Quintic interpolation from 0 to 1 between `edge0` and `edge1`. */\nexport function smootherstep(edge0, edge1, value) {\n if (!Number.isFinite(value)) return 0;\n if (edge0 === edge1) return value < edge0 ? 0 : 1;\n const t = clamp((value - edge0) / (edge1 - edge0));\n return t * t * t * (t * (t * 6 - 15) + 10);\n}\n\n/** Positive fractional part, including for negative inputs. */\nexport function fract(value) {\n if (!Number.isFinite(value)) return 0;\n return value - Math.floor(value);\n}\n\n/** Standard 8x8 Bayer ordered-dither matrix, containing each value 0..63. */\nexport const BAYER8 = Object.freeze([\n Object.freeze([0, 48, 12, 60, 3, 51, 15, 63]),\n Object.freeze([32, 16, 44, 28, 35, 19, 47, 31]),\n Object.freeze([8, 56, 4, 52, 11, 59, 7, 55]),\n Object.freeze([40, 24, 36, 20, 43, 27, 39, 23]),\n Object.freeze([2, 50, 14, 62, 1, 49, 13, 61]),\n Object.freeze([34, 18, 46, 30, 33, 17, 45, 29]),\n Object.freeze([10, 58, 6, 54, 9, 57, 5, 53]),\n Object.freeze([42, 26, 38, 22, 41, 25, 37, 21]),\n]);\n\n/** Return the centered Bayer threshold for an integer pixel coordinate. */\nexport function bayer8(x, y) {\n const ix = Number.isFinite(x) ? Math.floor(x) & 7 : 0;\n const iy = Number.isFinite(y) ? Math.floor(y) & 7 : 0;\n return (BAYER8[iy][ix] + 0.5) / 64;\n}\n\nexport const bayerThreshold = bayer8;\n\n/**\n * Quantize a 0..1 value with ordered dithering.\n * `levels=2` produces a binary pixel switch; larger values produce terraces.\n */\nexport function orderedDither(value, x, y, levels = 2) {\n const count = clamp(Math.floor(levels), 2, 256);\n const scaled = clamp(value) * (count - 1);\n const low = Math.floor(scaled);\n const high = Math.min(count - 1, low + 1);\n const mix = scaled - low;\n return (mix > bayer8(x, y) ? high : low) / (count - 1);\n}\n\n/** Stable 32-bit hash for numeric, string, bigint, boolean, or null seeds. */\nexport function hashSeed(seed = 0) {\n let hash = 0x811c9dc5;\n\n if (typeof seed === \"number\" && Number.isFinite(seed)) {\n if (Number.isInteger(seed)) {\n hash ^= seed >>> 0;\n hash = Math.imul(hash, 0x01000193);\n } else {\n const text = String(seed);\n for (let i = 0; i < text.length; i += 1) {\n hash ^= text.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n }\n } else {\n const text = typeof seed === \"string\" ? seed : String(seed ?? 0);\n for (let i = 0; i < text.length; i += 1) {\n hash ^= text.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n }\n\n hash ^= hash >>> 16;\n hash = Math.imul(hash, 0x7feb352d);\n hash ^= hash >>> 15;\n hash = Math.imul(hash, 0x846ca68b);\n hash ^= hash >>> 16;\n return hash >>> 0;\n}\n\nconst EMPTY_OPTIONS = Object.freeze({});\nconst SQRT2 = Math.SQRT2;\nconst INV_255 = 1 / 255;\n\nfunction finite(value, fallback = 0) {\n return Number.isFinite(value) ? value : fallback;\n}\n\nfunction option(options, key, fallback, min = -Infinity, max = Infinity) {\n if (!options) return fallback;\n const value = options[key];\n if (!Number.isFinite(value)) return fallback;\n return clamp(value, min, max);\n}\n\nfunction optionInt(options, key, fallback, min, max) {\n return Math.floor(option(options, key, fallback, min, max));\n}\n\nfunction contrast(value, amount) {\n return clamp((value - 0.5) * amount + 0.5);\n}\n\nfunction fade(value) {\n return value * value * value * (value * (value * 6 - 15) + 10);\n}\n\nfunction grad2(hash, x, y) {\n switch (hash & 7) {\n case 0: return x;\n case 1: return -x;\n case 2: return y;\n case 3: return -y;\n case 4: return (x + y) * Math.SQRT1_2;\n case 5: return (-x + y) * Math.SQRT1_2;\n case 6: return (x - y) * Math.SQRT1_2;\n default: return (-x - y) * Math.SQRT1_2;\n }\n}\n\nfunction grad3(hash, x, y, z) {\n const h = hash & 15;\n const u = h < 8 ? x : y;\n const v = h < 4 ? y : h === 12 || h === 14 ? x : z;\n return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);\n}\n\nfunction ensureKit(kit) {\n return kit instanceof FieldKit ? kit : DEFAULT_FIELD_KIT;\n}\n\n/**\n * Seeded noise and named-field sampler.\n *\n * Construct once and reuse:\n * `const fields = new FieldKit(\"product-icon\");`\n * `const alpha = fields.sample(\"electric\", x, y, seconds);`\n */\nexport class FieldKit {\n constructor(seed = 0) {\n const initialSeed =\n seed && typeof seed === \"object\" && \"seed\" in seed ? seed.seed : seed;\n\n this.seed = hashSeed(initialSeed);\n this.permutation = new Uint8Array(512);\n\n // Scalar scratch state keeps cellular/curl samplers allocation-free.\n this._cellF1 = 0;\n this._cellF2 = 0;\n this._cellHash = 0;\n this._curlX = 0;\n this._curlY = 0;\n this._curlMagnitude = 0;\n\n this._buildPermutation();\n }\n\n /** Rebuild the lookup table from a new seed and return this instance. */\n reseed(seed = 0) {\n this.seed = hashSeed(seed);\n this._buildPermutation();\n return this;\n }\n\n setSeed(seed = 0) {\n return this.reseed(seed);\n }\n\n /** Create an independent sampler with the same seed. */\n clone() {\n const copy = new FieldKit(0);\n copy.seed = this.seed;\n copy.permutation.set(this.permutation);\n return copy;\n }\n\n /** Frozen list of canonical named scalar fields. */\n list() {\n return FIELD_IDS;\n }\n\n has(id) {\n return resolveFieldId(id) !== null;\n }\n\n /**\n * Return the standalone sampler function for an ID, or undefined.\n * Standalone samplers accept `(x, y, time, options, kit)`.\n */\n get(id) {\n const resolved = resolveFieldId(id);\n return resolved === null ? undefined : FIELDS[resolved];\n }\n\n /** Sample a canonical field or alias. Unknown IDs safely return 0. */\n sample(id, x, y, time = 0, options = EMPTY_OPTIONS) {\n const resolved = resolveFieldId(id);\n if (resolved === null) return 0;\n const value = FIELDS[resolved](\n finite(x),\n finite(y),\n finite(time),\n options || EMPTY_OPTIONS,\n this,\n );\n return clamp(value);\n }\n\n /**\n * Seeded lattice hash in 0..1. Useful for deterministic sprite decisions.\n * Inputs are treated as integer lattice coordinates.\n */\n hash2(x, y, salt = 0) {\n const p = this.permutation;\n const xi = finite(Math.floor(x)) & 255;\n const yi = finite(Math.floor(y)) & 255;\n const si = finite(Math.floor(salt)) & 255;\n return p[xi + p[yi + p[si]]] * INV_255;\n }\n\n /** Signed 2D gradient noise in -1..1. */\n signedNoise2(x, y) {\n return this._signedNoise2(finite(x), finite(y));\n }\n\n /** 2D gradient noise remapped to 0..1. */\n noise2(x, y) {\n return this._signedNoise2(finite(x), finite(y)) * 0.5 + 0.5;\n }\n\n /** Signed 3D gradient noise in -1..1; use z as animation time. */\n signedNoise3(x, y, z) {\n return this._signedNoise3(finite(x), finite(y), finite(z));\n }\n\n /** 3D gradient noise remapped to 0..1. */\n noise3(x, y, z) {\n return this._signedNoise3(finite(x), finite(y), finite(z)) * 0.5 + 0.5;\n }\n\n /** Seeded smooth value noise in 0..1. */\n value2(x, y) {\n x = finite(x);\n y = finite(y);\n const x0 = Math.floor(x);\n const y0 = Math.floor(y);\n const tx = fade(x - x0);\n const ty = fade(y - y0);\n const p = this.permutation;\n const ax = x0 & 255;\n const ay = y0 & 255;\n const bx = (x0 + 1) & 255;\n const by = (y0 + 1) & 255;\n const a = p[ax + p[ay]] * INV_255;\n const b = p[bx + p[ay]] * INV_255;\n const c = p[ax + p[by]] * INV_255;\n const d = p[bx + p[by]] * INV_255;\n return clamp(lerp(lerp(a, b, tx), lerp(c, d, tx), ty));\n }\n\n /** Fractal gradient noise in 0..1. */\n fbmNoise(x, y, z = 0, options = EMPTY_OPTIONS) {\n const octaves = optionInt(options, \"octaves\", 5, 1, 9);\n const lacunarity = option(options, \"lacunarity\", 2, 1.01, 4);\n const gain = option(options, \"gain\", 0.5, 0.05, 0.95);\n return this._fbmSigned(\n finite(x),\n finite(y),\n finite(z),\n octaves,\n lacunarity,\n gain,\n ) * 0.5 + 0.5;\n }\n\n /** Multi-octave ridged noise in 0..1. */\n ridgedNoise(x, y, z = 0, options = EMPTY_OPTIONS) {\n return this._ridged(\n finite(x),\n finite(y),\n finite(z),\n optionInt(options, \"octaves\", 5, 1, 9),\n option(options, \"lacunarity\", 2.05, 1.01, 4),\n option(options, \"gain\", 0.52, 0.05, 0.95),\n );\n }\n\n /**\n * Normalized curl vector of a scalar noise potential.\n * Supply `out` (array or typed array) to avoid the one fallback allocation.\n */\n curl2(x, y, z = 0, epsilon = 0.0125, out) {\n this._setCurl(\n finite(x),\n finite(y),\n finite(z),\n clamp(finite(epsilon, 0.0125), 0.0001, 0.25),\n );\n const target = out || new Float32Array(2);\n target[0] = this._curlX;\n target[1] = this._curlY;\n return target;\n }\n\n /** Nearest animated Worley feature distance, normalized to 0..1. */\n cellular2(x, y, time = 0, options = EMPTY_OPTIONS) {\n this._setCellular(\n finite(x),\n finite(y),\n finite(time),\n option(options, \"jitter\", 0.88, 0, 1),\n option(options, \"motion\", 0.1, 0, 0.45),\n );\n return clamp(Math.sqrt(this._cellF1) / SQRT2);\n }\n\n /** Difference between smoothly seed-morphed Voronoi features, in 0..1. */\n voronoi2(x, y, time = 0, options = EMPTY_OPTIONS) {\n this._setCellular(\n finite(x),\n finite(y),\n finite(time) + option(options, \"phase\", 0, -10000, 10000),\n option(options, \"jitter\", 0.88, 0, 1),\n option(options, \"motion\", 0.16, 0, 0.45),\n option(options, \"seedRate\", 0.9, 0, 8),\n );\n return clamp((Math.sqrt(this._cellF2) - Math.sqrt(this._cellF1)) / SQRT2);\n }\n\n // Named convenience methods mirror `sample` without string lookup.\n fbm(x, y, time = 0, options) {\n return fbm(x, y, time, options, this);\n }\n\n ridged(x, y, time = 0, options) {\n return ridged(x, y, time, options, this);\n }\n\n domainWarp(x, y, time = 0, options) {\n return domainWarp(x, y, time, options, this);\n }\n\n curl(x, y, time = 0, options) {\n return curl(x, y, time, options, this);\n }\n\n flow(x, y, time = 0, options) {\n return flow(x, y, time, options, this);\n }\n\n worley(x, y, time = 0, options) {\n return worley(x, y, time, options, this);\n }\n\n voronoi(x, y, time = 0, options) {\n return voronoi(x, y, time, options, this);\n }\n\n plasma(x, y, time = 0, options) {\n return plasma(x, y, time, options, this);\n }\n\n interference(x, y, time = 0, options) {\n return interference(x, y, time, options, this);\n }\n\n vortex(x, y, time = 0, options) {\n return vortex(x, y, time, options, this);\n }\n\n metaballs(x, y, time = 0, options) {\n return metaballs(x, y, time, options, this);\n }\n\n caustics(x, y, time = 0, options) {\n return caustics(x, y, time, options, this);\n }\n\n strata(x, y, time = 0, options) {\n return strata(x, y, time, options, this);\n }\n\n radar(x, y, time = 0, options) {\n return radar(x, y, time, options, this);\n }\n\n constellation(x, y, time = 0, options) {\n return constellation(x, y, time, options, this);\n }\n\n liquid(x, y, time = 0, options) {\n return liquid(x, y, time, options, this);\n }\n\n electric(x, y, time = 0, options) {\n return electric(x, y, time, options, this);\n }\n\n ripple(x, y, time = 0, options) {\n return ripple(x, y, time, options, this);\n }\n\n kaleidoscope(x, y, time = 0, options) {\n return kaleidoscope(x, y, time, options, this);\n }\n\n _buildPermutation() {\n const p = this.permutation;\n for (let i = 0; i < 256; i += 1) p[i] = i;\n\n let state = this.seed || 0x6d2b79f5;\n for (let i = 255; i > 0; i -= 1) {\n state += 0x6d2b79f5;\n let random = state;\n random = Math.imul(random ^ (random >>> 15), random | 1);\n random ^= random + Math.imul(random ^ (random >>> 7), random | 61);\n random = (random ^ (random >>> 14)) >>> 0;\n const j = Math.floor((random / 0x100000000) * (i + 1));\n const swap = p[i];\n p[i] = p[j];\n p[j] = swap;\n }\n\n for (let i = 0; i < 256; i += 1) p[i + 256] = p[i];\n }\n\n _signedNoise2(x, y) {\n const x0 = Math.floor(x);\n const y0 = Math.floor(y);\n const xf = x - x0;\n const yf = y - y0;\n const u = fade(xf);\n const v = fade(yf);\n const p = this.permutation;\n const xi = x0 & 255;\n const yi = y0 & 255;\n\n const aa = p[xi + p[yi]];\n const ba = p[xi + 1 + p[yi]];\n const ab = p[xi + p[yi + 1]];\n const bb = p[xi + 1 + p[yi + 1]];\n\n const low = lerp(grad2(aa, xf, yf), grad2(ba, xf - 1, yf), u);\n const high = lerp(\n grad2(ab, xf, yf - 1),\n grad2(bb, xf - 1, yf - 1),\n u,\n );\n return clamp(lerp(low, high, v) * 1.55, -1, 1);\n }\n\n _signedNoise3(x, y, z) {\n const x0 = Math.floor(x);\n const y0 = Math.floor(y);\n const z0 = Math.floor(z);\n const xf = x - x0;\n const yf = y - y0;\n const zf = z - z0;\n const u = fade(xf);\n const v = fade(yf);\n const w = fade(zf);\n const p = this.permutation;\n const xi = x0 & 255;\n const yi = y0 & 255;\n const zi = z0 & 255;\n\n const a = p[xi] + yi;\n const aa = p[a] + zi;\n const ab = p[a + 1] + zi;\n const b = p[xi + 1] + yi;\n const ba = p[b] + zi;\n const bb = p[b + 1] + zi;\n\n const zLow = lerp(\n lerp(\n grad3(p[aa], xf, yf, zf),\n grad3(p[ba], xf - 1, yf, zf),\n u,\n ),\n lerp(\n grad3(p[ab], xf, yf - 1, zf),\n grad3(p[bb], xf - 1, yf - 1, zf),\n u,\n ),\n v,\n );\n const zHigh = lerp(\n lerp(\n grad3(p[aa + 1], xf, yf, zf - 1),\n grad3(p[ba + 1], xf - 1, yf, zf - 1),\n u,\n ),\n lerp(\n grad3(p[ab + 1], xf, yf - 1, zf - 1),\n grad3(p[bb + 1], xf - 1, yf - 1, zf - 1),\n u,\n ),\n v,\n );\n return clamp(lerp(zLow, zHigh, w) * 0.94, -1, 1);\n }\n\n _fbmSigned(x, y, z, octaves, lacunarity, gain) {\n let sum = 0;\n let amplitude = 0.5;\n let normalization = 0;\n\n for (let octave = 0; octave < octaves; octave += 1) {\n sum += this._signedNoise3(x, y, z) * amplitude;\n normalization += amplitude;\n\n // Rotate and offset between octaves to suppress axial lattice artifacts.\n const nextX = (x * 0.8 - y * 0.6) * lacunarity + 17.17;\n y = (x * 0.6 + y * 0.8) * lacunarity - 9.23;\n x = nextX;\n z = z * lacunarity + 5.71;\n amplitude *= gain;\n }\n\n return normalization > 0 ? clamp(sum / normalization, -1, 1) : 0;\n }\n\n _ridged(x, y, z, octaves, lacunarity, gain) {\n let sum = 0;\n let amplitude = 0.5;\n let normalization = 0;\n let weight = 1;\n\n for (let octave = 0; octave < octaves; octave += 1) {\n let ridge = 1 - Math.abs(this._signedNoise3(x, y, z));\n ridge *= ridge;\n ridge *= weight;\n weight = clamp(ridge * 2.25);\n sum += ridge * amplitude;\n normalization += amplitude;\n\n const nextX = (x * 0.764 - y * 0.645) * lacunarity + 11.37;\n y = (x * 0.645 + y * 0.764) * lacunarity + 3.19;\n x = nextX;\n z = z * lacunarity - 4.31;\n amplitude *= gain;\n }\n\n return normalization > 0 ? clamp(sum / normalization) : 0;\n }\n\n _setCurl(x, y, z, epsilon) {\n const dY =\n (this._signedNoise3(x, y + epsilon, z) -\n this._signedNoise3(x, y - epsilon, z)) /\n (epsilon * 2);\n const dX =\n (this._signedNoise3(x + epsilon, y, z) -\n this._signedNoise3(x - epsilon, y, z)) /\n (epsilon * 2);\n const vx = dY;\n const vy = -dX;\n const magnitude = Math.hypot(vx, vy);\n this._curlMagnitude = Number.isFinite(magnitude) ? magnitude : 0;\n\n if (magnitude > 1e-9 && Number.isFinite(magnitude)) {\n this._curlX = vx / magnitude;\n this._curlY = vy / magnitude;\n } else {\n this._curlX = 0;\n this._curlY = 0;\n }\n }\n\n _setCellular(x, y, time, jitter, motion, seedRate) {\n const baseX = Math.floor(x);\n const baseY = Math.floor(y);\n let f1 = Infinity;\n let f2 = Infinity;\n let nearestHash = 0;\n const p = this.permutation;\n const morphing = seedRate !== undefined && motion > 0;\n const seedPhase = morphing ? time * seedRate : 0;\n const seedEpoch = Math.floor(seedPhase);\n const seedU = morphing ? fract(seedPhase) : 0;\n const seedU2 = seedU * seedU;\n const seedU3 = seedU2 * seedU;\n const seedB0 = ((1 - seedU) * (1 - seedU) * (1 - seedU)) / 6;\n const seedB1 = (4 - 6 * seedU2 + 3 * seedU3) / 6;\n const seedB2 = (1 + 3 * seedU + 3 * seedU2 - 3 * seedU3) / 6;\n const seedB3 = seedU3 / 6;\n const seedSalt0 = morphing ? p[(seedEpoch - 1) & 255] : 0;\n const seedSalt1 = morphing ? p[seedEpoch & 255] : 0;\n const seedSalt2 = morphing ? p[(seedEpoch + 1) & 255] : 0;\n const seedSalt3 = morphing ? p[(seedEpoch + 2) & 255] : 0;\n const anchorScale = morphing ? Math.max(0, 1 - motion * 2) : 1;\n\n for (let oy = -1; oy <= 1; oy += 1) {\n const cellY = baseY + oy;\n const py = cellY & 255;\n for (let ox = -1; ox <= 1; ox += 1) {\n const cellX = baseX + ox;\n const px = cellX & 255;\n const hashA = p[px + p[py]];\n const hashB = p[px + p[py + 71]];\n const hashC = p[px + p[py + 149]];\n\n let featureX = 0.5 + (hashA * INV_255 - 0.5) * jitter * anchorScale;\n let featureY = 0.5 + (hashB * INV_255 - 0.5) * jitter * anchorScale;\n if (morphing) {\n const offsetX =\n (p[hashA + seedSalt0] * seedB0 +\n p[hashA + seedSalt1] * seedB1 +\n p[hashA + seedSalt2] * seedB2 +\n p[hashA + seedSalt3] * seedB3) *\n INV_255 *\n 2 -\n 1;\n const offsetY =\n (p[hashB + seedSalt0] * seedB0 +\n p[hashB + seedSalt1] * seedB1 +\n p[hashB + seedSalt2] * seedB2 +\n p[hashB + seedSalt3] * seedB3) *\n INV_255 *\n 2 -\n 1;\n featureX += offsetX * motion;\n featureY += offsetY * motion;\n } else {\n const phase = hashC * INV_255 * TAU;\n featureX += Math.sin(time + phase) * motion;\n featureY += Math.cos(time * 0.91 + phase) * motion;\n }\n featureX = clamp(featureX, 0.015, 0.985);\n featureY = clamp(featureY, 0.015, 0.985);\n\n const dx = cellX + featureX - x;\n const dy = cellY + featureY - y;\n const distance = dx * dx + dy * dy;\n\n if (distance < f1) {\n f2 = f1;\n f1 = distance;\n nearestHash = hashC;\n } else if (distance < f2) {\n f2 = distance;\n }\n }\n }\n\n this._cellF1 = Number.isFinite(f1) ? f1 : 0;\n this._cellF2 = Number.isFinite(f2) ? f2 : this._cellF1;\n this._cellHash = nearestHash;\n }\n}\n\n/** Soft multi-octave gradient noise. */\nexport function fbm(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 2.35, 0.001, 256);\n const speed = option(options, \"speed\", 0.18, -20, 20);\n const value =\n kit._fbmSigned(\n finite(x) * frequency,\n finite(y) * frequency,\n finite(time) * speed + option(options, \"phase\", 0, -10000, 10000),\n optionInt(options, \"octaves\", 5, 1, 9),\n option(options, \"lacunarity\", 2, 1.01, 4),\n option(options, \"gain\", 0.5, 0.05, 0.95),\n ) *\n 0.5 +\n 0.5;\n return contrast(value, option(options, \"contrast\", 1.08, 0, 8));\n}\n\n/** Sharp mountain/filament ridges with octave feedback. */\nexport function ridged(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 2.7, 0.001, 256);\n const value = kit._ridged(\n finite(x) * frequency,\n finite(y) * frequency,\n finite(time) * option(options, \"speed\", 0.22, -20, 20),\n optionInt(options, \"octaves\", 5, 1, 9),\n option(options, \"lacunarity\", 2.04, 1.01, 4),\n option(options, \"gain\", 0.53, 0.05, 0.95),\n );\n return contrast(value, option(options, \"contrast\", 1.32, 0, 8));\n}\n\n/** fBm evaluated through a second pair of animated fBm coordinate fields. */\nexport function domainWarp(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 2.1, 0.001, 256);\n const speed = option(options, \"speed\", 0.2, -20, 20);\n const strength = option(options, \"warp\", 0.72, 0, 6);\n const px = finite(x) * frequency;\n const py = finite(y) * frequency;\n const pz = finite(time) * speed;\n const qx = kit._fbmSigned(px + 5.2, py + 1.3, pz, 3, 2, 0.5);\n const qy = kit._fbmSigned(px - 2.8, py + 8.1, pz + 3.4, 3, 2, 0.5);\n const value =\n kit._fbmSigned(\n px + qx * strength,\n py + qy * strength,\n pz + (qx - qy) * 0.22,\n optionInt(options, \"octaves\", 5, 1, 9),\n option(options, \"lacunarity\", 2.02, 1.01, 4),\n option(options, \"gain\", 0.5, 0.05, 0.95),\n ) *\n 0.5 +\n 0.5;\n return contrast(value, option(options, \"contrast\", 1.18, 0, 8));\n}\n\n/** Curl magnitude of animated gradient noise, useful as turbulent density. */\nexport function curl(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 2.2, 0.001, 256);\n const px = finite(x) * frequency;\n const py = finite(y) * frequency;\n const pz = finite(time) * option(options, \"speed\", 0.18, -20, 20);\n kit._setCurl(\n px,\n py,\n pz,\n option(options, \"epsilon\", 0.018, 0.0001, 0.2),\n );\n const density =\n 1 -\n Math.exp(\n -kit._curlMagnitude * option(options, \"strength\", 0.72, 0.001, 20),\n );\n const modulation =\n kit._fbmSigned(px * 0.63, py * 0.63, pz + 4.7, 3, 2, 0.5) * 0.5 + 0.5;\n return contrast(\n density * 0.74 + modulation * 0.26,\n option(options, \"contrast\", 1.2, 0, 8),\n );\n}\n\n/** Animated ribbons advected along a normalized curl-noise direction. */\nexport function flow(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 2.15, 0.001, 256);\n const speed = option(options, \"speed\", 0.24, -20, 20);\n const px = finite(x) * frequency;\n const py = finite(y) * frequency;\n const pz = finite(time) * speed;\n kit._setCurl(px * 0.72, py * 0.72, pz, 0.02);\n const warp = option(options, \"warp\", 0.65, 0, 6);\n const ax = px + kit._curlX * warp;\n const ay = py + kit._curlY * warp;\n const base = kit._fbmSigned(ax, ay, pz, 4, 2.03, 0.5);\n const directional =\n ax * kit._curlX * 0.37 + ay * kit._curlY * 0.37 + base * 1.35;\n const ribbon =\n 0.5 +\n Math.sin(\n directional * TAU -\n pz * option(options, \"travel\", 1.7, -20, 20),\n ) *\n 0.5;\n const value = ribbon * 0.72 + (base * 0.5 + 0.5) * 0.28;\n return contrast(value, option(options, \"contrast\", 1.28, 0, 8));\n}\n\n/** Animated Worley feature islands (bright centers, dark gaps). */\nexport function worley(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 3.4, 0.001, 256);\n kit._setCellular(\n finite(x) * frequency,\n finite(y) * frequency,\n finite(time) * option(options, \"speed\", 0.52, -20, 20),\n option(options, \"jitter\", 0.9, 0, 1),\n option(options, \"motion\", 0.1, 0, 0.45),\n );\n const distance = Math.sqrt(kit._cellF1) / SQRT2;\n const value =\n 1 -\n smoothstep(\n option(options, \"inner\", 0.035, 0, 1),\n option(options, \"outer\", 0.62, 0.001, 2),\n distance,\n );\n return contrast(value, option(options, \"contrast\", 1.1, 0, 8));\n}\n\n/** Voronoi borders whose feature sites ease between deterministic seed states. */\nexport function voronoi(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 3.25, 0.001, 256);\n kit._setCellular(\n finite(x) * frequency,\n finite(y) * frequency,\n finite(time) * option(options, \"speed\", 0.43, -20, 20) +\n option(options, \"phase\", 0, -10000, 10000),\n option(options, \"jitter\", 0.92, 0, 1),\n option(options, \"motion\", 0.16, 0, 0.45),\n option(options, \"seedRate\", 0.9, 0, 8),\n );\n const edgeDistance = Math.sqrt(kit._cellF2) - Math.sqrt(kit._cellF1);\n const width = option(options, \"width\", 0.085, 0.001, 1);\n const value = 1 - smoothstep(width, width * 2.75, edgeDistance);\n return contrast(value, option(options, \"contrast\", 1.32, 0, 8));\n}\n\n/** Layered sinusoidal plasma with a noise-driven phase field. */\nexport function plasma(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 1.45, 0.001, 256);\n const px = finite(x) * frequency;\n const py = finite(y) * frequency;\n const phase =\n finite(time) * option(options, \"speed\", 0.62, -20, 20) +\n option(options, \"phase\", 0, -10000, 10000);\n const noise = kit._fbmSigned(px * 0.7, py * 0.7, phase * 0.18, 3, 2, 0.5);\n const a = Math.sin(px * TAU + phase + noise * 1.6);\n const b = Math.sin(py * TAU * 1.13 - phase * 1.17 - noise * 1.35);\n const c = Math.sin((px + py) * TAU * 0.61 + phase * 0.71 + noise * 2.1);\n const d = Math.sin(Math.hypot(px - 0.5, py - 0.5) * TAU * 2.2 - phase);\n return contrast(\n 0.5 + (a + b + c + d) * 0.125,\n option(options, \"contrast\", 1.12, 0, 8),\n );\n}\n\n/** Traveling circular waves from multiple moving emitters. */\nexport function interference(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 8.5, 0.01, 256);\n const phase = finite(time) * option(options, \"speed\", 2.1, -50, 50);\n const px = finite(x);\n const py = finite(y);\n const sourceA = phase * 0.23 + kit.seed * 1e-7;\n const sourceB = -phase * 0.19 + kit.seed * 1.7e-7;\n const ax = 0.24 + Math.sin(sourceA) * 0.09;\n const ay = 0.35 + Math.cos(sourceA * 0.87) * 0.11;\n const bx = 0.76 + Math.cos(sourceB) * 0.1;\n const by = 0.64 + Math.sin(sourceB * 1.09) * 0.09;\n const da = Math.hypot(px - ax, py - ay);\n const db = Math.hypot(px - bx, py - by);\n const waveA = Math.sin(da * frequency * TAU - phase);\n const waveB = Math.sin(db * frequency * TAU * 1.037 - phase * 1.11);\n const beat = Math.sin((da - db) * frequency * TAU * 0.53 + phase * 0.31);\n return contrast(\n 0.5 + (waveA + waveB) * 0.19 + beat * 0.12,\n option(options, \"contrast\", 1.2, 0, 8),\n );\n}\n\n/** Rotating noisy spiral arms around a configurable center. */\nexport function vortex(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const centerX = option(options, \"centerX\", 0, -1000, 1000);\n const centerY = option(options, \"centerY\", 0, -1000, 1000);\n const frequency = option(options, \"frequency\", 1, 0.001, 256);\n const dx = (finite(x) - centerX) * frequency;\n const dy = (finite(y) - centerY) * frequency;\n const radius = Math.hypot(dx, dy);\n const angle = Math.atan2(dy, dx);\n const phase = finite(time) * option(options, \"speed\", 1.4, -50, 50);\n const turbulence = kit._fbmSigned(\n dx * 2.1 + 4.2,\n dy * 2.1 - 3.7,\n phase * 0.11,\n 4,\n 2,\n 0.5,\n );\n const arms = optionInt(options, \"arms\", 4, 1, 16);\n const turns = option(options, \"turns\", 3.8, -30, 30);\n const spiral =\n angle * arms + radius * turns * TAU + turbulence * 2.2 - phase;\n const value = 0.5 + Math.sin(spiral) * 0.5;\n const limit = option(options, \"radius\", 1.15, 0.01, 20);\n const envelope = 1 - smoothstep(limit * 0.72, limit, radius);\n return contrast(\n value * (0.58 + envelope * 0.42),\n option(options, \"contrast\", 1.25, 0, 8),\n );\n}\n\n/** Smooth union of deterministic, independently moving metaballs. */\nexport function metaballs(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 1, 0.001, 256);\n const px = fract(finite(x) * frequency);\n const py = fract(finite(y) * frequency);\n const phase = finite(time) * option(options, \"speed\", 0.7, -20, 20);\n const count = optionInt(options, \"count\", 6, 2, 16);\n const radius = option(options, \"radius\", 0.115, 0.005, 0.5);\n const radiusSquared = radius * radius;\n let influence = 0;\n\n for (let i = 0; i < count; i += 1) {\n const seedX = kit.hash2(i, 17, 41);\n const seedY = kit.hash2(i, 79, 113);\n const seedP = kit.hash2(i, 151, 197) * TAU;\n const cx = fract(seedX + Math.sin(phase * (0.52 + seedY) + seedP) * 0.17);\n const cy = fract(seedY + Math.cos(phase * (0.48 + seedX) + seedP) * 0.17);\n let dx = Math.abs(px - cx);\n let dy = Math.abs(py - cy);\n dx = Math.min(dx, 1 - dx);\n dy = Math.min(dy, 1 - dy);\n const distanceSquared = dx * dx + dy * dy;\n influence += radiusSquared / (distanceSquared + radiusSquared);\n }\n\n const threshold = option(options, \"threshold\", 0.72, 0.05, 8);\n return contrast(\n smoothstep(threshold * 0.55, threshold * 1.35, influence),\n option(options, \"contrast\", 1.15, 0, 8),\n );\n}\n\n/** Refractive, sharp cellular light bands resembling water caustics. */\nexport function caustics(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 4, 0.001, 256);\n const phase = finite(time) * option(options, \"speed\", 0.56, -20, 20);\n const px = finite(x) * frequency;\n const py = finite(y) * frequency;\n const warpX = kit._fbmSigned(px * 0.41, py * 0.41, phase * 0.2, 3, 2, 0.5);\n const warpY = kit._fbmSigned(\n px * 0.41 + 8.7,\n py * 0.41 - 2.4,\n phase * 0.2,\n 3,\n 2,\n 0.5,\n );\n kit._setCellular(\n px + warpX * 0.42,\n py + warpY * 0.42,\n phase,\n 0.96,\n 0.12,\n );\n const delta = Math.sqrt(kit._cellF2) - Math.sqrt(kit._cellF1);\n const width = option(options, \"width\", 0.075, 0.001, 0.5);\n const cellularLine = 1 - smoothstep(width, width * 3.1, delta);\n const noiseLine =\n 1 -\n smoothstep(\n 0.04,\n 0.3,\n Math.abs(\n kit._signedNoise3(px * 0.72 + warpX, py * 0.72 + warpY, phase * 0.24),\n ),\n );\n return contrast(\n Math.max(cellularLine, noiseLine * 0.68),\n option(options, \"contrast\", 1.45, 0, 8),\n );\n}\n\n/** Noise-warped sedimentary bands with controllable slope and sharpness. */\nexport function strata(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 1, 0.001, 256);\n const px = finite(x) * frequency;\n const py = finite(y) * frequency;\n const phase = finite(time) * option(options, \"speed\", 0.12, -20, 20);\n const warp = kit._fbmSigned(px * 1.7, py * 1.1, phase, 5, 2, 0.5);\n const grain = kit._signedNoise3(px * 6.3, py * 2.2, phase * 0.7);\n const bands = option(options, \"bands\", 8, 0.1, 128);\n const slope = option(options, \"slope\", 0.12, -20, 20);\n const position =\n (py +\n px * slope +\n warp * option(options, \"warp\", 0.16, 0, 5) -\n phase * 0.03) *\n bands;\n let value = 0.5 + Math.sin(position * TAU + grain * 0.35) * 0.5;\n value = Math.pow(value, option(options, \"sharpness\", 1.5, 0.1, 12));\n value = value * 0.86 + (grain * 0.5 + 0.5) * 0.14;\n return contrast(value, option(options, \"contrast\", 1.18, 0, 8));\n}\n\n/** Rotating radar beam, concentric rings, and deterministic target blips. */\nexport function radar(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const dx = finite(x) - option(options, \"centerX\", 0, -1000, 1000);\n const dy = finite(y) - option(options, \"centerY\", 0, -1000, 1000);\n const radius = Math.hypot(dx, dy);\n const angle = Math.atan2(dy, dx);\n const sweep =\n finite(time) * option(options, \"speed\", 1.15, -50, 50) +\n option(options, \"phase\", 0, -10000, 10000);\n let angleDelta = angle - sweep;\n angleDelta = Math.atan2(Math.sin(angleDelta), Math.cos(angleDelta));\n const beamWidth = option(options, \"beamWidth\", 0.18, 0.005, Math.PI);\n const beam = Math.exp(-Math.abs(angleDelta) / beamWidth);\n const limit = option(options, \"radius\", 1.05, 0.01, 20);\n const envelope = 1 - smoothstep(limit * 0.86, limit, radius);\n const rings = option(options, \"rings\", 5, 1, 64);\n const ringPhase = fract((radius / limit) * rings);\n const ringDistance = Math.min(ringPhase, 1 - ringPhase);\n const ringLine =\n (1 -\n smoothstep(\n option(options, \"ringWidth\", 0.025, 0.001, 0.49),\n 0.12,\n ringDistance,\n )) *\n envelope *\n 0.34;\n let blip = 0;\n const targets = optionInt(options, \"targets\", 7, 0, 20);\n\n for (let i = 0; i < targets; i += 1) {\n const tx = kit.hash2(i, 31, 97) * 1.72 - 0.86;\n const ty = kit.hash2(i, 83, 181) * 1.72 - 0.86;\n const bx = finite(x) - tx;\n const by = finite(y) - ty;\n const spot = Math.exp(\n -(bx * bx + by * by) /\n option(options, \"blipSize\", 0.00085, 0.00001, 0.1),\n );\n if (spot > blip) blip = spot;\n }\n\n return clamp(\n Math.max(beam * envelope * 0.78, ringLine, blip * (0.28 + beam * 0.72)),\n );\n}\n\n/** Twinkling cellular stars connected by faint Voronoi filaments. */\nexport function constellation(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 4.2, 0.001, 256);\n const px = finite(x) * frequency;\n const py = finite(y) * frequency;\n const phase = finite(time) * option(options, \"speed\", 1.7, -50, 50);\n kit._setCellular(px, py, 0, option(options, \"jitter\", 0.96, 0, 1), 0);\n const nearest = Math.sqrt(kit._cellF1);\n const edge = Math.sqrt(kit._cellF2) - nearest;\n const starSize = option(options, \"starSize\", 0.12, 0.005, 1);\n const star = 1 - smoothstep(starSize * 0.16, starSize, nearest);\n const twinkle =\n 0.68 +\n 0.32 *\n Math.sin(phase + kit._cellHash * INV_255 * TAU + nearest * 8.0);\n const lineWidth = option(options, \"lineWidth\", 0.035, 0.001, 0.5);\n const network =\n (1 - smoothstep(lineWidth, lineWidth * 3.8, edge)) *\n option(options, \"lineOpacity\", 0.28, 0, 1);\n return contrast(\n Math.max(star * twinkle, network),\n option(options, \"contrast\", 1.3, 0, 8),\n );\n}\n\n/** Layered, domain-warped water surface with traveling highlights. */\nexport function liquid(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 2.3, 0.001, 256);\n const phase = finite(time) * option(options, \"speed\", 0.35, -20, 20);\n const px = finite(x) * frequency;\n const py = finite(y) * frequency;\n const qx = kit._fbmSigned(px * 0.72, py * 0.72, phase, 4, 2, 0.52);\n const qy = kit._fbmSigned(\n px * 0.72 + 6.2,\n py * 0.72 - 1.7,\n phase + 2.1,\n 4,\n 2,\n 0.52,\n );\n const warp = option(options, \"warp\", 0.58, 0, 6);\n const surface = kit._fbmSigned(\n px + qx * warp,\n py + qy * warp,\n phase * 0.72,\n optionInt(options, \"octaves\", 5, 1, 9),\n 2.03,\n 0.51,\n );\n const wave =\n 0.5 +\n Math.sin((py + qx * 0.42) * TAU * 1.35 - phase * 2.4) * 0.5;\n const highlight = Math.pow(\n 1 - Math.abs(kit._signedNoise3(px * 1.8 + qy, py * 1.8, phase) || 0),\n 5,\n );\n return contrast(\n (surface * 0.5 + 0.5) * 0.52 + wave * 0.31 + highlight * 0.17,\n option(options, \"contrast\", 1.18, 0, 8),\n );\n}\n\n/** Repeating branching lightning channels with traveling spark intensity. */\nexport function electric(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const frequency = option(options, \"frequency\", 1.6, 0.001, 256);\n const px = fract(finite(x) * frequency) - 0.5;\n const py = finite(y) * frequency;\n const phase = finite(time) * option(options, \"speed\", 1.8, -50, 50);\n const path =\n kit._fbmSigned(py * 0.46, phase * 0.17, phase * 0.11, 6, 2.12, 0.54) *\n option(options, \"wander\", 0.26, 0, 0.48);\n const forkNoise = kit._fbmSigned(\n py * 0.93 + 8.4,\n phase * 0.23,\n phase * 0.19,\n 4,\n 2,\n 0.5,\n );\n const width = option(options, \"width\", 0.025, 0.001, 0.3);\n const main = Math.exp(-Math.abs(px - path) / width);\n const forkOffset = 0.1 + Math.abs(forkNoise) * 0.17;\n const forkGate = smoothstep(-0.2, 0.65, forkNoise);\n const forkA =\n Math.exp(-Math.abs(px - path - forkOffset) / (width * 1.8)) * forkGate;\n const forkB =\n Math.exp(-Math.abs(px - path + forkOffset) / (width * 1.8)) *\n (1 - forkGate);\n const pulse =\n 0.67 +\n 0.33 *\n Math.sin(\n py * TAU * option(options, \"travel\", 2.4, -30, 30) - phase * 4.1,\n );\n const sparks = kit._ridged(px * 14, py * 8, phase * 0.5, 3, 2.2, 0.46);\n return contrast(\n Math.max(main, forkA * 0.7, forkB * 0.7) * pulse * (0.72 + sparks * 0.28),\n option(options, \"contrast\", 1.65, 0, 8),\n );\n}\n\n/** Noise-distorted radial rings radiating from a configurable origin. */\nexport function ripple(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const centerX = option(options, \"centerX\", 0, -1000, 1000);\n const centerY = option(options, \"centerY\", 0, -1000, 1000);\n const dx = finite(x) - centerX;\n const dy = finite(y) - centerY;\n const phase = finite(time) * option(options, \"speed\", 1.5, -50, 50);\n const distortion = kit._fbmSigned(dx * 4, dy * 4, phase * 0.12, 4, 2, 0.5);\n const radius =\n Math.hypot(dx, dy) +\n distortion * option(options, \"warp\", 0.035, 0, 1);\n const rings = option(options, \"frequency\", 11, 0.01, 256);\n const wave = 0.5 + Math.sin(radius * rings * TAU - phase * TAU) * 0.5;\n const envelope =\n 1 -\n smoothstep(\n option(options, \"radius\", 1.15, 0.01, 20) * 0.82,\n option(options, \"radius\", 1.15, 0.01, 20),\n radius,\n );\n return contrast(\n Math.pow(wave, option(options, \"sharpness\", 2.1, 0.1, 16)) * envelope,\n option(options, \"contrast\", 1.2, 0, 8),\n );\n}\n\n/** Folded polar noise producing rotating mandala-like symmetry. */\nexport function kaleidoscope(x, y, time = 0, options = EMPTY_OPTIONS, kit) {\n kit = ensureKit(kit);\n options = options || EMPTY_OPTIONS;\n const dx = finite(x) - option(options, \"centerX\", 0, -1000, 1000);\n const dy = finite(y) - option(options, \"centerY\", 0, -1000, 1000);\n const radius = Math.hypot(dx, dy);\n const segments = optionInt(options, \"segments\", 8, 2, 32);\n const sector = TAU / segments;\n let angle =\n Math.atan2(dy, dx) +\n finite(time) * option(options, \"speed\", 0.32, -20, 20);\n angle = fract(angle / sector) * sector;\n angle = Math.abs(angle - sector * 0.5);\n const px = Math.cos(angle) * radius;\n const py = Math.sin(angle) * radius;\n const frequency = option(options, \"frequency\", 7, 0.001, 256);\n const noise = kit._fbmSigned(\n px * frequency,\n py * frequency,\n finite(time) * 0.16,\n optionInt(options, \"octaves\", 5, 1, 9),\n 2.03,\n 0.5,\n );\n const rings =\n 0.5 +\n Math.sin(\n radius * TAU * option(options, \"rings\", 5, 0, 64) + noise * 2.3,\n ) *\n 0.5;\n return contrast(\n (noise * 0.5 + 0.5) * 0.58 + rings * 0.42,\n option(options, \"contrast\", 1.24, 0, 8),\n );\n}\n\n/**\n * Canonical named samplers. Each function accepts\n * `(x, y, time = 0, options = {}, kit = DEFAULT_FIELD_KIT)` and returns 0..1.\n */\nexport const FIELDS = Object.freeze({\n fbm,\n ridged,\n \"domain-warp\": domainWarp,\n curl,\n flow,\n worley,\n voronoi,\n plasma,\n interference,\n vortex,\n metaballs,\n caustics,\n strata,\n radar,\n constellation,\n liquid,\n electric,\n ripple,\n kaleidoscope,\n});\n\nexport const FIELD_IDS = Object.freeze(Object.keys(FIELDS));\n\n/** Compatibility aliases accepted by `FieldKit.sample`, `get`, and `has`. */\nexport const FIELD_ALIASES = Object.freeze({\n noise: \"fbm\",\n turbulence: \"fbm\",\n ridge: \"ridged\",\n domainWarp: \"domain-warp\",\n domain_warp: \"domain-warp\",\n warp: \"domain-warp\",\n \"curl-flow\": \"flow\",\n curlFlow: \"flow\",\n cellular: \"worley\",\n cells: \"voronoi\",\n cell: \"voronoi\",\n water: \"liquid\",\n lightning: \"electric\",\n waves: \"ripple\",\n mandala: \"kaleidoscope\",\n});\n\nfunction resolveFieldId(id) {\n if (typeof id !== \"string\") return null;\n if (Object.prototype.hasOwnProperty.call(FIELDS, id)) return id;\n if (Object.prototype.hasOwnProperty.call(FIELD_ALIASES, id)) {\n return FIELD_ALIASES[id];\n }\n\n // Normalize only the uncommon path so canonical per-pixel sampling allocates\n // no temporary strings.\n const normalized = id.trim().toLowerCase().replace(/[\\s_]+/g, \"-\");\n if (Object.prototype.hasOwnProperty.call(FIELDS, normalized)) {\n return normalized;\n }\n if (Object.prototype.hasOwnProperty.call(FIELD_ALIASES, normalized)) {\n return FIELD_ALIASES[normalized];\n }\n return null;\n}\n\n/** Shared seed-0 kit for the standalone sampler functions. */\nexport const DEFAULT_FIELD_KIT = new FieldKit(0);\n\n/** Factory form for consumers that prefer functions over constructors. */\nexport function createFieldKit(seed = 0) {\n return new FieldKit(seed);\n}\n\nexport default FieldKit;\n");
const glyphsUrl = createModuleUrl("/**\n * Analytic glyph samplers for Orby.\n *\n * Coordinates are normalized to a square from -1 to 1. Every sampler is pure,\n * allocation-light, and returns coverage in the 0..1 range. The relatively\n * heavy strokes are intentional: the semantic silhouette survives a 16px grid\n * before a field, dither, or pixel-switch operator is applied.\n *\n * Common context values:\n * size | resolution raster width used to select an anti-aliasing feather\n * reducedMotion freeze decorative motion at a representative phase\n * timelinePhase normalized 0..1 position in the recipe's macro cycle\n * progress | value normalized progress for progress/generating/transfer\n * audioLevel | level normalized listening/speaking energy\n * direction \"up\", \"down\", \"left\", \"right\", or \"sync\"\n * customSampler (x, y, t, context) => coverage\n */\n\nconst TAU = Math.PI * 2;\nconst HALF_PI = Math.PI * 0.5;\nconst EMPTY_CONTEXT = Object.freeze({});\n\nconst clamp = (value, min, max) => Math.min(max, Math.max(min, value));\nconst saturate = (value) => clamp(value, 0, 1);\nconst finite = (value, fallback = 0) =>\n Number.isFinite(Number(value)) ? Number(value) : fallback;\nconst fract = (value) => value - Math.floor(value);\nconst positiveAngle = (value) => ((value % TAU) + TAU) % TAU;\n\nfunction smoothstep(edge0, edge1, value) {\n if (edge0 === edge1) return value < edge0 ? 0 : 1;\n const unit = saturate((value - edge0) / (edge1 - edge0));\n return unit * unit * (3 - 2 * unit);\n}\n\nfunction motionTime(t, context) {\n if (!context?.reducedMotion) return finite(t);\n return finite(context.reducedPhase, 0.23) * TAU;\n}\n\nfunction featherFor(context) {\n const explicit = finite(context?.feather, -1);\n if (explicit >= 0) return clamp(explicit, 0.006, 0.18);\n const size = clamp(\n finite(context?.resolution ?? context?.size, 24),\n 8,\n 1024,\n );\n // Keep the analytic edge below half a raster cell. The previous feather was\n // wide enough to turn small rings and adjacent strokes into soft blobs once\n // the 1-bit gate was applied.\n return clamp(0.86 / size, 0.008, 0.085);\n}\n\nfunction coverage(distance, context, featherScale = 1) {\n const feather = featherFor(context) * featherScale;\n return 1 - smoothstep(-feather, feather, distance);\n}\n\nfunction sdCircle(x, y, radius) {\n return Math.hypot(x, y) - radius;\n}\n\nfunction sdRoundBox(x, y, halfWidth, halfHeight, radius) {\n const corner = Math.min(radius, halfWidth, halfHeight);\n const qx = Math.abs(x) - halfWidth + corner;\n const qy = Math.abs(y) - halfHeight + corner;\n return (\n Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) +\n Math.min(Math.max(qx, qy), 0) -\n corner\n );\n}\n\nfunction sdSegment(x, y, ax, ay, bx, by, radius) {\n const vx = bx - ax;\n const vy = by - ay;\n const denominator = vx * vx + vy * vy || 1;\n const amount = saturate(((x - ax) * vx + (y - ay) * vy) / denominator);\n return Math.hypot(x - (ax + vx * amount), y - (ay + vy * amount)) - radius;\n}\n\nfunction sdArc(x, y, radius, halfWidth, start, span) {\n const safeSpan = clamp(span, 0, TAU);\n if (safeSpan >= TAU - 1e-5) {\n return Math.abs(Math.hypot(x, y) - radius) - halfWidth;\n }\n\n const relative = positiveAngle(Math.atan2(y, x) - start);\n if (relative <= safeSpan) {\n return Math.abs(Math.hypot(x, y) - radius) - halfWidth;\n }\n\n const end = start + safeSpan;\n const startDistance =\n Math.hypot(x - Math.cos(start) * radius, y - Math.sin(start) * radius) -\n halfWidth;\n const endDistance =\n Math.hypot(x - Math.cos(end) * radius, y - Math.sin(end) * radius) -\n halfWidth;\n return Math.min(startDistance, endDistance);\n}\n\nfunction sdPolygon(x, y, points) {\n let inside = false;\n let minimumSquared = Infinity;\n let previous = points.length - 1;\n\n for (let current = 0; current < points.length; current += 1) {\n const ax = points[previous][0];\n const ay = points[previous][1];\n const bx = points[current][0];\n const by = points[current][1];\n const vx = bx - ax;\n const vy = by - ay;\n const denominator = vx * vx + vy * vy || 1;\n const amount = saturate(((x - ax) * vx + (y - ay) * vy) / denominator);\n const dx = x - (ax + vx * amount);\n const dy = y - (ay + vy * amount);\n minimumSquared = Math.min(minimumSquared, dx * dx + dy * dy);\n\n if (\n (ay > y) !== (by > y) &&\n x < ((bx - ax) * (y - ay)) / (by - ay || Number.EPSILON) + ax\n ) {\n inside = !inside;\n }\n previous = current;\n }\n\n return Math.sqrt(minimumSquared) * (inside ? -1 : 1);\n}\n\nfunction hash(index, seed = 0) {\n return fract(Math.sin(index * 127.1 + seed * 311.7) * 43758.5453123);\n}\n\nfunction stableUnit(index, seed = 0) {\n let value = (Math.trunc(seed) ^ Math.imul(index + 1, 0x9e3779b1)) >>> 0;\n value = Math.imul(value ^ (value >>> 16), 0x21f0aaad);\n value = Math.imul(value ^ (value >>> 15), 0x735a2d97);\n return ((value ^ (value >>> 15)) >>> 0) / 4294967296;\n}\n\n/**\n * A seed-stable set of four irregular arc neighborhoods. The gaps are as\n * important as the marks: proximity and good continuation let the eye close\n * the circle without turning the boundary into a spinner or a literal rim.\n */\nexport function gestaltArcSupport(\n angle,\n context = EMPTY_CONTEXT,\n openness = 0.5,\n) {\n const seed = finite(context?.seed, 0);\n const open = clamp(finite(openness, 0.5), 0, 1);\n const phase = stableUnit(701, seed) * TAU;\n const halfWidthScale = 1.14 - open * 0.34;\n let support = 0;\n\n for (let index = 0; index < 4; index += 1) {\n const center =\n phase +\n (index * TAU) / 4 +\n (stableUnit(733 + index * 29, seed) - 0.5) * 0.54;\n const halfWidth =\n (0.32 + stableUnit(811 + index * 37, seed) * 0.17) * halfWidthScale;\n const delta = Math.abs(positiveAngle(angle - center + Math.PI) - Math.PI);\n const arc = 1 - smoothstep(halfWidth * 0.7, halfWidth, delta);\n support = Math.max(support, arc);\n }\n\n const stableGrain = 0.92 + saturate(finite(context?.random, 0.5)) * 0.08;\n return saturate(support * stableGrain);\n}\n\nconst TRIANGLE = Object.freeze([\n Object.freeze([0, -0.72]),\n Object.freeze([0.69, 0.56]),\n Object.freeze([-0.69, 0.56]),\n]);\n\nconst DIAMOND = Object.freeze([\n Object.freeze([0, -0.64]),\n Object.freeze([0.62, 0]),\n Object.freeze([0, 0.64]),\n Object.freeze([-0.62, 0]),\n]);\n\nconst HOURGLASS_TOP_SAND = Object.freeze([\n Object.freeze([-0.25, -0.34]),\n Object.freeze([0.25, -0.34]),\n Object.freeze([0, -0.055]),\n]);\n\nconst HOURGLASS_BOTTOM_SAND = Object.freeze([\n Object.freeze([0, 0.055]),\n Object.freeze([0.25, 0.34]),\n Object.freeze([-0.25, 0.34]),\n]);\n\nconst STILL_WORKING_CYCLE_SECONDS = 3.2;\n\nconst SPEAKER = Object.freeze([\n Object.freeze([-0.57, -0.2]),\n Object.freeze([-0.32, -0.2]),\n Object.freeze([0.02, -0.49]),\n Object.freeze([0.02, 0.49]),\n Object.freeze([-0.32, 0.2]),\n Object.freeze([-0.57, 0.2]),\n]);\n\nconst STAR = Object.freeze(\n Array.from({ length: 10 }, (_, index) => {\n const angle = -HALF_PI + (index * Math.PI) / 5;\n const radius = index % 2 === 0 ? 0.67 : 0.29;\n return Object.freeze([Math.cos(angle) * radius, Math.sin(angle) * radius]);\n }),\n);\n\nconst TRANSFER_ROTATION = Object.freeze({\n down: 0,\n up: Math.PI,\n right: -HALF_PI,\n left: HALF_PI,\n});\n\nfunction sampleIdle(x, y, t = 0, context = EMPTY_CONTEXT) {\n const phase = context?.reducedMotion\n ? saturate(finite(context.reducedPhase, 0.18))\n : Number.isFinite(Number(context?.timelinePhase))\n ? fract(Number(context.timelinePhase))\n : fract(finite(t) / 14.4);\n const breath = Math.sin(phase * TAU * 2 - HALF_PI);\n const radius = 0.67 + breath * 0.022;\n const radial = Math.hypot(x, y);\n const normalizedX = x / radius;\n const normalizedY = y / radius;\n const normalZ = Math.sqrt(\n Math.max(0, 1 - normalizedX * normalizedX - normalizedY * normalizedY),\n );\n\n const orbit = phase * TAU - 0.72;\n let lightX = Math.cos(orbit);\n let lightY = -0.28;\n let lightZ = Math.sin(orbit) * 0.9;\n const lightLength = Math.hypot(lightX, lightY, lightZ) || 1;\n lightX /= lightLength;\n lightY /= lightLength;\n lightZ /= lightLength;\n const illumination =\n normalizedX * lightX + normalizedY * lightY + normalZ * lightZ;\n // A moon terminator is a lighting gradient, not a silhouette edge. Blend a\n // broad penumbra with diffuse light so the phase remains crisp at the rim\n // while its interior shades through several pixel-density bands.\n const rasterFeather = featherFor(context);\n const directLight = Math.pow(Math.max(0, illumination), 1.25);\n const penumbra = smoothstep(\n -0.24 - rasterFeather,\n 0.26 + rasterFeather,\n illumination,\n );\n const lit = saturate(directLight * 0.82 + penumbra * 0.28);\n const sphereShade = 0.68 + normalZ * 0.32;\n const disk = coverage(sdCircle(x, y, radius), context);\n const surface = disk * lit * sphereShade * (1 + breath * 0.035);\n\n if (context?.orbBoundary === \"gestalt\") {\n const resolution = clamp(\n finite(context?.resolution ?? context?.size, 68),\n 8,\n 1024,\n );\n const cell = 2 / resolution;\n const boundaryBand = clamp(cell * 3.1, 0.06, 0.2);\n const arcSupport = gestaltArcSupport(\n Math.atan2(y, x),\n context,\n context?.gestaltOpenness,\n );\n const edgeBand =\n smoothstep(\n radius - boundaryBand * 1.16,\n radius - boundaryBand * 0.42,\n radial,\n ) * disk;\n const surfaceTone = saturate(\n lit * sphereShade * (1 + breath * 0.035),\n );\n const cueNeed = 1 - smoothstep(0.42, 0.74, surfaceTone);\n const impliedLimb =\n edgeBand *\n arcSupport *\n (0.34 + lit * 0.38 + sphereShade * 0.08) *\n cueNeed;\n return saturate(Math.max(surface, impliedLimb));\n }\n\n const rimWidth = 0.038 + (breath + 1) * 0.002;\n const rim = coverage(Math.abs(radial - radius) - rimWidth, context);\n return saturate(Math.max(rim * 0.86, surface));\n}\n\nfunction sampleListening(x, y, t = 0, context = EMPTY_CONTEXT) {\n const time = motionTime(t, context);\n const level = saturate(\n finite(context.audioLevel ?? context.level ?? context.energy, 0.28),\n );\n const microphone = sdRoundBox(x, y + 0.09, 0.155, 0.34, 0.15);\n const cradle = sdArc(x, y + 0.04, 0.39, 0.068, 0.05, Math.PI - 0.1);\n const stem = sdSegment(x, y, 0, 0.43, 0, 0.64, 0.062);\n const foot = sdSegment(x, y, -0.2, 0.64, 0.2, 0.64, 0.062);\n let result = coverage(\n Math.min(microphone, cradle, stem, foot),\n context,\n );\n\n const pulse = fract(time * 0.34);\n const rippleRadius = 0.47 + pulse * 0.25;\n const ripple = coverage(\n sdArc(x, y + 0.03, rippleRadius, 0.038 + level * 0.025, -0.7, 1.4),\n context,\n );\n const oppositeRipple = coverage(\n sdArc(\n x,\n y + 0.03,\n rippleRadius,\n 0.038 + level * 0.025,\n Math.PI - 0.7,\n 1.4,\n ),\n context,\n );\n result = Math.max(result, (ripple + oppositeRipple) * 0.5 * (0.35 + level));\n return saturate(result);\n}\n\nfunction sampleThinking(x, y, t = 0, context = EMPTY_CONTEXT) {\n const time = motionTime(t, context);\n const angle = time * 0.68 - HALF_PI;\n const orbitRadius = 0.51;\n let shape = sdArc(\n x,\n y,\n orbitRadius,\n 0.052,\n angle + 0.36,\n TAU - 0.98,\n );\n for (let index = 0; index < 3; index += 1) {\n const nodeAngle = angle + (index * TAU) / 3;\n const node = sdCircle(\n x - Math.cos(nodeAngle) * orbitRadius,\n y - Math.sin(nodeAngle) * orbitRadius,\n index === 0 ? 0.115 : 0.09,\n );\n shape = Math.min(shape, node);\n }\n const core = sdCircle(x, y, 0.115);\n return coverage(Math.min(shape, core), context);\n}\n\nfunction sampleThinkingDeep(x, y, t = 0, context = EMPTY_CONTEXT) {\n const time = motionTime(t, context);\n const outerAngle = time * 0.24 - HALF_PI;\n const middleAngle = -time * 0.37 + 0.3;\n const innerAngle = time * 0.52 + 1.1;\n const outer = sdArc(x, y, 0.62, 0.064, outerAngle, TAU - 1.02);\n const middle = sdArc(x, y, 0.4, 0.061, middleAngle, TAU - 1.16);\n const inner = sdArc(x, y, 0.21, 0.058, innerAngle, TAU - 1.32);\n const outerBeadAngle = outerAngle + TAU - 1.02;\n const middleBeadAngle = middleAngle + TAU - 1.16;\n const outerBead = sdCircle(\n x - Math.cos(outerBeadAngle) * 0.62,\n y - Math.sin(outerBeadAngle) * 0.62,\n 0.092,\n );\n const middleBead = sdCircle(\n x - Math.cos(middleBeadAngle) * 0.4,\n y - Math.sin(middleBeadAngle) * 0.4,\n 0.078,\n );\n const core = sdCircle(x, y, 0.07 + Math.sin(time * 0.8) * 0.012);\n return coverage(\n Math.min(outer, middle, inner, outerBead, middleBead, core),\n context,\n );\n}\n\nfunction sampleStillWorking(x, y, t = 0, context = EMPTY_CONTEXT) {\n const reducedMotion = Boolean(context?.reducedMotion);\n const elapsed = Math.max(\n 0,\n finite(context?.age ?? context?.elapsed, finite(t)),\n );\n const cyclePhase = fract(elapsed / STILL_WORKING_CYCLE_SECONDS);\n const phase = reducedMotion\n ? saturate(finite(context?.reducedPhase, 0.46))\n : cyclePhase >= 1 - 1e-9\n ? 0\n : cyclePhase;\n\n // Sand drains while the frame is upright, then the complete hourglass makes\n // a deliberate half-turn. The end pose is visually identical to the start,\n // so the short cycle closes without a snap.\n const drain = smoothstep(0.03, 0.48, phase);\n const turn = smoothstep(0.5, 0.76, phase);\n const rotation = reducedMotion ? 0 : turn * Math.PI;\n const cosine = Math.cos(rotation);\n const sine = Math.sin(rotation);\n const localX = x * cosine + y * sine;\n const localY = -x * sine + y * cosine;\n\n const topRail = sdSegment(\n localX,\n localY,\n -0.32,\n -0.43,\n 0.32,\n -0.43,\n 0.06,\n );\n const bottomRail = sdSegment(\n localX,\n localY,\n -0.32,\n 0.43,\n 0.32,\n 0.43,\n 0.06,\n );\n const upperLeft = sdSegment(\n localX,\n localY,\n -0.29,\n -0.37,\n -0.075,\n 0,\n 0.055,\n );\n const lowerLeft = sdSegment(\n localX,\n localY,\n -0.075,\n 0,\n -0.29,\n 0.37,\n 0.055,\n );\n const upperRight = sdSegment(\n localX,\n localY,\n 0.29,\n -0.37,\n 0.075,\n 0,\n 0.055,\n );\n const lowerRight = sdSegment(\n localX,\n localY,\n 0.075,\n 0,\n 0.29,\n 0.37,\n 0.055,\n );\n const frame = coverage(\n Math.min(\n topRail,\n bottomRail,\n upperLeft,\n lowerLeft,\n upperRight,\n lowerRight,\n ),\n context,\n );\n\n const topSurface = -0.34 + drain * 0.285;\n const bottomSurface = 0.34 - drain * 0.285;\n const topSand = coverage(\n Math.max(\n sdPolygon(localX, localY, HOURGLASS_TOP_SAND),\n topSurface - localY,\n ),\n context,\n 0.82,\n );\n const bottomSand = coverage(\n Math.max(\n sdPolygon(localX, localY, HOURGLASS_BOTTOM_SAND),\n bottomSurface - localY,\n ),\n context,\n 0.82,\n );\n const falling = Math.sin(drain * Math.PI);\n const stream =\n coverage(\n sdSegment(localX, localY, 0, -0.08, 0, 0.17, 0.026),\n context,\n ) * falling;\n const fallingGrain =\n coverage(\n sdCircle(localX, localY - (-0.03 + drain * 0.25), 0.036),\n context,\n ) * falling;\n\n // Two restrained flip guides rotate with the object. They clarify the turn\n // without competing with the hourglass at thumbnail scale.\n const guideStart = -0.44;\n const guideSpan = 0.78;\n const guideA = sdArc(localX, localY, 0.67, 0.036, guideStart, guideSpan);\n const guideB = sdArc(\n localX,\n localY,\n 0.67,\n 0.036,\n guideStart + Math.PI,\n guideSpan,\n );\n const guideEndA = guideStart + guideSpan;\n const guideEndB = guideEndA + Math.PI;\n const guideDots = Math.min(\n sdCircle(\n localX - Math.cos(guideEndA) * 0.67,\n localY - Math.sin(guideEndA) * 0.67,\n 0.065,\n ),\n sdCircle(\n localX - Math.cos(guideEndB) * 0.67,\n localY - Math.sin(guideEndB) * 0.67,\n 0.065,\n ),\n );\n const guides = coverage(Math.min(guideA, guideB, guideDots), context) * 0.68;\n\n return saturate(\n Math.max(frame, topSand, bottomSand, stream, fallingGrain, guides),\n );\n}\n\nfunction sampleLoading(x, y, t = 0, context = EMPTY_CONTEXT) {\n const time = motionTime(t, context);\n const segmentCount = clamp(\n Math.round(finite(context.segments, 8)),\n 6,\n 12,\n );\n const head = positiveAngle(time * finite(context.rotationSpeed, 1.35));\n let result = 0;\n\n for (let index = 0; index < segmentCount; index += 1) {\n const start = (index / segmentCount) * TAU - HALF_PI;\n const span = (TAU / segmentCount) * 0.56;\n const segment = coverage(sdArc(x, y, 0.53, 0.09, start, span), context);\n const behind = positiveAngle(head - (start + span * 0.5));\n const tail = Math.exp(-behind * 0.68);\n result = Math.max(result, segment * (0.56 + tail * 0.44));\n }\n return saturate(result);\n}\n\nfunction sampleProgress(x, y, t = 0, context = EMPTY_CONTEXT) {\n const progress = saturate(finite(context.progress ?? context.value, 0.62));\n const track = coverage(\n Math.abs(Math.hypot(x, y) - 0.54) - 0.075,\n context,\n );\n const amount = progress <= 0 ? 0 : Math.max(0.012, TAU * progress);\n const fill =\n progress <= 0\n ? 0\n : coverage(\n sdArc(x, y, 0.54, 0.105, -HALF_PI, amount),\n context,\n );\n const capAngle = -HALF_PI + amount;\n const cap =\n progress <= 0\n ? 0\n : coverage(\n sdCircle(\n x - Math.cos(capAngle) * 0.54,\n y - Math.sin(capAngle) * 0.54,\n 0.105,\n ),\n context,\n );\n // The inactive track must remain legible after a 1-bit gate at 1618 px.\n let result = Math.max(track * 0.4, fill, cap);\n if (progress >= 0.995) {\n result = Math.max(\n result,\n coverage(\n Math.min(\n sdSegment(x, y, -0.27, 0.01, -0.07, 0.22, 0.085),\n sdSegment(x, y, -0.07, 0.22, 0.33, -0.25, 0.085),\n ),\n context,\n ),\n );\n }\n return saturate(result);\n}\n\nfunction sampleGenerating(x, y, t = 0, context = EMPTY_CONTEXT) {\n const time = motionTime(t, context);\n const progress = saturate(\n finite(context.progress ?? context.value, 0.48),\n );\n let result = coverage(\n Math.max(sdPolygon(x, y, DIAMOND), -sdPolygon(x, y, DIAMOND) - 0.105),\n context,\n );\n\n const cross = Math.min(\n sdSegment(x, y, -0.25, 0, 0.25, 0, 0.058),\n sdSegment(x, y, 0, -0.25, 0, 0.25, 0.058),\n );\n result = Math.max(result, coverage(cross, context));\n\n const targetGlyph = context.targetGlyph;\n if (\n typeof targetGlyph === \"string\" &&\n resolveGlyphId(targetGlyph) !== \"generating\"\n ) {\n const targetCoverage =\n typeof context.glyphSampler === \"function\"\n ? context.glyphSampler(targetGlyph, x, y, t, context)\n : getGlyph(targetGlyph)(x, y, t, context);\n result = Math.max(result * (1 - progress * 0.7), targetCoverage * progress);\n }\n\n const seed = finite(context.seed, 0);\n for (let index = 0; index < 5; index += 1) {\n const phase = fract(time * (0.11 + index * 0.009) + hash(index, seed));\n const angle = hash(index + 19, seed) * TAU + time * 0.19;\n const radius = 0.78 - phase * 0.57;\n const px = Math.cos(angle) * radius;\n const py = Math.sin(angle) * radius;\n const particle = coverage(\n sdCircle(x - px, y - py, 0.045 + phase * 0.025),\n context,\n );\n result = Math.max(result, particle * (0.35 + progress * 0.65));\n }\n return saturate(result);\n}\n\nfunction sampleSearching(x, y, t = 0, context = EMPTY_CONTEXT) {\n const time = motionTime(t, context);\n const lensX = x + 0.1;\n const lensY = y + 0.11;\n const lens = Math.abs(Math.hypot(lensX, lensY) - 0.4) - 0.085;\n const handle = sdSegment(x, y, 0.21, 0.2, 0.59, 0.58, 0.095);\n const sweepAngle = time * 1.12 - HALF_PI;\n const sweep = sdSegment(\n lensX,\n lensY,\n 0,\n 0,\n Math.cos(sweepAngle) * 0.34,\n Math.sin(sweepAngle) * 0.34,\n 0.047,\n );\n let result = coverage(Math.min(lens, handle), context);\n result = Math.max(result, coverage(sweep, context) * 0.72);\n\n const hitCount = clamp(Math.round(finite(context.hits, 2)), 0, 8);\n for (let index = 0; index < hitCount; index += 1) {\n const angle = hash(index + 7, finite(context.seed, 0)) * TAU;\n const radius = 0.12 + hash(index + 13, finite(context.seed, 0)) * 0.18;\n const hit = coverage(\n sdCircle(\n lensX - Math.cos(angle) * radius,\n lensY - Math.sin(angle) * radius,\n 0.045,\n ),\n context,\n );\n result = Math.max(result, hit);\n }\n return saturate(result);\n}\n\nfunction sampleToolUse(x, y, t = 0, context = EMPTY_CONTEXT) {\n const time = motionTime(t, context);\n const steps = Math.max(1, Math.round(finite(context.steps, 12)));\n const angle = context.reducedMotion\n ? finite(context.reducedPhase, 0.125) * TAU\n : Math.floor(time * 1.8 * steps) / steps;\n const cosine = Math.cos(angle);\n const sine = Math.sin(angle);\n const rx = x * cosine + y * sine;\n const ry = -x * sine + y * cosine;\n const radius = Math.hypot(rx, ry);\n const sector = Math.cos(Math.atan2(ry, rx) * 8);\n const tooth = smoothstep(0.18, 0.55, sector) * 0.105;\n const outer = radius - (0.5 + tooth);\n const inner = radius - 0.275;\n const gear = Math.max(outer, -inner);\n const hub = Math.abs(radius - 0.125) - 0.043;\n const spokes = Math.min(\n sdRoundBox(rx, ry, 0.38, 0.044, 0.025),\n sdRoundBox(rx, ry, 0.044, 0.38, 0.025),\n );\n const clippedSpokes = Math.max(spokes, 0.165 - radius, radius - 0.41);\n return coverage(Math.min(gear, hub, clippedSpokes), context);\n}\n\nfunction sampleSpeaking(x, y, t = 0, context = EMPTY_CONTEXT) {\n const time = motionTime(t, context);\n const fallbackEnvelope =\n 0.34 +\n Math.max(0, Math.sin(time * 5.1)) * 0.32 +\n Math.max(0, Math.sin(time * 7.7 + 1.3)) * 0.2;\n const level = saturate(\n finite(context.audioLevel ?? context.level, fallbackEnvelope),\n );\n let result = coverage(sdPolygon(x, y, SPEAKER), context);\n const first = coverage(\n sdArc(x - 0.01, y, 0.36, 0.065, -0.72, 1.44),\n context,\n );\n const second = coverage(\n sdArc(x - 0.01, y, 0.59, 0.065, -0.68, 1.36),\n context,\n );\n result = Math.max(result, first * (0.42 + level * 0.58));\n result = Math.max(result, second * smoothstep(0.18, 0.78, level));\n return saturate(result);\n}\n\nfunction sampleAwaitingInput(x, y, t = 0, context = EMPTY_CONTEXT) {\n const phase = context?.reducedMotion\n ? saturate(finite(context.reducedPhase, 0.44))\n : Number.isFinite(Number(context?.timelinePhase))\n ? fract(Number(context.timelinePhase))\n : fract(finite(t) / 10.8);\n const elapsed = phase * 10.8;\n const firstBeat =\n 1 - smoothstep(0.18, 0.5, Math.abs(elapsed - 0.64));\n const secondBeat =\n (1 - smoothstep(0.16, 0.46, Math.abs(elapsed - 1.42))) * 0.76;\n const acknowledgement = saturate(\n finite(context?.acknowledgement ?? context?.acknowledged, 0),\n );\n const invitation = Math.max(firstBeat, secondBeat) * (1 - acknowledgement);\n const resolution = clamp(\n finite(context?.resolution ?? context?.size, 68),\n 8,\n 1024,\n );\n const smallGrid = saturate((28 - resolution) / 12);\n const radius = Math.hypot(x, y);\n const angle = Math.atan2(y, x);\n const orbit = phase * TAU;\n const ringRadius = 0.65 + invitation * 0.014 - acknowledgement * 0.008;\n const ring = coverage(\n Math.abs(radius - ringRadius) -\n (0.027 + smallGrid * 0.01 + invitation * 0.005),\n context,\n );\n // A low-cost harmonic mask makes seven irregular points wander and twinkle\n // around the focus ring without turning the state into a loading spinner.\n const fireflyRadius =\n 0.755 + x * y * 0.035;\n const fireflyBand =\n 1 - smoothstep(0.008, 0.034, Math.abs(radius - fireflyRadius));\n let fireflyLobes = Math.max(\n 0,\n Math.cos(\n angle * 7 + Math.sin(orbit) * 0.42 + x * 1.1 - y * 0.65,\n ),\n );\n fireflyLobes *= fireflyLobes;\n fireflyLobes *= fireflyLobes;\n fireflyLobes *= fireflyLobes;\n fireflyLobes *= fireflyLobes;\n const fireflyTwinkle =\n 0.82 + (x - y) * 0.06;\n const fireflies = saturate(\n fireflyBand *\n fireflyLobes *\n (fireflyTwinkle + invitation * 0.24 + acknowledgement * 0.12),\n );\n const questionStroke = 0.055 + smallGrid * 0.008;\n const question = Math.min(\n sdArc(x + 0.04, y + 0.18, 0.24, questionStroke, -2.72, 3.58),\n sdSegment(x, y, 0.117, 0.002, 0.005, 0.145, questionStroke - 0.001),\n sdSegment(x, y, 0.005, 0.145, 0.005, 0.235, questionStroke - 0.001),\n sdCircle(\n x - 0.005,\n y - 0.405,\n 0.066 + smallGrid * 0.014 + invitation * 0.01,\n ),\n );\n const prompt = coverage(question, context);\n return saturate(\n Math.max(\n prompt,\n ring *\n (0.52 +\n smallGrid * 0.26 +\n invitation * 0.28 +\n acknowledgement * 0.14),\n fireflies,\n ),\n );\n}\n\nfunction sampleSuccess(x, y, t = 0, context = EMPTY_CONTEXT) {\n const age = Math.max(0, finite(context.age ?? context.elapsed, t));\n const tail = sdSegment(x, y, -0.48, 0.02, -0.13, 0.34, 0.105);\n const head = sdSegment(x, y, -0.13, 0.34, 0.49, -0.38, 0.105);\n let result = coverage(Math.min(tail, head), context);\n if (!context.reducedMotion && age < 0.72) {\n const haloRadius = 0.5 + smoothstep(0, 0.72, age) * 0.28;\n const halo = coverage(\n Math.abs(Math.hypot(x, y) - haloRadius) - 0.04,\n context,\n );\n result = Math.max(result, halo * (1 - smoothstep(0.2, 0.72, age)));\n }\n return saturate(result);\n}\n\nfunction sampleWarning(x, y, t = 0, context = EMPTY_CONTEXT) {\n const triangleDistance = sdPolygon(x, y, TRIANGLE);\n const outline = Math.abs(triangleDistance) - 0.075;\n const stem = sdRoundBox(x, y + 0.08, 0.077, 0.245, 0.055);\n const dot = sdCircle(x, y - 0.36, 0.087);\n return coverage(Math.min(outline, stem, dot), context);\n}\n\nfunction sampleError(x, y, t = 0, context = EMPTY_CONTEXT) {\n const age = Math.max(0, finite(context.age ?? context.elapsed, t));\n const shake =\n context.reducedMotion || age > 0.44\n ? 0\n : Math.sin(age * 54) * 0.065 * (1 - age / 0.44);\n const first = sdSegment(\n x - shake,\n y,\n -0.43,\n -0.43,\n 0.43,\n 0.43,\n 0.105,\n );\n const second = sdSegment(\n x - shake,\n y,\n 0.43,\n -0.43,\n -0.43,\n 0.43,\n 0.105,\n );\n return coverage(Math.min(first, second), context);\n}\n\nfunction samplePaused(x, y, t = 0, context = EMPTY_CONTEXT) {\n const left = sdRoundBox(x + 0.22, y, 0.105, 0.5, 0.055);\n const right = sdRoundBox(x - 0.22, y, 0.105, 0.5, 0.055);\n return coverage(Math.min(left, right), context);\n}\n\nfunction sampleCancelled(x, y, t = 0, context = EMPTY_CONTEXT) {\n const time = motionTime(t, context);\n const settle = context.reducedMotion\n ? 1\n : smoothstep(0, 0.36, Math.max(0, finite(context.age, time)));\n const extent = 0.39 + (1 - settle) * 0.12;\n const stop = sdRoundBox(x, y, extent, extent, 0.075);\n const inset = sdRoundBox(x, y, extent - 0.13, extent - 0.13, 0.04);\n const ring = Math.max(stop, -inset);\n return coverage(Math.min(ring, sdCircle(x, y, 0.095)), context);\n}\n\nfunction sampleOffline(x, y, t = 0, context = EMPTY_CONTEXT) {\n const time = motionTime(t, context);\n const upper = sdArc(x, y, 0.54, 0.075, -2.92, 2.28);\n const lower = sdArc(x, y, 0.54, 0.075, 0.22, 2.28);\n const slash = sdSegment(x, y, -0.49, -0.49, 0.49, 0.49, 0.09);\n let result = coverage(Math.min(upper, lower, slash), context);\n const retry = saturate(\n finite(context.retry ?? context.signalStrength, 0.25),\n );\n const packetPhase = fract(time * (0.19 + retry * 0.16));\n const packetAngle = -2.92 + packetPhase * 2.28;\n const packet = coverage(\n sdCircle(\n x - Math.cos(packetAngle) * 0.54,\n y - Math.sin(packetAngle) * 0.54,\n 0.07,\n ),\n context,\n );\n result = Math.max(result, packet * (0.28 + retry * 0.72));\n return saturate(result);\n}\n\nfunction sampleTransfer(x, y, t = 0, context = EMPTY_CONTEXT) {\n const direction = String(context.direction ?? \"down\").toLowerCase();\n const progress = saturate(\n finite(context.progress ?? context.value, 0.54),\n );\n if (direction === \"sync\") {\n const time = motionTime(t, context);\n const angle = time * 0.85;\n const first = sdArc(x, y, 0.5, 0.08, angle, 2.35);\n const second = sdArc(x, y, 0.5, 0.08, angle + Math.PI, 2.35);\n const firstTipX = Math.cos(angle + 2.35) * 0.5;\n const firstTipY = Math.sin(angle + 2.35) * 0.5;\n const secondTipX = Math.cos(angle + Math.PI + 2.35) * 0.5;\n const secondTipY = Math.sin(angle + Math.PI + 2.35) * 0.5;\n const arrowA = sdSegment(\n x,\n y,\n firstTipX,\n firstTipY,\n firstTipX - 0.17,\n firstTipY - 0.03,\n 0.07,\n );\n const arrowB = sdSegment(\n x,\n y,\n secondTipX,\n secondTipY,\n secondTipX + 0.17,\n secondTipY + 0.03,\n 0.07,\n );\n return coverage(Math.min(first, second, arrowA, arrowB), context);\n }\n\n const rotation = TRANSFER_ROTATION[direction] ?? TRANSFER_ROTATION.down;\n const cosine = Math.cos(rotation);\n const sine = Math.sin(rotation);\n const rx = x * cosine - y * sine;\n const ry = x * sine + y * cosine;\n const shaft = sdRoundBox(rx, ry + 0.13, 0.1, 0.43, 0.05);\n const leftHead = sdSegment(rx, ry, 0, 0.48, -0.32, 0.14, 0.09);\n const rightHead = sdSegment(rx, ry, 0, 0.48, 0.32, 0.14, 0.09);\n const fillLine = -0.58 + progress * 1.16;\n const fillMask = ry - fillLine;\n const arrow = Math.min(shaft, leftHead, rightHead);\n const trackCoverage = coverage(arrow, context) * 0.58;\n const fillCoverage = coverage(Math.max(arrow, fillMask), context);\n return saturate(Math.max(trackCoverage, fillCoverage));\n}\n\nfunction sampleHandoff(x, y, t = 0, context = EMPTY_CONTEXT) {\n const time = motionTime(t, context);\n const accepted = Boolean(context.accepted ?? context.handoffAccepted);\n const leftNode = Math.abs(Math.hypot(x + 0.52, y) - 0.16) - 0.057;\n const rightNode = Math.abs(Math.hypot(x - 0.52, y) - 0.16) - 0.057;\n const path = sdSegment(x, y, -0.3, 0, 0.29, 0, 0.057);\n const arrowA = sdSegment(x, y, 0.29, 0, 0.1, -0.15, 0.06);\n const arrowB = sdSegment(x, y, 0.29, 0, 0.1, 0.15, 0.06);\n let result = coverage(\n Math.min(leftNode, rightNode, path, arrowA, arrowB),\n context,\n );\n\n const phase = accepted ? 1 : fract(time * 0.34);\n const packetX = -0.29 + phase * 0.58;\n const packetY = 0;\n const packet = coverage(\n sdCircle(x - packetX, y - packetY, accepted ? 0.09 : 0.06),\n context,\n );\n result = Math.max(result, packet);\n return saturate(result);\n}\n\nfunction sampleCelebration(x, y, t = 0, context = EMPTY_CONTEXT) {\n const age = Math.max(0, finite(context.age ?? context.elapsed, t));\n let result = coverage(sdPolygon(x, y, STAR), context);\n if (context.reducedMotion) return result;\n\n const burst = 1 - smoothstep(0.42, 0.84, age);\n const seed = finite(context.seed, 0);\n for (let index = 0; index < 7; index += 1) {\n const angle = hash(index + 31, seed) * TAU;\n const speed = 0.47 + hash(index + 47, seed) * 0.36;\n const radius = Math.min(age, 0.84) * speed + 0.45;\n const particleX = Math.cos(angle) * radius;\n const particleY = Math.sin(angle) * radius;\n const particle =\n index % 2 === 0\n ? sdCircle(x - particleX, y - particleY, 0.04)\n : sdRoundBox(x - particleX, y - particleY, 0.038, 0.075, 0.015);\n result = Math.max(result, coverage(particle, context) * burst);\n }\n return saturate(result);\n}\n\nfunction sampleCustom(x, y, t = 0, context = EMPTY_CONTEXT) {\n const customSampler =\n context?.customSampler ?? context?.glyphSampler ?? context?.sampler;\n if (typeof customSampler === \"function\" && customSampler !== sampleCustom) {\n const sampled = Number(customSampler(x, y, t, context));\n if (Number.isFinite(sampled)) return saturate(sampled);\n }\n\n const diamond = Math.abs(sdPolygon(x, y, DIAMOND)) - 0.085;\n const core = sdCircle(x, y, 0.12);\n return coverage(Math.min(diamond, core), context);\n}\n\n/**\n * Canonical, un-namespaced glyph IDs. Sprite IDs are accepted by sampleGlyph\n * through GLYPH_ALIASES so recipes can stay semantically namespaced.\n */\nexport const GLYPHS = Object.freeze({\n idle: sampleIdle,\n listening: sampleListening,\n thinking: sampleThinking,\n \"thinking-deep\": sampleThinkingDeep,\n \"still-working\": sampleStillWorking,\n loading: sampleLoading,\n progress: sampleProgress,\n generating: sampleGenerating,\n searching: sampleSearching,\n \"tool-use\": sampleToolUse,\n speaking: sampleSpeaking,\n \"awaiting-input\": sampleAwaitingInput,\n success: sampleSuccess,\n warning: sampleWarning,\n error: sampleError,\n paused: samplePaused,\n cancelled: sampleCancelled,\n offline: sampleOffline,\n transfer: sampleTransfer,\n handoff: sampleHandoff,\n celebration: sampleCelebration,\n custom: sampleCustom,\n});\n\nexport const GLYPH_IDS = Object.freeze(Object.keys(GLYPHS));\n\nexport const GLYPH_ALIASES = Object.freeze({\n ready: \"idle\",\n listen: \"listening\",\n reasoning: \"thinking\",\n \"deep-thinking\": \"thinking-deep\",\n working: \"still-working\",\n spinner: \"loading\",\n determinate: \"progress\",\n generate: \"generating\",\n search: \"searching\",\n tool: \"tool-use\",\n voice: \"speaking\",\n prompt: \"awaiting-input\",\n check: \"success\",\n alert: \"warning\",\n failure: \"error\",\n pause: \"paused\",\n cancel: \"cancelled\",\n disconnected: \"offline\",\n sync: \"transfer\",\n star: \"celebration\",\n \"ai.idle\": \"idle\",\n \"ai.listening\": \"listening\",\n \"ai.thinking\": \"thinking\",\n \"ai.thinking-deep\": \"thinking-deep\",\n \"ai.still-working\": \"still-working\",\n \"ai.loading\": \"loading\",\n \"ai.progress\": \"progress\",\n \"ai.generating\": \"generating\",\n \"ai.searching\": \"searching\",\n \"ai.tool-use\": \"tool-use\",\n \"ai.speaking\": \"speaking\",\n \"ai.awaiting-input\": \"awaiting-input\",\n \"status.success\": \"success\",\n \"status.warning\": \"warning\",\n \"status.error\": \"error\",\n \"status.paused\": \"paused\",\n \"status.cancelled\": \"cancelled\",\n \"status.offline\": \"offline\",\n \"transfer.active\": \"transfer\",\n \"workflow.handoff\": \"handoff\",\n \"status.celebration\": \"celebration\",\n});\n\nexport function resolveGlyphId(id) {\n const normalized = String(id ?? \"custom\").trim().toLowerCase();\n if (Object.prototype.hasOwnProperty.call(GLYPHS, normalized)) {\n return normalized;\n }\n return GLYPH_ALIASES[normalized] ?? \"custom\";\n}\n\nexport function getGlyph(id) {\n return GLYPHS[resolveGlyphId(id)];\n}\n\nexport function listGlyphs() {\n return GLYPH_IDS;\n}\n\nexport function sampleGlyph(\n id,\n x,\n y,\n t = 0,\n context = EMPTY_CONTEXT,\n) {\n const safeContext =\n context && typeof context === \"object\" ? context : EMPTY_CONTEXT;\n const sampler = getGlyph(id);\n const sampled = sampler(\n finite(x),\n finite(y),\n finite(t),\n safeContext,\n );\n return saturate(Number.isFinite(sampled) ? sampled : 0);\n}\n\nexport default GLYPHS;\n");
const spritesUrl = createModuleUrl("/**\n * Immutable semantic recipes for Orby.\n *\n * A sprite is intentionally data, not a baked animation. Renderers combine its\n * glyph silhouette, field stack, switching operator, transition contract, and\n * interaction mappings. This keeps the catalogue portable across Canvas 2D,\n * WebGL, SVG exporters, sprite-sheet bakers, and reduced-motion renderers.\n */\n\nconst deepFreeze = (value, seen = new WeakSet()) => {\n if (\n value === null ||\n (typeof value !== \"object\" && typeof value !== \"function\") ||\n seen.has(value)\n ) {\n return value;\n }\n seen.add(value);\n for (const child of Object.values(value)) deepFreeze(child, seen);\n return Object.freeze(value);\n};\n\nexport const FIELD_IDS = Object.freeze([\n \"fbm\",\n \"ridged\",\n \"domain-warp\",\n \"curl\",\n \"flow\",\n \"worley\",\n \"voronoi\",\n \"plasma\",\n \"interference\",\n \"vortex\",\n \"metaballs\",\n \"caustics\",\n \"strata\",\n \"radar\",\n \"constellation\",\n \"liquid\",\n \"electric\",\n \"ripple\",\n \"kaleidoscope\",\n]);\n\nconst FIELD_ID_SET = new Set(FIELD_IDS);\n\nconst PALETTE_SOURCE = {\n neutral: {\n name: \"neutral\",\n background: \"#07090c\",\n shadow: \"#1b222b\",\n ink: \"#eef3f8\",\n accent: \"#93cfff\",\n },\n info: {\n name: \"info\",\n background: \"#071019\",\n shadow: \"#15344a\",\n ink: \"#edf8ff\",\n accent: \"#4fc3ff\",\n },\n success: {\n name: \"success\",\n background: \"#07110e\",\n shadow: \"#16392d\",\n ink: \"#f0fff9\",\n accent: \"#54d99b\",\n },\n warning: {\n name: \"warning\",\n background: \"#151006\",\n shadow: \"#493514\",\n ink: \"#fff9e9\",\n accent: \"#f5b94c\",\n },\n danger: {\n name: \"danger\",\n background: \"#15090b\",\n shadow: \"#4c1c25\",\n ink: \"#fff2f4\",\n accent: \"#ff647c\",\n },\n celebration: {\n name: \"celebration\",\n background: \"#100a19\",\n shadow: \"#352052\",\n ink: \"#fff7ff\",\n accent: \"#cf86ff\",\n },\n};\n\nexport const PALETTES = deepFreeze(PALETTE_SOURCE);\n\nfunction layer(field, weight, scale, blend = \"add\", options = {}) {\n if (!FIELD_ID_SET.has(field)) {\n throw new TypeError(`Unknown field \"${field}\" in built-in sprite recipe.`);\n }\n return {\n field,\n weight,\n scale,\n blend,\n ...(Number.isFinite(options.segments)\n ? { segments: Math.round(options.segments) }\n : {}),\n phase: options.phase ?? 0,\n speed: options.speed ?? 1,\n contrast: options.contrast ?? 1,\n warp: options.warp ?? 0,\n };\n}\n\nconst defaultPointer = {\n enabled: true,\n minSize: 20,\n effect: \"steer-light\",\n strength: 0.16,\n reducedMotion: false,\n};\n\nconst defaultPress = {\n enabled: true,\n effect: \"spring-compress\",\n strength: 0.07,\n durationMs: 180,\n};\n\nfunction defineSprite({\n id,\n category,\n meaning,\n glyph,\n field,\n fieldMix,\n composition = {},\n palette = \"neutral\",\n threshold = 0.5,\n density = 0.72,\n speed = 1,\n pixelShape = \"rounded-square\",\n pixelGap = 0.12,\n pixelScale = 0.94,\n silhouetteFloor = 0.38,\n pixelSwitch = {},\n timeline = {},\n transition = {},\n interactions = {},\n reducedMotion = {},\n labels,\n terminal = false,\n urgency = \"normal\",\n}) {\n if (!FIELD_ID_SET.has(field)) {\n throw new TypeError(`Unknown primary field \"${field}\" for \"${id}\".`);\n }\n if (!labels?.default) {\n throw new TypeError(`Sprite \"${id}\" must define labels.default.`);\n }\n\n const mix = fieldMix?.length\n ? fieldMix\n : [layer(field, 1, 1, \"replace\")];\n const totalWeight = mix.reduce((sum, item) => sum + item.weight, 0);\n const compositionConfig =\n typeof composition === \"string\"\n ? { mode: composition }\n : composition && typeof composition === \"object\"\n ? composition\n : {};\n\n const recipe = {\n schemaVersion: 1,\n id,\n semantic: {\n category,\n meaning,\n terminal,\n urgency,\n announce: labels.live ?? (urgency === \"high\" ? \"assertive\" : \"polite\"),\n decorativeCanvas: true,\n monochromeSafe: true,\n minimumReadableSize: 16,\n },\n glyph,\n field,\n composition: {\n mode: \"glyph\",\n ...compositionConfig,\n },\n fieldMix: mix.map((item) => ({\n ...item,\n weight: totalWeight > 0 ? item.weight / totalWeight : 0,\n })),\n palette: PALETTES[palette] ?? PALETTES.neutral,\n threshold,\n density,\n speed,\n pixel: {\n shape: pixelShape,\n gap: pixelGap,\n scale: pixelScale,\n quantize: \"1bit\",\n silhouetteFloor,\n },\n pixelSwitch: {\n mode: pixelSwitch.mode ?? \"threshold-hysteresis\",\n dither: pixelSwitch.dither ?? \"bayer8\",\n hysteresis: pixelSwitch.hysteresis ?? 0.055,\n temporalRate: pixelSwitch.temporalRate ?? speed,\n neighborhood: pixelSwitch.neighborhood ?? 0,\n direction: pixelSwitch.direction ?? \"field\",\n monotonic: pixelSwitch.monotonic ?? false,\n maxContrastFlashesPerSecond:\n pixelSwitch.maxContrastFlashesPerSecond ?? 3,\n },\n timeline: {\n mode: timeline.mode ?? \"loop\",\n durationMs: timeline.durationMs ?? Math.round(1000 / Math.max(speed, 0.01)),\n macroCycleMs: timeline.macroCycleMs ?? null,\n holdMs: timeline.holdMs ?? 0,\n replay: timeline.replay ?? \"continuous\",\n next: timeline.next ?? null,\n },\n transition: {\n enter: transition.enter ?? \"field-morph\",\n exit: transition.exit ?? \"field-morph\",\n durationMs: transition.durationMs ?? 360,\n exitDurationMs:\n transition.exitDurationMs ?? transition.durationMs ?? 360,\n preservePhase: transition.preservePhase ?? true,\n preserveBuffer: transition.preserveBuffer ?? false,\n interruptible: transition.interruptible ?? true,\n },\n interactions: {\n pointer: {\n ...defaultPointer,\n ...(interactions.pointer ?? {}),\n },\n press: {\n ...defaultPress,\n ...(interactions.press ?? {}),\n },\n signals: interactions.signals ?? [],\n },\n reducedMotion: {\n mode: reducedMotion.mode ?? \"representative-phase\",\n phase: reducedMotion.phase ?? 0.25,\n speed: 0,\n transition: reducedMotion.transition ?? \"crossfade\",\n transitionMs: reducedMotion.transitionMs ?? 120,\n pointer: false,\n spatialMotion: false,\n representation:\n reducedMotion.representation ?? `static ${labels.default.toLowerCase()} glyph`,\n dataUpdates: reducedMotion.dataUpdates ?? \"discrete\",\n },\n labels: {\n default: labels.default,\n aria: labels.aria ?? labels.default,\n live: labels.live ?? (urgency === \"high\" ? \"assertive\" : \"polite\"),\n value: labels.value ?? null,\n variants: labels.variants ?? null,\n },\n };\n\n return deepFreeze(recipe);\n}\n\nconst RECIPES = [\n defineSprite({\n id: \"ai.idle\",\n category: \"presence\",\n meaning: \"ready and available\",\n glyph: \"idle\",\n field: \"fbm\",\n composition: {\n gestaltBoundary: true,\n gestaltOpenness: 0.58,\n },\n fieldMix: [\n layer(\"fbm\", 0.72, 1.15, \"replace\", { speed: 0.2, contrast: 0.9 }),\n layer(\"caustics\", 0.28, 1.7, \"soft-light\", {\n speed: -0.09,\n phase: 0.19,\n }),\n ],\n palette: \"neutral\",\n threshold: 0.51,\n density: 0.68,\n speed: 0.14,\n pixelShape: \"rounded-square\",\n pixelSwitch: {\n mode: \"ordered-dither\",\n dither: \"bayer8\",\n hysteresis: 0.045,\n temporalRate: 0.1,\n direction: \"field\",\n },\n timeline: {\n durationMs: 7200,\n macroCycleMs: 14400,\n replay: \"continuous\",\n },\n transition: {\n enter: \"field-morph\",\n exit: \"field-morph\",\n durationMs: 520,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"steer-highlight\", strength: 0.14 },\n press: { effect: \"spring-compress\", strength: 0.055 },\n },\n reducedMotion: {\n phase: 0.18,\n representation: \"static side-lit lunar sphere with a persistent rim\",\n },\n labels: { default: \"Ready\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.ambient-idle\",\n category: \"ambient\",\n meaning: \"calm field-only presence\",\n glyph: \"idle\",\n field: \"fbm\",\n composition: {\n mode: \"field-orb\",\n gestaltBoundary: true,\n gestaltOpenness: 0.46,\n lighting: \"crescent\",\n radius: 0.88,\n fieldContrast: 1.26,\n fieldDetail: 0.86,\n lightOrbit: 0.1,\n breathe: 0.045,\n rim: 0.055,\n },\n fieldMix: [\n layer(\"fbm\", 0.62, 1.14, \"replace\", {\n speed: 0.18,\n contrast: 1.24,\n }),\n layer(\"caustics\", 0.38, 2.05, \"soft-light\", {\n speed: -0.11,\n contrast: 1.08,\n phase: 0.31,\n }),\n ],\n palette: \"neutral\",\n threshold: 0.49,\n density: 0.76,\n speed: 0.22,\n pixelShape: \"rounded-square\",\n pixelScale: 0.9,\n pixelSwitch: {\n mode: \"ordered-dither\",\n dither: \"bayer8\",\n hysteresis: 0.045,\n temporalRate: 0.18,\n direction: \"surface-drift\",\n },\n timeline: {\n durationMs: 7200,\n macroCycleMs: 14400,\n replay: \"continuous\",\n },\n transition: {\n enter: \"field-morph\",\n exit: \"field-morph\",\n durationMs: 520,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"steer-light\", strength: 0.18 },\n press: { effect: \"surface-ripple\", strength: 0.09 },\n },\n reducedMotion: {\n phase: 0.18,\n representation: \"static side-lit dithered field orb\",\n },\n labels: { default: \"Ambient ready\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.ambient-thinking\",\n category: \"ambient\",\n meaning: \"reasoning expressed only through an animated field\",\n glyph: \"thinking\",\n field: \"domain-warp\",\n composition: {\n mode: \"field-orb\",\n gestaltBoundary: true,\n gestaltOpenness: 0.62,\n lighting: \"sphere\",\n radius: 0.84,\n fieldContrast: 1.48,\n fieldDetail: 0.96,\n lightOrbit: 0.2,\n breathe: 0.09,\n rim: 0.045,\n },\n fieldMix: [\n layer(\"domain-warp\", 0.46, 1.08, \"replace\", {\n speed: 0.52,\n contrast: 1.34,\n warp: 0.7,\n }),\n layer(\"curl\", 0.32, 1.52, \"add\", {\n speed: -0.38,\n contrast: 1.22,\n }),\n layer(\"ridged\", 0.22, 2.18, \"soft-light\", {\n speed: 0.24,\n contrast: 1.36,\n }),\n ],\n palette: \"info\",\n threshold: 0.5,\n density: 0.8,\n speed: 0.72,\n pixelShape: \"rounded-square\",\n pixelScale: 0.89,\n pixelSwitch: {\n mode: \"temporal-blue-noise\",\n dither: \"blue-noise64\",\n hysteresis: 0.048,\n temporalRate: 0.68,\n direction: \"counter-flow\",\n },\n timeline: {\n durationMs: 4600,\n macroCycleMs: 9200,\n replay: \"continuous\",\n },\n transition: {\n enter: \"radial-cascade\",\n exit: \"field-morph\",\n durationMs: 460,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"bend-flow-and-light\", strength: 0.16 },\n press: { effect: \"damped-vortex\", strength: 0.18, durationMs: 460 },\n signals: [\n { name: \"reasoning.depth\", mapsTo: \"shellEnergy\", range: [0, 1] },\n ],\n },\n reducedMotion: {\n phase: 0.34,\n representation: \"static front-lit dithered reasoning field\",\n },\n labels: { default: \"Ambient thinking\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.ambient-thinking-symmetric\",\n category: \"ambient\",\n meaning: \"reasoning expressed through a clean, symmetrical animated field\",\n glyph: \"thinking\",\n field: \"kaleidoscope\",\n composition: {\n mode: \"field-orb\",\n lighting: \"flat\",\n radius: 0.87,\n fieldContrast: 1.32,\n fieldDetail: 0.84,\n lightOrbit: 0,\n breathe: 0.045,\n rim: 0.04,\n },\n fieldMix: [\n layer(\"kaleidoscope\", 0.74, 0.82, \"replace\", {\n segments: 8,\n speed: 0.2,\n contrast: 1.25,\n }),\n layer(\"kaleidoscope\", 0.26, 1.42, \"soft-light\", {\n segments: 8,\n speed: -0.12,\n contrast: 1.08,\n }),\n ],\n palette: \"info\",\n threshold: 0.5,\n density: 0.78,\n speed: 0.5,\n pixelShape: \"square\",\n pixelGap: 0.14,\n pixelScale: 0.9,\n pixelSwitch: {\n mode: \"threshold-hysteresis\",\n dither: \"bayer8\",\n hysteresis: 0.058,\n temporalRate: 0.34,\n direction: \"radial\",\n },\n timeline: {\n durationMs: 6400,\n macroCycleMs: 12800,\n replay: \"continuous\",\n },\n transition: {\n enter: \"radial-cascade\",\n exit: \"field-morph\",\n durationMs: 500,\n preservePhase: true,\n },\n interactions: {\n pointer: { enabled: false },\n press: { enabled: false },\n signals: [\n { name: \"reasoning.depth\", mapsTo: \"shellEnergy\", range: [0, 1] },\n ],\n },\n reducedMotion: {\n phase: 0.25,\n representation: \"static symmetrical reasoning field\",\n },\n labels: { default: \"Ambient thinking — symmetric\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.ambient-speaking\",\n category: \"ambient\",\n meaning: \"voice output expressed only through an animated field\",\n glyph: \"speaking\",\n field: \"interference\",\n composition: {\n mode: \"field-orb\",\n gestaltBoundary: true,\n gestaltOpenness: 0.5,\n lighting: \"gibbous\",\n radius: 0.87,\n fieldContrast: 1.38,\n fieldDetail: 0.92,\n lightOrbit: 0.13,\n breathe: 0.065,\n voice: 0.2,\n rim: 0.05,\n },\n fieldMix: [\n layer(\"interference\", 0.46, 1.26, \"replace\", {\n speed: 1.05,\n contrast: 1.28,\n }),\n layer(\"plasma\", 0.34, 1.7, \"screen\", {\n speed: -0.72,\n contrast: 1.18,\n }),\n layer(\"ripple\", 0.2, 1.12, \"add\", {\n speed: 0.82,\n contrast: 1.24,\n }),\n ],\n palette: \"info\",\n threshold: 0.48,\n density: 0.82,\n speed: 1.02,\n pixelShape: \"circle\",\n pixelScale: 0.9,\n pixelSwitch: {\n mode: \"ordered-dither\",\n dither: \"bayer8\",\n hysteresis: 0.042,\n temporalRate: 0.96,\n direction: \"audio-surface\",\n },\n timeline: {\n mode: \"signal-reactive\",\n durationMs: 1900,\n macroCycleMs: 5700,\n },\n transition: {\n enter: \"radial-cascade\",\n exit: \"field-morph\",\n durationMs: 300,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"steer-light\", strength: 0.13 },\n press: { enabled: false },\n signals: [\n {\n name: \"audio.level\",\n mapsTo: \"envelope\",\n range: [0, 1],\n decayMs: 220,\n },\n { name: \"audio.syllable\", mapsTo: \"bandEnergy\", range: [0, 1] },\n ],\n },\n reducedMotion: {\n mode: \"data-step\",\n phase: 0.38,\n representation: \"static gibbous field with discrete audio levels\",\n dataUpdates: \"three-level-step\",\n },\n labels: { default: \"Ambient speaking\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.listening\",\n category: \"activity\",\n meaning: \"capturing voice or input\",\n glyph: \"listening\",\n field: \"interference\",\n fieldMix: [\n layer(\"interference\", 0.46, 1.4, \"replace\", {\n speed: 0.68,\n contrast: 1.18,\n }),\n layer(\"radar\", 0.32, 1.05, \"add\", { speed: -0.46 }),\n layer(\"liquid\", 0.22, 1.8, \"soft-light\", { speed: 0.3 }),\n ],\n palette: \"info\",\n threshold: 0.48,\n density: 0.77,\n speed: 0.78,\n pixelShape: \"circle\",\n pixelSwitch: {\n mode: \"radial-cascade\",\n dither: \"bayer8\",\n hysteresis: 0.05,\n direction: \"inward\",\n },\n timeline: { durationMs: 2400, macroCycleMs: 4800 },\n transition: {\n enter: \"sdf-wavefront\",\n exit: \"radial-cascade\",\n durationMs: 340,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"ripple-bias\", strength: 0.12 },\n press: { effect: \"emit-inbound-ripple\", strength: 0.18 },\n signals: [\n { name: \"audio.level\", mapsTo: \"energy\", range: [0, 1], decayMs: 90 },\n { name: \"input.pulse\", mapsTo: \"ripple\", range: [0, 1] },\n ],\n },\n reducedMotion: {\n phase: 0.36,\n representation: \"static listening ring with discrete level steps\",\n dataUpdates: \"three-level-step\",\n },\n labels: { default: \"Listening\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.thinking\",\n category: \"activity\",\n meaning: \"reasoning\",\n glyph: \"thinking\",\n field: \"flow\",\n fieldMix: [\n layer(\"flow\", 0.47, 1.35, \"replace\", {\n speed: 0.82,\n contrast: 1.08,\n }),\n layer(\"vortex\", 0.35, 1.05, \"add\", { speed: -0.62, warp: 0.22 }),\n layer(\"fbm\", 0.18, 2.1, \"soft-light\", { speed: 0.25 }),\n ],\n palette: \"info\",\n threshold: 0.5,\n density: 0.74,\n speed: 0.92,\n pixelShape: \"rounded-square\",\n pixelSwitch: {\n mode: \"temporal-blue-noise\",\n dither: \"blue-noise64\",\n hysteresis: 0.062,\n temporalRate: 0.82,\n },\n timeline: { durationMs: 3100, macroCycleMs: 6200 },\n transition: {\n enter: \"field-morph\",\n exit: \"field-morph\",\n durationMs: 430,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"bend-flow\", strength: 0.13 },\n press: { effect: \"damped-vortex\", strength: 0.2, durationMs: 460 },\n },\n reducedMotion: {\n phase: 0.3,\n representation: \"static faceted spiral\",\n },\n labels: { default: \"Thinking\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.thinking-deep\",\n category: \"activity\",\n meaning: \"deliberate extended reasoning\",\n glyph: \"thinking-deep\",\n field: \"domain-warp\",\n fieldMix: [\n layer(\"domain-warp\", 0.46, 0.95, \"replace\", {\n speed: 0.34,\n warp: 0.48,\n }),\n layer(\"ridged\", 0.31, 1.8, \"multiply\", {\n speed: 0.18,\n contrast: 1.24,\n }),\n layer(\"vortex\", 0.23, 0.72, \"add\", { speed: -0.27 }),\n ],\n palette: \"info\",\n threshold: 0.52,\n density: 0.79,\n speed: 0.38,\n pixelShape: \"diamond\",\n pixelGap: 0.1,\n pixelSwitch: {\n mode: \"neighbor-propagation\",\n dither: \"bayer8\",\n hysteresis: 0.07,\n temporalRate: 0.36,\n neighborhood: 8,\n direction: \"shell-to-shell\",\n },\n timeline: {\n durationMs: 9600,\n macroCycleMs: 19200,\n replay: \"continuous\",\n },\n transition: {\n enter: \"field-morph\",\n exit: \"field-morph\",\n durationMs: 680,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"separate-shells\", strength: 0.09 },\n press: { effect: \"pinch-core\", strength: 0.16, durationMs: 520 },\n signals: [\n {\n name: \"reasoning.depth\",\n mapsTo: \"shellEnergy\",\n range: [0, 1],\n },\n ],\n },\n reducedMotion: {\n phase: 0.42,\n representation: \"static nested rings with a brighter inner core\",\n },\n labels: { default: \"Reasoning deeply\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.still-working\",\n category: \"activity\",\n meaning: \"work is taking longer than expected\",\n glyph: \"still-working\",\n field: \"strata\",\n fieldMix: [\n layer(\"strata\", 0.44, 1.2, \"replace\", {\n speed: 0.21,\n contrast: 1.08,\n }),\n layer(\"radar\", 0.34, 0.82, \"add\", { speed: 0.17 }),\n layer(\"flow\", 0.22, 1.55, \"soft-light\", { speed: 0.18 }),\n ],\n palette: \"neutral\",\n threshold: 0.5,\n density: 0.71,\n speed: 0.24,\n pixelShape: \"rounded-square\",\n pixelSwitch: {\n mode: \"radial-cascade\",\n dither: \"bayer8\",\n hysteresis: 0.055,\n temporalRate: 0.62,\n direction: \"outward-beacon\",\n },\n timeline: {\n durationMs: 3200,\n macroCycleMs: 3200,\n replay: \"continuous\",\n },\n transition: {\n enter: \"field-morph\",\n exit: \"field-morph\",\n durationMs: 560,\n preservePhase: false,\n },\n interactions: {\n pointer: { effect: \"expand-beacon\", strength: 0.1 },\n press: { effect: \"host-action\", strength: 0.05 },\n signals: [\n { name: \"latency.elapsed\", mapsTo: \"beaconEnergy\", range: [0, 1] },\n ],\n },\n reducedMotion: {\n phase: 0.46,\n representation: \"upright hourglass with settled sand and flip guides\",\n },\n labels: { default: \"Still working\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.loading\",\n category: \"activity\",\n meaning: \"indeterminate startup or wait\",\n glyph: \"loading\",\n field: \"plasma\",\n fieldMix: [\n layer(\"plasma\", 0.58, 1.15, \"replace\", {\n speed: 1.15,\n contrast: 1.16,\n }),\n layer(\"flow\", 0.42, 1.65, \"add\", { speed: 0.88 }),\n ],\n palette: \"info\",\n threshold: 0.52,\n density: 0.7,\n speed: 1.38,\n pixelShape: \"square\",\n pixelSwitch: {\n mode: \"ordered-dither\",\n dither: \"bayer8\",\n hysteresis: 0.052,\n temporalRate: 1.3,\n direction: \"radial-tail\",\n },\n timeline: { durationMs: 1440, macroCycleMs: 5760 },\n transition: {\n enter: \"radial-cascade\",\n exit: \"field-morph\",\n durationMs: 280,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"steer-highlight\", strength: 0.08 },\n press: { enabled: false },\n },\n reducedMotion: {\n phase: 0.63,\n representation: \"static segmented loading ring\",\n },\n labels: { default: \"Loading\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.progress\",\n category: \"activity\",\n meaning: \"determinate completion progress\",\n glyph: \"progress\",\n field: \"strata\",\n fieldMix: [\n layer(\"strata\", 0.54, 1.25, \"replace\", {\n speed: 0.25,\n contrast: 1.1,\n }),\n layer(\"constellation\", 0.46, 1.9, \"add\", {\n speed: 0.1,\n phase: 0.37,\n }),\n ],\n palette: \"info\",\n threshold: 0.47,\n density: 0.84,\n speed: 0.32,\n pixelShape: \"circle\",\n pixelSwitch: {\n mode: \"temporal-blue-noise\",\n dither: \"blue-noise64\",\n hysteresis: 0.045,\n temporalRate: 0.28,\n direction: \"contour-forward\",\n monotonic: true,\n },\n timeline: { mode: \"data-driven\", durationMs: 240, replay: \"on-update\" },\n transition: {\n enter: \"contour-trace\",\n exit: \"path-draw\",\n durationMs: 320,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"reveal-value\", strength: 0 },\n press: { effect: \"spring-compress\", strength: 0.045 },\n signals: [\n {\n name: \"progress\",\n mapsTo: \"progress\",\n range: [0, 1],\n monotonic: true,\n },\n ],\n },\n reducedMotion: {\n mode: \"data-step\",\n phase: 0,\n representation: \"static progress contour updated from data\",\n dataUpdates: \"discrete-monotonic\",\n },\n labels: {\n default: \"In progress\",\n value: \"{percent}% complete\",\n live: \"polite\",\n },\n }),\n\n defineSprite({\n id: \"ai.generating\",\n category: \"activity\",\n meaning: \"producing content\",\n glyph: \"generating\",\n field: \"electric\",\n fieldMix: [\n layer(\"electric\", 0.4, 1.75, \"replace\", {\n speed: 0.84,\n contrast: 1.23,\n }),\n layer(\"metaballs\", 0.34, 1.1, \"add\", {\n speed: 0.54,\n warp: 0.18,\n }),\n layer(\"constellation\", 0.26, 2.05, \"screen\", {\n speed: 0.38,\n }),\n ],\n palette: \"info\",\n threshold: 0.5,\n density: 0.78,\n speed: 0.88,\n pixelShape: \"diamond\",\n pixelSwitch: {\n mode: \"curl-advect\",\n dither: \"blue-noise64\",\n hysteresis: 0.058,\n temporalRate: 0.8,\n neighborhood: 4,\n direction: \"core-to-target\",\n },\n timeline: {\n mode: \"signal-reactive\",\n durationMs: 3600,\n macroCycleMs: 7200,\n },\n transition: {\n enter: \"seeded-dissolve\",\n exit: \"field-morph\",\n durationMs: 460,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"attract-unlocked-pixels\", strength: 0.11 },\n press: { effect: \"core-pulse\", strength: 0.08 },\n signals: [\n { name: \"token\", mapsTo: \"spark\", range: [0, 1], bounded: true },\n { name: \"chunk\", mapsTo: \"spark\", range: [0, 1], bounded: true },\n { name: \"frame\", mapsTo: \"spark\", range: [0, 1], bounded: true },\n { name: \"stream.energy\", mapsTo: \"activity\", range: [0, 1] },\n ],\n },\n reducedMotion: {\n mode: \"data-step\",\n phase: 0.72,\n representation: \"target glyph filled in non-moving chunks\",\n dataUpdates: \"chunked-fill\",\n },\n labels: { default: \"Generating\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.searching\",\n category: \"activity\",\n meaning: \"searching or retrieving information\",\n glyph: \"searching\",\n field: \"radar\",\n fieldMix: [\n layer(\"radar\", 0.53, 0.95, \"replace\", {\n speed: 0.72,\n contrast: 1.18,\n }),\n layer(\"worley\", 0.29, 1.62, \"add\", { speed: 0.2 }),\n layer(\"caustics\", 0.18, 2.1, \"screen\", { speed: 0.16 }),\n ],\n palette: \"info\",\n threshold: 0.49,\n density: 0.73,\n speed: 0.74,\n pixelShape: \"circle\",\n pixelSwitch: {\n mode: \"sdf-wavefront\",\n dither: \"bayer8\",\n hysteresis: 0.057,\n temporalRate: 0.72,\n direction: \"radar-sweep\",\n },\n timeline: { durationMs: 3300, macroCycleMs: 6600 },\n transition: {\n enter: \"radial-cascade\",\n exit: \"field-morph\",\n durationMs: 380,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"bias-decorative-beam\", strength: 0.1 },\n press: { enabled: false },\n signals: [\n {\n name: \"search.hit\",\n mapsTo: \"worleyBloom\",\n range: [0, 1],\n decayMs: 620,\n },\n { name: \"search.results\", mapsTo: \"hitCount\", range: [0, 8] },\n ],\n },\n reducedMotion: {\n phase: 0.2,\n representation: \"static magnifier with discrete result dots\",\n dataUpdates: \"result-dot\",\n },\n labels: { default: \"Searching\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.tool-use\",\n category: \"activity\",\n meaning: \"executing a tool or action\",\n glyph: \"tool-use\",\n field: \"strata\",\n fieldMix: [\n layer(\"strata\", 0.43, 1.3, \"replace\", {\n speed: 0.46,\n contrast: 1.12,\n }),\n layer(\"electric\", 0.34, 1.8, \"add\", { speed: 0.68 }),\n layer(\"voronoi\", 0.23, 1.05, \"multiply\", { speed: 0.22 }),\n ],\n palette: \"neutral\",\n threshold: 0.52,\n density: 0.8,\n speed: 0.68,\n pixelShape: \"square\",\n pixelSwitch: {\n mode: \"neighbor-propagation\",\n dither: \"bayer8\",\n hysteresis: 0.062,\n temporalRate: 0.62,\n neighborhood: 6,\n direction: \"gear-step\",\n },\n timeline: {\n mode: \"signal-reactive\",\n durationMs: 2400,\n macroCycleMs: 7200,\n },\n transition: {\n enter: \"axis-flip\",\n exit: \"field-morph\",\n durationMs: 420,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"tilt-light\", strength: 0.13 },\n press: { effect: \"mechanism-compress\", strength: 0.09 },\n signals: [\n { name: \"tool.step\", mapsTo: \"toothPulse\", range: [0, 1] },\n { name: \"tool.wait\", mapsTo: \"stepHold\", range: [0, 1] },\n { name: \"tool.result\", mapsTo: \"coreState\", range: [0, 1] },\n ],\n },\n reducedMotion: {\n phase: 0.125,\n representation: \"static gear with data-driven step dots\",\n dataUpdates: \"step-dot\",\n },\n labels: { default: \"Using a tool\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.speaking\",\n category: \"activity\",\n meaning: \"producing voice output\",\n glyph: \"speaking\",\n field: \"interference\",\n fieldMix: [\n layer(\"interference\", 0.44, 1.2, \"replace\", {\n speed: 1.05,\n contrast: 1.16,\n }),\n layer(\"plasma\", 0.33, 1.62, \"add\", { speed: 0.74 }),\n layer(\"liquid\", 0.23, 0.92, \"soft-light\", {\n speed: 0.55,\n warp: 0.2,\n }),\n ],\n palette: \"info\",\n threshold: 0.47,\n density: 0.81,\n speed: 1.08,\n pixelShape: \"circle\",\n pixelSwitch: {\n mode: \"radial-cascade\",\n dither: \"bayer8\",\n hysteresis: 0.05,\n temporalRate: 1.02,\n direction: \"outward-audio\",\n },\n timeline: {\n mode: \"signal-reactive\",\n durationMs: 1900,\n macroCycleMs: 5700,\n },\n transition: {\n enter: \"sdf-wavefront\",\n exit: \"radial-cascade\",\n durationMs: 260,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"steer-light\", strength: 0.11 },\n press: { enabled: false },\n signals: [\n {\n name: \"audio.level\",\n mapsTo: \"envelope\",\n range: [0, 1],\n decayMs: 240,\n },\n { name: \"audio.syllable\", mapsTo: \"bandEnergy\", range: [0, 1] },\n ],\n },\n reducedMotion: {\n mode: \"data-step\",\n phase: 0.38,\n representation: \"static speaker with three discrete level states\",\n dataUpdates: \"three-level-step\",\n },\n labels: { default: \"Speaking\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"ai.awaiting-input\",\n category: \"activity\",\n meaning: \"user action is required\",\n glyph: \"awaiting-input\",\n field: \"fbm\",\n fieldMix: [\n layer(\"fbm\", 0.72, 0.92, \"replace\", {\n speed: 0.18,\n contrast: 0.94,\n }),\n layer(\"caustics\", 0.28, 1.55, \"soft-light\", {\n speed: 0.1,\n contrast: 0.9,\n }),\n ],\n palette: \"info\",\n threshold: 0.48,\n density: 0.8,\n speed: 0.22,\n pixelShape: \"rounded-square\",\n pixelSwitch: {\n mode: \"threshold-hysteresis\",\n dither: \"bayer8\",\n hysteresis: 0.052,\n temporalRate: 0.08,\n direction: \"focus\",\n },\n timeline: {\n mode: \"hold\",\n durationMs: 1800,\n macroCycleMs: 10800,\n replay: \"two-pulses-then-hold\",\n },\n transition: {\n enter: \"radial-cascade\",\n exit: \"field-morph\",\n durationMs: 420,\n preservePhase: false,\n },\n interactions: {\n pointer: { effect: \"turn-aperture\", strength: 0.1 },\n press: {\n effect: \"acknowledge\",\n strength: 0.06,\n durationMs: 160,\n emits: \"activate\",\n },\n signals: [{ name: \"activate\", mapsTo: \"acknowledgement\", range: [0, 1] }],\n },\n reducedMotion: {\n phase: 0.44,\n representation: \"static question mark inside a firefly focus ring\",\n },\n labels: { default: \"Your input is needed\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"status.success\",\n category: \"status\",\n meaning: \"completed successfully\",\n glyph: \"success\",\n field: \"caustics\",\n fieldMix: [\n layer(\"caustics\", 0.57, 1.3, \"replace\", {\n speed: 0.72,\n contrast: 1.15,\n }),\n layer(\"constellation\", 0.43, 1.75, \"screen\", { speed: 0.35 }),\n ],\n palette: \"success\",\n threshold: 0.46,\n density: 0.9,\n speed: 0.82,\n pixelShape: \"rounded-square\",\n pixelSwitch: {\n mode: \"path-draw\",\n dither: \"blue-noise64\",\n hysteresis: 0.042,\n temporalRate: 1,\n direction: \"tail-to-head\",\n monotonic: false,\n },\n timeline: {\n mode: \"one-shot\",\n durationMs: 500,\n holdMs: 800,\n replay: \"on-enter\",\n },\n transition: {\n enter: \"path-draw\",\n exit: \"field-morph\",\n durationMs: 500,\n exitDurationMs: 300,\n preservePhase: false,\n },\n interactions: {\n pointer: { effect: \"edge-sparkle\", strength: 0.04 },\n press: { enabled: false },\n },\n reducedMotion: {\n mode: \"static\",\n phase: 1,\n representation: \"static checkmark\",\n transitionMs: 120,\n },\n labels: { default: \"Completed\", aria: \"Completed successfully\", live: \"polite\" },\n terminal: true,\n }),\n\n defineSprite({\n id: \"status.warning\",\n category: \"status\",\n meaning: \"attention is needed for a nonfatal issue\",\n glyph: \"warning\",\n field: \"strata\",\n fieldMix: [\n layer(\"strata\", 0.58, 1.18, \"replace\", {\n speed: 0.38,\n contrast: 1.22,\n }),\n layer(\"interference\", 0.42, 1.55, \"add\", { speed: 0.33 }),\n ],\n palette: \"warning\",\n threshold: 0.49,\n density: 0.86,\n speed: 0.42,\n pixelShape: \"diamond\",\n pixelSwitch: {\n mode: \"contour-trace\",\n dither: \"bayer8\",\n hysteresis: 0.065,\n temporalRate: 0.38,\n direction: \"bottom-to-contour\",\n },\n timeline: {\n mode: \"one-shot-hold\",\n durationMs: 1100,\n holdMs: 1600,\n replay: \"two-pulses-then-hold\",\n },\n transition: {\n enter: \"sdf-wavefront\",\n exit: \"field-morph\",\n durationMs: 430,\n preservePhase: false,\n },\n interactions: {\n pointer: { effect: \"lean-highlight\", strength: 0.08 },\n press: { effect: \"contained-ripple\", strength: 0.08 },\n },\n reducedMotion: {\n mode: \"static\",\n phase: 1,\n representation: \"static outlined triangle and exclamation\",\n },\n labels: { default: \"Warning\", live: \"polite\" },\n urgency: \"medium\",\n }),\n\n defineSprite({\n id: \"status.error\",\n category: \"status\",\n meaning: \"operation failed\",\n glyph: \"error\",\n field: \"electric\",\n fieldMix: [\n layer(\"electric\", 0.61, 1.4, \"replace\", {\n speed: 0.82,\n contrast: 1.3,\n }),\n layer(\"ridged\", 0.39, 1.85, \"multiply\", {\n speed: 0.35,\n contrast: 1.22,\n }),\n ],\n palette: \"danger\",\n threshold: 0.51,\n density: 0.88,\n speed: 0.78,\n pixelShape: \"square\",\n pixelSwitch: {\n mode: \"axis-flip\",\n dither: \"bayer8\",\n hysteresis: 0.055,\n temporalRate: 0.9,\n direction: \"diagonal-fracture\",\n },\n timeline: {\n mode: \"one-shot-hold\",\n durationMs: 410,\n holdMs: 1200,\n replay: \"on-enter\",\n },\n transition: {\n enter: \"axis-flip\",\n exit: \"seeded-dissolve\",\n durationMs: 410,\n preservePhase: false,\n },\n interactions: {\n pointer: { effect: \"edge-repel\", strength: 0.055 },\n press: { enabled: false },\n },\n reducedMotion: {\n mode: \"static\",\n phase: 1,\n representation: \"static X\",\n transitionMs: 100,\n },\n labels: { default: \"Error\", aria: \"Operation failed\", live: \"assertive\" },\n terminal: true,\n urgency: \"high\",\n }),\n\n defineSprite({\n id: \"status.paused\",\n category: \"status\",\n meaning: \"work is suspended and can resume\",\n glyph: \"paused\",\n field: \"fbm\",\n fieldMix: [\n layer(\"fbm\", 0.55, 1.1, \"replace\", { speed: 0 }),\n layer(\"strata\", 0.45, 1.4, \"multiply\", { speed: 0 }),\n ],\n palette: \"neutral\",\n threshold: 0.5,\n density: 0.9,\n speed: 0,\n pixelShape: \"rounded-square\",\n pixelSwitch: {\n mode: \"axis-flip\",\n dither: \"bayer8\",\n hysteresis: 0.08,\n temporalRate: 0,\n direction: \"columns\",\n },\n timeline: {\n mode: \"hold\",\n durationMs: 1,\n replay: \"resume-reversible\",\n },\n transition: {\n enter: \"axis-flip\",\n exit: \"axis-flip\",\n durationMs: 280,\n preservePhase: true,\n preserveBuffer: true,\n },\n interactions: {\n pointer: { effect: \"steer-light-only\", strength: 0.06 },\n press: { effect: \"bar-squeeze\", strength: 0.045 },\n signals: [{ name: \"resume\", mapsTo: \"reverseTransition\", range: [0, 1] }],\n },\n reducedMotion: {\n mode: \"static\",\n phase: 1,\n representation: \"static pause bars\",\n },\n labels: { default: \"Paused\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"status.cancelled\",\n category: \"status\",\n meaning: \"operation was stopped\",\n glyph: \"cancelled\",\n field: \"vortex\",\n fieldMix: [\n layer(\"vortex\", 0.57, 0.88, \"replace\", {\n speed: -0.68,\n contrast: 1.12,\n }),\n layer(\"metaballs\", 0.43, 1.25, \"add\", { speed: -0.42 }),\n ],\n palette: \"neutral\",\n threshold: 0.52,\n density: 0.85,\n speed: 0.62,\n pixelShape: \"square\",\n pixelSwitch: {\n mode: \"seeded-dissolve\",\n dither: \"blue-noise64\",\n hysteresis: 0.065,\n temporalRate: 0.72,\n direction: \"reverse-to-center\",\n },\n timeline: {\n mode: \"one-shot-hold\",\n durationMs: 360,\n holdMs: 900,\n replay: \"on-enter\",\n },\n transition: {\n enter: \"seeded-dissolve\",\n exit: \"seeded-dissolve\",\n durationMs: 360,\n preservePhase: false,\n },\n interactions: {\n pointer: { enabled: false },\n press: { effect: \"spring-compress\", strength: 0.035 },\n },\n reducedMotion: {\n mode: \"static\",\n phase: 1,\n representation: \"static stop square\",\n },\n labels: { default: \"Cancelled\", live: \"polite\" },\n terminal: true,\n }),\n\n defineSprite({\n id: \"status.offline\",\n category: \"status\",\n meaning: \"disconnected or unavailable\",\n glyph: \"offline\",\n field: \"radar\",\n fieldMix: [\n layer(\"radar\", 0.56, 0.72, \"replace\", {\n speed: 0.16,\n contrast: 1.16,\n }),\n layer(\"electric\", 0.44, 1.55, \"add\", { speed: 0.11 }),\n ],\n palette: \"neutral\",\n threshold: 0.53,\n density: 0.72,\n speed: 0.18,\n pixelShape: \"rounded-square\",\n pixelSwitch: {\n mode: \"path-draw\",\n dither: \"bayer8\",\n hysteresis: 0.075,\n temporalRate: 0.16,\n direction: \"bridge-and-retreat\",\n },\n timeline: {\n mode: \"signal-reactive\",\n durationMs: 4200,\n macroCycleMs: 8400,\n replay: \"continuous\",\n },\n transition: {\n enter: \"contour-trace\",\n exit: \"field-morph\",\n durationMs: 480,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"weak-echo\", strength: 0.045 },\n press: { effect: \"host-retry\", strength: 0.04 },\n signals: [\n { name: \"network.retry\", mapsTo: \"retryEnergy\", range: [0, 1] },\n { name: \"network.strength\", mapsTo: \"signalStrength\", range: [0, 1] },\n ],\n },\n reducedMotion: {\n phase: 0.31,\n representation: \"static broken ring with a diagonal disconnect mark\",\n dataUpdates: \"retry-dot\",\n },\n labels: { default: \"Offline\", live: \"polite\" },\n urgency: \"medium\",\n }),\n\n defineSprite({\n id: \"transfer.active\",\n category: \"workflow\",\n meaning: \"uploading, downloading, or synchronizing data\",\n glyph: \"transfer\",\n field: \"flow\",\n fieldMix: [\n layer(\"flow\", 0.46, 1.15, \"replace\", {\n speed: 0.88,\n contrast: 1.14,\n }),\n layer(\"liquid\", 0.31, 1.55, \"add\", { speed: 0.65 }),\n layer(\"strata\", 0.23, 1.9, \"multiply\", { speed: 0.44 }),\n ],\n palette: \"info\",\n threshold: 0.48,\n density: 0.79,\n speed: 0.9,\n pixelShape: \"square\",\n pixelSwitch: {\n mode: \"axis-flip\",\n dither: \"blue-noise64\",\n hysteresis: 0.052,\n temporalRate: 0.85,\n direction: \"data-direction\",\n monotonic: false,\n },\n timeline: {\n mode: \"data-or-loop\",\n durationMs: 2200,\n macroCycleMs: 6600,\n },\n transition: {\n enter: \"axis-flip\",\n exit: \"field-morph\",\n durationMs: 380,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"lateral-wind\", strength: 0.07, minSize: 28 },\n press: { effect: \"spring-compress\", strength: 0.045 },\n signals: [\n {\n name: \"progress\",\n mapsTo: \"progress\",\n range: [0, 1],\n monotonic: true,\n },\n {\n name: \"direction\",\n mapsTo: \"direction\",\n values: [\"up\", \"down\", \"sync\"],\n },\n ],\n },\n reducedMotion: {\n mode: \"data-step\",\n phase: 0.5,\n representation: \"static directional arrow with data-driven fill\",\n dataUpdates: \"discrete-monotonic\",\n },\n labels: {\n default: \"Transferring\",\n live: \"polite\",\n value: \"{percent}% transferred\",\n variants: {\n up: \"Uploading\",\n down: \"Downloading\",\n sync: \"Syncing\",\n },\n },\n }),\n\n defineSprite({\n id: \"workflow.handoff\",\n category: \"workflow\",\n meaning: \"passing work to another agent or person\",\n glyph: \"handoff\",\n field: \"flow\",\n fieldMix: [\n layer(\"flow\", 0.44, 1.02, \"replace\", {\n speed: 0.66,\n contrast: 1.1,\n }),\n layer(\"constellation\", 0.34, 1.6, \"screen\", { speed: 0.31 }),\n layer(\"metaballs\", 0.22, 0.85, \"add\", { speed: 0.38 }),\n ],\n palette: \"info\",\n threshold: 0.49,\n density: 0.77,\n speed: 0.68,\n pixelShape: \"circle\",\n pixelSwitch: {\n mode: \"path-draw\",\n dither: \"blue-noise64\",\n hysteresis: 0.055,\n temporalRate: 0.62,\n direction: \"source-to-destination\",\n },\n timeline: {\n mode: \"signal-reactive\",\n durationMs: 3000,\n macroCycleMs: 6000,\n replay: \"loop-until-accepted\",\n },\n transition: {\n enter: \"field-morph\",\n exit: \"path-draw\",\n durationMs: 460,\n preservePhase: true,\n },\n interactions: {\n pointer: { effect: \"bend-path-midpoint\", strength: 0.09 },\n press: { enabled: false },\n signals: [\n {\n name: \"handoff.accepted\",\n mapsTo: \"accepted\",\n range: [0, 1],\n latches: true,\n },\n ],\n },\n reducedMotion: {\n phase: 0.58,\n representation: \"two static nodes joined by a directional arrow\",\n dataUpdates: \"destination-highlight\",\n },\n labels: { default: \"Handing off\", live: \"polite\" },\n }),\n\n defineSprite({\n id: \"status.celebration\",\n category: \"status\",\n meaning: \"a milestone or high-value success\",\n glyph: \"celebration\",\n field: \"constellation\",\n fieldMix: [\n layer(\"constellation\", 0.45, 1.45, \"replace\", {\n speed: 0.92,\n contrast: 1.2,\n }),\n layer(\"caustics\", 0.31, 1.9, \"screen\", { speed: 0.62 }),\n layer(\"electric\", 0.24, 2.2, \"add\", { speed: 0.74 }),\n ],\n palette: \"celebration\",\n threshold: 0.45,\n density: 0.88,\n speed: 1,\n pixelShape: \"diamond\",\n pixelSwitch: {\n mode: \"radial-cascade\",\n dither: \"blue-noise64\",\n hysteresis: 0.045,\n temporalRate: 1.1,\n direction: \"bounded-burst\",\n },\n timeline: {\n mode: \"one-shot\",\n durationMs: 700,\n holdMs: 900,\n replay: \"on-enter-once\",\n next: \"status.success\",\n },\n transition: {\n enter: \"radial-cascade\",\n exit: \"field-morph\",\n durationMs: 700,\n preservePhase: false,\n },\n interactions: {\n pointer: { effect: \"push-fragments\", strength: 0.06, minSize: 32 },\n press: { enabled: false },\n },\n reducedMotion: {\n mode: \"static\",\n phase: 1,\n representation: \"static star with a central success mark\",\n transitionMs: 120,\n },\n labels: {\n default: \"Milestone completed\",\n aria: \"Milestone completed successfully\",\n live: \"polite\",\n },\n terminal: true,\n }),\n];\n\nexport const SPRITES = deepFreeze(\n Object.fromEntries(RECIPES.map((recipe) => [recipe.id, recipe])),\n);\n\nexport const SPRITE_IDS = Object.freeze(RECIPES.map((recipe) => recipe.id));\nconst SPRITE_LIST = Object.freeze([...RECIPES]);\n\nexport const SPRITE_ALIASES = Object.freeze({\n idle: \"ai.idle\",\n ready: \"ai.idle\",\n ambient: \"ai.ambient-idle\",\n \"ambient-idle\": \"ai.ambient-idle\",\n \"field-idle\": \"ai.ambient-idle\",\n \"ambient-thinking\": \"ai.ambient-thinking\",\n \"field-thinking\": \"ai.ambient-thinking\",\n \"ambient-thinking-symmetric\": \"ai.ambient-thinking-symmetric\",\n \"field-thinking-symmetric\": \"ai.ambient-thinking-symmetric\",\n \"symmetric-thinking\": \"ai.ambient-thinking-symmetric\",\n \"ambient-speaking\": \"ai.ambient-speaking\",\n \"field-speaking\": \"ai.ambient-speaking\",\n listening: \"ai.listening\",\n thinking: \"ai.thinking\",\n \"thinking-deep\": \"ai.thinking-deep\",\n \"deep-thinking\": \"ai.thinking-deep\",\n \"still-working\": \"ai.still-working\",\n working: \"ai.still-working\",\n loading: \"ai.loading\",\n progress: \"ai.progress\",\n generating: \"ai.generating\",\n searching: \"ai.searching\",\n \"tool-use\": \"ai.tool-use\",\n tool: \"ai.tool-use\",\n speaking: \"ai.speaking\",\n \"awaiting-input\": \"ai.awaiting-input\",\n prompt: \"ai.awaiting-input\",\n success: \"status.success\",\n warning: \"status.warning\",\n error: \"status.error\",\n paused: \"status.paused\",\n cancelled: \"status.cancelled\",\n offline: \"status.offline\",\n transfer: \"transfer.active\",\n upload: \"transfer.active\",\n download: \"transfer.active\",\n sync: \"transfer.active\",\n handoff: \"workflow.handoff\",\n celebration: \"status.celebration\",\n});\n\nexport function resolveSpriteId(id) {\n const normalized = String(id ?? \"\").trim().toLowerCase();\n if (Object.prototype.hasOwnProperty.call(SPRITES, normalized)) {\n return normalized;\n }\n return SPRITE_ALIASES[normalized];\n}\n\nexport function getSprite(id) {\n const resolved = resolveSpriteId(id);\n return resolved ? SPRITES[resolved] : undefined;\n}\n\nexport function listSprites() {\n return SPRITE_LIST;\n}\n\nexport function hasSprite(id) {\n return resolveSpriteId(id) !== undefined;\n}\n\nexport default SPRITES;\n");
const configSource = "import { FIELD_ALIASES, FIELD_IDS } from \"./fields.js\";\nimport { SPRITE_ALIASES, SPRITE_IDS } from \"./sprites.js\";\n\nexport const TRANSITION_IDS = Object.freeze([\n \"field-morph\",\n \"seeded-dissolve\",\n \"radial-cascade\",\n \"angular-sweep\",\n \"scanline\",\n \"contour-trace\",\n \"path-draw\",\n \"axis-flip\",\n \"cluster-dissolve\",\n \"neighbor-ignite\",\n \"glitch-bands\",\n \"instant\",\n]);\n\nexport const PIXEL_SWITCH_IDS = Object.freeze([\n \"ordered-dither\",\n \"temporal-blue-noise\",\n \"threshold-hysteresis\",\n \"sdf-wavefront\",\n \"contour-trace\",\n \"curl-advect\",\n \"neighbor-propagation\",\n \"radial-cascade\",\n \"path-draw\",\n \"axis-flip\",\n \"seeded-dissolve\",\n \"field-morph\",\n]);\n\nexport const PIXEL_SHAPE_IDS = Object.freeze([\n \"disc\",\n \"square\",\n \"diamond\",\n \"capsule\",\n \"line\",\n \"ring\",\n \"cross\",\n \"square-cross\",\n \"square-cross-ring\",\n]);\n\nconst TRANSITION_ALIASES = Object.freeze({\n dissolve: \"seeded-dissolve\",\n radial: \"radial-cascade\",\n bloom: \"radial-cascade\",\n spiral: \"angular-sweep\",\n wave: \"scanline\",\n shutter: \"axis-flip\",\n glitch: \"glitch-bands\",\n \"sdf-wavefront\": \"contour-trace\",\n \"neighbor-propagation\": \"neighbor-ignite\",\n});\n\nconst PIXEL_SWITCH_ALIASES = Object.freeze({\n \"cluster-dissolve\": \"seeded-dissolve\",\n \"neighbor-ignite\": \"neighbor-propagation\",\n});\n\nconst PIXEL_SHAPE_ALIASES = Object.freeze({\n circle: \"disc\",\n \"rounded-square\": \"square\",\n dot: \"disc\",\n});\n\nconst ENGINE_OPTION_KEYS = Object.freeze([\n \"ariaLive\",\n \"autoResize\",\n \"autoplay\",\n \"background\",\n \"canvas\",\n \"contrast\",\n \"density\",\n \"direction\",\n \"dprMax\",\n \"field\",\n \"fieldMode\",\n \"fps\",\n \"gridSize\",\n \"interactive\",\n \"nonErrorPalette\",\n \"offPixels\",\n \"orbBoundary\",\n \"orbBackgroundColor\",\n \"orbBackgroundMode\",\n \"package\",\n \"palette\",\n \"paletteOverride\",\n \"pixelShape\",\n \"pixelSwitch\",\n \"previewMode\",\n \"progress\",\n \"quality\",\n \"reducedMotion\",\n \"seed\",\n \"speed\",\n \"sprite\",\n \"targetGlyph\",\n \"transition\",\n \"version\",\n]);\n\nconst NUMBER_OPTIONS = Object.freeze({\n contrast: { minimum: 0.25, maximum: 2 },\n density: { minimum: 0.35, maximum: 1.6 },\n dprMax: { minimum: 0.25, maximum: 8 },\n fps: { minimum: 1, maximum: 120, integer: true },\n gridSize: { minimum: 8, maximum: 96, integer: true },\n progress: { minimum: 0, maximum: 1 },\n speed: { minimum: 0.05, maximum: 5 },\n});\n\nconst BOOLEAN_OPTIONS = Object.freeze([\n \"autoResize\",\n \"autoplay\",\n \"background\",\n \"interactive\",\n \"offPixels\",\n]);\n\nconst normalizeKey = (value) =>\n String(value ?? \"\")\n .trim()\n .toLowerCase()\n .replace(/[\\s_]+/g, \"-\");\n\nfunction distanceBetween(left, right) {\n const a = [...String(left)];\n const b = [...String(right)];\n const previous = Array.from({ length: b.length + 1 }, (_, index) => index);\n const current = new Array(b.length + 1);\n\n for (let row = 1; row <= a.length; row += 1) {\n current[0] = row;\n for (let column = 1; column <= b.length; column += 1) {\n current[column] = Math.min(\n current[column - 1] + 1,\n previous[column] + 1,\n previous[column - 1] + (a[row - 1] === b[column - 1] ? 0 : 1),\n );\n }\n for (let column = 0; column <= b.length; column += 1) {\n previous[column] = current[column];\n }\n }\n return previous[b.length];\n}\n\nexport function nearestName(value, candidates, options = {}) {\n const input = normalizeKey(value);\n if (!input || !Array.isArray(candidates) || candidates.length === 0) {\n return null;\n }\n let nearest = null;\n let nearestDistance = Infinity;\n for (const candidate of candidates) {\n const distance = distanceBetween(input, normalizeKey(candidate));\n if (distance < nearestDistance) {\n nearest = candidate;\n nearestDistance = distance;\n }\n }\n const maximum =\n options.maxDistance ?? Math.max(2, Math.floor(input.length * 0.34));\n return nearestDistance <= maximum ? nearest : null;\n}\n\nexport class JoanConfigurationError extends RangeError {\n constructor(message, details = {}) {\n super(message);\n this.name = \"JoanConfigurationError\";\n this.code = details.code || \"invalid_configuration\";\n this.path = details.path || null;\n this.value = details.value;\n this.suggestion = details.suggestion || null;\n this.allowed = details.allowed ? [...details.allowed] : null;\n }\n\n toIssue() {\n return {\n code: this.code,\n message: this.message,\n path: this.path,\n value: this.value,\n suggestion: this.suggestion,\n severity: \"error\",\n };\n }\n}\n\nfunction aliasLookup(aliases) {\n return new Map(\n Object.entries(aliases || {}).map(([alias, canonical]) => [\n normalizeKey(alias),\n canonical,\n ]),\n );\n}\n\nexport function validateKnownValue(value, allowed, options = {}) {\n const label = options.label || \"value\";\n const path = options.path || label;\n const normalized = normalizeKey(value);\n const canonical = new Map(\n allowed.map((candidate) => [normalizeKey(candidate), candidate]),\n );\n if (canonical.has(normalized)) return canonical.get(normalized);\n\n const aliases = aliasLookup(options.aliases);\n if (aliases.has(normalized)) return aliases.get(normalized);\n\n const accepted = [...allowed, ...Object.keys(options.aliases || {})];\n const suggestion = nearestName(value, accepted);\n const suffix = suggestion ? ` Did you mean \"${suggestion}\"?` : \"\";\n throw new JoanConfigurationError(\n `Unknown ${label} \"${String(value)}\".${suffix}`,\n {\n code: `unknown_${label.replaceAll(\" \", \"_\")}`,\n path,\n value,\n suggestion,\n allowed,\n },\n );\n}\n\nexport const validateSpriteId = (value) =>\n validateKnownValue(value, SPRITE_IDS, {\n aliases: SPRITE_ALIASES,\n label: \"sprite\",\n path: \"sprite\",\n });\n\nexport const validateFieldId = (value) =>\n validateKnownValue(value, FIELD_IDS, {\n aliases: FIELD_ALIASES,\n label: \"field\",\n path: \"field\",\n });\n\nexport const validateTransition = (value) =>\n validateKnownValue(\n typeof value === \"string\"\n ? value\n : value?.name || value?.type || value?.enter,\n TRANSITION_IDS,\n {\n aliases: TRANSITION_ALIASES,\n label: \"transition\",\n path: \"transition\",\n },\n );\n\nexport const validatePixelShape = (value) =>\n validateKnownValue(\n typeof value === \"string\" ? value : value?.shape || value?.name,\n PIXEL_SHAPE_IDS,\n {\n aliases: PIXEL_SHAPE_ALIASES,\n label: \"pixel shape\",\n path: \"pixelShape\",\n },\n );\n\nexport const validatePixelSwitch = (value) =>\n validateKnownValue(\n typeof value === \"string\"\n ? value\n : value?.mode || value?.name || value?.type,\n PIXEL_SWITCH_IDS,\n {\n aliases: PIXEL_SWITCH_ALIASES,\n label: \"pixel switch\",\n path: \"pixelSwitch\",\n },\n );\n\nfunction issueFrom(error, fallbackPath) {\n if (error instanceof JoanConfigurationError) return error.toIssue();\n return {\n code: \"invalid_configuration\",\n message: error?.message || \"Invalid engine configuration.\",\n path: fallbackPath || null,\n value: undefined,\n suggestion: null,\n severity: \"error\",\n };\n}\n\nfunction inspectNumber(key, value, definition, coerce) {\n if (!coerce && typeof value !== \"number\") {\n throw new JoanConfigurationError(`${key} must be a number.`, {\n code: \"invalid_type\",\n path: key,\n value,\n });\n }\n let next = coerce ? Number(value) : value;\n if (!Number.isFinite(next)) {\n throw new JoanConfigurationError(`${key} must be a finite number.`, {\n code: \"invalid_number\",\n path: key,\n value,\n });\n }\n if (definition.integer && !coerce && !Number.isInteger(next)) {\n throw new JoanConfigurationError(`${key} must be an integer.`, {\n code: \"invalid_number\",\n path: key,\n value,\n });\n }\n if (definition.integer) next = Math.round(next);\n if (next < definition.minimum || next > definition.maximum) {\n if (!coerce) {\n throw new JoanConfigurationError(\n `${key} must be between ${definition.minimum} and ${definition.maximum}.`,\n {\n code: \"out_of_range\",\n path: key,\n value,\n },\n );\n }\n next = Math.min(Math.max(next, definition.minimum), definition.maximum);\n }\n return next;\n}\n\nfunction validatePalette(value, path, { allowVariants = true } = {}) {\n if (value === null) return null;\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new JoanConfigurationError(`${path} must be a palette object or null.`, {\n code: \"invalid_type\",\n path,\n value,\n });\n }\n const allowed = new Set([\n \"name\",\n \"background\",\n \"shadow\",\n \"off\",\n \"ink\",\n \"accent\",\n \"glow\",\n ...(allowVariants ? [\"variants\"] : []),\n ]);\n const next = {};\n for (const [key, color] of Object.entries(value)) {\n if (!allowed.has(key)) {\n const suggestion = nearestName(key, [...allowed]);\n throw new JoanConfigurationError(\n `Unknown ${path} channel \"${key}\".${\n suggestion ? ` Did you mean \"${suggestion}\"?` : \"\"\n }`,\n {\n code: \"unknown_palette_channel\",\n path: `${path}.${key}`,\n value: color,\n suggestion,\n },\n );\n }\n if (key === \"variants\") {\n if (!color || typeof color !== \"object\" || Array.isArray(color)) {\n throw new JoanConfigurationError(`${path}.variants must be an object.`, {\n code: \"invalid_type\",\n path: `${path}.variants`,\n value: color,\n });\n }\n next.variants = Object.fromEntries(\n Object.entries(color).map(([name, palette]) => [\n name,\n validatePalette(palette, `${path}.variants.${name}`, {\n allowVariants: false,\n }),\n ]),\n );\n continue;\n }\n if (typeof color !== \"string\" || !color.trim()) {\n throw new JoanConfigurationError(`${path}.${key} must be a CSS color string.`, {\n code: \"invalid_type\",\n path: `${path}.${key}`,\n value: color,\n });\n }\n next[key] = color.trim();\n }\n return next;\n}\n\nconst PORTABLE_COLOR_KEYWORDS = new Set([\n \"aqua\",\n \"black\",\n \"blue\",\n \"fuchsia\",\n \"gray\",\n \"green\",\n \"grey\",\n \"lime\",\n \"maroon\",\n \"navy\",\n \"olive\",\n \"orange\",\n \"purple\",\n \"red\",\n \"silver\",\n \"teal\",\n \"transparent\",\n \"white\",\n \"yellow\",\n]);\n\nfunction validateOptionalColor(value, path) {\n if (value === null) return null;\n if (typeof value !== \"string\" || !value.trim()) {\n throw new JoanConfigurationError(`${path} must be a CSS color string or null.`, {\n code: \"invalid_type\",\n path,\n value,\n });\n }\n const candidate = value.trim();\n if (candidate.toLowerCase() === \"transparent\") return null;\n const functionMatch =\n /^(rgb|rgba|hsl|hsla)\\(([\\d\\s.,%+/-]+)\\)$/i.exec(candidate);\n const portable =\n /^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$/i.test(candidate) ||\n PORTABLE_COLOR_KEYWORDS.has(candidate.toLowerCase()) ||\n (Boolean(functionMatch) && /\\d/.test(functionMatch[2]));\n if (!portable) {\n throw new JoanConfigurationError(\n `${path} must be a portable CSS color (hex, rgb, hsl, a supported keyword) or null.`,\n {\n code: \"invalid_color\",\n path,\n value,\n },\n );\n }\n return candidate;\n}\n\nexport function inspectEngineOptions(input, options = {}) {\n const coerce = options.coerce !== false;\n if (!input || typeof input !== \"object\" || Array.isArray(input)) {\n const error = new JoanConfigurationError(\n \"Engine options must be an object.\",\n { code: \"invalid_type\", path: \"options\", value: input },\n );\n return { ok: false, value: {}, issues: [error.toIssue()] };\n }\n\n const value = { ...input };\n const issues = [];\n const allowedKeys = new Set(ENGINE_OPTION_KEYS);\n if (options.allowUnknown !== true) {\n for (const key of Object.keys(value)) {\n if (allowedKeys.has(key)) continue;\n const suggestion = nearestName(key, ENGINE_OPTION_KEYS);\n issues.push({\n code: \"unknown_option\",\n message: `Unknown engine option \"${key}\".${\n suggestion ? ` Did you mean \"${suggestion}\"?` : \"\"\n }`,\n path: key,\n value: value[key],\n suggestion,\n severity: \"error\",\n });\n delete value[key];\n }\n }\n\n const apply = (key, callback) => {\n if (!Object.hasOwn(value, key) || value[key] === undefined) return;\n try {\n value[key] = callback(value[key]);\n } catch (error) {\n issues.push(issueFrom(error, key));\n delete value[key];\n }\n };\n\n apply(\"sprite\", (sprite) =>\n sprite && typeof sprite === \"object\" ? validateRecipe(sprite) : validateSpriteId(sprite),\n );\n apply(\"field\", (field) => (field === null ? null : validateFieldId(field)));\n apply(\"transition\", (transition) =>\n transition === null\n ? null\n : transition && typeof transition === \"object\"\n ? {\n ...transition,\n name: validateTransition(\n transition.name || transition.type || transition.enter,\n ),\n }\n : validateTransition(transition),\n );\n apply(\"pixelShape\", (shape) =>\n shape === null ? null : validatePixelShape(shape),\n );\n apply(\"pixelSwitch\", (pixelSwitch) =>\n pixelSwitch === null\n ? null\n : pixelSwitch && typeof pixelSwitch === \"object\"\n ? {\n ...pixelSwitch,\n mode: validatePixelSwitch(\n pixelSwitch.mode || pixelSwitch.name || pixelSwitch.type,\n ),\n }\n : validatePixelSwitch(pixelSwitch),\n );\n apply(\"fieldMode\", (mode) =>\n validateKnownValue(mode, [\"recipe\", \"override\"], {\n label: \"field mode\",\n path: \"fieldMode\",\n }),\n );\n apply(\"quality\", (quality) =>\n validateKnownValue(quality, [\"low\", \"balanced\", \"high\", \"auto\"], {\n label: \"quality\",\n path: \"quality\",\n }),\n );\n apply(\"previewMode\", (mode) =>\n validateKnownValue(mode, [\"fit\", \"actual\"], {\n label: \"preview mode\",\n path: \"previewMode\",\n }),\n );\n apply(\"orbBoundary\", (boundary) =>\n validateKnownValue(boundary, [\"defined\", \"gestalt\"], {\n label: \"orb boundary\",\n path: \"orbBoundary\",\n }),\n );\n apply(\"orbBackgroundColor\", (color) =>\n validateOptionalColor(color, \"orbBackgroundColor\"),\n );\n apply(\"orbBackgroundMode\", (mode) =>\n validateKnownValue(mode, [\"none\", \"solid\", \"pixelated\"], {\n label: \"orb background mode\",\n path: \"orbBackgroundMode\",\n }),\n );\n apply(\"reducedMotion\", (mode) => {\n if (typeof mode === \"boolean\" || mode === \"system\") return mode;\n throw new JoanConfigurationError(\n 'reducedMotion must be true, false, or \"system\".',\n { code: \"invalid_type\", path: \"reducedMotion\", value: mode },\n );\n });\n\n for (const [key, definition] of Object.entries(NUMBER_OPTIONS)) {\n apply(key, (number) => inspectNumber(key, number, definition, coerce));\n }\n for (const key of BOOLEAN_OPTIONS) {\n apply(key, (boolean) => {\n if (typeof boolean === \"boolean\") return boolean;\n if (coerce && (boolean === \"true\" || boolean === \"false\")) {\n return boolean === \"true\";\n }\n throw new JoanConfigurationError(`${key} must be a boolean.`, {\n code: \"invalid_type\",\n path: key,\n value: boolean,\n });\n });\n }\n for (const key of [\"palette\", \"paletteOverride\", \"nonErrorPalette\"]) {\n apply(key, (palette) => validatePalette(palette, key));\n }\n\n return { ok: issues.length === 0, value, issues };\n}\n\nexport function validateEngineOptions(input, options = {}) {\n const strict = options.strict !== false;\n const result = inspectEngineOptions(input, {\n ...options,\n coerce: options.coerce ?? !strict,\n });\n if (!result.ok && strict) {\n const issue = result.issues[0];\n throw new JoanConfigurationError(issue.message, issue);\n }\n return result.value;\n}\n\nexport function validateRecipe(input) {\n if (!input || typeof input !== \"object\" || Array.isArray(input)) {\n throw new JoanConfigurationError(\"A recipe must be an object.\", {\n code: \"invalid_type\",\n path: \"recipe\",\n value: input,\n });\n }\n if (typeof input.id !== \"string\" || !input.id.trim()) {\n throw new JoanConfigurationError(\"A recipe requires a non-empty id.\", {\n code: \"missing_recipe_id\",\n path: \"recipe.id\",\n value: input.id,\n });\n }\n if (typeof input.glyph !== \"string\" || !input.glyph.trim()) {\n throw new JoanConfigurationError(\"A recipe requires a glyph id.\", {\n code: \"missing_recipe_glyph\",\n path: \"recipe.glyph\",\n value: input.glyph,\n });\n }\n\n const recipe = { ...input, id: input.id.trim(), glyph: input.glyph.trim() };\n if (recipe.field !== undefined) recipe.field = validateFieldId(recipe.field);\n if (Array.isArray(recipe.fieldMix)) {\n recipe.fieldMix = recipe.fieldMix.map((layer, index) => {\n if (!layer || typeof layer !== \"object\") {\n throw new JoanConfigurationError(`recipe.fieldMix[${index}] must be an object.`, {\n code: \"invalid_type\",\n path: `recipe.fieldMix[${index}]`,\n value: layer,\n });\n }\n return { ...layer, field: validateFieldId(layer.field) };\n });\n } else if (!recipe.field) {\n throw new JoanConfigurationError(\"A recipe requires field or fieldMix.\", {\n code: \"missing_recipe_field\",\n path: \"recipe.field\",\n value: recipe.field,\n });\n }\n\n if (recipe.pixelShape !== undefined) {\n recipe.pixelShape = validatePixelShape(recipe.pixelShape);\n }\n if (recipe.pixel?.shape !== undefined) {\n recipe.pixel = {\n ...recipe.pixel,\n shape: validatePixelShape(recipe.pixel.shape),\n };\n }\n if (recipe.pixelSwitch !== undefined) {\n if (typeof recipe.pixelSwitch === \"string\") {\n recipe.pixelSwitch = validatePixelSwitch(recipe.pixelSwitch);\n } else if (recipe.pixelSwitch?.mode !== undefined) {\n recipe.pixelSwitch = {\n ...recipe.pixelSwitch,\n mode: validatePixelSwitch(recipe.pixelSwitch.mode),\n };\n }\n }\n if (recipe.transition !== undefined) {\n if (typeof recipe.transition === \"string\") {\n recipe.transition = validateTransition(recipe.transition);\n } else if (recipe.transition && typeof recipe.transition === \"object\") {\n const transition = { ...recipe.transition };\n for (const key of [\"name\", \"type\", \"enter\", \"exit\"]) {\n if (transition[key] !== undefined) {\n transition[key] = validateTransition(transition[key]);\n }\n }\n recipe.transition = transition;\n }\n }\n return recipe;\n}\n\nfunction deepFreeze(value, seen = new WeakSet()) {\n if (!value || typeof value !== \"object\" || seen.has(value)) return value;\n seen.add(value);\n for (const nested of Object.values(value)) deepFreeze(nested, seen);\n return Object.freeze(value);\n}\n\nexport function defineRecipe(recipe) {\n return deepFreeze(validateRecipe(recipe));\n}\n"
.replaceAll("./fields.js", fieldsUrl)
.replaceAll("./sprites.js", spritesUrl);
const configUrl = createModuleUrl(configSource);
const previewLayoutUrl = createModuleUrl("export const PREVIEW_MODES = Object.freeze([\"fit\", \"actual\"]);\n\nconst DEFAULT_RESOLUTION = 68;\nconst MIN_RESOLUTION = 8;\nconst MAX_RESOLUTION = 96;\nconst FIT_INSET = 0.075;\n\nfunction finite(value, fallback) {\n return Number.isFinite(Number(value)) ? Number(value) : fallback;\n}\n\nexport function normalizePreviewMode(mode) {\n return mode === \"actual\" ? \"actual\" : \"fit\";\n}\n\nexport function normalizePreviewResolution(resolution) {\n return Math.min(\n MAX_RESOLUTION,\n Math.max(\n MIN_RESOLUTION,\n Math.round(finite(resolution, DEFAULT_RESOLUTION)),\n ),\n );\n}\n\nexport function previewModeDetails(mode, resolution) {\n const normalizedMode = normalizePreviewMode(mode);\n const normalizedResolution = normalizePreviewResolution(resolution);\n return {\n mode: normalizedMode,\n resolution: normalizedResolution,\n cellSize: normalizedMode === \"actual\" ? 1 : null,\n footprint: normalizedMode === \"actual\" ? normalizedResolution : null,\n };\n}\n\nexport function canvasGridLayout(mode, resolution, width, height) {\n const details = previewModeDetails(mode, resolution);\n const safeWidth = Math.max(1, finite(width, details.resolution));\n const safeHeight = Math.max(1, finite(height, details.resolution));\n\n if (details.mode === \"actual\") {\n const stageSize = details.resolution;\n return {\n ...details,\n stageSize,\n cellSize: 1,\n offsetX: Math.round((safeWidth - stageSize) / 2),\n offsetY: Math.round((safeHeight - stageSize) / 2),\n };\n }\n\n const shortSide = Math.min(safeWidth, safeHeight);\n const margin = shortSide * FIT_INSET;\n const stageSize = shortSide - margin * 2;\n return {\n ...details,\n stageSize,\n cellSize: stageSize / details.resolution,\n offsetX: (safeWidth - stageSize) / 2,\n offsetY: (safeHeight - stageSize) / 2,\n };\n}\n");
const previewThumbnailsUrl = createModuleUrl("const DEFAULT_PREVIEW_PROGRESS = 0.48;\n\nfunction clamp01(value) {\n return Math.min(1, Math.max(0, Number(value) || 0));\n}\n\nfunction representativeTime(recipe) {\n const phase = clamp01(recipe?.reducedMotion?.phase ?? 0.25);\n const cycleMs =\n Number(recipe?.timeline?.macroCycleMs) ||\n Number(recipe?.timeline?.durationMs) ||\n 4000;\n return (Math.max(1, cycleMs) / 1000) * phase;\n}\n\n/**\n * One-shot recipes may advance to a different state while their card is active.\n * Atlas previews retain the requested recipe so their identity and resting frame\n * always match the card label.\n */\nexport function createThumbnailRecipe(recipe) {\n if (!recipe?.timeline?.next) return recipe;\n return {\n ...recipe,\n timeline: {\n ...recipe.timeline,\n next: null,\n },\n };\n}\n\nexport function shouldAnimateThumbnail(\n id,\n {\n selectedId = null,\n mainRunning = false,\n hoveredIds = new Set(),\n focusedIds = new Set(),\n } = {},\n) {\n return (\n (id === selectedId && mainRunning) ||\n hoveredIds.has(id) ||\n focusedIds.has(id)\n );\n}\n\n/**\n * Render a deterministic, authored representative phase without leaving the\n * preview in reduced-motion mode. JoanGlyphEngine maps time zero to each\n * recipe's reducedMotion.phase while reduced motion is enabled.\n */\nexport function renderThumbnailRestFrame(\n preview,\n { progress = DEFAULT_PREVIEW_PROGRESS } = {},\n) {\n if (!preview || preview.destroyed) return null;\n\n const nextProgress = clamp01(progress);\n if (Math.abs(preview.progress - nextProgress) > 1e-6) {\n preview.setProgress(nextProgress);\n }\n\n // Neighbor-propagation previews otherwise retain their previous gate buffer,\n // making the supposedly static pose depend on how long the card was hovered.\n preview.stateElapsed = representativeTime(preview.currentRecipe);\n preview.gates?.fill(0);\n preview.neighborBuffer?.fill(0);\n preview.dwell?.fill(0);\n\n const previousReducedMotion = preview.reducedMotion;\n preview.reducedMotion = true;\n let stats;\n try {\n stats = preview.renderOnce(0);\n } finally {\n preview.reducedMotion = previousReducedMotion;\n }\n\n if (preview.canvas?.dataset) preview.canvas.dataset.previewState = \"ready\";\n return stats;\n}\n");
const engineSource = "import { FieldKit, bayer8, clamp } from \"./fields.js\";\nimport { gestaltArcSupport, sampleGlyph } from \"./glyphs.js\";\nimport {\n canvasGridLayout,\n normalizePreviewMode,\n} from \"./preview-layout.js\";\nimport { SPRITES, getSprite, listSprites } from \"./sprites.js\";\nimport {\n validateFieldId,\n validatePixelShape,\n validatePixelSwitch,\n validateRecipe,\n validateSpriteId,\n validateTransition,\n validateEngineOptions,\n} from \"./config.js\";\n\nconst TAU = Math.PI * 2;\nconst DEFAULT_PALETTE = Object.freeze({\n background: \"#07080b\",\n off: \"#20232b\",\n ink: \"#f1f3f7\",\n accent: \"#8f9dff\",\n glow: \"#6678ff\",\n});\n\nconst POINTER_EFFECT_PROFILES = Object.freeze({\n \"steer-light\": { warp: 0.28, light: 1, surface: 0.35 },\n \"steer-light-only\": { warp: 0, light: 1, surface: 0 },\n \"steer-highlight\": { warp: 0.4, light: 0.9, surface: 0.72 },\n \"bend-flow\": { warp: 1, light: 0.35, surface: 0.6 },\n \"bend-flow-and-light\": { warp: 1, light: 0.9, surface: 0.58 },\n \"bend-path-midpoint\": { warp: 0.82, light: 0.45, surface: 0.7 },\n \"bias-decorative-beam\": { warp: 0.22, light: 0.75, surface: 0.45 },\n \"edge-repel\": { warp: -0.64, light: 0.4, surface: -0.3 },\n \"edge-sparkle\": { warp: 0.12, light: 0.9, surface: 0.82 },\n \"expand-beacon\": { warp: -0.36, light: 0.7, surface: 0.62 },\n \"lateral-wind\": { warp: 0.88, light: 0.3, surface: 0.44 },\n \"lean-highlight\": { warp: 0.3, light: 0.85, surface: 0.48 },\n \"push-fragments\": { warp: -0.72, light: 0.5, surface: 0.38 },\n \"reveal-value\": { warp: 0, light: 0.4, surface: 1 },\n \"ripple-bias\": { warp: 0.5, light: 0.5, surface: 0.72 },\n \"separate-shells\": { warp: -0.42, light: 0.55, surface: 0.5 },\n \"tilt-light\": { warp: 0.08, light: 1, surface: 0.15 },\n \"turn-aperture\": { warp: 0.55, light: 0.72, surface: 0.36 },\n \"weak-echo\": { warp: 0.18, light: 0.35, surface: 0.2 },\n});\n\nconst PRESS_EFFECT_PROFILES = Object.freeze({\n acknowledge: { surface: 0.4 },\n \"bar-squeeze\": { surface: -0.38 },\n \"contained-ripple\": { surface: 0.78 },\n \"core-pulse\": { surface: 0.9 },\n \"damped-vortex\": { surface: 0.68 },\n \"emit-inbound-ripple\": { surface: 0.86 },\n \"host-action\": { surface: 0.18 },\n \"host-retry\": { surface: 0.28 },\n \"mechanism-compress\": { surface: -0.34 },\n \"pinch-core\": { surface: -0.5 },\n \"spring-compress\": { surface: 0.62 },\n \"surface-ripple\": { surface: 0.82 },\n});\n\nexport const TRANSITIONS = Object.freeze([\n \"field-morph\",\n \"seeded-dissolve\",\n \"radial-cascade\",\n \"angular-sweep\",\n \"scanline\",\n \"contour-trace\",\n \"path-draw\",\n \"axis-flip\",\n \"cluster-dissolve\",\n \"neighbor-ignite\",\n \"glitch-bands\",\n \"instant\",\n]);\n\nexport const PIXEL_SWITCHES = Object.freeze([\n \"ordered-dither\",\n \"temporal-blue-noise\",\n \"threshold-hysteresis\",\n \"sdf-wavefront\",\n \"contour-trace\",\n \"curl-advect\",\n \"neighbor-propagation\",\n \"radial-cascade\",\n \"path-draw\",\n \"axis-flip\",\n \"seeded-dissolve\",\n \"field-morph\",\n]);\n\nexport const ORB_BACKGROUND_MODES = Object.freeze([\n \"none\",\n \"solid\",\n \"pixelated\",\n]);\n\nexport const PIXEL_SHAPES = Object.freeze([\n \"disc\",\n \"square\",\n \"diamond\",\n \"capsule\",\n \"line\",\n \"ring\",\n \"cross\",\n \"square-cross\",\n \"square-cross-ring\",\n]);\n\nconst DEFAULT_OPTIONS = Object.freeze({\n sprite: \"ai.idle\",\n seed: \"joan-v5\",\n gridSize: 68,\n speed: 1,\n density: 1,\n contrast: 1,\n fps: 60,\n pixelShape: null,\n pixelSwitch: null,\n transition: null,\n palette: null,\n nonErrorPalette: null,\n orbBoundary: \"defined\",\n orbBackgroundColor: null,\n orbBackgroundMode: \"none\",\n previewMode: \"fit\",\n background: true,\n offPixels: true,\n interactive: true,\n autoplay: true,\n autoResize: true,\n dprMax: 2,\n reducedMotion: \"system\",\n quality: \"auto\",\n ariaLive: null,\n});\n\nconst clamp01 = (value) => clamp(value, 0, 1);\nconst fract = (value) => value - Math.floor(value);\nconst easeInOut = (value) => {\n const t = clamp01(value);\n return t * t * (3 - 2 * t);\n};\nconst smoothRange = (edge0, edge1, value) =>\n edge0 === edge1\n ? value < edge0\n ? 0\n : 1\n : easeInOut((value - edge0) / (edge1 - edge0));\nconst expEase = (current, target, dt, tau) =>\n current + (target - current) * (1 - Math.exp(-dt / Math.max(0.001, tau)));\n\nfunction hashNumber(value) {\n let h = 2166136261;\n const input = String(value);\n for (let index = 0; index < input.length; index += 1) {\n h ^= input.charCodeAt(index);\n h = Math.imul(h, 16777619);\n }\n return h >>> 0;\n}\n\nfunction hash01(value) {\n let n = value | 0;\n n ^= n >>> 16;\n n = Math.imul(n, 0x7feb352d);\n n ^= n >>> 15;\n n = Math.imul(n, 0x846ca68b);\n n ^= n >>> 16;\n return (n >>> 0) / 4294967296;\n}\n\nfunction normalizeTransition(value) {\n const id =\n typeof value === \"string\"\n ? value\n : value?.name || value?.type || value?.enter;\n const aliases = {\n dissolve: \"seeded-dissolve\",\n radial: \"radial-cascade\",\n bloom: \"radial-cascade\",\n spiral: \"angular-sweep\",\n wave: \"scanline\",\n shutter: \"axis-flip\",\n glitch: \"glitch-bands\",\n \"sdf-wavefront\": \"contour-trace\",\n \"neighbor-propagation\": \"neighbor-ignite\",\n };\n return TRANSITIONS.includes(id) ? id : aliases[id] || \"field-morph\";\n}\n\nfunction normalizePixelSwitch(value) {\n const id =\n typeof value === \"string\"\n ? value\n : value?.name || value?.type || value?.mode;\n const aliases = {\n \"cluster-dissolve\": \"seeded-dissolve\",\n \"neighbor-ignite\": \"neighbor-propagation\",\n };\n const normalized = aliases[id] || id;\n return PIXEL_SWITCHES.includes(normalized) ? normalized : \"ordered-dither\";\n}\n\nfunction normalizePixelShape(value) {\n const aliases = {\n circle: \"disc\",\n \"rounded-square\": \"square\",\n dot: \"disc\",\n };\n const normalized = aliases[value] || value;\n return PIXEL_SHAPES.includes(normalized) ? normalized : \"disc\";\n}\n\nconst COMPOSITE_PIXEL_SHAPES = Object.freeze({\n \"square-cross\": Object.freeze([\"square\", \"cross\"]),\n \"square-cross-ring\": Object.freeze([\"square\", \"cross\", \"ring\"]),\n});\n\nfunction resolvePixelShape(shape, luminance) {\n const variants = COMPOSITE_PIXEL_SHAPES[shape];\n if (!variants) return shape;\n const bin = Math.min(8, Math.floor(clamp01(Number(luminance) || 0) * 8.999));\n if (variants.length === 2) return bin <= 4 ? variants[0] : variants[1];\n if (bin <= 4) return variants[0];\n if (bin <= 6) return variants[1];\n return variants[2];\n}\n\nfunction colorChannels(input, fallback) {\n const value = typeof input === \"string\" ? input.trim() : fallback;\n const hex = /^#([\\da-f]{3}|[\\da-f]{6})$/i.exec(value);\n if (hex) {\n const raw =\n hex[1].length === 3\n ? hex[1]\n .split(\"\")\n .map((part) => part + part)\n .join(\"\")\n : hex[1];\n return [\n Number.parseInt(raw.slice(0, 2), 16),\n Number.parseInt(raw.slice(2, 4), 16),\n Number.parseInt(raw.slice(4, 6), 16),\n ];\n }\n const rgb = value?.match?.(/[\\d.]+/g);\n if (rgb?.length >= 3) return rgb.slice(0, 3).map(Number);\n return colorChannels(fallback, \"#ffffff\");\n}\n\nfunction mixColor(from, to, amount, alpha = 1) {\n const t = clamp01(amount);\n const a = colorChannels(from, \"#ffffff\");\n const b = colorChannels(to, \"#ffffff\");\n return `rgba(${Math.round(a[0] + (b[0] - a[0]) * t)},${Math.round(\n a[1] + (b[1] - a[1]) * t,\n )},${Math.round(a[2] + (b[2] - a[2]) * t)},${clamp01(alpha).toFixed(3)})`;\n}\n\nfunction mixGlowColor(from, to, amount) {\n const t = clamp01(amount);\n const fromTransparent =\n String(from).trim().toLowerCase() === \"transparent\";\n const toTransparent = String(to).trim().toLowerCase() === \"transparent\";\n if (fromTransparent && toTransparent) return \"transparent\";\n if (fromTransparent) {\n const color = colorChannels(to, \"#ffffff\");\n return `rgba(${color[0]},${color[1]},${color[2]},${t.toFixed(3)})`;\n }\n if (toTransparent) {\n const color = colorChannels(from, \"#ffffff\");\n return `rgba(${color[0]},${color[1]},${color[2]},${(1 - t).toFixed(3)})`;\n }\n return mixColor(from, to, t, 1);\n}\n\nfunction xmlSafeString(value) {\n let output = \"\";\n for (const character of String(value)) {\n const codePoint = character.codePointAt(0);\n if (\n codePoint === 0x09 ||\n codePoint === 0x0a ||\n codePoint === 0x0d ||\n (codePoint >= 0x20 && codePoint <= 0xd7ff) ||\n (codePoint >= 0xe000 && codePoint <= 0xfffd) ||\n (codePoint >= 0x10000 && codePoint <= 0x10ffff)\n ) {\n output += character;\n } else {\n output += \"\\ufffd\";\n }\n }\n return output;\n}\n\nfunction escapeXml(value) {\n return xmlSafeString(value)\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\")\n .replaceAll(\"'\", \"&apos;\");\n}\n\nfunction escapeXmlAttribute(value) {\n return escapeXml(value)\n .replaceAll(\"\\t\", \"&#9;\")\n .replaceAll(\"\\n\", \"&#10;\")\n .replaceAll(\"\\r\", \"&#13;\");\n}\n\nconst SVG_COLOR_KEYWORDS = new Set([\n \"aqua\",\n \"black\",\n \"blue\",\n \"fuchsia\",\n \"gray\",\n \"green\",\n \"grey\",\n \"lime\",\n \"maroon\",\n \"navy\",\n \"olive\",\n \"orange\",\n \"purple\",\n \"red\",\n \"silver\",\n \"teal\",\n \"transparent\",\n \"white\",\n \"yellow\",\n]);\n\nfunction safeSvgColor(value, fallback) {\n const candidate = typeof value === \"string\" ? value.trim() : \"\";\n const normalized = candidate.toLowerCase();\n const isHex = /^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$/i.test(candidate);\n const functionMatch =\n /^(rgb|rgba|hsl|hsla)\\(([\\d\\s.,%+/-]+)\\)$/i.exec(candidate);\n const isNumericColorFunction =\n Boolean(functionMatch) && /\\d/.test(functionMatch[2]);\n\n if (\n isHex ||\n SVG_COLOR_KEYWORDS.has(normalized) ||\n isNumericColorFunction\n ) {\n return candidate;\n }\n return fallback;\n}\n\nconst ORB_BACKGROUND_RUN_BASE = 128;\n\nfunction orbBackgroundRowRun(gridSize, radius, row) {\n const size = Math.max(1, Math.round(Number(gridSize) || 1));\n const safeRadius = Math.max(0, Number(radius) || 0);\n const y = -1 + ((row + 0.5) * 2) / size;\n if (Math.abs(y) > safeRadius) return 0;\n const halfWidth = Math.sqrt(Math.max(0, safeRadius ** 2 - y ** 2));\n const start = Math.max(\n 0,\n Math.ceil(((1 - halfWidth) * size) / 2 - 0.5),\n );\n const end = Math.min(\n size - 1,\n Math.floor(((1 + halfWidth) * size) / 2 - 0.5),\n );\n if (end < start) return 0;\n return start * ORB_BACKGROUND_RUN_BASE + (end - start + 1);\n}\n\nfunction drawPixelatedOrbBackground(\n context,\n gridSize,\n radius,\n offsetX,\n offsetY,\n cell,\n) {\n for (let row = 0; row < gridSize; row += 1) {\n const run = orbBackgroundRowRun(gridSize, radius, row);\n const count = run % ORB_BACKGROUND_RUN_BASE;\n if (!count) continue;\n const start = (run - count) / ORB_BACKGROUND_RUN_BASE;\n context.fillRect(\n offsetX + start * cell,\n offsetY + row * cell,\n count * cell,\n cell,\n );\n }\n}\n\nfunction svgPixelatedOrbBackground(\n gridSize,\n radius,\n offset,\n cell,\n color,\n) {\n const rows = [];\n for (let row = 0; row < gridSize; row += 1) {\n const run = orbBackgroundRowRun(gridSize, radius, row);\n const count = run % ORB_BACKGROUND_RUN_BASE;\n if (!count) continue;\n const start = (run - count) / ORB_BACKGROUND_RUN_BASE;\n rows.push(\n `<rect x=\"${(offset + start * cell).toFixed(3)}\" y=\"${(\n offset + row * cell\n ).toFixed(3)}\" width=\"${(count * cell).toFixed(3)}\" height=\"${cell.toFixed(\n 3,\n )}\"/>`,\n );\n }\n if (!rows.length) return \"\";\n return `<g data-joan-layer=\"orb-background\" data-joan-mode=\"pixelated\" fill=\"${escapeXmlAttribute(\n color,\n )}\" shape-rendering=\"crispEdges\">${rows.join(\"\")}</g>`;\n}\n\nfunction animatedSvgPixelatedOrbBackground(\n gridSize,\n radiusSamples,\n offset,\n cell,\n color,\n keyTimes,\n duration,\n) {\n const rows = [];\n for (let row = 0; row < gridSize; row += 1) {\n const runs = Array.from(radiusSamples, (radius) =>\n orbBackgroundRowRun(gridSize, radius, row),\n );\n if (!runs.some(Boolean)) continue;\n const xValues = runs.map((run) => {\n const count = run % ORB_BACKGROUND_RUN_BASE;\n const start = count\n ? (run - count) / ORB_BACKGROUND_RUN_BASE\n : gridSize / 2;\n return (offset + start * cell).toFixed(3);\n });\n const widthValues = runs.map(\n (run) => ((run % ORB_BACKGROUND_RUN_BASE) * cell).toFixed(3),\n );\n rows.push(\n `<rect x=\"${xValues[0]}\" y=\"${(offset + row * cell).toFixed(\n 3,\n )}\" width=\"${widthValues[0]}\" height=\"${cell.toFixed(3)}\"><animate attributeName=\"x\" values=\"${xValues.join(\n \";\",\n )}\" keyTimes=\"${keyTimes}\" dur=\"${duration}s\" calcMode=\"discrete\" repeatCount=\"indefinite\"/><animate attributeName=\"width\" values=\"${widthValues.join(\n \";\",\n )}\" keyTimes=\"${keyTimes}\" dur=\"${duration}s\" calcMode=\"discrete\" repeatCount=\"indefinite\"/></rect>`,\n );\n }\n if (!rows.length) return \"\";\n return `<g data-joan-layer=\"orb-background\" data-joan-mode=\"pixelated\" fill=\"${escapeXmlAttribute(\n color,\n )}\" shape-rendering=\"crispEdges\">${rows.join(\"\")}</g>`;\n}\n\nfunction svgPalette(recipe, override) {\n const palette = recipePalette(recipe, override);\n return {\n background: safeSvgColor(\n palette.background,\n DEFAULT_PALETTE.background,\n ),\n off: safeSvgColor(palette.off, DEFAULT_PALETTE.off),\n ink: safeSvgColor(palette.ink, DEFAULT_PALETTE.ink),\n accent: safeSvgColor(palette.accent, DEFAULT_PALETTE.accent),\n glow: safeSvgColor(palette.glow, DEFAULT_PALETTE.glow),\n };\n}\n\nfunction svgShapeMarkup(\n shape,\n cx,\n cy,\n width,\n height,\n attributes = \"\",\n children = \"\",\n angle = 0,\n strokeWidth = null,\n) {\n const x = Number(cx);\n const y = Number(cy);\n const w = Number(width);\n const h = Number(height);\n const attrs = attributes ? ` ${attributes}` : \"\";\n if (shape === \"square\") {\n return `<rect x=\"${(x - w / 2).toFixed(2)}\" y=\"${(\n y -\n h / 2\n ).toFixed(2)}\" width=\"${w.toFixed(2)}\" height=\"${h.toFixed(\n 2,\n )}\"${attrs}>${children}</rect>`;\n }\n if (shape === \"diamond\") {\n const half = w * 0.52;\n return `<path d=\"M ${x.toFixed(2)} ${(y - half).toFixed(2)} L ${(\n x + half\n ).toFixed(2)} ${y.toFixed(2)} L ${x.toFixed(2)} ${(y + half).toFixed(\n 2,\n )} L ${(x - half).toFixed(2)} ${y.toFixed(2)} Z\"${attrs}>${children}</path>`;\n }\n if (shape === \"capsule\") {\n return `<rect x=\"${(x - w / 2).toFixed(2)}\" y=\"${(\n y -\n h * 0.32\n ).toFixed(2)}\" width=\"${w.toFixed(2)}\" height=\"${(h * 0.64).toFixed(\n 2,\n )}\" rx=\"${(h * 0.32).toFixed(2)}\"${attrs}>${children}</rect>`;\n }\n if (shape === \"line\") {\n return `<rect x=\"${(x - w / 2).toFixed(2)}\" y=\"${(\n y -\n h * 0.11\n ).toFixed(2)}\" width=\"${w.toFixed(2)}\" height=\"${(h * 0.22).toFixed(\n 2,\n )}\" transform=\"rotate(${angle.toFixed(2)} ${x.toFixed(2)} ${y.toFixed(\n 2,\n )})\"${attrs}>${children}</rect>`;\n }\n if (shape === \"ring\") {\n const ringStroke = Number.isFinite(Number(strokeWidth))\n ? Number(strokeWidth)\n : Math.max(0.12, Math.min(w, h) * 0.14);\n return `<ellipse cx=\"${x.toFixed(2)}\" cy=\"${y.toFixed(2)}\" rx=\"${(\n w / 2\n ).toFixed(2)}\" ry=\"${(h / 2).toFixed(\n 2,\n )}\" fill=\"none\" stroke-width=\"${ringStroke.toFixed(\n 3,\n )}\" vector-effect=\"non-scaling-stroke\"${attrs}>${children}</ellipse>`;\n }\n if (shape === \"cross\") {\n return `<g${attrs}><rect x=\"${(x - w / 2).toFixed(2)}\" y=\"${(\n y -\n h * 0.1\n ).toFixed(2)}\" width=\"${w.toFixed(2)}\" height=\"${(h * 0.2).toFixed(\n 2,\n )}\"/><rect x=\"${(x - w * 0.1).toFixed(2)}\" y=\"${(\n y -\n h / 2\n ).toFixed(2)}\" width=\"${(w * 0.2).toFixed(2)}\" height=\"${h.toFixed(\n 2,\n )}\"/>${children}</g>`;\n }\n return `<circle cx=\"${x.toFixed(2)}\" cy=\"${y.toFixed(2)}\" r=\"${(\n w / 2\n ).toFixed(2)}\"${attrs}>${children}</circle>`;\n}\n\nfunction animatedSvgShapeMarkup(\n shape,\n width,\n height,\n luminances,\n keyTimes,\n duration,\n angle = 0,\n ringStrokeWidths = [],\n) {\n const initialRingStroke = Number(ringStrokeWidths[0]) || null;\n const ringStrokeAnimation = ringStrokeWidths.length\n ? `<animate attributeName=\"stroke-width\" values=\"${ringStrokeWidths.join(\n \";\",\n )}\" keyTimes=\"${keyTimes}\" dur=\"${duration}s\" calcMode=\"discrete\" repeatCount=\"indefinite\"/>`\n : \"\";\n const variants = COMPOSITE_PIXEL_SHAPES[shape];\n if (!variants) {\n return svgShapeMarkup(\n shape,\n 0,\n 0,\n width,\n height,\n \"\",\n shape === \"ring\" ? ringStrokeAnimation : \"\",\n angle,\n initialRingStroke,\n );\n }\n const resolved = Array.from(luminances, (luminance) =>\n resolvePixelShape(shape, luminance),\n );\n const layers = variants.map((variant) => {\n const visibility = resolved.map((active) => (active === variant ? \"1\" : \"0\"));\n const values = visibility.join(\";\");\n return `<g data-joan-shape=\"${variant}\" opacity=\"${visibility[0]}\"><animate attributeName=\"opacity\" values=\"${values}\" keyTimes=\"${keyTimes}\" dur=\"${duration}s\" calcMode=\"discrete\" repeatCount=\"indefinite\"/>${svgShapeMarkup(\n variant,\n 0,\n 0,\n width,\n height,\n \"\",\n variant === \"ring\" ? ringStrokeAnimation : \"\",\n angle,\n initialRingStroke,\n )}</g>`;\n });\n return `<g data-joan-composite=\"${shape}\">${layers.join(\"\")}</g>`;\n}\n\nfunction recipeValue(recipe, key, fallback) {\n const direct = recipe?.[key];\n if (direct !== undefined) return direct;\n if (recipe?.render?.[key] !== undefined) return recipe.render[key];\n if (recipe?.pixel?.[key] !== undefined) return recipe.pixel[key];\n if (key === \"pixelShape\" && recipe?.pixel?.shape !== undefined) {\n return recipe.pixel.shape;\n }\n if (key === \"pixelScale\" && recipe?.pixel?.scale !== undefined) {\n return recipe.pixel.scale;\n }\n return fallback;\n}\n\nfunction recipePalette(recipe, override) {\n const source = recipe?.palette || {};\n return {\n ...DEFAULT_PALETTE,\n ...source,\n off: source.off || source.shadow || DEFAULT_PALETTE.off,\n glow: source.glow || source.accent || DEFAULT_PALETTE.glow,\n ...(override || {}),\n };\n}\n\nfunction resolvePaletteOverride(recipe, candidate) {\n if (!candidate || typeof candidate !== \"object\") return null;\n const { variants, ...base } = candidate;\n const paletteName = recipe?.palette?.name;\n const variant =\n paletteName &&\n variants &&\n typeof variants === \"object\" &&\n variants[paletteName] &&\n typeof variants[paletteName] === \"object\"\n ? variants[paletteName]\n : null;\n return variant ? { ...variant, ...base } : base;\n}\n\nfunction paletteOverrideFor(recipe, options) {\n const globalPalette = resolvePaletteOverride(recipe, options?.palette);\n const nonErrorPalette =\n recipe?.id !== \"status.error\" &&\n options?.nonErrorPalette &&\n typeof options.nonErrorPalette === \"object\"\n ? resolvePaletteOverride(recipe, options.nonErrorPalette)\n : null;\n\n if (!globalPalette) return nonErrorPalette;\n if (!nonErrorPalette) return globalPalette;\n return { ...globalPalette, ...nonErrorPalette };\n}\n\nfunction activePalette(recipe, options) {\n return recipePalette(recipe, paletteOverrideFor(recipe, options));\n}\n\nfunction activeSvgPalette(recipe, options) {\n return svgPalette(recipe, paletteOverrideFor(recipe, options));\n}\n\nfunction mixPalette(from, to, amount) {\n const t = clamp01(amount);\n return {\n background: mixColor(from.background, to.background, t, 1),\n off: mixColor(from.off, to.off, t, 1),\n ink: mixColor(from.ink, to.ink, t, 1),\n accent: mixColor(from.accent, to.accent, t, 1),\n glow: mixGlowColor(from.glow, to.glow, t),\n };\n}\n\nfunction recipePixelSwitch(recipe, override) {\n if (override && typeof override === \"object\") {\n return { ...override, mode: normalizePixelSwitch(override) };\n }\n if (override) return { mode: normalizePixelSwitch(override) };\n if (recipe?.pixelSwitch && typeof recipe.pixelSwitch === \"object\") {\n return {\n ...recipe.pixelSwitch,\n mode: normalizePixelSwitch(recipe.pixelSwitch),\n };\n }\n return { mode: normalizePixelSwitch(recipe?.pixelSwitch) };\n}\n\nfunction recipePixelScale(recipe) {\n const scale = clamp(\n Number(recipeValue(recipe, \"pixelScale\", 0.76)),\n 0.24,\n 1.12,\n );\n const gap = clamp(Number(recipe?.pixel?.gap) || 0, 0, 0.72);\n return scale * (1 - gap);\n}\n\nfunction semanticLabel(recipe) {\n return (\n recipe?.semantic?.label ||\n recipe?.labels?.aria ||\n recipe?.labels?.default ||\n recipe?.label ||\n recipe?.id ||\n \"Procedural AI glyph\"\n );\n}\n\nfunction signalPayloadValue(type, payload = {}) {\n if (Array.isArray(payload.results)) return payload.results.length;\n if (type === \"search.results\" && Number.isFinite(Number(payload.results))) {\n return Number(payload.results);\n }\n if (type === \"handoff.accepted\" && payload.accepted !== undefined) {\n return payload.accepted ? 1 : 0;\n }\n if (type === \"tool.wait\" && payload.waiting !== undefined) {\n return payload.waiting ? 1 : 0;\n }\n const candidates = [\n payload.value,\n payload.energy,\n payload.count,\n payload.depth,\n payload.wait,\n payload.strength,\n ];\n for (const candidate of candidates) {\n const value = Number(candidate);\n if (Number.isFinite(value)) return value;\n }\n return 0.7;\n}\n\nfunction copyPixelBuffer(engine, recipe, recipeSource = engine.recipeSource) {\n return {\n recipe,\n recipeSource,\n phase: engine.phase,\n stateElapsed: engine.stateElapsed,\n values: engine.values.slice(),\n velocities: engine.velocities.slice(),\n targets: engine.targets.slice(),\n luminance: engine.luminance.slice(),\n gates: engine.gates.slice(),\n dwell: engine.dwell.slice(),\n afterglow: engine.afterglow.slice(),\n neighborBuffer: engine.neighborBuffer.slice(),\n };\n}\n\nfunction withFieldOverride(recipe, field) {\n return {\n ...recipe,\n field,\n fieldMix: [\n {\n field,\n weight: 1,\n scale: 1,\n blend: \"replace\",\n phase: 0,\n speed: 1,\n contrast: 1,\n },\n ],\n };\n}\n\n/**\n * Produces a stable 0..1 activation delay for every transition strategy.\n * Exported so product teams can preview or synchronize the engine's wavefronts.\n */\nexport function transitionDelay(\n transition,\n x,\n y,\n random = 0.5,\n origin = { x: 0, y: 0 },\n) {\n const id = normalizeTransition(transition);\n const nx = clamp01((x + 1) * 0.5);\n const ny = clamp01((y + 1) * 0.5);\n const radius = clamp01(Math.hypot(x - origin.x, y - origin.y) / Math.SQRT2);\n const angle = fract((Math.atan2(y - origin.y, x - origin.x) + Math.PI) / TAU);\n\n switch (id) {\n case \"seeded-dissolve\":\n return random;\n case \"radial-cascade\":\n return clamp01(radius * 0.86 + random * 0.14);\n case \"angular-sweep\":\n return clamp01(angle * 0.78 + radius * 0.16 + random * 0.06);\n case \"scanline\":\n return clamp01(ny * 0.82 + Math.sin(x * 9) * 0.05 + random * 0.13);\n case \"contour-trace\":\n return clamp01(Math.abs(radius - 0.52) * 1.65 + random * 0.08);\n case \"path-draw\":\n return clamp01((nx * 0.42 + ny * 0.58) * 0.9 + random * 0.1);\n case \"axis-flip\":\n return clamp01((Math.floor(nx * 12) % 2) * 0.18 + ny * 0.72 + random * 0.1);\n case \"cluster-dissolve\":\n return clamp01(\n fract(Math.sin(Math.floor(x * 5) * 91.7 + Math.floor(y * 5) * 43.1) * 991.31) *\n 0.72 +\n random * 0.28,\n );\n case \"neighbor-ignite\":\n return clamp01(radius * 0.58 + (nx + ny) * 0.16 + random * 0.1);\n case \"glitch-bands\":\n return clamp01(fract(Math.floor(ny * 11) * 0.381 + random * 0.29));\n case \"instant\":\n return 0;\n case \"field-morph\":\n default:\n return clamp01(random * 0.42 + radius * 0.28 + ny * 0.3);\n }\n}\n\n/**\n * Canvas 2D renderer for deterministic, semantic pixel glyphs.\n *\n * The pipeline is:\n * glyph/SDF → procedural field → semantic envelope → transition wavefront →\n * ordered/temporal quantizer → hysteresis/dwell gate → spring → pixel renderer.\n */\nexport class JoanGlyphEngine extends EventTarget {\n static sprites = SPRITES;\n\n static listSprites() {\n return listSprites();\n }\n\n constructor(canvas, options = {}) {\n super();\n if (!canvas?.getContext) {\n throw new TypeError(\"JoanGlyphEngine requires a canvas element.\");\n }\n\n this.canvas = canvas;\n this.context = canvas.getContext(\"2d\", {\n alpha: true,\n desynchronized: true,\n });\n if (!this.context) throw new Error(\"Canvas 2D is not available.\");\n\n const normalizedOptions = validateEngineOptions(options, {\n strict: true,\n coerce: false,\n allowUnknown: false,\n });\n this.options = { ...DEFAULT_OPTIONS, ...normalizedOptions };\n if (!Object.hasOwn(normalizedOptions, \"orbBackgroundMode\")) {\n this.options.orbBackgroundMode = this.options.orbBackgroundColor\n ? \"solid\"\n : \"none\";\n }\n if (Object.hasOwn(options, \"paletteOverride\")) {\n this.options.palette =\n options.paletteOverride && typeof options.paletteOverride === \"object\"\n ? { ...options.paletteOverride }\n : null;\n }\n if (\n this.options.nonErrorPalette &&\n typeof this.options.nonErrorPalette === \"object\"\n ) {\n this.options.nonErrorPalette = { ...this.options.nonErrorPalette };\n }\n this.options.gridSize = Math.round(\n clamp(Number(this.options.gridSize) || 68, 8, 96),\n );\n this.options.speed = clamp(Number(this.options.speed) || 1, 0.05, 5);\n this.options.density = clamp(Number(this.options.density) || 1, 0.35, 1.6);\n this.options.previewMode = normalizePreviewMode(this.options.previewMode);\n\n this.seed = String(this.options.seed);\n this.seedHash = hashNumber(this.seed);\n this.fieldKit = new FieldKit(this.seed);\n this.customGlyphs = new Map();\n this.glyphSampler = (id, x, y, time, context) =>\n this.sampleCustomGlyph(id, x, y, time, context);\n this.fieldOptionsScratch = {};\n this.recipeMetadataCache = new WeakMap();\n this.semanticEnvelopeCache = new WeakMap();\n this.paintColorCache = {\n key: null,\n off: null,\n source: null,\n target: null,\n steady: null,\n transition: null,\n };\n this.pixelSwitchCache = { recipe: null, override: null, value: null };\n this.clock = 0;\n this.phase = 0;\n this.stateElapsed = 0;\n this.progress = clamp01(Number(this.options.progress) || 0);\n this.audioLevel = 0;\n this.audioTarget = 0;\n this.activityEnergy = 0;\n this.press = 0;\n this.running = false;\n this.destroyed = false;\n this.visible = true;\n this.documentVisible = true;\n this.intersectionVisible = true;\n this.frameHandle = null;\n this.frameTick = null;\n this.lastFrameTime = null;\n this.lastPaintTime = -Infinity;\n this.simulationAccumulator = 0;\n this.simulationStep =\n 1 / Math.min(60, clamp(Number(this.options.fps) || 60, 1, 120));\n this.maxSimulationSteps = 12;\n this.maxCatchUpSeconds = 0.2;\n this.qualitySamplePhase = 0;\n this.transitionElapsed = Infinity;\n this.transitionDuration = 0;\n this.transitionName = \"instant\";\n this.transitionSerial = 0;\n this.completedTransitionSerial = 0;\n this.pendingSpriteTransition = null;\n this.preservedTransition = null;\n this.reducedCrossfade = null;\n this.reducedFrameHandle = null;\n this.configurationBatchDepth = 0;\n this.configurationNeedsRender = false;\n this.pendingConfigurationDetail = null;\n this.impulses = [];\n this.signals = new Map();\n this.semanticSignals = new Map();\n this.semanticContext = Object.create(null);\n this.signalState = {\n accepted: false,\n direction: this.options.direction || \"down\",\n hits: 0,\n retry: 0,\n rotationSpeed: 1,\n segments: 8,\n signalStrength: 0,\n steps: 12,\n targetGlyph: this.options.targetGlyph || null,\n toolPhase: 0,\n };\n this.listeners = [];\n this.interactionListeners = [];\n this.qualityTier = this.options.quality === \"auto\" ? \"high\" : this.options.quality;\n this.qualityPressure = 0;\n this.qualityRecovery = 0;\n this.stats = {\n fps: 0,\n activePixels: 0,\n frameMs: 0,\n resolution: 0,\n simulationSteps: 0,\n sampledPixels: 0,\n quality: this.qualityTier,\n };\n this.fpsAccumulator = { time: 0, frames: 0 };\n this.pointerSampleScratch = { x: 0, y: 0, speed: 0 };\n this.sampleContextScratch = Object.create(null);\n this.sampleToScratch = Object.create(null);\n this.sampleFromScratch = Object.create(null);\n this.pointer = {\n x: 0,\n y: 0,\n targetX: 0,\n targetY: 0,\n speed: 0,\n inside: false,\n down: false,\n lastX: 0,\n lastY: 0,\n lastTime: 0,\n };\n\n this.reducedMotion = this.resolveReducedMotion();\n this.currentRecipe = this.resolveRecipe(this.options.sprite);\n this.recipeSource = this.currentRecipe;\n this.fieldOverride =\n this.options.fieldMode === \"recipe\" ? null : this.options.field || null;\n if (this.fieldOverride) {\n this.currentRecipe = withFieldOverride(\n this.currentRecipe,\n this.fieldOverride,\n );\n }\n this.previousRecipe = this.currentRecipe;\n this.allocate(this.options.gridSize);\n this.bindCanvas();\n this.observe();\n this.resize(true);\n this.updateAria();\n this.renderFrame(0, 0, true);\n if (this.options.autoplay && !this.reducedMotion) this.play();\n }\n\n resolveReducedMotion() {\n if (this.options.reducedMotion === true) return true;\n if (this.options.reducedMotion === false) return false;\n return Boolean(\n typeof window !== \"undefined\" &&\n window.matchMedia?.(\"(prefers-reduced-motion: reduce)\")?.matches,\n );\n }\n\n resolveRecipe(input) {\n if (input && typeof input === \"object\") return validateRecipe(input);\n const id = validateSpriteId(input ?? \"ai.idle\");\n return getSprite(id);\n }\n\n allocate(size) {\n const count = size * size;\n this.gridSize = size;\n this.values = new Float32Array(count);\n this.velocities = new Float32Array(count);\n this.targets = new Float32Array(count);\n this.luminance = new Float32Array(count);\n this.gates = new Uint8Array(count);\n this.dwell = new Float32Array(count);\n this.afterglow = new Float32Array(count);\n this.random = new Float32Array(count);\n this.xs = new Float32Array(count);\n this.ys = new Float32Array(count);\n this.neighborBuffer = new Float32Array(count);\n this.transitionMixes = new Float32Array(count);\n this.transitionMixes.fill(1);\n\n for (let y = 0; y < size; y += 1) {\n for (let x = 0; x < size; x += 1) {\n const index = y * size + x;\n this.xs[index] = ((x + 0.5) / size) * 2 - 1;\n this.ys[index] = ((y + 0.5) / size) * 2 - 1;\n this.random[index] = hash01(index ^ this.seedHash);\n this.dwell[index] = this.random[index] * 0.08;\n }\n }\n this.stats.resolution = size;\n }\n\n bind(target, type, handler, options) {\n target?.addEventListener?.(type, handler, options);\n this.listeners.push(() => target?.removeEventListener?.(type, handler, options));\n }\n\n bindInteraction(target, type, handler, options) {\n target?.addEventListener?.(type, handler, options);\n this.interactionListeners.push(() =>\n target?.removeEventListener?.(type, handler, options),\n );\n }\n\n activate(options = {}) {\n const x = clamp(Number(options.x) || 0, -1, 1);\n const y = clamp(Number(options.y) || 0, -1, 1);\n const source = options.source || \"programmatic\";\n const pressConfig = this.currentRecipe?.interactions?.press || {};\n\n if (pressConfig.enabled !== false) {\n this.press = 1;\n const pressStrength = clamp(Number(pressConfig.strength ?? 0.07), 0, 1);\n this.emitImpulse(\"press\", {\n x,\n y,\n energy: clamp01(\n options.energy === undefined\n ? 0.42 + pressStrength * 6.8\n : Number(options.energy),\n ),\n life: clamp(\n options.life === undefined\n ? Number(pressConfig.durationMs) / 1000 || 0.18\n : Number(options.life),\n 0.1,\n 4,\n ),\n });\n }\n if (typeof pressConfig.emits === \"string\" && pressConfig.emits) {\n this.signal(pressConfig.emits, {\n value: 1,\n x,\n y,\n source,\n });\n }\n this.emit(\"activate\", {\n sprite: this.currentRecipe.id,\n x,\n y,\n source,\n key: options.key || null,\n });\n this.renderWhenPaused();\n return this;\n }\n\n bindCanvas() {\n if (!this.options.interactive) return;\n\n const move = (event) => {\n const rect = this.canvas.getBoundingClientRect();\n if (!rect.width || !rect.height) return;\n const now = event.timeStamp / 1000 || performance.now() / 1000;\n const nextX = clamp(((event.clientX - rect.left) / rect.width) * 2 - 1, -1, 1);\n const nextY = clamp(((event.clientY - rect.top) / rect.height) * 2 - 1, -1, 1);\n const dt = Math.max(1 / 240, now - (this.pointer.lastTime || now));\n const distance = Math.hypot(nextX - this.pointer.lastX, nextY - this.pointer.lastY);\n this.pointer.speed = Math.min(\n 2,\n this.pointer.speed * 0.5 + (distance / dt) * 0.035,\n );\n this.pointer.targetX = nextX;\n this.pointer.targetY = nextY;\n this.pointer.lastX = nextX;\n this.pointer.lastY = nextY;\n this.pointer.lastTime = now;\n this.pointer.inside = true;\n };\n const enter = (event) => {\n this.pointer.inside = true;\n move(event);\n };\n const leave = () => {\n this.pointer.inside = false;\n this.pointer.down = false;\n this.pointer.targetX = 0;\n this.pointer.targetY = 0;\n };\n const down = (event) => {\n move(event);\n this.pointer.down = true;\n this.activate({\n x: this.pointer.targetX,\n y: this.pointer.targetY,\n source: \"pointer\",\n });\n this.canvas.setPointerCapture?.(event.pointerId);\n };\n const up = (event) => {\n this.pointer.down = false;\n this.canvas.releasePointerCapture?.(event.pointerId);\n };\n const keydown = (event) => {\n const isEnter = event.key === \"Enter\";\n const isSpace = event.key === \" \" || event.key === \"Spacebar\";\n if ((!isEnter && !isSpace) || event.repeat) return;\n if (isSpace) event.preventDefault?.();\n this.activate({\n source: \"keyboard\",\n key: isEnter ? \"Enter\" : \"Space\",\n });\n };\n\n this.bindInteraction(this.canvas, \"pointermove\", move, { passive: true });\n this.bindInteraction(this.canvas, \"pointerenter\", enter, { passive: true });\n this.bindInteraction(this.canvas, \"pointerleave\", leave, { passive: true });\n this.bindInteraction(this.canvas, \"pointerdown\", down, { passive: true });\n this.bindInteraction(this.canvas, \"pointerup\", up, { passive: true });\n this.bindInteraction(this.canvas, \"pointercancel\", up, { passive: true });\n this.bindInteraction(this.canvas, \"keydown\", keydown);\n }\n\n observe() {\n if (typeof document !== \"undefined\") {\n this.documentVisible = !document.hidden;\n this.visible = this.documentVisible && this.intersectionVisible;\n }\n if (this.options.autoResize && typeof ResizeObserver !== \"undefined\") {\n this.resizeObserver = new ResizeObserver(() => this.resize());\n this.resizeObserver.observe(this.canvas);\n }\n\n if (typeof IntersectionObserver !== \"undefined\") {\n this.intersectionObserver = new IntersectionObserver((entries) => {\n const wasVisible = this.visible;\n this.intersectionVisible = entries[0]?.isIntersecting !== false;\n this.visible = this.documentVisible && this.intersectionVisible;\n if (this.visible !== wasVisible) {\n this.lastFrameTime = null;\n this.simulationAccumulator = 0;\n if (this.visible) this.scheduleFrame();\n else this.cancelScheduledFrame();\n }\n });\n this.intersectionObserver.observe(this.canvas);\n }\n\n if (typeof window !== \"undefined\") {\n const media = window.matchMedia?.(\"(prefers-reduced-motion: reduce)\");\n if (media) {\n this.motionMedia = media;\n const update = () => {\n if (this.options.reducedMotion !== \"system\") return;\n this.reducedMotion = media.matches;\n if (this.reducedMotion) {\n this.pause();\n this.renderFrame(this.clock, 0, true);\n } else if (this.options.autoplay) {\n this.play();\n }\n };\n this.bind(media, \"change\", update);\n }\n this.bind(document, \"visibilitychange\", () => {\n const wasVisible = this.visible;\n this.documentVisible = !document.hidden;\n this.visible = this.documentVisible && this.intersectionVisible;\n this.lastFrameTime = null;\n if (this.visible !== wasVisible) {\n this.simulationAccumulator = 0;\n if (this.visible) this.scheduleFrame();\n else this.cancelScheduledFrame();\n }\n });\n }\n }\n\n resize(force = false) {\n const rect = this.canvas.getBoundingClientRect?.() || {};\n const cssWidth = Math.max(\n 1,\n Number(rect.width || this.canvas.clientWidth || this.canvas.width || 512),\n );\n const cssHeight = Math.max(\n 1,\n Number(rect.height || this.canvas.clientHeight || this.canvas.height || 512),\n );\n const dpr = Math.min(\n this.options.dprMax,\n typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1,\n );\n const nextWidth = Math.round(cssWidth * dpr);\n const nextHeight = Math.round(cssHeight * dpr);\n if (\n force ||\n this.canvas.width !== nextWidth ||\n this.canvas.height !== nextHeight ||\n this.width !== cssWidth ||\n this.height !== cssHeight\n ) {\n this.canvas.width = nextWidth;\n this.canvas.height = nextHeight;\n this.width = cssWidth;\n this.height = cssHeight;\n this.dpr = dpr;\n this.renderScaleX = nextWidth / cssWidth;\n this.renderScaleY = nextHeight / cssHeight;\n this.context.setTransform(\n this.renderScaleX,\n 0,\n 0,\n this.renderScaleY,\n 0,\n 0,\n );\n this.context.imageSmoothingEnabled = false;\n this.paint();\n }\n }\n\n updateAria() {\n const label = semanticLabel(this.currentRecipe);\n if (this.options.interactive) {\n this.canvas.setAttribute?.(\"role\", \"button\");\n this.canvas.setAttribute?.(\"tabindex\", \"0\");\n } else {\n this.canvas.setAttribute?.(\"role\", \"img\");\n this.canvas.removeAttribute?.(\"tabindex\");\n }\n this.canvas.setAttribute?.(\"aria-label\", `${label} — animated procedural glyph`);\n if (this.options.ariaLive) {\n const announcement =\n this.currentRecipe?.semantic?.announce ||\n this.currentRecipe?.labels?.live ||\n \"polite\";\n if ([\"polite\", \"assertive\", \"off\"].includes(announcement)) {\n this.options.ariaLive.setAttribute?.(\"aria-live\", announcement);\n }\n this.options.ariaLive.textContent = label;\n }\n }\n\n emit(type, detail) {\n if (type === \"configchange\" && this.configurationBatchDepth > 0) {\n this.pendingConfigurationDetail = {\n ...(this.pendingConfigurationDetail || {}),\n ...(detail || {}),\n };\n return;\n }\n let event;\n if (typeof CustomEvent === \"function\") {\n event = new CustomEvent(type, { detail });\n } else {\n event = new Event(type);\n Object.defineProperty(event, \"detail\", { value: detail });\n }\n this.dispatchEvent(event);\n }\n\n on(type, listener, options) {\n this.addEventListener(type, listener, options);\n return () => this.removeEventListener(type, listener, options);\n }\n\n isTransitionActive() {\n return (\n this.transitionDuration > 0 &&\n this.transitionElapsed < this.transitionDuration &&\n this.previousRecipe !== this.currentRecipe\n );\n }\n\n finishTransition(reason = \"completed\") {\n const serial = this.transitionSerial;\n if (!serial || this.completedTransitionSerial === serial) {\n this.previousRecipe = this.currentRecipe;\n this.transitionMixes?.fill?.(1);\n return null;\n }\n const detail = {\n from: this.previousRecipe?.id || null,\n to: this.currentRecipe?.id || null,\n duration: this.transitionDuration,\n elapsed: Math.min(this.transitionElapsed, this.transitionDuration),\n transition: this.transitionName,\n status: reason === \"interrupted\" ? \"interrupted\" : \"completed\",\n reason,\n serial,\n };\n this.transitionElapsed = this.transitionDuration;\n this.previousRecipe = this.currentRecipe;\n this.transitionMixes?.fill?.(1);\n this.completedTransitionSerial = serial;\n this.emit(\"transitioncomplete\", detail);\n return detail;\n }\n\n whenTransitionComplete(options = {}) {\n const signal = options.signal;\n if (!this.isTransitionActive()) {\n return Promise.resolve({\n from: this.previousRecipe?.id || null,\n to: this.currentRecipe?.id || null,\n duration: this.transitionDuration,\n elapsed: this.transitionElapsed,\n transition: this.transitionName,\n status: \"completed\",\n reason: \"already-settled\",\n serial: this.transitionSerial,\n });\n }\n const serial = this.transitionSerial;\n return new Promise((resolve, reject) => {\n const abort = () => {\n cleanup();\n const error = new Error(\"Transition wait was aborted.\");\n error.name = \"AbortError\";\n reject(error);\n };\n const complete = (event) => {\n if (event.detail?.serial !== serial) return;\n cleanup();\n resolve(event.detail);\n };\n const cleanup = () => {\n this.removeEventListener(\"transitioncomplete\", complete);\n signal?.removeEventListener?.(\"abort\", abort);\n };\n if (signal?.aborted) {\n abort();\n return;\n }\n this.addEventListener(\"transitioncomplete\", complete);\n signal?.addEventListener?.(\"abort\", abort, { once: true });\n });\n }\n\n transitionTo(sprite, options = {}) {\n const target =\n typeof sprite === \"string\" ? validateSpriteId(sprite) : validateRecipe(sprite);\n const targetId = typeof target === \"string\" ? target : target.id;\n const signal = options.signal;\n return new Promise((resolve, reject) => {\n const abort = () => {\n cleanup();\n const error = new Error(`Transition to \"${targetId}\" was aborted.`);\n error.name = \"AbortError\";\n reject(error);\n };\n const complete = (event) => {\n if (event.detail?.to !== targetId) return;\n cleanup();\n resolve(event.detail);\n };\n const cleanup = () => {\n this.removeEventListener(\"transitioncomplete\", complete);\n signal?.removeEventListener?.(\"abort\", abort);\n };\n if (signal?.aborted) {\n abort();\n return;\n }\n this.addEventListener(\"transitioncomplete\", complete);\n signal?.addEventListener?.(\"abort\", abort, { once: true });\n try {\n const { signal: _signal, ...transitionOptions } = options;\n this.setSprite(target, transitionOptions);\n } catch (error) {\n cleanup();\n reject(error);\n }\n });\n }\n\n clearSemanticSignals() {\n this.semanticSignals.clear();\n this.semanticContext = Object.create(null);\n this.sampleContextScratch = Object.create(null);\n this.signalState.accepted = false;\n this.signalState.hits = 0;\n this.signalState.retry = 0;\n this.signalState.signalStrength = 0;\n this.signalState.steps = 12;\n }\n\n applyRecipeSignal(type, payload, rawValue) {\n const declaration = this.currentRecipe?.interactions?.signals?.find?.(\n (signal) => signal.name === type,\n );\n if (!declaration?.mapsTo) return null;\n\n let mappedValue;\n if (Array.isArray(declaration.values)) {\n const candidate = String(\n payload.value ?? payload.direction ?? rawValue,\n ).toLowerCase();\n mappedValue = declaration.values.includes(candidate)\n ? candidate\n : declaration.values[0];\n } else {\n const [minimum = 0, maximum = 1] = declaration.range || [0, 1];\n mappedValue = clamp(Number(rawValue), Number(minimum), Number(maximum));\n const previous = Number(this.semanticContext[declaration.mapsTo]);\n if (declaration.monotonic && Number.isFinite(previous)) {\n mappedValue = Math.max(previous, mappedValue);\n }\n if (declaration.latches && Number.isFinite(previous) && previous > 0) {\n mappedValue = Math.max(previous, mappedValue);\n }\n }\n\n const entry = {\n declaration,\n value: mappedValue,\n decaySeconds: Math.max(0, Number(declaration.decayMs) || 0) / 1000,\n };\n this.semanticSignals.set(declaration.mapsTo, entry);\n this.semanticContext[declaration.mapsTo] = mappedValue;\n\n switch (declaration.mapsTo) {\n case \"hitCount\":\n this.signalState.hits = Math.max(0, Math.round(Number(mappedValue) || 0));\n break;\n case \"accepted\":\n this.signalState.accepted = Number(mappedValue) > 0;\n break;\n case \"retryEnergy\":\n this.signalState.retry = clamp01(Number(mappedValue) || 0);\n break;\n case \"signalStrength\":\n this.signalState.signalStrength = clamp01(Number(mappedValue) || 0);\n break;\n case \"direction\":\n this.signalState.direction = String(mappedValue);\n break;\n default:\n break;\n }\n return entry;\n }\n\n restorePixelBuffer(snapshot) {\n if (!snapshot || snapshot.values?.length !== this.values.length) return false;\n this.values.set(snapshot.values);\n this.velocities.set(snapshot.velocities);\n this.targets.set(snapshot.targets);\n this.luminance.set(snapshot.luminance);\n this.gates.set(snapshot.gates);\n this.dwell.set(snapshot.dwell);\n this.afterglow.set(snapshot.afterglow);\n this.neighborBuffer.set(snapshot.neighborBuffer);\n this.phase = snapshot.phase;\n this.stateElapsed = snapshot.stateElapsed;\n return true;\n }\n\n cancelReducedCrossfade() {\n if (this.reducedFrameHandle !== null && typeof cancelAnimationFrame === \"function\") {\n cancelAnimationFrame(this.reducedFrameHandle);\n }\n this.reducedFrameHandle = null;\n this.reducedCrossfade = null;\n }\n\n startReducedCrossfade(sourceSnapshot) {\n if (\n !sourceSnapshot ||\n this.currentRecipe?.reducedMotion?.transition === \"instant\" ||\n typeof requestAnimationFrame !== \"function\"\n ) {\n return false;\n }\n\n this.settleGrid(this.clock, { completeTransition: false });\n const targetSnapshot = copyPixelBuffer(\n this,\n this.currentRecipe,\n this.recipeSource,\n );\n this.restorePixelBuffer(sourceSnapshot);\n this.previousRecipe = sourceSnapshot.recipe || this.previousRecipe;\n this.transitionElapsed = 0;\n this.transitionMixes.fill(0);\n const crossfade = {\n source: sourceSnapshot,\n target: targetSnapshot,\n startedAt: null,\n duration: Math.max(0.001, this.transitionDuration),\n };\n this.reducedCrossfade = crossfade;\n\n const tick = (timestamp) => {\n if (this.destroyed || this.reducedCrossfade !== crossfade) return;\n if (crossfade.startedAt === null) crossfade.startedAt = timestamp;\n const progress = easeInOut(\n (timestamp - crossfade.startedAt) / 1000 / crossfade.duration,\n );\n let activePixels = 0;\n for (let index = 0; index < this.values.length; index += 1) {\n this.values[index] =\n crossfade.source.values[index] +\n (crossfade.target.values[index] - crossfade.source.values[index]) *\n progress;\n this.luminance[index] =\n crossfade.source.luminance[index] +\n (crossfade.target.luminance[index] -\n crossfade.source.luminance[index]) *\n progress;\n this.transitionMixes[index] = progress;\n if (this.values[index] > 0.32) activePixels += 1;\n }\n this.transitionElapsed = progress * crossfade.duration;\n this.stats.activePixels = activePixels;\n this.paint();\n\n if (progress < 1) {\n this.reducedFrameHandle = requestAnimationFrame(tick);\n return;\n }\n this.restorePixelBuffer(crossfade.target);\n this.reducedCrossfade = null;\n this.reducedFrameHandle = null;\n this.transitionElapsed = this.transitionDuration;\n this.finishTransition(\"reduced-motion-crossfade\");\n this.paint();\n };\n\n this.reducedFrameHandle = requestAnimationFrame(tick);\n return true;\n }\n\n resume(options = {}) {\n if (this.currentRecipe?.id !== \"status.paused\") return this;\n const pausedRecipe = this.currentRecipe;\n const snapshot = this.preservedTransition;\n const destination =\n options.sprite ||\n snapshot?.recipeSource ||\n snapshot?.recipe ||\n getSprite(\"ai.idle\");\n const exitDuration =\n Number(pausedRecipe?.transition?.exitDurationMs) /\n 1000 || Number(pausedRecipe?.transition?.durationMs) / 1000 || 0.28;\n\n this.setSprite(destination, {\n duration: options.duration ?? exitDuration,\n transition: options.transition || pausedRecipe?.transition?.exit || \"axis-flip\",\n preservePhase: true,\n forceTransition: true,\n skipPreserveCapture: true,\n skipPausedRender: true,\n });\n const restored = this.restorePixelBuffer(snapshot);\n this.previousRecipe = pausedRecipe;\n this.transitionElapsed = 0;\n this.preservedTransition = null;\n this.paint();\n this.emit(\"resume\", {\n from: pausedRecipe.id,\n to: this.currentRecipe.id,\n restoredBuffer: restored,\n source: options.source || \"signal\",\n });\n return this;\n }\n\n setSprite(sprite, options = {}) {\n const transitionActive = this.isTransitionActive();\n if (\n transitionActive &&\n this.currentRecipe?.transition?.interruptible === false &&\n !options.forceTransition\n ) {\n this.pendingSpriteTransition = {\n sprite,\n options: { ...options },\n };\n this.emit(\"transitionqueued\", {\n from: this.currentRecipe?.id,\n to: typeof sprite === \"string\" ? sprite : sprite?.id,\n });\n return this;\n }\n if (this.reducedCrossfade) this.cancelReducedCrossfade();\n if (transitionActive) this.finishTransition(\"interrupted\");\n\n const resolved =\n sprite === this.currentRecipe && this.recipeSource\n ? this.recipeSource\n : this.resolveRecipe(sprite);\n if (!resolved) throw new RangeError(`Unknown sprite: ${sprite}`);\n if (options.useRecipeFields) this.fieldOverride = null;\n const previousSource = this.recipeSource || this.currentRecipe;\n this.recipeSource = resolved;\n const next = this.fieldOverride\n ? withFieldOverride(resolved, this.fieldOverride)\n : resolved;\n const previous = this.currentRecipe;\n const reducedSourceSnapshot =\n this.reducedMotion && previous\n ? copyPixelBuffer(this, previous, previousSource)\n : null;\n const transitionConfig = next.transition || {};\n const preserveBuffer =\n options.preserveBuffer ?? transitionConfig.preserveBuffer ?? false;\n if (preserveBuffer && previous && !options.skipPreserveCapture) {\n this.preservedTransition = copyPixelBuffer(\n this,\n previous,\n previousSource,\n );\n } else if (!options.skipPreserveCapture) {\n this.preservedTransition = null;\n }\n this.previousRecipe = previous || next;\n this.currentRecipe = next;\n this.transitionSerial += 1;\n this.stateElapsed = 0;\n this.clearSemanticSignals();\n this.transitionElapsed = 0;\n const explicitTransition = options.transition || this.options.transition;\n const explicitTransitionConfig =\n explicitTransition && typeof explicitTransition === \"object\"\n ? explicitTransition\n : {};\n this.transitionDuration = this.reducedMotion\n ? clamp(\n Number(options.duration) ||\n Number(next?.reducedMotion?.transitionMs) / 1000 ||\n 0.12,\n 0,\n 0.2,\n )\n : clamp(\n Number(options.duration) ||\n Number(explicitTransitionConfig.duration) ||\n Number(explicitTransitionConfig.durationMs) / 1000 ||\n Number(transitionConfig.duration) ||\n Number(transitionConfig.durationMs) / 1000 ||\n 0.42,\n 0,\n 3,\n );\n const explicitTransitionName =\n explicitTransition && typeof explicitTransition === \"object\"\n ? explicitTransition.name ||\n explicitTransition.type ||\n explicitTransition.enter\n : explicitTransition;\n this.transitionName = normalizeTransition(\n (explicitTransitionName\n ? validateTransition(explicitTransitionName)\n : null) ||\n transitionConfig.name ||\n transitionConfig.type ||\n transitionConfig,\n );\n if (options.immediate) {\n this.transitionElapsed = this.transitionDuration;\n this.transitionName = \"instant\";\n }\n this.transitionMixes.fill(\n !options.immediate &&\n this.transitionDuration > 0 &&\n this.previousRecipe !== this.currentRecipe\n ? 0\n : 1,\n );\n const preservePhase =\n options.preservePhase ??\n explicitTransitionConfig.preservePhase ??\n transitionConfig.preservePhase ??\n true;\n if (!preservePhase) this.phase = 0;\n const reducedCrossfadeStarted =\n this.reducedMotion &&\n !options.immediate &&\n this.transitionDuration > 0 &&\n this.startReducedCrossfade(reducedSourceSnapshot);\n this.updateAria();\n this.emit(\"spritechange\", {\n from: previous?.id,\n to: next.id,\n label: semanticLabel(next),\n });\n if (!this.running) {\n if (this.configurationBatchDepth > 0) {\n this.configurationNeedsRender = true;\n } else if (\n reducedCrossfadeStarted ||\n preserveBuffer ||\n options.skipPausedRender\n ) {\n this.paint();\n } else {\n this.renderFrame(this.clock, 1 / 60, true);\n }\n }\n if (\n this.configurationBatchDepth === 0 &&\n !this.isTransitionActive() &&\n !reducedCrossfadeStarted\n ) {\n this.finishTransition(options.immediate ? \"immediate\" : \"settled\");\n }\n return this;\n }\n\n setField(field) {\n const canonicalField = validateFieldId(field);\n this.fieldOverride = canonicalField;\n this.currentRecipe = withFieldOverride(\n this.recipeSource || this.currentRecipe,\n canonicalField,\n );\n this.emit(\"configchange\", { field: canonicalField });\n if (!this.running && this.configurationBatchDepth === 0) {\n this.renderFrame(this.clock, 1 / 60, true);\n } else if (this.configurationBatchDepth > 0) {\n this.configurationNeedsRender = true;\n }\n return this;\n }\n\n useRecipeFields() {\n if (!this.fieldOverride) return this;\n this.fieldOverride = null;\n this.setSprite(this.recipeSource || this.currentRecipe, {\n transition: \"field-morph\",\n preservePhase: true,\n useRecipeFields: true,\n });\n this.emit(\"configchange\", { field: null, fieldMode: \"recipe\" });\n return this;\n }\n\n setPixelSwitch(pixelSwitch) {\n const requested =\n typeof pixelSwitch === \"string\"\n ? pixelSwitch\n : pixelSwitch?.name || pixelSwitch?.type || pixelSwitch?.mode;\n const mode = validatePixelSwitch(requested);\n this.options.pixelSwitch =\n pixelSwitch && typeof pixelSwitch === \"object\"\n ? { ...pixelSwitch, mode }\n : mode;\n this.emit(\"configchange\", { pixelSwitch: this.options.pixelSwitch });\n this.renderWhenPaused();\n return this;\n }\n\n setPixelShape(pixelShape) {\n const canonicalShape = validatePixelShape(pixelShape);\n this.options.pixelShape = canonicalShape;\n this.emit(\"configchange\", { pixelShape: canonicalShape });\n this.renderWhenPaused();\n return this;\n }\n\n setResolution(size) {\n const next = Math.round(clamp(Number(size) || this.gridSize, 8, 96));\n if (next !== this.gridSize) {\n this.options.gridSize = next;\n this.allocate(next);\n this.emit(\"configchange\", { gridSize: next });\n if (this.configurationBatchDepth > 0) {\n this.configurationNeedsRender = true;\n } else {\n this.renderFrame(this.clock, 1 / 60, true);\n }\n }\n return this;\n }\n\n setSeed(seed) {\n this.seed = String(seed || \"joan-v5\");\n this.seedHash = hashNumber(this.seed);\n this.fieldKit.reseed?.(this.seed);\n for (let index = 0; index < this.random.length; index += 1) {\n this.random[index] = hash01(index ^ this.seedHash);\n }\n this.emit(\"configchange\", { seed: this.seed });\n this.renderWhenPaused();\n return this;\n }\n\n setProgress(value) {\n this.progress = clamp01(Number(value) || 0);\n this.signal(\"progress\", { value: this.progress });\n return this;\n }\n\n setAudioLevel(value) {\n this.audioTarget = clamp01(Number(value) || 0);\n if (!this.running) this.audioLevel = this.audioTarget;\n this.renderWhenPaused();\n return this;\n }\n\n setNonErrorPalette(palette, options = {}) {\n this.options.nonErrorPalette =\n palette && typeof palette === \"object\" ? { ...palette } : null;\n if (options.render !== false) this.renderWhenPaused();\n return this;\n }\n\n configure(options = {}, configureOptions = {}) {\n const strict = configureOptions.strict !== false;\n const normalized = validateEngineOptions(options, {\n strict,\n coerce: !strict,\n allowUnknown: false,\n });\n const wasInteractive = Boolean(this.options.interactive);\n const wasReducedMotion = this.reducedMotion;\n this.configurationBatchDepth += 1;\n this.configurationNeedsRender = true;\n try {\n if (normalized.seed !== undefined) this.setSeed(normalized.seed);\n if (normalized.gridSize !== undefined) {\n this.setResolution(normalized.gridSize);\n }\n if (\n normalized.fieldMode === \"recipe\" ||\n (Object.hasOwn(normalized, \"field\") && normalized.field === null)\n ) {\n this.fieldOverride = null;\n this.emit(\"configchange\", { field: null, fieldMode: \"recipe\" });\n } else if (normalized.field !== undefined) {\n this.setField(normalized.field);\n }\n if (Object.hasOwn(normalized, \"pixelShape\")) {\n if (normalized.pixelShape === null) {\n this.options.pixelShape = null;\n this.emit(\"configchange\", { pixelShape: null });\n } else {\n this.setPixelShape(normalized.pixelShape);\n }\n }\n if (Object.hasOwn(normalized, \"pixelSwitch\")) {\n if (normalized.pixelSwitch === null) {\n this.options.pixelSwitch = null;\n this.emit(\"configchange\", { pixelSwitch: null });\n } else {\n this.setPixelSwitch(normalized.pixelSwitch);\n }\n }\n\n const directlyAssigned = [\n \"ariaLive\",\n \"autoResize\",\n \"autoplay\",\n \"background\",\n \"contrast\",\n \"density\",\n \"direction\",\n \"dprMax\",\n \"fps\",\n \"interactive\",\n \"offPixels\",\n \"orbBoundary\",\n \"orbBackgroundColor\",\n \"orbBackgroundMode\",\n \"previewMode\",\n \"quality\",\n \"reducedMotion\",\n \"speed\",\n \"targetGlyph\",\n \"transition\",\n ];\n for (const key of directlyAssigned) {\n if (Object.hasOwn(normalized, key)) this.options[key] = normalized[key];\n }\n if (\n Object.hasOwn(normalized, \"orbBackgroundColor\") &&\n !Object.hasOwn(normalized, \"orbBackgroundMode\")\n ) {\n this.options.orbBackgroundMode = normalized.orbBackgroundColor\n ? this.options.orbBackgroundMode === \"pixelated\"\n ? \"pixelated\"\n : \"solid\"\n : \"none\";\n }\n if (Object.hasOwn(normalized, \"reducedMotion\")) {\n this.reducedMotion = this.resolveReducedMotion();\n }\n this.options.previewMode = normalizePreviewMode(this.options.previewMode);\n if (Object.hasOwn(normalized, \"palette\")) {\n this.options.palette = normalized.palette\n ? { ...normalized.palette }\n : null;\n }\n if (Object.hasOwn(normalized, \"paletteOverride\")) {\n this.options.palette = normalized.paletteOverride\n ? { ...normalized.paletteOverride }\n : null;\n }\n if (Object.hasOwn(normalized, \"nonErrorPalette\")) {\n this.options.nonErrorPalette = normalized.nonErrorPalette\n ? { ...normalized.nonErrorPalette }\n : null;\n }\n if (Object.hasOwn(normalized, \"progress\")) {\n this.progress = normalized.progress;\n }\n if (normalized.sprite !== undefined) {\n this.setSprite(normalized.sprite, {\n transition: normalized.transition || undefined,\n preservePhase: true,\n useRecipeFields: normalized.fieldMode === \"recipe\",\n });\n } else if (!this.fieldOverride && normalized.fieldMode === \"recipe\") {\n this.currentRecipe = this.recipeSource || this.currentRecipe;\n }\n\n if (\n normalized.interactive !== undefined &&\n Boolean(this.options.interactive) !== wasInteractive\n ) {\n for (const remove of this.interactionListeners.splice(0)) remove();\n if (this.options.interactive) this.bindCanvas();\n this.updateAria();\n }\n if (normalized.autoResize === false && this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n } else if (\n normalized.autoResize === true &&\n !this.resizeObserver &&\n typeof ResizeObserver !== \"undefined\"\n ) {\n this.resizeObserver = new ResizeObserver(() => this.resize());\n this.resizeObserver.observe(this.canvas);\n }\n if (Object.hasOwn(normalized, \"quality\")) {\n this.qualityTier =\n normalized.quality === \"auto\" ? \"high\" : normalized.quality;\n this.stats.quality = this.qualityTier;\n }\n if (\n Object.hasOwn(normalized, \"quality\") ||\n Object.hasOwn(normalized, \"fps\")\n ) {\n this.qualityPressure = 0;\n this.qualityRecovery = 0;\n this.qualitySamplePhase = 0;\n }\n if (Object.hasOwn(normalized, \"fps\")) {\n this.simulationStep =\n 1 / Math.min(60, clamp(Number(this.options.fps) || 60, 1, 120));\n this.simulationAccumulator = 0;\n }\n } finally {\n this.configurationBatchDepth -= 1;\n }\n\n const detail = {\n ...(this.pendingConfigurationDetail || {}),\n configured: Object.keys(normalized),\n };\n this.pendingConfigurationDetail = null;\n if (this.configurationBatchDepth === 0 && this.configurationNeedsRender) {\n this.configurationNeedsRender = false;\n if (this.running) this.paint();\n else this.renderFrame(this.clock, 1 / 60, true);\n }\n this.emit(\"configchange\", detail);\n this.updateAria();\n if (this.reducedMotion || normalized.autoplay === false) {\n this.pause();\n } else if (\n normalized.autoplay === true ||\n (wasReducedMotion && Object.hasOwn(normalized, \"reducedMotion\"))\n ) {\n this.play();\n }\n return this;\n }\n\n setOptions(options = {}) {\n return this.configure(options, { strict: false });\n }\n\n signal(type, payload = {}) {\n const rawValue = signalPayloadValue(type, payload);\n const value = clamp01(rawValue);\n const mapped = this.applyRecipeSignal(type, payload, rawValue);\n if (type === \"audio.level\") {\n this.audioTarget = value;\n if (!this.running) this.audioLevel = value;\n } else if (type === \"progress\") {\n this.progress = clamp01(Number(mapped?.value ?? payload.value) || 0);\n } else {\n this.activityEnergy = Math.max(this.activityEnergy, value);\n const life = clamp(Number(payload.life) || 1.2, 0.1, 12);\n this.signals.set(type, { age: 0, life, value, payload: { ...payload } });\n if (type === \"search.hit\") {\n this.signalState.hits = Math.min(8, this.signalState.hits + 1);\n } else if (type === \"search.results\") {\n this.signalState.hits = Math.max(\n 0,\n Math.round(Number(mapped?.value ?? rawValue) || 0),\n );\n } else if (type === \"network.retry\") {\n this.signalState.retry = value;\n } else if (type === \"tool.step\") {\n this.signalState.steps = Math.max(\n 1,\n Math.round(Number(payload.steps ?? payload.step) || this.signalState.steps + 1),\n );\n } else if (type === \"handoff.accepted\") {\n this.signalState.accepted = payload.accepted !== false;\n } else if (type === \"transfer.direction\" || type === \"direction\") {\n this.signalState.direction = String(\n payload.direction || payload.value || \"down\",\n );\n }\n if (payload.direction) this.signalState.direction = String(payload.direction);\n if (payload.targetGlyph) this.signalState.targetGlyph = payload.targetGlyph;\n if (payload.segments) {\n this.signalState.segments = Math.max(1, Math.round(Number(payload.segments)));\n }\n if (payload.rotationSpeed) {\n this.signalState.rotationSpeed = Number(payload.rotationSpeed) || 1;\n }\n this.signalState.signalStrength = Math.max(\n this.signalState.signalStrength,\n value,\n );\n this.emitImpulse(type, {\n x: Number(payload.x ?? this.pointer.x),\n y: Number(payload.y ?? this.pointer.y),\n energy: value,\n life: Number(payload.life) || 1.2,\n });\n }\n this.emit(\"signal\", { type, payload });\n if (type === \"resume\" && value > 0) {\n this.resume({ source: payload.source || \"signal\" });\n return this;\n }\n this.renderWhenPaused();\n return this;\n }\n\n renderWhenPaused() {\n if (this.configurationBatchDepth > 0) {\n this.configurationNeedsRender = true;\n return;\n }\n if (!this.running && !this.destroyed) this.renderOnce(this.clock);\n }\n\n emitImpulse(type, options = {}) {\n this.impulses.push({\n type,\n x: clamp(Number(options.x) || 0, -1, 1),\n y: clamp(Number(options.y) || 0, -1, 1),\n age: 0,\n life: clamp(Number(options.life) || 1, 0.1, 4),\n energy: clamp01(Number(options.energy) || 0.7),\n });\n if (this.impulses.length > 12) this.impulses.shift();\n }\n\n registerGlyph(id, source, dimensions = {}) {\n if (!id || (!source && source !== 0)) {\n throw new TypeError(\"registerGlyph requires an id and sampler or bitmap.\");\n }\n if (typeof source === \"function\") {\n this.customGlyphs.set(id, source);\n return this;\n }\n\n const rows = Array.isArray(source) && Array.isArray(source[0]) ? source : null;\n const width = Math.max(1, dimensions.width || rows?.[0]?.length || 1);\n const height = Math.max(1, dimensions.height || rows?.length || 1);\n const flat = rows ? rows.flat() : Array.from(source);\n this.customGlyphs.set(id, (x, y) => {\n const px = Math.round(clamp01((x + 1) * 0.5) * (width - 1));\n const py = Math.round(clamp01((y + 1) * 0.5) * (height - 1));\n return clamp01(Number(flat[py * width + px]) || 0);\n });\n return this;\n }\n\n async loadGlyphFile(file, options = {}) {\n if (typeof document === \"undefined\") {\n throw new Error(\"Image glyph loading requires a browser.\");\n }\n const id = options.id || \"custom\";\n const resolution = Math.round(clamp(options.resolution || 48, 8, 128));\n const url = URL.createObjectURL(file);\n try {\n const image = await new Promise((resolve, reject) => {\n const element = new Image();\n element.onload = () => resolve(element);\n element.onerror = () => reject(new Error(\"The glyph image could not be decoded.\"));\n element.src = url;\n });\n const scratch = document.createElement(\"canvas\");\n scratch.width = resolution;\n scratch.height = resolution;\n const context = scratch.getContext(\"2d\", { willReadFrequently: true });\n const scale = Math.min(resolution / image.width, resolution / image.height) * 0.84;\n const width = image.width * scale;\n const height = image.height * scale;\n context.clearRect(0, 0, resolution, resolution);\n context.drawImage(\n image,\n (resolution - width) / 2,\n (resolution - height) / 2,\n width,\n height,\n );\n const pixels = context.getImageData(0, 0, resolution, resolution).data;\n const bitmap = new Float32Array(resolution * resolution);\n for (let index = 0; index < bitmap.length; index += 1) {\n const offset = index * 4;\n const luminance =\n (pixels[offset] * 0.2126 +\n pixels[offset + 1] * 0.7152 +\n pixels[offset + 2] * 0.0722) /\n 255;\n bitmap[index] = (pixels[offset + 3] / 255) * Math.max(0.3, luminance);\n }\n this.registerGlyph(id, bitmap, { width: resolution, height: resolution });\n const base = getSprite(options.baseSprite || \"ai.generating\") || this.currentRecipe;\n this.setSprite(\n {\n ...base,\n id,\n glyph: id,\n label: options.label || \"Custom glyph\",\n semantic: {\n ...(base.semantic || {}),\n label: options.label || \"Custom glyph\",\n category: \"custom\",\n },\n },\n { transition: options.transition || \"field-morph\" },\n );\n return id;\n } finally {\n URL.revokeObjectURL(url);\n }\n }\n\n sampleCustomGlyph(id, x, y, time, context) {\n const sampler = this.customGlyphs.get(id);\n return sampler\n ? clamp01(Number(sampler(x, y, time, context)) || 0)\n : sampleGlyph(id, x, y, time, context);\n }\n\n scheduleFrame() {\n if (\n !this.running ||\n this.destroyed ||\n !this.visible ||\n this.frameHandle !== null ||\n typeof requestAnimationFrame !== \"function\"\n ) {\n return;\n }\n this.frameHandle = requestAnimationFrame(this.frameTick);\n }\n\n cancelScheduledFrame() {\n if (\n this.frameHandle !== null &&\n typeof cancelAnimationFrame === \"function\"\n ) {\n cancelAnimationFrame(this.frameHandle);\n }\n this.frameHandle = null;\n }\n\n play() {\n if (this.running || this.destroyed || this.reducedMotion) return this;\n this.running = true;\n this.lastFrameTime = null;\n if (!this.frameTick) {\n this.frameTick = (timestamp) => {\n this.frameHandle = null;\n if (!this.running || this.destroyed || !this.visible) return;\n const now = timestamp / 1000;\n if (this.lastFrameTime === null) {\n this.lastFrameTime = now;\n this.scheduleFrame();\n return;\n }\n const minInterval = 1 / clamp(Number(this.options.fps) || 60, 1, 120);\n const elapsed = now - this.lastPaintTime;\n if (elapsed + 0.002 >= minInterval) {\n const dt = Math.max(0, now - this.lastFrameTime);\n this.lastFrameTime = now;\n this.lastPaintTime = now;\n this.renderFrame(now, dt);\n }\n this.scheduleFrame();\n };\n }\n this.scheduleFrame();\n this.emit(\"play\", { sprite: this.currentRecipe.id });\n return this;\n }\n\n pause() {\n if (!this.running) return this;\n this.running = false;\n this.cancelScheduledFrame();\n this.emit(\"pause\", { sprite: this.currentRecipe.id });\n return this;\n }\n\n toggle() {\n return this.running ? this.pause() : this.play();\n }\n\n updateDynamics(dt) {\n const speed = this.options.speed;\n this.clock += dt;\n this.phase += dt * speed;\n const toolHold = clamp01(Number(this.semanticContext.stepHold) || 0);\n this.signalState.toolPhase += dt * speed * (1 - toolHold);\n this.stateElapsed += dt;\n this.transitionElapsed += dt;\n this.pointer.x = expEase(\n this.pointer.x,\n this.pointer.inside ? this.pointer.targetX : 0,\n dt,\n 0.13,\n );\n this.pointer.y = expEase(\n this.pointer.y,\n this.pointer.inside ? this.pointer.targetY : 0,\n dt,\n 0.13,\n );\n this.pointer.speed = expEase(this.pointer.speed, 0, dt, 0.2);\n const pressConfig = this.currentRecipe?.interactions?.press || {};\n const pressStrength = clamp(Number(pressConfig.strength ?? 0.07), 0, 1);\n const pressDuration = clamp(\n Number(pressConfig.durationMs) / 1000 || 0.18,\n 0.08,\n 1.2,\n );\n this.press = expEase(\n this.press,\n this.pointer.down ? clamp(0.2 + pressStrength * 3.6, 0.2, 1) : 0,\n dt,\n pressDuration * 0.72,\n );\n this.audioLevel = expEase(this.audioLevel, this.audioTarget, dt, 0.09);\n this.audioTarget = expEase(this.audioTarget, 0, dt, 0.28);\n this.activityEnergy = expEase(this.activityEnergy, 0, dt, 0.5);\n this.signalState.retry = expEase(this.signalState.retry, 0, dt, 0.75);\n this.signalState.signalStrength = expEase(\n this.signalState.signalStrength,\n 0,\n dt,\n 0.5,\n );\n\n if (this.impulses.length) {\n for (const impulse of this.impulses) impulse.age += dt;\n this.impulses = this.impulses.filter((impulse) => impulse.age < impulse.life);\n }\n for (const [name, signal] of this.signals) {\n signal.age += dt;\n if (signal.age >= signal.life) this.signals.delete(name);\n }\n for (const [mapsTo, semanticSignal] of this.semanticSignals) {\n if (\n semanticSignal.decaySeconds > 0 &&\n typeof semanticSignal.value === \"number\"\n ) {\n semanticSignal.value = expEase(\n semanticSignal.value,\n 0,\n dt,\n semanticSignal.decaySeconds,\n );\n if (Math.abs(semanticSignal.value) < 0.001) semanticSignal.value = 0;\n this.semanticContext[mapsTo] = semanticSignal.value;\n }\n }\n\n if (\n this.previousRecipe !== this.currentRecipe &&\n this.completedTransitionSerial !== this.transitionSerial &&\n this.transitionElapsed >= this.transitionDuration\n ) {\n this.finishTransition(\"completed\");\n }\n\n if (\n this.pendingSpriteTransition &&\n this.transitionElapsed >= this.transitionDuration\n ) {\n const pending = this.pendingSpriteTransition;\n this.pendingSpriteTransition = null;\n this.setSprite(pending.sprite, {\n ...pending.options,\n forceTransition: true,\n });\n }\n\n const timeline = this.currentRecipe?.timeline;\n const timelineEnd =\n (Number(timeline?.durationMs) + Number(timeline?.holdMs || 0)) / 1000;\n if (\n timeline?.next &&\n Number.isFinite(timelineEnd) &&\n timelineEnd > 0 &&\n this.stateElapsed >= timelineEnd\n ) {\n const completed = this.currentRecipe;\n this.setSprite(timeline.next, {\n duration:\n Number(completed?.transition?.exitDurationMs) / 1000 || undefined,\n transition: completed?.transition?.exit || \"field-morph\",\n forceTransition: true,\n });\n this.emit(\"timelinecomplete\", {\n from: completed.id,\n to: this.currentRecipe.id,\n });\n }\n }\n\n recipeMetadata(recipe) {\n const key = recipe && typeof recipe === \"object\" ? recipe : this.currentRecipe;\n const cached = this.recipeMetadataCache.get(key);\n if (cached) return cached;\n const meaning = String(key?.semantic?.meaning || \"\").toLowerCase();\n const category = String(key?.semantic?.category || \"\").toLowerCase();\n const metadata = {\n meaning,\n category,\n toolAction: /executing a tool|tool action/.test(meaning),\n deliberateReasoning: /deliberate.*reasoning/.test(meaning),\n voiceOutput:\n /voice output|speaking/.test(meaning) ||\n Number(key?.composition?.voice) > 0,\n listening: /capturing voice|listening/.test(meaning),\n presence:\n category === \"presence\" ||\n /calm.*presence|ready and available/.test(meaning),\n ambientReasoning: category === \"ambient\" && /reasoning/.test(meaning),\n };\n this.recipeMetadataCache.set(key, metadata);\n return metadata;\n }\n\n resolvedPixelSwitch(recipe = this.currentRecipe) {\n const override = this.options.pixelSwitch;\n if (\n this.pixelSwitchCache.recipe !== recipe ||\n this.pixelSwitchCache.override !== override\n ) {\n this.pixelSwitchCache.recipe = recipe;\n this.pixelSwitchCache.override = override;\n this.pixelSwitchCache.value = recipePixelSwitch(recipe, override);\n }\n return this.pixelSwitchCache.value;\n }\n\n semanticEnvelope(recipe, time, metadata = this.recipeMetadata(recipe)) {\n const cacheKey = recipe && typeof recipe === \"object\" ? recipe : this.currentRecipe;\n let cached = this.semanticEnvelopeCache.get(cacheKey);\n if (cached?.time === time && cached.audioLevel === this.audioLevel) {\n return cached.value;\n }\n const gentleBreath =\n 0.6 * Math.sin(time * 0.9) + 0.4 * Math.sin(time * 0.333 + 1.3);\n const syllable =\n 0.5 + 0.5 * Math.sin(time * 9) * Math.sin(time * 3.7 + 1.1);\n const gate = Math.max(\n 0,\n Math.sin(time * 2.3) + 0.35 * Math.sin(time * 5.9),\n );\n const voice = (0.25 + 0.75 * syllable) * Math.min(1, gate);\n let value;\n if (metadata.voiceOutput) {\n value = clamp01(this.audioLevel || voice * 0.8);\n } else if (metadata.listening) {\n value = clamp01(this.audioLevel * 0.8 + 0.2);\n } else if (metadata.presence) {\n value = 0.5 + gentleBreath * 0.14;\n } else if (\n metadata.deliberateReasoning ||\n metadata.ambientReasoning\n ) {\n const cycle = fract(time * 0.22);\n value = cycle < 0.45\n ? 0.5 - 0.5 * Math.cos(Math.PI * (cycle / 0.45))\n : 0.5 + 0.5 * Math.cos(Math.PI * ((cycle - 0.45) / 0.55));\n } else {\n value = 0.5 + gentleBreath * 0.08;\n }\n if (!cached) {\n cached = { time, value, audioLevel: this.audioLevel };\n this.semanticEnvelopeCache.set(cacheKey, cached);\n } else {\n cached.time = time;\n cached.value = value;\n cached.audioLevel = this.audioLevel;\n }\n return value;\n }\n\n orbVisualRadius(recipe, time = this.clock) {\n const composition = recipe?.composition || {};\n if (composition.gestaltBoundary !== true) return 0;\n\n if (composition.mode === \"field-orb\") {\n const baseRadius = clamp(Number(composition.radius) || 0.86, 0.45, 0.98);\n const breathe = clamp(Number(composition.breathe) || 0, 0, 0.22);\n const envelope = this.semanticEnvelope(\n recipe,\n time,\n this.recipeMetadata(recipe),\n );\n return baseRadius * (1 + (envelope - 0.5) * breathe * 0.7);\n }\n\n const cycleMs =\n Number(recipe?.timeline?.macroCycleMs) ||\n Number(recipe?.timeline?.durationMs) ||\n 14400;\n const phase = this.reducedMotion\n ? clamp01(Number(recipe?.reducedMotion?.phase) || 0.18)\n : fract(time / Math.max(0.001, cycleMs / 1000));\n const breath = Math.sin(phase * TAU * 2 - Math.PI / 2);\n const baseRadius = clamp(Number(composition.radius) || 0.67, 0.45, 0.98);\n return baseRadius + breath * 0.022;\n }\n\n orbBackgroundRadius(recipe, time = this.clock) {\n const radius = this.orbVisualRadius(recipe, time);\n if (radius <= 0) return 0;\n const composition = recipe?.composition || {};\n const normalizedCell = 2 / Math.max(1, this.gridSize);\n const halfPixelDiagonal =\n normalizedCell * recipePixelScale(recipe) * (Math.SQRT2 / 2);\n const clearance = normalizedCell * 0.25;\n const motionClearance =\n composition.mode === \"field-orb\"\n ? clamp(Number(composition.radius) || 0.86, 0.45, 0.98) *\n clamp(Number(composition.breathe) || 0, 0, 0.22) *\n 0.7\n : recipe?.glyph === \"idle\"\n ? 0.044\n : 0;\n return Math.min(\n 1.1,\n radius + motionClearance + halfPixelDiagonal + clearance,\n );\n }\n\n impulseAt(x, y) {\n let energy = 0;\n for (const impulse of this.impulses) {\n const distance = Math.hypot(x - impulse.x, y - impulse.y);\n const life = 1 - impulse.age / impulse.life;\n const wave = Math.exp(\n -Math.abs(distance - impulse.age * (0.7 + impulse.energy * 0.5)) * 18,\n );\n energy += wave * life * impulse.energy;\n }\n return clamp01(energy);\n }\n\n recipeSample(recipe, x, y, time, index, result = null) {\n const shellEnergy = clamp01(Number(this.semanticContext.shellEnergy) || 0);\n const metadata = this.recipeMetadata(recipe);\n const baseTime = metadata.toolAction ? this.signalState.toolPhase : time;\n const semanticSpeed =\n metadata.deliberateReasoning\n ? 1 + shellEnergy * 0.55\n : 1;\n const recipeTime =\n baseTime *\n clamp(Number(recipe?.speed) || 1, 0.02, 8) *\n semanticSpeed;\n const pointerConfig = recipe?.interactions?.pointer;\n const pointerEnabled =\n this.options.interactive &&\n this.gridSize >= Number(pointerConfig?.minSize ?? 20) &&\n !this.reducedMotion &&\n recipe?.interactions !== false &&\n pointerConfig?.enabled !== false;\n const pointerStrength = clamp(Number(pointerConfig?.strength ?? 0.16), 0, 1);\n const pointerEffect = pointerConfig?.effect || \"steer-light\";\n const pointerProfile = POINTER_EFFECT_PROFILES[pointerEffect] || {\n warp: 0.35,\n light: 0.5,\n surface: 0.45,\n };\n const pointer = this.pointerSampleScratch;\n pointer.x = pointerEnabled ? this.pointer.x : 0;\n pointer.y = pointerEnabled ? this.pointer.y : 0;\n pointer.speed = pointerEnabled ? this.pointer.speed : 0;\n const envelope = this.semanticEnvelope(recipe, time, metadata);\n const distanceToPointer = Math.hypot(x - pointer.x, y - pointer.y);\n const pointerInfluence =\n pointerEnabled && this.pointer.inside\n ? Math.max(0, 1 - distanceToPointer / 1.15)\n : 0;\n const warp =\n pointerInfluence *\n pointerStrength *\n (0.075 + pointer.speed * 0.15) *\n pointerProfile.warp;\n const sampleX = x - (x - pointer.x) * warp;\n const sampleY = y - (y - pointer.y) * warp;\n const impulse = this.impulseAt(x, y);\n const context = this.sampleContextScratch;\n context.accepted = this.signalState.accepted;\n context.age = this.stateElapsed;\n context.audioLevel = this.audioLevel;\n context.direction = this.signalState.direction;\n context.motionDirection = recipe?.pixelSwitch?.direction || \"field\";\n context.elapsed = this.stateElapsed;\n context.energy = clamp01(this.activityEnergy + impulse);\n context.envelope = envelope;\n context.handoffAccepted = this.signalState.accepted;\n context.hits = this.signalState.hits;\n context.glyphSampler = this.glyphSampler;\n context.pointer = pointer;\n context.pointerEffect = pointerEffect;\n context.pointerInfluence = pointerInfluence;\n context.press = this.press;\n context.pressEffect =\n recipe?.interactions?.press?.effect || \"spring-compress\";\n context.progress = this.progress;\n context.random = this.random[index];\n context.retry = this.signalState.retry;\n context.rotationSpeed = this.signalState.rotationSpeed;\n context.resolution = this.gridSize;\n context.segments = this.signalState.segments;\n context.size = this.gridSize;\n context.seed = this.seedHash;\n context.signalStrength = this.signalState.signalStrength;\n context.signals = this.signals;\n context.steps = this.signalState.steps;\n context.targetGlyph = this.signalState.targetGlyph;\n context.reducedMotion = this.reducedMotion;\n context.reducedPhase = recipe?.reducedMotion?.phase;\n const timelineCycleMs =\n Number(recipe?.timeline?.macroCycleMs) ||\n Number(recipe?.timeline?.durationMs) ||\n 1000;\n context.timelinePhase = this.reducedMotion\n ? clamp01(Number(recipe?.reducedMotion?.phase) || 0.25)\n : fract(time / Math.max(0.001, timelineCycleMs / 1000));\n Object.assign(context, this.semanticContext);\n const composition = recipe?.composition || {};\n const compositionMode = composition.mode || \"glyph\";\n const usesGestaltBoundary =\n composition.gestaltBoundary === true &&\n this.options.orbBoundary === \"gestalt\";\n context.orbBoundary = usesGestaltBoundary ? \"gestalt\" : \"defined\";\n const authoredGestaltOpenness = Number(composition.gestaltOpenness);\n context.gestaltOpenness = clamp(\n Number.isFinite(authoredGestaltOpenness)\n ? authoredGestaltOpenness\n : 0.5,\n 0,\n 1,\n );\n const radius = Math.hypot(sampleX, sampleY);\n const baseOrbRadius = clamp(Number(composition.radius) || 0.86, 0.45, 0.98);\n const breathe = clamp(Number(composition.breathe) || 0, 0, 0.22);\n const orbRadius =\n baseOrbRadius * (1 + (envelope - 0.5) * breathe * 0.7);\n const orbEdge = clamp(1.15 / this.gridSize, 0.022, 0.095);\n const orbMask =\n 1 - smoothRange(orbRadius - orbEdge, orbRadius + orbEdge, radius);\n const glyphId = recipe?.glyph || \"idle\";\n const sampledGlyph =\n compositionMode === \"field-orb\"\n ? orbMask\n : this.sampleCustomGlyph(\n glyphId,\n sampleX,\n sampleY,\n recipeTime,\n context,\n );\n const glyph = clamp01(sampledGlyph);\n const layers =\n Array.isArray(recipe?.fieldMix) && recipe.fieldMix.length\n ? recipe.fieldMix\n : [\n {\n field:\n recipe?.field ||\n recipe?.fields?.[0]?.id ||\n recipe?.fields?.[0]?.type ||\n \"fbm\",\n weight: 1,\n scale: 1,\n speed: 1,\n phase: 0,\n contrast: 1,\n blend: \"replace\",\n },\n ];\n let field = 0.5;\n let accumulatedWeight = 0;\n for (let layerIndex = 0; layerIndex < layers.length; layerIndex += 1) {\n const layer = layers[layerIndex];\n const weight = clamp(Number(layer.weight) || 0, 0, 1);\n const scale = clamp(Number(layer.scale) || 1, 0.05, 24);\n const layerTime = recipeTime;\n const fieldOptions = Object.assign(this.fieldOptionsScratch, context, layer);\n let sampled = this.fieldKit.sample(\n layer.field || layer.id || layer.type || recipe?.field || \"fbm\",\n sampleX * scale,\n sampleY * scale,\n layerTime,\n fieldOptions,\n );\n sampled = clamp01(sampled);\n const blend = layer.blend || \"add\";\n if (accumulatedWeight === 0) {\n // The primary layer establishes the full 0..1 range. Weight describes\n // how much later layers may influence it, not how much it is dimmed.\n field = sampled;\n } else if (blend === \"replace\") {\n field = field * (1 - weight) + sampled * weight;\n } else if (blend === \"screen\") {\n const screened = 1 - (1 - field) * (1 - sampled);\n field = field * (1 - weight) + screened * weight;\n } else if (blend === \"multiply\") {\n const multiplied = field * sampled;\n field = field * (1 - weight) + multiplied * weight;\n } else if (blend === \"max\" || blend === \"lighten\") {\n field = field * (1 - weight) + Math.max(field, sampled) * weight;\n } else if (blend === \"soft-light\") {\n const softened =\n sampled < 0.5\n ? field - (1 - 2 * sampled) * field * (1 - field)\n : field +\n (2 * sampled - 1) *\n (Math.sqrt(Math.max(0, field)) - field);\n field = field * (1 - weight) + softened * weight;\n } else {\n // Add detail around the midpoint so an additive layer creates texture\n // without washing the entire stack toward white.\n field += (sampled - 0.5) * weight * 1.35;\n }\n accumulatedWeight += weight;\n }\n const fieldContrast = clamp(\n Number(composition.fieldContrast) ||\n Number(recipe?.fieldContrast) ||\n 1.16,\n 0.5,\n 3,\n );\n field = clamp01((field - 0.5) * fieldContrast + 0.5);\n const radialWindow = clamp01((1.12 - radius) / 0.22);\n const ambientMix = clamp(\n Number(\n recipe?.ambientMix ??\n recipe?.fields?.[0]?.mix ??\n 0.055 + (Number(recipe?.density) || 0.72) * 0.1,\n ),\n 0,\n 0.4,\n );\n const detail = clamp(\n Number(\n composition.fieldDetail ??\n recipe?.detail ??\n recipe?.fields?.[0]?.detail ??\n 0.68,\n ),\n 0,\n 1.5,\n );\n const pressStrength = clamp(\n Number(recipe?.interactions?.press?.strength ?? 0.07),\n 0,\n 1,\n );\n const pressEffect =\n recipe?.interactions?.press?.effect || \"spring-compress\";\n const pressProfile = PRESS_EFFECT_PROFILES[pressEffect] || { surface: 0.55 };\n const interaction =\n impulse *\n radialWindow *\n clamp(pressStrength * 6.85, 0, 1) *\n pressProfile.surface +\n pointerInfluence *\n pointerStrength *\n 0.22 *\n pointerProfile.surface;\n const recipeThreshold = clamp(Number(recipe?.threshold) || 0.5, 0, 1);\n const recipeDensity = clamp(Number(recipe?.density) || 0.72, 0, 1.4);\n const recipeBias = (0.5 - recipeThreshold) * 0.32 + (recipeDensity - 0.72) * 0.1;\n\n let raw;\n if (compositionMode === \"field-orb\") {\n const normalizedX = sampleX / Math.max(0.001, orbRadius);\n const normalizedY = sampleY / Math.max(0.001, orbRadius);\n const normalZ = Math.sqrt(\n Math.max(0, 1 - normalizedX * normalizedX - normalizedY * normalizedY),\n );\n const lighting = composition.lighting || \"sphere\";\n let shade = 0;\n if (lighting !== \"flat\") {\n let lightX =\n lighting === \"crescent\"\n ? -0.92\n : lighting === \"gibbous\"\n ? -0.55\n : -0.42;\n let lightY =\n lighting === \"crescent\"\n ? -0.24\n : lighting === \"gibbous\"\n ? -0.3\n : -0.48;\n let lightZ =\n lighting === \"crescent\"\n ? 0.32\n : lighting === \"gibbous\"\n ? 0.82\n : 0.76;\n const lightOrbit = clamp(Number(composition.lightOrbit) || 0, 0, 0.8);\n lightX += Math.cos(recipeTime * 0.6) * lightOrbit;\n lightY += Math.sin(recipeTime * 0.43) * lightOrbit;\n if (pointerEnabled && this.pointer.inside) {\n const lightSteer =\n clamp(pointerStrength / 0.16, 0, 2) *\n 0.28 *\n pointerProfile.light;\n lightX += pointer.x * lightSteer;\n lightY += pointer.y * lightSteer;\n }\n const lightLength = Math.hypot(lightX, lightY, lightZ) || 1;\n lightX /= lightLength;\n lightY /= lightLength;\n lightZ /= lightLength;\n shade = Math.max(\n 0,\n normalizedX * lightX + normalizedY * lightY + normalZ * lightZ,\n );\n shade = Math.pow(shade, lighting === \"crescent\" ? 1.58 : 0.88);\n }\n const centeredField = (field - 0.5) * detail;\n const voiceGlow =\n clamp(Number(composition.voice) || 0, 0, 0.5) * envelope * 0.62;\n const breathGlow = breathe * (envelope - 0.5) * 0.52;\n const surface =\n lighting === \"flat\"\n ? 0.5 + centeredField * 0.92 + voiceGlow + breathGlow\n : lighting === \"crescent\"\n ? shade * (0.76 + centeredField * 1.08 + voiceGlow) + breathGlow\n : 0.15 +\n shade * 0.58 +\n centeredField * 0.72 +\n voiceGlow +\n breathGlow;\n if (usesGestaltBoundary) {\n const cell = 2 / this.gridSize;\n const boundaryBand = clamp(cell * 3.05, 0.055, 0.2);\n const arcSupport = gestaltArcSupport(\n Math.atan2(sampleY, sampleX),\n context,\n context.gestaltOpenness,\n );\n const edgeBand =\n smoothRange(\n orbRadius - boundaryBand * 1.14,\n orbRadius - boundaryBand * 0.4,\n radius,\n ) * orbMask;\n const renderedSurface = surface * orbMask;\n const cueNeed =\n 1 -\n smoothRange(0.42, 0.74, clamp01(surface + recipeBias));\n const impliedEdge =\n edgeBand *\n arcSupport *\n (0.47 + field * 0.14 + shade * 0.05) *\n cueNeed;\n raw = clamp01(\n Math.max(renderedSurface, impliedEdge) +\n interaction +\n recipeBias,\n );\n } else {\n const authoredRim = Number(composition.rim);\n const rimWidth = clamp(\n Number.isFinite(authoredRim) ? authoredRim : 0.045,\n 0.012,\n 0.12,\n );\n const rimDistance = Math.abs(radius - orbRadius);\n const rim =\n 1 - smoothRange(rimWidth, rimWidth + orbEdge, rimDistance);\n const rimValue = rim * (0.46 + field * 0.18);\n raw = clamp01(\n Math.max(surface * orbMask, rimValue) + interaction + recipeBias,\n );\n }\n } else {\n const crispGlyph = smoothRange(0.16, 0.76, glyph);\n const scaffoldDensity = clamp(\n Number(recipe?.pixel?.silhouetteFloor) || 0.38,\n 0.12,\n 0.72,\n );\n const gx = index % this.gridSize;\n const gy = Math.floor(index / this.gridSize);\n const scaffoldThreshold = bayer8(\n gx + (this.seedHash & 7),\n gy + ((this.seedHash >>> 3) & 7),\n );\n const coreGlyph = smoothRange(0.56, 0.9, glyph);\n const scaffold =\n scaffoldThreshold < scaffoldDensity\n ? coreGlyph * (0.9 + field * 0.08)\n : 0;\n const texturedFill = clamp01(0.1 + field * (0.82 + detail * 0.32));\n const faintCoverage =\n Math.min(glyph, 0.5) * (0.68 + field * 0.18);\n const surface = Math.max(\n faintCoverage,\n crispGlyph * texturedFill,\n scaffold,\n );\n const ambient =\n field *\n radialWindow *\n ambientMix *\n (1 - crispGlyph * 0.82) *\n (0.72 + envelope * 0.24 + impulse * 0.5);\n raw = clamp01(\n Math.max(surface, ambient) + interaction + recipeBias,\n );\n }\n const output = result || {};\n output.raw = raw;\n output.field = field;\n output.glyph = glyph;\n output.envelope = envelope;\n output.impulse = impulse;\n output.pointerInfluence = pointerInfluence;\n return output;\n }\n\n directionalThreshold(direction, x, y, time) {\n switch (direction) {\n case \"inward\":\n case \"reverse-to-center\":\n return 1 - clamp01(Math.hypot(x, y) / Math.SQRT2);\n case \"radial\":\n case \"radial-tail\":\n case \"outward-audio\":\n case \"outward-beacon\":\n case \"bounded-burst\":\n case \"core-to-target\":\n case \"shell-to-shell\":\n return clamp01(Math.hypot(x, y) / Math.SQRT2);\n case \"radar-sweep\":\n return fract(\n fract((Math.atan2(y, x) + Math.PI) / TAU) - time * 0.12,\n );\n case \"bottom-to-contour\":\n return 1 - clamp01((y + 1) * 0.5);\n case \"diagonal-fracture\":\n case \"source-to-destination\":\n case \"tail-to-head\":\n return (\n clamp01((x + 1) * 0.5) + clamp01((y + 1) * 0.5)\n ) * 0.5;\n case \"counter-flow\":\n return 1 - clamp01((x + 1) * 0.5);\n case \"columns\":\n case \"gear-step\":\n return fract(clamp01((x + 1) * 0.5) * 8);\n case \"aperture\":\n case \"bridge-and-retreat\":\n return Math.abs(clamp01((x + 1) * 0.5) - 0.5) * 2;\n case \"data-direction\":\n if (this.signalState.direction === \"up\") {\n return clamp01((y + 1) * 0.5);\n }\n if (this.signalState.direction === \"left\") {\n return clamp01((x + 1) * 0.5);\n }\n if (this.signalState.direction === \"right\") {\n return 1 - clamp01((x + 1) * 0.5);\n }\n return 1 - clamp01((y + 1) * 0.5);\n case \"audio-surface\":\n case \"contour-forward\":\n case \"surface-drift\":\n return fract(\n clamp01((x + 1) * 0.5) * 0.55 +\n clamp01((y + 1) * 0.5) * 0.3 -\n time * 0.08,\n );\n case \"field\":\n default:\n return 0.5;\n }\n }\n\n switchThreshold(config, index, x, y, time, raw) {\n const mode = config.mode;\n if (mode === \"threshold-hysteresis\") return 0.5;\n const random = this.random[index];\n if (mode === \"seeded-dissolve\") return random;\n const size = this.gridSize;\n const gx = index % size;\n const gy = Math.floor(index / size);\n const temporalRate = clamp(Number(config.temporalRate ?? 1), 0, 8);\n const phase = Math.floor(time * 12 * temporalRate) & 15;\n const dither = String(config.dither || \"bayer8\");\n const ordered =\n dither === \"blue-noise64\"\n ? hash01(\n (gx + gy * 64) ^\n this.seedHash ^\n (dither.startsWith(\"temporal\") ? phase * 0x9e3779b9 : 0),\n )\n : bayer8(gx, gy);\n const directional = this.directionalThreshold(\n config.direction || \"field\",\n x,\n y,\n time,\n );\n const directedOrdered = clamp01(ordered * 0.86 + directional * 0.14);\n\n switch (mode) {\n case \"temporal-blue-noise\":\n return clamp01(\n directedOrdered * 0.52 +\n hash01(index ^ this.seedHash ^ (phase * 0x9e3779b9)) * 0.48,\n );\n case \"sdf-wavefront\":\n return clamp01(\n directedOrdered * 0.78 +\n Math.abs(\n Math.hypot(x, y) / Math.SQRT2 -\n fract(time * 0.35 * temporalRate),\n ) * 0.22,\n );\n case \"contour-trace\":\n return clamp01(\n directedOrdered * 0.82 + Math.abs(fract(raw * 4) - 0.5) * 0.18,\n );\n case \"curl-advect\":\n return clamp01(\n directedOrdered +\n Math.sin(x * 8 + y * 5 - time * 2 * temporalRate) * 0.09,\n );\n case \"neighbor-propagation\":\n return clamp01(directedOrdered - this.neighborBuffer[index] * 0.16);\n case \"radial-cascade\":\n return clamp01(\n directedOrdered * 0.8 +\n (Math.hypot(x, y) / Math.SQRT2) * 0.2,\n );\n case \"path-draw\":\n return clamp01(directedOrdered * 0.78 + ((x + y + 2) * 0.25) * 0.22);\n case \"axis-flip\":\n return clamp01(directedOrdered * 0.86 + (gx % 2) * 0.08);\n case \"field-morph\":\n return clamp01(directedOrdered * 0.68 + random * 0.32);\n case \"ordered-dither\":\n default:\n return directedOrdered;\n }\n }\n\n updateNeighborBuffer(neighborhood = 4) {\n const size = this.gridSize;\n const diagonalWeight = clamp((Number(neighborhood) - 4) / 4, 0, 1);\n for (let y = 0; y < size; y += 1) {\n for (let x = 0; x < size; x += 1) {\n const index = y * size + x;\n let total = 0;\n let count = 0;\n if (x > 0) {\n total += this.gates[index - 1];\n count += 1;\n }\n if (x < size - 1) {\n total += this.gates[index + 1];\n count += 1;\n }\n if (y > 0) {\n total += this.gates[index - size];\n count += 1;\n }\n if (y < size - 1) {\n total += this.gates[index + size];\n count += 1;\n }\n if (diagonalWeight > 0) {\n if (x > 0 && y > 0) {\n total += this.gates[index - size - 1] * diagonalWeight;\n count += diagonalWeight;\n }\n if (x < size - 1 && y > 0) {\n total += this.gates[index - size + 1] * diagonalWeight;\n count += diagonalWeight;\n }\n if (x > 0 && y < size - 1) {\n total += this.gates[index + size - 1] * diagonalWeight;\n count += diagonalWeight;\n }\n if (x < size - 1 && y < size - 1) {\n total += this.gates[index + size + 1] * diagonalWeight;\n count += diagonalWeight;\n }\n }\n this.neighborBuffer[index] = count ? total / count : 0;\n }\n }\n }\n\n evaluateGrid(dt, options = {}) {\n const recipe = this.currentRecipe;\n const previous = this.previousRecipe || recipe;\n const samplingStride = options.forceFull\n ? 1\n : this.qualityTier === \"low\"\n ? 3\n : this.qualityTier === \"balanced\"\n ? 2\n : 1;\n const samplingPhase = samplingStride === 1\n ? 0\n : this.qualitySamplePhase % samplingStride;\n if (samplingStride > 1) {\n this.qualitySamplePhase = (samplingPhase + 1) % samplingStride;\n } else {\n this.qualitySamplePhase = 0;\n }\n const transitionProgress =\n this.transitionDuration <= 0\n ? 1\n : clamp01(this.transitionElapsed / this.transitionDuration);\n const pixelSwitch = this.resolvedPixelSwitch(recipe);\n const densityBias = (this.options.density - 1) * 0.17;\n const contrast = this.options.contrast;\n const hysteresis = clamp(\n Number(\n recipe?.hysteresis ??\n recipe?.pixelSwitch?.hysteresis ??\n recipe?.pixel?.hysteresis ??\n 0.055,\n ),\n 0.005,\n 0.2,\n );\n const rise = clamp(\n Number(recipe?.rise ?? recipe?.pixel?.rise ?? 0.075),\n 0.025,\n 0.5,\n );\n const fall = clamp(\n Number(recipe?.fall ?? recipe?.pixel?.fall ?? 0.14),\n 0.025,\n 0.7,\n );\n const origin =\n this.pointer.inside && !this.reducedMotion\n ? { x: this.pointer.x, y: this.pointer.y }\n : { x: 0, y: 0 };\n\n if (pixelSwitch.mode === \"neighbor-propagation\") {\n this.updateNeighborBuffer(pixelSwitch.neighborhood);\n }\n const maximumFlashes = clamp(\n Number(pixelSwitch.maxContrastFlashesPerSecond ?? 3),\n 0.5,\n 12,\n );\n const minimumDwell = 0.5 / maximumFlashes;\n\n let activePixels = 0;\n let sampledPixels = 0;\n for (let index = 0; index < this.values.length; index += 1) {\n this.dwell[index] = Math.max(0, this.dwell[index] - dt);\n if (samplingStride === 1 || index % samplingStride === samplingPhase) {\n sampledPixels += 1;\n const x = this.xs[index];\n const y = this.ys[index];\n const to = this.recipeSample(\n recipe,\n x,\n y,\n this.phase,\n index,\n this.sampleToScratch,\n );\n let raw = to.raw;\n let field = to.field;\n let localProgress = 1;\n if (transitionProgress < 1 && previous !== recipe) {\n const from = this.recipeSample(\n previous,\n x,\n y,\n this.phase,\n index,\n this.sampleFromScratch,\n );\n const delay = transitionDelay(\n this.transitionName,\n x,\n y,\n this.random[index],\n origin,\n );\n localProgress = this.reducedMotion\n ? easeInOut(transitionProgress)\n : easeInOut((transitionProgress - delay * 0.58) / 0.42);\n raw = from.raw + (to.raw - from.raw) * localProgress;\n field = from.field + (to.field - from.field) * localProgress;\n }\n this.transitionMixes[index] = localProgress;\n raw = clamp01((raw - 0.5) * contrast + 0.5 + densityBias);\n this.luminance[index] = clamp01(field * 0.45 + raw * 0.55);\n\n const threshold = this.switchThreshold(\n pixelSwitch,\n index,\n x,\n y,\n this.phase,\n raw,\n );\n const gate = this.gates[index];\n if (this.dwell[index] <= 0) {\n if (!gate && raw > threshold + hysteresis) {\n this.gates[index] = 1;\n this.dwell[index] = 0.045 + this.random[index] * 0.075;\n this.afterglow[index] = 1;\n } else if (\n gate &&\n !pixelSwitch.monotonic &&\n raw < threshold - hysteresis\n ) {\n this.gates[index] = 0;\n this.dwell[index] = Math.max(\n minimumDwell,\n 0.06 + this.random[index] * 0.1,\n );\n }\n }\n }\n\n this.afterglow[index] = Math.max(\n this.gates[index],\n this.afterglow[index] * Math.exp(-dt / fall),\n );\n const target = this.gates[index] ? 1 : this.afterglow[index] * 0.18;\n this.targets[index] = target;\n const tau = target > this.values[index] ? rise : fall;\n const stiffness = 1 - Math.exp(-dt / tau);\n const damping = Math.exp(-dt * (target > this.values[index] ? 11 : 8));\n this.velocities[index] =\n (this.velocities[index] + (target - this.values[index]) * stiffness * 22) *\n damping;\n this.values[index] = clamp01(\n this.values[index] + this.velocities[index] * dt,\n );\n if (this.values[index] > 0.32) activePixels += 1;\n }\n this.stats.activePixels = activePixels;\n this.stats.sampledPixels = sampledPixels;\n }\n\n settleGrid(time = this.clock, options = {}) {\n const requestedTime = Number.isFinite(Number(time)) ? Number(time) : this.clock;\n let representativeTime = requestedTime;\n if (this.reducedMotion && requestedTime === 0) {\n const reducedPhase = clamp(\n Number(this.currentRecipe?.reducedMotion?.phase) || 0.25,\n 0,\n 1,\n );\n const cycleMs =\n Number(this.currentRecipe?.timeline?.macroCycleMs) ||\n Number(this.currentRecipe?.timeline?.durationMs) ||\n 4000;\n representativeTime = (cycleMs / 1000) * reducedPhase;\n }\n this.clock = requestedTime;\n this.phase = representativeTime * this.options.speed;\n this.transitionElapsed = this.transitionDuration;\n this.dwell.fill(0);\n this.evaluateGrid(1 / 60, { forceFull: true });\n\n let activePixels = 0;\n for (let index = 0; index < this.values.length; index += 1) {\n const target = this.gates[index] ? 1 : 0;\n this.values[index] = target;\n this.targets[index] = target;\n this.velocities[index] = 0;\n this.afterglow[index] = target;\n if (target) activePixels += 1;\n }\n this.stats.activePixels = activePixels;\n if (options.completeTransition !== false) this.finishTransition(\"settled\");\n }\n\n drawPixel(shape, context, x, y, width, height, value, angle = 0) {\n const scale = 0.18 + value * 0.82;\n const halfWidth = (width * scale) / 2;\n const halfHeight = (height * scale) / 2;\n\n switch (shape) {\n case \"square\":\n context.fillRect(\n x - halfWidth,\n y - halfHeight,\n halfWidth * 2,\n halfHeight * 2,\n );\n break;\n case \"diamond\":\n context.save();\n context.translate(x, y);\n context.rotate(Math.PI / 4);\n context.fillRect(\n -halfWidth * 0.74,\n -halfHeight * 0.74,\n halfWidth * 1.48,\n halfHeight * 1.48,\n );\n context.restore();\n break;\n case \"capsule\": {\n const radius = Math.min(halfWidth, halfHeight);\n context.beginPath();\n context.roundRect?.(\n x - halfWidth,\n y - halfHeight * 0.64,\n halfWidth * 2,\n halfHeight * 1.28,\n radius,\n );\n if (!context.roundRect) {\n context.ellipse(x, y, halfWidth, halfHeight * 0.64, 0, 0, TAU);\n }\n context.fill();\n break;\n }\n case \"line\":\n context.save();\n context.translate(x, y);\n if (angle) context.rotate(angle);\n context.fillRect(-halfWidth, -halfHeight * 0.22, halfWidth * 2, halfHeight * 0.44);\n context.restore();\n break;\n case \"ring\":\n context.lineWidth = Math.max(0.12, Math.min(width, height) * 0.14);\n context.beginPath();\n context.ellipse(x, y, halfWidth, halfHeight, 0, 0, TAU);\n context.stroke();\n break;\n case \"cross\":\n context.fillRect(\n x - halfWidth,\n y - halfHeight * 0.2,\n halfWidth * 2,\n halfHeight * 0.4,\n );\n context.fillRect(\n x - halfWidth * 0.2,\n y - halfHeight,\n halfWidth * 0.4,\n halfHeight * 2,\n );\n break;\n case \"disc\":\n default:\n context.beginPath();\n context.ellipse(x, y, halfWidth, halfHeight, 0, 0, TAU);\n context.fill();\n break;\n }\n }\n\n paint() {\n const context = this.context;\n const width = this.width || this.canvas.width / (this.dpr || 1);\n const height = this.height || this.canvas.height / (this.dpr || 1);\n const transitionActive = this.isTransitionActive();\n const transitionProgress = transitionActive\n ? easeInOut(this.transitionElapsed / Math.max(0.001, this.transitionDuration))\n : 1;\n const targetPalette = activePalette(this.currentRecipe, this.options);\n const sourcePalette = transitionActive\n ? activePalette(this.previousRecipe || this.currentRecipe, this.options)\n : targetPalette;\n const palette = transitionActive\n ? mixPalette(sourcePalette, targetPalette, transitionProgress)\n : targetPalette;\n const pixelShape = normalizePixelShape(\n this.options.pixelShape ||\n recipeValue(this.currentRecipe, \"pixelShape\", \"disc\"),\n );\n const switchMode = this.resolvedPixelSwitch(this.currentRecipe).mode;\n const { stageSize, offsetX, offsetY, cellSize: cell } = canvasGridLayout(\n this.options.previewMode,\n this.gridSize,\n width,\n height,\n );\n const targetPixelScale = recipePixelScale(this.currentRecipe);\n const sourcePixelScale = transitionActive\n ? recipePixelScale(this.previousRecipe || this.currentRecipe)\n : targetPixelScale;\n const pixel =\n cell *\n (sourcePixelScale +\n (targetPixelScale - sourcePixelScale) * transitionProgress);\n\n context.save();\n context.setTransform(\n this.renderScaleX || this.dpr || 1,\n 0,\n 0,\n this.renderScaleY || this.dpr || 1,\n 0,\n 0,\n );\n context.clearRect(0, 0, width, height);\n if (this.options.background && palette.background !== \"transparent\") {\n context.fillStyle = palette.background;\n context.fillRect(0, 0, width, height);\n }\n context.imageSmoothingEnabled = false;\n\n const orbBackgroundColor = safeSvgColor(\n this.options.orbBackgroundColor,\n \"transparent\",\n );\n if (\n this.options.orbBackgroundMode !== \"none\" &&\n orbBackgroundColor !== \"transparent\"\n ) {\n const sourceRecipe = transitionActive\n ? this.previousRecipe || this.currentRecipe\n : this.currentRecipe;\n const sourceRadius = this.orbBackgroundRadius(sourceRecipe, this.clock);\n const targetRadius = this.orbBackgroundRadius(\n this.currentRecipe,\n this.clock,\n );\n const sourceVisible = sourceRadius > 0;\n const targetVisible = targetRadius > 0;\n const orbOpacity = transitionActive\n ? (sourceVisible ? 1 - transitionProgress : 0) +\n (targetVisible ? transitionProgress : 0)\n : targetVisible\n ? 1\n : 0;\n const orbRadius =\n sourceVisible && targetVisible\n ? sourceRadius + (targetRadius - sourceRadius) * transitionProgress\n : targetVisible\n ? targetRadius\n : sourceRadius;\n if (orbOpacity > 0.001 && orbRadius > 0) {\n context.globalAlpha = clamp01(orbOpacity);\n context.fillStyle = orbBackgroundColor;\n if (this.options.orbBackgroundMode === \"pixelated\") {\n drawPixelatedOrbBackground(\n context,\n this.gridSize,\n orbRadius,\n offsetX,\n offsetY,\n cell,\n );\n } else {\n context.beginPath();\n context.arc(\n offsetX + stageSize / 2,\n offsetY + stageSize / 2,\n (stageSize * orbRadius) / 2,\n 0,\n TAU,\n );\n context.fill();\n }\n context.globalAlpha = 1;\n }\n }\n\n if (this.options.offPixels) {\n const offColorKey = `${palette.off}|${palette.ink}`;\n if (this.paintColorCache.off?.key !== offColorKey) {\n this.paintColorCache.off = {\n key: offColorKey,\n value: mixColor(palette.off, palette.ink, 0.05, 0.58),\n };\n }\n context.fillStyle = this.paintColorCache.off.value;\n const offSize = Math.max(0.16, pixel * 0.18);\n for (let index = 0; index < this.values.length; index += 1) {\n const gx = index % this.gridSize;\n const gy = Math.floor(index / this.gridSize);\n const cx = offsetX + (gx + 0.5) * cell;\n const cy = offsetY + (gy + 0.5) * cell;\n context.fillRect(cx - offSize / 2, cy - offSize / 2, offSize, offSize);\n }\n }\n\n const colorKey = [\n sourcePalette.ink,\n sourcePalette.accent,\n targetPalette.ink,\n targetPalette.accent,\n ].join(\"|\");\n if (this.paintColorCache.key !== colorKey) {\n const source = Array.from({ length: 9 }, (_, index) =>\n mixColor(sourcePalette.ink, sourcePalette.accent, index / 8, 1),\n );\n const target = Array.from({ length: 9 }, (_, index) =>\n mixColor(targetPalette.ink, targetPalette.accent, index / 8, 1),\n );\n this.paintColorCache.key = colorKey;\n this.paintColorCache.source = source;\n this.paintColorCache.target = target;\n this.paintColorCache.steady = [target];\n this.paintColorCache.transition = null;\n }\n if (transitionActive && !this.paintColorCache.transition) {\n const source = this.paintColorCache.source;\n const target = this.paintColorCache.target;\n this.paintColorCache.transition = Array.from(\n { length: 9 },\n (_, transitionIndex) =>\n Array.from({ length: 9 }, (_, colorIndex) =>\n mixColor(\n source[colorIndex],\n target[colorIndex],\n transitionIndex / 8,\n 1,\n ),\n ),\n );\n }\n const transitionColorBins = transitionActive\n ? this.paintColorCache.transition\n : this.paintColorCache.steady;\n const glowFade = clamp((72 - this.gridSize) / 24, 0, 1);\n const glowEnabled =\n this.qualityTier !== \"low\" &&\n this.gridSize <= 48 &&\n glowFade > 0.04 &&\n recipeValue(this.currentRecipe, \"glow\", true) !== false &&\n String(palette.glow).trim().toLowerCase() !== \"transparent\";\n if (glowEnabled) {\n context.shadowColor = palette.glow;\n const qualityGlow = this.qualityTier === \"balanced\" ? 0.58 : 1;\n context.shadowBlur = Math.max(0.25, cell * 0.52 * glowFade * qualityGlow);\n }\n\n for (let index = 0; index < this.values.length; index += 1) {\n const value = this.values[index];\n if (value < 0.035) continue;\n const gx = index % this.gridSize;\n const gy = Math.floor(index / this.gridSize);\n const cx = offsetX + (gx + 0.5) * cell;\n const cy = offsetY + (gy + 0.5) * cell;\n const luminance = this.luminance[index];\n const renderedShape = resolvePixelShape(pixelShape, luminance);\n const bin = Math.min(8, Math.floor(luminance * 8.999));\n const alpha = clamp01(value * (0.72 + luminance * 0.28));\n context.globalAlpha = alpha;\n const transitionBin = transitionActive\n ? Math.min(8, Math.round(this.transitionMixes[index] * 8))\n : 0;\n const color = transitionColorBins[transitionBin][bin];\n context.fillStyle = color;\n context.strokeStyle = color;\n\n let widthScale = 1;\n let heightScale = 1;\n if (\n switchMode === \"axis-flip\" ||\n (transitionActive && this.transitionName === \"axis-flip\")\n ) {\n heightScale = Math.max(0.07, Math.sin(value * Math.PI * 0.5));\n } else if (switchMode === \"path-draw\") {\n widthScale = 0.35 + value * 0.65;\n } else if (switchMode === \"radial-cascade\") {\n widthScale = heightScale = 0.55 + value * 0.45;\n }\n const angle =\n renderedShape === \"line\"\n ? Math.atan2(this.ys[index], this.xs[index]) + Math.PI / 2\n : 0;\n this.drawPixel(\n renderedShape,\n context,\n cx,\n cy,\n pixel * widthScale,\n pixel * heightScale,\n value,\n angle,\n );\n }\n context.globalAlpha = 1;\n context.shadowBlur = 0;\n context.restore();\n }\n\n updateAdaptiveQuality(frameMs) {\n if (this.options.quality !== \"auto\") {\n this.qualityTier = this.options.quality;\n this.stats.quality = this.qualityTier;\n return;\n }\n const budget = 1000 / clamp(Number(this.options.fps) || 60, 1, 120);\n if (frameMs > budget * 1.12) {\n this.qualityPressure += 1;\n this.qualityRecovery = Math.max(0, this.qualityRecovery - 2);\n } else if (frameMs < budget * 0.62) {\n this.qualityRecovery += 1;\n this.qualityPressure = Math.max(0, this.qualityPressure - 1);\n } else {\n this.qualityPressure = Math.max(0, this.qualityPressure - 0.25);\n this.qualityRecovery = Math.max(0, this.qualityRecovery - 0.5);\n }\n\n let next = this.qualityTier;\n if (this.qualityPressure >= 18) {\n next = next === \"high\" ? \"balanced\" : \"low\";\n this.qualityPressure = 0;\n this.qualityRecovery = 0;\n } else if (this.qualityRecovery >= 180) {\n next = next === \"low\" ? \"balanced\" : \"high\";\n this.qualityPressure = 0;\n this.qualityRecovery = 0;\n }\n if (next !== this.qualityTier) {\n const previous = this.qualityTier;\n this.qualityTier = next;\n this.emit(\"qualitychange\", {\n from: previous,\n to: next,\n frameMs,\n budgetMs: budget,\n });\n }\n this.stats.quality = this.qualityTier;\n }\n\n renderFrame(time = 0, dt = 1 / 60, force = false) {\n if (this.destroyed) return this.stats;\n const started =\n typeof performance !== \"undefined\" ? performance.now() : Date.now();\n const elapsed = this.reducedMotion && !force\n ? 0\n : clamp(Number(dt) || 0, 0, this.maxCatchUpSeconds);\n let simulationSteps = 0;\n let simulatedElapsed = 0;\n if (force) {\n this.simulationAccumulator = 0;\n this.settleGrid(time);\n } else {\n this.simulationAccumulator = Math.min(\n this.maxCatchUpSeconds,\n this.simulationAccumulator + elapsed,\n );\n while (\n this.simulationAccumulator + 1e-9 >= this.simulationStep &&\n simulationSteps < this.maxSimulationSteps\n ) {\n this.updateDynamics(this.simulationStep);\n this.simulationAccumulator -= this.simulationStep;\n simulationSteps += 1;\n simulatedElapsed += this.simulationStep;\n }\n if (simulationSteps === this.maxSimulationSteps) {\n this.simulationAccumulator %= this.simulationStep;\n }\n if (simulationSteps > 0) {\n // Global clocks retain fixed 60 Hz determinism, while the expensive\n // field/grid pass is coalesced to the painted frame. This prevents a\n // slow frame from multiplying its own workload during catch-up.\n this.evaluateGrid(simulatedElapsed);\n } else {\n this.stats.sampledPixels = 0;\n }\n }\n this.stats.simulationSteps = simulationSteps;\n this.paint();\n\n const ended =\n typeof performance !== \"undefined\" ? performance.now() : Date.now();\n this.stats.frameMs = ended - started;\n this.updateAdaptiveQuality(this.stats.frameMs);\n this.fpsAccumulator.time += elapsed;\n this.fpsAccumulator.frames += 1;\n if (this.fpsAccumulator.time >= 0.5) {\n this.stats.fps = Math.round(\n this.fpsAccumulator.frames / this.fpsAccumulator.time,\n );\n this.fpsAccumulator.time = 0;\n this.fpsAccumulator.frames = 0;\n this.emit(\"stats\", { ...this.stats });\n }\n return { ...this.stats };\n }\n\n renderOnce(time = this.clock) {\n return this.renderFrame(time, 0, true);\n }\n\n inspect() {\n const transitionProgress = this.transitionDuration <= 0\n ? 1\n : clamp01(this.transitionElapsed / this.transitionDuration);\n return {\n sprite: this.currentRecipe?.id || null,\n recipeSource: this.recipeSource?.id || null,\n previousSprite: this.previousRecipe?.id || null,\n fieldMode: this.fieldOverride ? \"override\" : \"recipe\",\n field: this.fieldOverride || this.currentRecipe?.field || null,\n running: this.running,\n visible: this.visible,\n destroyed: this.destroyed,\n reducedMotion: this.reducedMotion,\n transition: {\n active: this.isTransitionActive(),\n name: this.transitionName,\n progress: transitionProgress,\n elapsed: this.transitionElapsed,\n duration: this.transitionDuration,\n pending:\n typeof this.pendingSpriteTransition?.sprite === \"string\"\n ? this.pendingSpriteTransition.sprite\n : this.pendingSpriteTransition?.sprite?.id || null,\n },\n signals: Array.from(this.signals, ([type, signal]) => ({\n type,\n value: signal.value,\n age: signal.age,\n life: signal.life,\n })),\n semanticSignals: Object.fromEntries(\n Array.from(this.semanticSignals, ([name, signal]) => [\n name,\n signal.value,\n ]),\n ),\n palette: { ...activePalette(this.currentRecipe, this.options) },\n orbBoundary: this.options.orbBoundary,\n orbBackgroundColor: this.options.orbBackgroundColor,\n orbBackgroundMode: this.options.orbBackgroundMode,\n quality: {\n requested: this.options.quality,\n effective: this.qualityTier,\n },\n stats: { ...this.stats },\n };\n }\n\n exportConfig() {\n return {\n package: \"@joan/procedural-glyph-engine\",\n version: \"5.0.0\",\n sprite: this.currentRecipe.id,\n seed: this.seed,\n gridSize: this.gridSize,\n speed: this.options.speed,\n density: this.options.density,\n contrast: this.options.contrast,\n fps: this.options.fps,\n field: this.currentRecipe.field,\n fieldMode: this.fieldOverride ? \"override\" : \"recipe\",\n pixelShape: this.options.pixelShape || null,\n pixelSwitch:\n this.options.pixelSwitch && typeof this.options.pixelSwitch === \"object\"\n ? { ...this.options.pixelSwitch }\n : this.options.pixelSwitch || null,\n transition:\n this.options.transition && typeof this.options.transition === \"object\"\n ? {\n ...this.options.transition,\n name: validateTransition(this.options.transition),\n }\n : this.options.transition || null,\n palette: activePalette(this.currentRecipe, this.options),\n paletteOverride:\n this.options.palette && typeof this.options.palette === \"object\"\n ? { ...this.options.palette }\n : null,\n nonErrorPalette:\n this.options.nonErrorPalette &&\n typeof this.options.nonErrorPalette === \"object\"\n ? { ...this.options.nonErrorPalette }\n : null,\n orbBoundary: this.options.orbBoundary,\n orbBackgroundColor: this.options.orbBackgroundColor,\n orbBackgroundMode: this.options.orbBackgroundMode,\n progress: this.progress,\n background: this.options.background,\n offPixels: this.options.offPixels,\n interactive: this.options.interactive,\n autoplay: this.options.autoplay,\n autoResize: this.options.autoResize,\n dprMax: this.options.dprMax,\n quality: this.options.quality,\n reducedMotion: this.options.reducedMotion,\n direction: this.options.direction || null,\n targetGlyph: this.options.targetGlyph || null,\n };\n }\n\n toSVG(options = {}) {\n const size = Math.round(clamp(Number(options.size) || 512, 64, 4096));\n const palette = activeSvgPalette(this.currentRecipe, this.options);\n const shape = normalizePixelShape(\n this.options.pixelShape ||\n recipeValue(this.currentRecipe, \"pixelShape\", \"disc\"),\n );\n const margin = size * 0.075;\n const stage = size - margin * 2;\n const cell = stage / this.gridSize;\n const pixel = cell * recipePixelScale(this.currentRecipe);\n const switchMode = recipePixelSwitch(\n this.currentRecipe,\n this.options.pixelSwitch,\n ).mode;\n const parts = [\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"0 0 ${size} ${size}\" role=\"img\" aria-label=\"${escapeXmlAttribute(\n semanticLabel(this.currentRecipe),\n )}\">`,\n `<title>${escapeXml(semanticLabel(this.currentRecipe))}</title>`,\n ];\n if (options.background !== false && palette.background !== \"transparent\") {\n parts.push(\n `<rect width=\"${size}\" height=\"${size}\" fill=\"${escapeXmlAttribute(\n palette.background,\n )}\"/>`,\n );\n }\n const orbBackgroundColor = safeSvgColor(\n this.options.orbBackgroundColor,\n \"transparent\",\n );\n const orbRadius = this.orbBackgroundRadius(this.currentRecipe, this.clock);\n if (\n this.options.orbBackgroundMode !== \"none\" &&\n orbBackgroundColor !== \"transparent\" &&\n orbRadius > 0\n ) {\n if (this.options.orbBackgroundMode === \"pixelated\") {\n parts.push(\n svgPixelatedOrbBackground(\n this.gridSize,\n orbRadius,\n margin,\n cell,\n orbBackgroundColor,\n ),\n );\n } else {\n parts.push(\n `<circle data-joan-layer=\"orb-background\" data-joan-mode=\"solid\" cx=\"${(\n size / 2\n ).toFixed(2)}\" cy=\"${(size / 2).toFixed(2)}\" r=\"${(\n (stage * orbRadius) /\n 2\n ).toFixed(2)}\" fill=\"${escapeXmlAttribute(orbBackgroundColor)}\"/>`,\n );\n }\n }\n if (options.offPixels ?? this.options.offPixels) {\n const offSize = Math.max(0.16, pixel * 0.18);\n parts.push(\n `<g fill=\"${escapeXmlAttribute(\n palette.off,\n )}\" opacity=\".58\"><g data-joan-layer=\"off-pixels\">`,\n );\n for (let index = 0; index < this.values.length; index += 1) {\n const gx = index % this.gridSize;\n const gy = Math.floor(index / this.gridSize);\n const cx = margin + (gx + 0.5) * cell;\n const cy = margin + (gy + 0.5) * cell;\n parts.push(\n `<rect x=\"${(cx - offSize / 2).toFixed(2)}\" y=\"${(\n cy -\n offSize / 2\n ).toFixed(2)}\" width=\"${offSize.toFixed(2)}\" height=\"${offSize.toFixed(\n 2,\n )}\"/>`,\n );\n }\n parts.push(\"</g></g>\");\n }\n parts.push(\n `<g data-joan-layer=\"active-pixels\" fill=\"${escapeXmlAttribute(\n palette.ink,\n )}\" stroke=\"${escapeXmlAttribute(palette.ink)}\">`,\n );\n for (let index = 0; index < this.values.length; index += 1) {\n const value = this.values[index];\n if (value < 0.035) continue;\n const gx = index % this.gridSize;\n const gy = Math.floor(index / this.gridSize);\n const cx = margin + (gx + 0.5) * cell;\n const cy = margin + (gy + 0.5) * cell;\n const scale = 0.18 + value * 0.82;\n let widthScale = 1;\n let heightScale = 1;\n if (switchMode === \"axis-flip\") {\n heightScale = Math.max(0.07, Math.sin(value * Math.PI * 0.5));\n } else if (switchMode === \"path-draw\") {\n widthScale = 0.35 + value * 0.65;\n } else if (switchMode === \"radial-cascade\") {\n widthScale = heightScale = 0.55 + value * 0.45;\n }\n const width = pixel * scale * widthScale;\n const height = pixel * scale * heightScale;\n const opacity = clamp01(value * (0.72 + this.luminance[index] * 0.28));\n const colorBin = Math.min(\n 8,\n Math.floor(this.luminance[index] * 8.999),\n );\n const color = mixColor(\n palette.ink,\n palette.accent,\n colorBin / 8,\n 1,\n );\n const attributes = `opacity=\"${opacity.toFixed(3)}\"`;\n const renderedShape = resolvePixelShape(shape, this.luminance[index]);\n const ringStrokeWidth =\n renderedShape === \"ring\"\n ? Math.max(\n 0.12,\n Math.min(pixel * widthScale, pixel * heightScale) * 0.14,\n )\n : null;\n const angle =\n renderedShape === \"line\"\n ? ((Math.atan2(this.ys[index], this.xs[index]) + Math.PI / 2) * 180) /\n Math.PI\n : 0;\n parts.push(\n `<g data-joan-cell=\"${index}\" data-joan-shape=\"${renderedShape}\" fill=\"${escapeXmlAttribute(color)}\" stroke=\"${escapeXmlAttribute(\n color,\n )}\">${svgShapeMarkup(\n renderedShape,\n cx,\n cy,\n width,\n height,\n attributes,\n \"\",\n angle,\n ringStrokeWidth,\n )}</g>`,\n );\n }\n parts.push(\"</g></svg>\");\n return parts.join(\"\");\n }\n\n async toAnimatedSVG(options = {}) {\n if (typeof document === \"undefined\") {\n throw new Error(\"Animated SVG export requires a browser canvas.\");\n }\n const duration = clamp(Number(options.duration) || 2.4, 0.5, 8);\n const fps = Math.round(clamp(Number(options.fps) || 12, 4, 30));\n const steps = Math.ceil(duration * fps);\n const frames = steps + 1;\n const gridSize = Math.min(\n this.gridSize,\n Math.round(clamp(Number(options.maxGridSize) || 48, 12, 64)),\n );\n const scratchCanvas = document.createElement(\"canvas\");\n scratchCanvas.width = 512;\n scratchCanvas.height = 512;\n const clone = new JoanGlyphEngine(scratchCanvas, {\n ...this.exportConfig(),\n sprite: this.currentRecipe,\n gridSize,\n autoplay: false,\n autoResize: false,\n interactive: false,\n reducedMotion: false,\n dprMax: 1,\n quality: \"low\",\n });\n clone.customGlyphs = new Map(this.customGlyphs);\n clone.progress = this.progress;\n clone.audioLevel = this.audioLevel;\n const samples = Array.from(\n { length: gridSize * gridSize },\n () => new Float32Array(frames),\n );\n const luminanceSamples = Array.from(\n { length: gridSize * gridSize },\n () => new Float32Array(frames),\n );\n const orbRadiusSamples = new Float32Array(frames);\n try {\n for (let frame = 0; frame < steps; frame += 1) {\n if (frame > 0) clone.renderFrame(frame / fps, 1 / fps, false);\n orbRadiusSamples[frame] = clone.orbBackgroundRadius(\n clone.currentRecipe,\n frame / fps,\n );\n for (let index = 0; index < clone.values.length; index += 1) {\n samples[index][frame] = clone.values[index];\n luminanceSamples[index][frame] = clone.luminance[index];\n }\n if (frame % 8 === 7) {\n await new Promise((resolve) => setTimeout(resolve, 0));\n }\n }\n const closureFrames = Math.min(3, Math.max(1, Math.floor(steps * 0.12)));\n for (\n let frame = Math.max(1, steps - closureFrames);\n frame < steps;\n frame += 1\n ) {\n const amount = easeInOut(\n (frame - (steps - closureFrames)) / closureFrames,\n );\n orbRadiusSamples[frame] +=\n (orbRadiusSamples[0] - orbRadiusSamples[frame]) * amount;\n }\n orbRadiusSamples[steps] = orbRadiusSamples[0];\n for (let index = 0; index < samples.length; index += 1) {\n for (\n let frame = Math.max(1, steps - closureFrames);\n frame < steps;\n frame += 1\n ) {\n const amount = easeInOut(\n (frame - (steps - closureFrames)) / closureFrames,\n );\n samples[index][frame] +=\n (samples[index][0] - samples[index][frame]) * amount;\n luminanceSamples[index][frame] +=\n (luminanceSamples[index][0] - luminanceSamples[index][frame]) *\n amount;\n }\n samples[index][steps] = samples[index][0];\n luminanceSamples[index][steps] = luminanceSamples[index][0];\n }\n const size = Math.round(clamp(Number(options.size) || 512, 64, 2048));\n const palette = activeSvgPalette(this.currentRecipe, this.options);\n const margin = size * 0.075;\n const stage = size - margin * 2;\n const cell = stage / gridSize;\n const pixel = cell * recipePixelScale(this.currentRecipe);\n const shape = normalizePixelShape(\n this.options.pixelShape ||\n recipeValue(this.currentRecipe, \"pixelShape\", \"disc\"),\n );\n const switchMode = recipePixelSwitch(\n this.currentRecipe,\n this.options.pixelSwitch,\n ).mode;\n const keyTimes = Array.from({ length: frames }, (_, index) =>\n (index / Math.max(1, frames - 1)).toFixed(4),\n ).join(\";\");\n const parts = [\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"0 0 ${size} ${size}\" role=\"img\" aria-label=\"${escapeXmlAttribute(\n `${semanticLabel(this.currentRecipe)} animation`,\n )}\">`,\n `<title>${escapeXml(semanticLabel(this.currentRecipe))}</title>`,\n ];\n if (options.background !== false && palette.background !== \"transparent\") {\n parts.push(\n `<rect width=\"${size}\" height=\"${size}\" fill=\"${escapeXmlAttribute(\n palette.background,\n )}\"/>`,\n );\n }\n const orbBackgroundColor = safeSvgColor(\n this.options.orbBackgroundColor,\n \"transparent\",\n );\n const maximumOrbRadius = Math.max(...orbRadiusSamples);\n if (\n this.options.orbBackgroundMode !== \"none\" &&\n orbBackgroundColor !== \"transparent\" &&\n maximumOrbRadius > 0\n ) {\n if (this.options.orbBackgroundMode === \"pixelated\") {\n parts.push(\n animatedSvgPixelatedOrbBackground(\n gridSize,\n orbRadiusSamples,\n margin,\n cell,\n orbBackgroundColor,\n keyTimes,\n duration,\n ),\n );\n } else {\n const radiusValues = Array.from(orbRadiusSamples, (radius) =>\n ((stage * radius) / 2).toFixed(3),\n ).join(\";\");\n parts.push(\n `<circle data-joan-layer=\"orb-background\" data-joan-mode=\"solid\" cx=\"${(\n size / 2\n ).toFixed(2)}\" cy=\"${(size / 2).toFixed(2)}\" r=\"${(\n (stage * orbRadiusSamples[0]) /\n 2\n ).toFixed(3)}\" fill=\"${escapeXmlAttribute(\n orbBackgroundColor,\n )}\"><animate attributeName=\"r\" values=\"${radiusValues}\" keyTimes=\"${keyTimes}\" dur=\"${duration}s\" calcMode=\"linear\" repeatCount=\"indefinite\"/></circle>`,\n );\n }\n }\n if (options.offPixels ?? this.options.offPixels) {\n const offSize = Math.max(0.16, pixel * 0.18);\n parts.push(\n `<g data-joan-layer=\"off-pixels\" fill=\"${escapeXmlAttribute(\n palette.off,\n )}\" opacity=\".58\">`,\n );\n for (let index = 0; index < samples.length; index += 1) {\n const gx = index % gridSize;\n const gy = Math.floor(index / gridSize);\n const cx = margin + (gx + 0.5) * cell;\n const cy = margin + (gy + 0.5) * cell;\n parts.push(\n `<rect x=\"${(cx - offSize / 2).toFixed(2)}\" y=\"${(\n cy -\n offSize / 2\n ).toFixed(2)}\" width=\"${offSize.toFixed(\n 2,\n )}\" height=\"${offSize.toFixed(2)}\"/>`,\n );\n }\n parts.push(\"</g>\");\n }\n parts.push(\n `<g data-joan-layer=\"active-pixels\" fill=\"${escapeXmlAttribute(\n palette.ink,\n )}\" stroke=\"${escapeXmlAttribute(palette.ink)}\">`,\n );\n for (let index = 0; index < samples.length; index += 1) {\n const values = samples[index];\n const luminances = luminanceSamples[index];\n let maximum = 0;\n for (let frame = 0; frame < frames; frame += 1) {\n maximum = Math.max(maximum, values[frame]);\n }\n if (maximum < 0.03) continue;\n const gx = index % gridSize;\n const gy = Math.floor(index / gridSize);\n const cx = margin + (gx + 0.5) * cell;\n const cy = margin + (gy + 0.5) * cell;\n const opacityValues = Array.from(values, (value, frame) =>\n clamp01(value * (0.72 + luminances[frame] * 0.28)).toFixed(3),\n );\n const opacities = opacityValues.join(\";\");\n const ringStrokeWidths = [];\n const scales = Array.from(values, (value) => {\n const scale = 0.18 + clamp01(value) * 0.82;\n let widthScale = 1;\n let heightScale = 1;\n if (switchMode === \"axis-flip\") {\n heightScale = Math.max(\n 0.07,\n Math.sin(clamp01(value) * Math.PI * 0.5),\n );\n } else if (switchMode === \"path-draw\") {\n widthScale = 0.35 + clamp01(value) * 0.65;\n } else if (switchMode === \"radial-cascade\") {\n widthScale = heightScale = 0.55 + clamp01(value) * 0.45;\n }\n ringStrokeWidths.push(\n Math.max(\n 0.12,\n Math.min(pixel * widthScale, pixel * heightScale) * 0.14,\n ).toFixed(3),\n );\n return `${(scale * widthScale).toFixed(3)} ${(\n scale * heightScale\n ).toFixed(3)}`;\n }).join(\";\");\n const initialScales = scales.split(\";\")[0].split(\" \");\n const colors = Array.from(luminances, (luminance) =>\n mixColor(palette.ink, palette.accent, luminance, 1),\n );\n const colorValues = colors\n .map((color) => escapeXmlAttribute(color))\n .join(\";\");\n const angle =\n shape === \"line\"\n ? ((Math.atan2(gy + 0.5 - gridSize / 2, gx + 0.5 - gridSize / 2) +\n Math.PI / 2) *\n 180) /\n Math.PI\n : 0;\n const opacityAnimation = `<animate attributeName=\"opacity\" values=\"${opacities}\" keyTimes=\"${keyTimes}\" dur=\"${duration}s\" calcMode=\"discrete\" repeatCount=\"indefinite\"/>`;\n const scaleAnimation = `<animateTransform attributeName=\"transform\" type=\"scale\" values=\"${scales}\" keyTimes=\"${keyTimes}\" dur=\"${duration}s\" calcMode=\"discrete\" repeatCount=\"indefinite\"/>`;\n const fillAnimation = `<animate attributeName=\"fill\" values=\"${colorValues}\" keyTimes=\"${keyTimes}\" dur=\"${duration}s\" calcMode=\"discrete\" repeatCount=\"indefinite\"/>`;\n const strokeAnimation = `<animate attributeName=\"stroke\" values=\"${colorValues}\" keyTimes=\"${keyTimes}\" dur=\"${duration}s\" calcMode=\"discrete\" repeatCount=\"indefinite\"/>`;\n const localShape = animatedSvgShapeMarkup(\n shape,\n pixel,\n pixel,\n luminances,\n keyTimes,\n duration,\n angle,\n ringStrokeWidths,\n );\n parts.push(\n `<g data-joan-cell=\"${index}\" data-joan-pixel-shape=\"${shape}\" transform=\"translate(${cx.toFixed(2)} ${cy.toFixed(\n 2,\n )})\" fill=\"${escapeXmlAttribute(colors[0])}\" stroke=\"${escapeXmlAttribute(\n colors[0],\n )}\" opacity=\"${opacityValues[0]}\">${opacityAnimation}${fillAnimation}${strokeAnimation}<g transform=\"scale(${initialScales[0]} ${initialScales[1]})\">${scaleAnimation}${localShape}</g></g>`,\n );\n }\n parts.push(\"</g></svg>\");\n return parts.join(\"\");\n } finally {\n clone.destroy();\n }\n }\n\n async downloadSVG(\n filename = `${this.currentRecipe.id.replaceAll(\".\", \"-\")}.svg`,\n options,\n ) {\n const source = this.toSVG(options);\n if (typeof document === \"undefined\") return source;\n const blob = new Blob([source], { type: \"image/svg+xml\" });\n const url = URL.createObjectURL(blob);\n try {\n const anchor = document.createElement(\"a\");\n anchor.href = url;\n anchor.download = filename;\n anchor.click();\n } finally {\n URL.revokeObjectURL(url);\n }\n return source;\n }\n\n async downloadAnimatedSVG(\n filename = `${this.currentRecipe.id.replaceAll(\".\", \"-\")}.animated.svg`,\n options,\n ) {\n const source = await this.toAnimatedSVG(options);\n const blob = new Blob([source], { type: \"image/svg+xml\" });\n const url = URL.createObjectURL(blob);\n try {\n const anchor = document.createElement(\"a\");\n anchor.href = url;\n anchor.download = filename;\n anchor.click();\n } finally {\n URL.revokeObjectURL(url);\n }\n return source;\n }\n\n toDataURL(type = \"image/png\", quality) {\n return this.canvas.toDataURL(type, quality);\n }\n\n toBlob(type = \"image/png\", quality) {\n return new Promise((resolve, reject) => {\n this.canvas.toBlob?.(\n (blob) => (blob ? resolve(blob) : reject(new Error(\"Canvas export failed.\"))),\n type,\n quality,\n );\n });\n }\n\n async downloadPNG(filename = `${this.currentRecipe.id.replaceAll(\".\", \"-\")}.png`) {\n if (typeof document === \"undefined\") return this.toBlob();\n const blob = await this.toBlob();\n const url = URL.createObjectURL(blob);\n try {\n const anchor = document.createElement(\"a\");\n anchor.href = url;\n anchor.download = filename;\n anchor.click();\n } finally {\n URL.revokeObjectURL(url);\n }\n return blob;\n }\n\n destroy() {\n if (this.destroyed) return;\n this.pause();\n this.cancelReducedCrossfade();\n this.destroyed = true;\n for (const remove of this.listeners.splice(0)) remove();\n for (const remove of this.interactionListeners.splice(0)) remove();\n this.resizeObserver?.disconnect();\n this.intersectionObserver?.disconnect();\n this.customGlyphs.clear();\n this.impulses.length = 0;\n this.emit(\"destroy\", {});\n }\n}\n\nexport function createGlyph(target, options = {}) {\n const canvas =\n typeof target === \"string\"\n ? typeof document !== \"undefined\"\n ? document.querySelector(target)\n : null\n : target;\n if (!canvas) {\n throw new TypeError(`No canvas matched \"${String(target)}\".`);\n }\n return new JoanGlyphEngine(canvas, options);\n}\n\nexport function createProceduralGlyph(input = {}, options = {}) {\n if (input?.canvas) {\n const { canvas, ...engineOptions } = input;\n return createGlyph(canvas, engineOptions);\n }\n return createGlyph(input, options);\n}\n\nexport function mountProceduralGlyph(target, options = {}) {\n return createGlyph(target, options);\n}\n\nexport { SPRITES, getSprite, listSprites };\nexport default JoanGlyphEngine;\n"
.replaceAll("./fields.js", fieldsUrl)
.replaceAll("./glyphs.js", glyphsUrl)
.replaceAll("./sprites.js", spritesUrl)
.replaceAll("./config.js", configUrl)
.replaceAll("./preview-layout.js", previewLayoutUrl);
const engineUrl = createModuleUrl(engineSource);
const controlHelpUrl = createModuleUrl("export const FIELD_HELP = Object.freeze({\n __recipe__:\n \"Uses the selected states authored blend of fields, preserving its intended visual character.\",\n fbm: \"Builds soft, organic variation by layering several scales of smooth gradient noise.\",\n ridged:\n \"Folds layered noise into sharp ridges and filaments for a more etched texture.\",\n \"domain-warp\":\n \"Distorts layered noise with two moving noise fields, creating fluid folds and swirls.\",\n curl: \"Turns the curl magnitude of animated noise into turbulent, smoke-like density.\",\n flow: \"Carries animated ribbons along a curl-noise direction for a flowing texture.\",\n worley:\n \"Creates bright cellular islands around moving feature points, separated by darker gaps.\",\n voronoi:\n \"Smoothly morphs between seeded cellular layouts while keeping their moving division lines crisp.\",\n plasma:\n \"Combines layered sine waves with noise-driven phase shifts for an energetic plasma pattern.\",\n interference:\n \"Overlaps circular waves from moving emitters to create traveling beats and bands.\",\n vortex:\n \"Spins noisy spiral arms around the center for a rotating whirlpool-like field.\",\n metaballs:\n \"Blends several seeded moving blobs so nearby forms merge into one liquid mass.\",\n caustics:\n \"Produces sharp cellular highlights that resemble refracted light moving across water.\",\n strata:\n \"Stacks noise-warped sediment-like bands with a directional slope and sharp edges.\",\n radar:\n \"Combines a rotating beam, concentric rings, and seeded target blips.\",\n constellation:\n \"Connects twinkling cellular stars with faint Voronoi filaments.\",\n liquid:\n \"Layers domain-warped waves and highlights into a gently moving water surface.\",\n electric:\n \"Generates repeating branching lightning channels with pulses and traveling sparks.\",\n ripple: \"Sends noise-distorted concentric rings outward from the center.\",\n kaleidoscope:\n \"Folds rotating polar noise into mirrored, mandala-like symmetry.\",\n});\n\nexport const TRANSITION_HELP = Object.freeze({\n \"field-morph\":\n \"Morphs cells on a soft schedule combining seeded randomness, radius, and vertical position.\",\n \"seeded-dissolve\":\n \"Changes pixels in a stable, seed-determined random order.\",\n \"radial-cascade\":\n \"Moves the new state outward from the origin with slight seeded variation.\",\n \"angular-sweep\":\n \"Rotates the new state around the origin with a subtle outward offset.\",\n scanline:\n \"Reveals the new state from top to bottom in a lightly rippled scan.\",\n \"contour-trace\":\n \"Starts on a circular mid-radius contour, then spreads inward and outward.\",\n \"path-draw\":\n \"Draws the new state diagonally from the upper left toward the lower right.\",\n \"axis-flip\":\n \"Staggers alternating columns while advancing top to bottom, producing a shutter-like flip.\",\n \"cluster-dissolve\":\n \"Changes coarse pixel clusters in a seeded random order for a chunkier dissolve.\",\n \"neighbor-ignite\":\n \"Spreads the new state in a connected-looking wave from the origin.\",\n \"glitch-bands\":\n \"Switches horizontal bands in a scrambled order for a brief glitch effect.\",\n instant: \"Applies the new state immediately with no spatial interpolation.\",\n});\n\nexport const PIXEL_SHAPE_HELP = Object.freeze({\n disc: \"Draws each cell as a filled round dot.\",\n square: \"Draws each cell as a filled square for a crisp grid texture.\",\n diamond: \"Draws each cell as a compact square rotated 45 degrees.\",\n capsule: \"Draws each cell as a rounded horizontal pill.\",\n line: \"Draws each cell as a thin bar aligned tangentially around the center.\",\n ring: \"Draws each cell as a hollow circular outline.\",\n cross: \"Draws each cell as a compact plus sign.\",\n \"square-cross\":\n \"Uses filled squares for dim sampled signal cells and crosses for bright cells.\",\n \"square-cross-ring\":\n \"Uses filled squares, crosses, then rings as sampled signal lightness rises.\",\n});\n\nexport const PIXEL_SWITCH_HELP = Object.freeze({\n \"ordered-dither\":\n \"Uses a fixed Bayer pattern for stable, evenly distributed pixel activation.\",\n \"temporal-blue-noise\":\n \"Blends an ordered grid with time-varying noise for lively, fine-grained switching.\",\n \"threshold-hysteresis\":\n \"Uses a uniform midpoint threshold with hysteresis for calm, stable switching.\",\n \"sdf-wavefront\":\n \"Adds a moving radial wavefront to ordered thresholds so activation travels in rings.\",\n \"contour-trace\":\n \"Modulates thresholds along repeating field-value contours so similar intensity bands switch together.\",\n \"curl-advect\":\n \"Offsets ordered thresholds with a traveling wave so activation appears to drift diagonally.\",\n \"neighbor-propagation\":\n \"Lowers a cells threshold when adjacent cells are on, encouraging activation to spread locally.\",\n \"radial-cascade\":\n \"Biases ordered activation from the center outward.\",\n \"path-draw\":\n \"Biases ordered activation along a top-left-to-bottom-right path.\",\n \"axis-flip\":\n \"Staggers alternating columns and compresses cells vertically as they turn on, creating a flip.\",\n \"seeded-dissolve\":\n \"Gives each cell a stable, seed-determined random threshold for a repeatable dissolve texture.\",\n \"field-morph\":\n \"Blends ordered and seed-random thresholds for both structure and organic variation.\",\n});\n\nexport const SELECT_CONTROL_HELP = Object.freeze({\n fieldSelect: Object.freeze({\n summary: \"Chooses the procedural texture sampled inside the glyph.\",\n options: FIELD_HELP,\n }),\n transitionSelect: Object.freeze({\n summary:\n \"Controls the spatial order used when moving between semantic states.\",\n options: TRANSITION_HELP,\n }),\n pixelShapeSelect: Object.freeze({\n summary: \"Changes the silhouette used to draw each active cell.\",\n options: PIXEL_SHAPE_HELP,\n }),\n pixelSwitchSelect: Object.freeze({\n summary:\n \"Controls how continuous field intensity becomes persistent on/off pixels.\",\n options: PIXEL_SWITCH_HELP,\n }),\n orbBackgroundModeSelect: Object.freeze({\n summary: \"Chooses how the optional orb background disk is constructed.\",\n options: Object.freeze({\n solid:\n \"Fills one smooth circle behind the orb while preserving the grid above it.\",\n pixelated:\n \"Builds the disk from full grid-aligned cells so its edge matches the selected resolution.\",\n }),\n }),\n});\n\nexport const VALUE_CONTROL_HELP = Object.freeze({\n resolutionRange:\n \"Sets the number of cells per side. Higher values add detail but increase rendering work roughly with the square.\",\n speedRange:\n \"Multiplies the procedural animation clock, making textures and sprite motion run slower or faster.\",\n densityRange:\n \"Biases activation so higher values keep more pixels on and lower values make the signal sparser.\",\n seedInput:\n \"A stable text key that determines noise and per-cell randomness. Reuse it with the same settings to reproduce the signal.\",\n randomizeButton:\n \"Creates a new seed and a new deterministic variation of the current signal.\",\n backgroundColorInput:\n \"Sets the canvas fill behind the grid across non-error states.\",\n offColorInput:\n \"Sets the faint color of grid cells that are currently off across non-error states.\",\n inkColorInput:\n \"Sets the base color of active pixels across non-error states.\",\n accentColorInput:\n \"Sets the color active pixels move toward as sampled luminance rises.\",\n glowEnabledInput:\n \"Turns the halo around active pixels on or off across non-error states.\",\n orbBoundaryInput:\n \"Defined draws the authored rim. Gestalt removes the continuous outline and lets clustered edge pixels suggest the circle through proximity, closure, and continuing arcs.\",\n orbBackgroundEnabledInput:\n \"Adds or removes the optional orb background layer in supported presence orbs. It is separate from the canvas background.\",\n orbBackgroundColorInput:\n \"Sets the optional color behind supported orb pixels. The color carries across states but stays visually inert for non-orb recipes.\",\n glowColorInput:\n \"Sets the halo around active pixels. Glow renders in high quality at resolutions up to 48×48.\",\n resetPaletteButton:\n \"Clears custom color overrides and returns non-error states to the current themes defaults.\",\n});\n\nconst clamp = (value, minimum, maximum) =>\n Math.min(Math.max(value, minimum), maximum);\n\nexport function placeControlTooltip(\n anchor,\n tooltip,\n viewport,\n options = {},\n) {\n const gap = Number(options.gap) || 8;\n const gutter = Number(options.gutter) || 12;\n const maximumLeft = Math.max(\n gutter,\n Number(viewport.width) - Number(tooltip.width) - gutter,\n );\n const centeredLeft =\n Number(anchor.left) +\n Number(anchor.width) / 2 -\n Number(tooltip.width) / 2;\n const left = clamp(centeredLeft, gutter, maximumLeft);\n const below = Number(anchor.bottom) + gap;\n const above = Number(anchor.top) - Number(tooltip.height) - gap;\n const fitsBelow =\n below + Number(tooltip.height) <= Number(viewport.height) - gutter;\n const fitsAbove = above >= gutter;\n const maximumTop = Math.max(\n gutter,\n Number(viewport.height) - Number(tooltip.height) - gutter,\n );\n const placement = !fitsBelow && fitsAbove ? \"top\" : \"bottom\";\n const preferredTop = placement === \"top\" ? above : below;\n\n return {\n left: Math.round(left),\n top: Math.round(clamp(preferredTop, gutter, maximumTop)),\n placement,\n };\n}\n");
const studioSource = "import JoanGlyphEngine, {\n PIXEL_SHAPES,\n PIXEL_SWITCHES,\n TRANSITIONS,\n listSprites,\n} from \"./joan-engine.js\";\nimport { FIELD_IDS } from \"./fields.js\";\nimport {\n normalizePreviewMode,\n previewModeDetails,\n} from \"./preview-layout.js\";\nimport {\n createThumbnailRecipe,\n renderThumbnailRestFrame,\n shouldAnimateThumbnail,\n} from \"./preview-thumbnails.js\";\nimport {\n SELECT_CONTROL_HELP,\n VALUE_CONTROL_HELP,\n placeControlTooltip,\n} from \"./control-help.js\";\n\nconst byId = (id) => document.getElementById(id);\nconst elements = {\n heroCanvas: byId(\"heroEngineCanvas\"),\n canvas: byId(\"engineCanvas\"),\n canvasFrame: byId(\"canvasFrame\"),\n canvasHint: byId(\"canvasHint\"),\n frameRenderBadge: byId(\"frameRenderBadge\"),\n previewFitButton: byId(\"previewFitButton\"),\n previewActualButton: byId(\"previewActualButton\"),\n liveRegion: byId(\"liveRegion\"),\n stateTitle: byId(\"stateTitle\"),\n stateDescription: byId(\"stateDescription\"),\n stateCategory: byId(\"stateCategory\"),\n stateSearchInput: byId(\"stateSearchInput\"),\n statePickerSelect: byId(\"statePickerSelect\"),\n statePickerStatus: byId(\"statePickerStatus\"),\n spriteGrid: byId(\"spriteGrid\"),\n fieldSelect: byId(\"fieldSelect\"),\n fieldScopeBadge: byId(\"fieldScopeBadge\"),\n transitionSelect: byId(\"transitionSelect\"),\n transitionScopeBadge: byId(\"transitionScopeBadge\"),\n pixelShapeSelect: byId(\"pixelShapeSelect\"),\n pixelShapeScopeBadge: byId(\"pixelShapeScopeBadge\"),\n pixelSwitchSelect: byId(\"pixelSwitchSelect\"),\n pixelSwitchScopeBadge: byId(\"pixelSwitchScopeBadge\"),\n orbBoundaryControl: byId(\"orbBoundaryControl\"),\n orbBoundaryInput: byId(\"orbBoundaryInput\"),\n orbBoundaryScopeBadge: byId(\"orbBoundaryScopeBadge\"),\n orbBoundaryAvailability: byId(\"orbBoundaryAvailability\"),\n orbBackgroundControl: byId(\"orbBackgroundControl\"),\n orbBackgroundEnabledInput: byId(\"orbBackgroundEnabledInput\"),\n orbBackgroundEnabledLabel: byId(\"orbBackgroundEnabledLabel\"),\n orbBackgroundModeSelect: byId(\"orbBackgroundModeSelect\"),\n orbBackgroundColorInput: byId(\"orbBackgroundColorInput\"),\n orbBackgroundScopeBadge: byId(\"orbBackgroundScopeBadge\"),\n orbBackgroundAvailability: byId(\"orbBackgroundAvailability\"),\n nonErrorPaletteSection: byId(\"nonErrorPaletteSection\"),\n paletteScopeBadge: byId(\"paletteScopeBadge\"),\n motionScopeBadge: byId(\"motionScopeBadge\"),\n paletteInputs: [...document.querySelectorAll(\"[data-palette-color]\")],\n glowColorControl: byId(\"glowColorControl\"),\n glowColorInput: byId(\"glowColorInput\"),\n glowEnabledInput: byId(\"glowEnabledInput\"),\n glowEnabledLabel: byId(\"glowEnabledLabel\"),\n resetPaletteButton: byId(\"resetPaletteButton\"),\n resolutionRange: byId(\"resolutionRange\"),\n resolutionValue: byId(\"resolutionValue\"),\n speedRange: byId(\"speedRange\"),\n speedValue: byId(\"speedValue\"),\n densityRange: byId(\"densityRange\"),\n densityValue: byId(\"densityValue\"),\n seedInput: byId(\"seedInput\"),\n randomizeButton: byId(\"randomizeButton\"),\n overrideSummary: byId(\"overrideSummary\"),\n workflowStatus: byId(\"workflowStatus\"),\n recentPresetSelect: byId(\"recentPresetSelect\"),\n savePresetButton: byId(\"savePresetButton\"),\n savePresetGalleryButton: byId(\"savePresetGalleryButton\"),\n savedPresetGallery: byId(\"savedPresetGallery\"),\n savedPresetEmpty: byId(\"savedPresetEmpty\"),\n savedPresetCount: byId(\"savedPresetCount\"),\n deletedPresetNotice: byId(\"deletedPresetNotice\"),\n deletedPresetMessage: byId(\"deletedPresetMessage\"),\n undoDeletePresetButton: byId(\"undoDeletePresetButton\"),\n undoStudioButton: byId(\"undoStudioButton\"),\n resetOverridesButton: byId(\"resetOverridesButton\"),\n openExportButton: byId(\"openExportButton\"),\n playButton: byId(\"playButton\"),\n autopilotButton: byId(\"autopilotButton\"),\n themeButton: byId(\"themeButton\"),\n openRecipeButton: byId(\"openRecipeButton\"),\n recipeFile: byId(\"recipeFile\"),\n importButton: byId(\"importButton\"),\n glyphFile: byId(\"glyphFile\"),\n exportPngButton: byId(\"exportPngButton\"),\n exportSvgButton: byId(\"exportSvgButton\"),\n exportAnimatedSvgButton: byId(\"exportAnimatedSvgButton\"),\n exportConfigButton: byId(\"exportConfigButton\"),\n copyCodeButton: byId(\"copyCodeButton\"),\n codeSnippet: byId(\"codeSnippet\"),\n fpsValue: byId(\"fpsValue\"),\n activeValue: byId(\"activeValue\"),\n currentStateValue: byId(\"currentStateValue\"),\n progressControl: byId(\"progressControl\"),\n progressRange: byId(\"progressRange\"),\n progressValue: byId(\"progressValue\"),\n signalTokenButton: byId(\"signalTokenButton\"),\n signalSearchButton: byId(\"signalSearchButton\"),\n signalAudioButton: byId(\"signalAudioButton\"),\n exportDialog: byId(\"exportDialog\"),\n exportForm: byId(\"exportForm\"),\n closeExportButton: byId(\"closeExportButton\"),\n cancelExportButton: byId(\"cancelExportButton\"),\n exportPreviewCanvas: byId(\"exportPreviewCanvas\"),\n exportPreviewNote: byId(\"exportPreviewNote\"),\n exportFormatSelect: byId(\"exportFormatSelect\"),\n exportSizeSelect: byId(\"exportSizeSelect\"),\n exportFilenameInput: byId(\"exportFilenameInput\"),\n exportBackgroundInput: byId(\"exportBackgroundInput\"),\n exportDimensionsValue: byId(\"exportDimensionsValue\"),\n exportCompositionValue: byId(\"exportCompositionValue\"),\n exportMotionValue: byId(\"exportMotionValue\"),\n exportBackgroundValue: byId(\"exportBackgroundValue\"),\n confirmExportButton: byId(\"confirmExportButton\"),\n};\n\nconst sprites = listSprites();\nconst spriteById = new Map(sprites.map((sprite) => [sprite.id, sprite]));\nconst builtInSpriteIds = new Set(sprites.map((sprite) => sprite.id));\nconst STUDIO_PACKAGE = \"@joan/procedural-glyph-engine\";\nconst STUDIO_VERSION = \"5.0.0\";\nconst RECENT_PRESET_KEY = \"joan.studio.recent-presets.v1\";\nconst THEME_STORAGE_KEY = \"joan.studio.theme.v1\";\nconst MAX_RECENT_PRESETS = 24;\nconst SAFE_COLOR =\n /^(?:#[\\da-f]{3,8}|(?:rgba?|hsla?)\\([\\d\\s.,%+\\/-]+\\)|transparent|aqua|black|blue|fuchsia|gray|green|grey|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow)$/i;\nconst humanize = (value) =>\n String(value)\n .replaceAll(\".\", \" · \")\n .replaceAll(\"-\", \" \")\n .replace(/\\b\\w/g, (letter) => letter.toUpperCase());\nconst formatNumber = (value, digits = 2) =>\n Number(value).toFixed(digits).replace(/\\.?0+$/, \"\");\nconst supportsGestaltBoundary = (recipe) =>\n recipe?.composition?.gestaltBoundary === true;\n\nfunction addOptions(select, values, getLabel = humanize) {\n if (!select) return;\n select.textContent = \"\";\n for (const value of values) {\n const option = document.createElement(\"option\");\n option.value = value;\n option.textContent = getLabel(value);\n select.append(option);\n }\n}\n\naddOptions(\n elements.fieldSelect,\n [\"__recipe__\", ...FIELD_IDS],\n (value) => (value === \"__recipe__\" ? \"Recipe field stack\" : humanize(value)),\n);\naddOptions(\n elements.transitionSelect,\n [\"__recipe__\", ...TRANSITIONS],\n (value) => (value === \"__recipe__\" ? \"Recipe transition\" : humanize(value)),\n);\nconst PIXEL_SHAPE_LABELS = Object.freeze({\n \"square-cross\": \"Square + cross\",\n \"square-cross-ring\": \"Square + cross + ring\",\n});\naddOptions(\n elements.pixelShapeSelect,\n PIXEL_SHAPES,\n (value) => PIXEL_SHAPE_LABELS[value] || humanize(value),\n);\naddOptions(elements.pixelSwitchSelect, PIXEL_SWITCHES);\n\nconst controlTipBindings = new Map();\nconst CONTROL_TOOLTIP_HOVER_DELAY_MS = 1000;\nlet activeControlTip = null;\nlet dismissedControlTip = null;\nlet controlTipOpenTimer = null;\nlet controlTipCloseTimer = null;\nlet controlTipHideTimer = null;\nconst controlTipPortal = document.createElement(\"div\");\ncontrolTipPortal.className = \"control-tooltip\";\ncontrolTipPortal.hidden = true;\ncontrolTipPortal.setAttribute(\"aria-hidden\", \"true\");\ndocument.body.append(controlTipPortal);\n\nfunction controlTipText(control) {\n const selectHelp = SELECT_CONTROL_HELP[control.id];\n if (selectHelp) {\n const optionHelp = selectHelp.options[control.value] || \"\";\n return `${selectHelp.summary} ${optionHelp}`.trim();\n }\n\n const base = VALUE_CONTROL_HELP[control.id] || \"\";\n switch (control.id) {\n case \"resolutionRange\":\n return `${base} Current grid: ${control.value}×${control.value}.`;\n case \"speedRange\":\n return `${base} Current speed: ${formatNumber(control.value)}×.`;\n case \"densityRange\":\n return `${base} Current density: ${formatNumber(control.value)}.`;\n case \"backgroundColorInput\":\n case \"offColorInput\":\n case \"inkColorInput\":\n case \"accentColorInput\":\n return `${base} Current color: ${control.value.toUpperCase()}.`;\n case \"glowColorInput\":\n return control.disabled\n ? `${base} Glow is currently off.`\n : `${base} Current color: ${control.value.toUpperCase()}.`;\n case \"glowEnabledInput\":\n return `${base} Glow is currently ${control.checked ? \"on\" : \"off\"}.`;\n case \"orbBoundaryInput\":\n return control.disabled\n ? `${base} This state does not use an orb boundary; your choice remains saved.`\n : `${base} Current mode: ${control.checked ? \"Gestalt\" : \"Defined\"}.`;\n case \"orbBackgroundEnabledInput\":\n return control.disabled\n ? `${base} This state does not use a configurable orb; your choice remains saved.`\n : `${base} The orb background is currently ${control.checked ? \"on\" : \"off\"}.`;\n case \"orbBackgroundColorInput\":\n return control.disabled\n ? `${base} Enable the orb background to choose a color.`\n : `${base} Current color: ${control.value.toUpperCase()}.`;\n default:\n return base;\n }\n}\n\nfunction positionActiveControlTip() {\n if (!activeControlTip || controlTipPortal.hidden) return;\n const { host } = activeControlTip;\n if (!host.isConnected) return;\n const anchor = host.getBoundingClientRect();\n const tooltip = controlTipPortal.getBoundingClientRect();\n const position = placeControlTooltip(\n anchor,\n tooltip,\n {\n width: window.innerWidth,\n height: window.innerHeight,\n },\n { gap: 8, gutter: 12 },\n );\n controlTipPortal.style.left = `${position.left}px`;\n controlTipPortal.style.top = `${position.top}px`;\n controlTipPortal.dataset.placement = position.placement;\n}\n\nfunction updateControlTip(control) {\n const binding = controlTipBindings.get(control);\n if (!binding) return;\n const text = controlTipText(control);\n binding.description.textContent = text;\n if (activeControlTip?.control === control) {\n controlTipPortal.textContent = text;\n positionActiveControlTip();\n }\n}\n\nfunction showControlTip(control) {\n const binding = controlTipBindings.get(control);\n if (!binding || dismissedControlTip === control) return;\n window.clearTimeout(controlTipOpenTimer);\n window.clearTimeout(controlTipCloseTimer);\n window.clearTimeout(controlTipHideTimer);\n activeControlTip = binding;\n updateControlTip(control);\n controlTipPortal.textContent = binding.description.textContent;\n controlTipPortal.hidden = false;\n controlTipPortal.classList.add(\"is-visible\");\n positionActiveControlTip();\n}\n\nfunction hideControlTip() {\n window.clearTimeout(controlTipOpenTimer);\n window.clearTimeout(controlTipCloseTimer);\n controlTipPortal.classList.remove(\"is-visible\");\n activeControlTip = null;\n controlTipHideTimer = window.setTimeout(() => {\n if (!activeControlTip) controlTipPortal.hidden = true;\n }, 150);\n}\n\nfunction scheduleControlTipOpen(control) {\n window.clearTimeout(controlTipOpenTimer);\n controlTipOpenTimer = window.setTimeout(() => {\n const binding = controlTipBindings.get(control);\n if (binding?.host.matches(\":hover\")) showControlTip(control);\n }, CONTROL_TOOLTIP_HOVER_DELAY_MS);\n}\n\nfunction scheduleControlTipClose() {\n window.clearTimeout(controlTipCloseTimer);\n controlTipCloseTimer = window.setTimeout(() => {\n const hostHasFocus = activeControlTip?.host.matches(\":focus-within\");\n const hostHasPointer = activeControlTip?.host.matches(\":hover\");\n if (!hostHasFocus && !hostHasPointer) {\n hideControlTip();\n }\n }, 120);\n}\n\nfunction appendDescriptionId(control, id) {\n const ids = new Set(\n (control.getAttribute(\"aria-describedby\") || \"\")\n .split(/\\s+/)\n .filter(Boolean),\n );\n ids.add(id);\n control.setAttribute(\"aria-describedby\", [...ids].join(\" \"));\n}\n\nfunction registerControlTip(control) {\n if (!control?.id || controlTipBindings.has(control)) return;\n const label = control.closest(\"label\");\n const labelControls = label?.querySelectorAll(\n \"button, input, select, textarea\",\n );\n const host =\n control.matches(\"button\") || labelControls?.length > 1 ? control : label;\n if (!host) return;\n\n const description = document.createElement(\"span\");\n description.className = \"visually-hidden\";\n description.id = `${control.id}Help`;\n description.setAttribute(\"role\", \"tooltip\");\n document.body.append(description);\n appendDescriptionId(control, description.id);\n host.classList.add(\"control-with-tooltip\");\n\n const binding = { control, host, description };\n controlTipBindings.set(control, binding);\n updateControlTip(control);\n\n host.addEventListener(\"pointerenter\", (event) => {\n if (event.pointerType !== \"touch\") scheduleControlTipOpen(control);\n });\n host.addEventListener(\"pointerleave\", () => {\n window.clearTimeout(controlTipOpenTimer);\n if (dismissedControlTip === control) dismissedControlTip = null;\n scheduleControlTipClose();\n });\n host.addEventListener(\"focusin\", () => {\n window.clearTimeout(controlTipOpenTimer);\n showControlTip(control);\n });\n host.addEventListener(\"focusout\", () => {\n if (dismissedControlTip === control) dismissedControlTip = null;\n scheduleControlTipClose();\n });\n control.addEventListener(\"input\", () => updateControlTip(control));\n control.addEventListener(\"change\", () => updateControlTip(control));\n}\n\nfunction setupControlTips() {\n for (const id of Object.keys(SELECT_CONTROL_HELP)) {\n registerControlTip(byId(id));\n }\n for (const id of Object.keys(VALUE_CONTROL_HELP)) {\n registerControlTip(byId(id));\n }\n\n document.addEventListener(\"pointerdown\", (event) => {\n if (\n activeControlTip &&\n !activeControlTip.host.contains(event.target) &&\n !controlTipPortal.contains(event.target)\n ) {\n hideControlTip();\n }\n });\n document.addEventListener(\"keydown\", (event) => {\n if (event.key !== \"Escape\" || !activeControlTip) return;\n dismissedControlTip = activeControlTip.control;\n hideControlTip();\n });\n window.addEventListener(\"scroll\", positionActiveControlTip, {\n passive: true,\n });\n window.addEventListener(\"resize\", positionActiveControlTip);\n}\n\nsetupControlTips();\n\nconst engine = new JoanGlyphEngine(elements.canvas, {\n sprite: \"ai.idle\",\n seed: elements.seedInput?.value || \"world-class-ai\",\n gridSize: Number(elements.resolutionRange?.value) || 68,\n speed: Number(elements.speedRange?.value) || 1,\n density: Number(elements.densityRange?.value) || 1,\n pixelShape: null,\n pixelSwitch: null,\n transition: null,\n orbBoundary: \"defined\",\n orbBackgroundColor: null,\n orbBackgroundMode: \"none\",\n ariaLive: elements.liveRegion,\n quality: \"auto\",\n dprMax: 1.5,\n});\n\nwindow.joanEngine = engine;\n\nlet heroEngine = null;\n\nconst previewEngines = [];\nconst previewById = new Map();\nconst previewDescriptors = new Map();\nconst hoveredPreviewIds = new Set();\nconst focusedPreviewIds = new Set();\nconst spriteButtons = new Map();\nconst reduceMotion = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\nlet selectedSpriteId = engine.currentRecipe.id;\nlet autopilotTimer = null;\nlet autopilotIndex = 0;\nlet audioTimer = null;\nlet isLight = document.documentElement.dataset.theme === \"light\";\nlet pixelShapeOverridden = false;\nlet pixelSwitchOverridden = false;\nlet transitionOverridden = false;\nlet transitionOverrideValue = null;\nlet customNonErrorPalette = {};\nlet savedGlowOverride = null;\nlet hadSavedGlowOverride = false;\nlet orbBackgroundColorChoice = \"#262626\";\nlet orbBackgroundModeChoice = \"solid\";\nlet previewMode = \"fit\";\nlet baselineStudioState = null;\nlet undoStack = [];\nlet isRestoringStudioState = false;\nlet recentPresets = [];\nlet deletedPreset = null;\nconst activeMutations = new Set();\nlet workflowStatusTimer = null;\nlet exportReturnFocus = null;\nlet previewResolutionVersion = 0;\nlet previewRefreshVersion = 0;\nlet previewsDisposed = false;\nlet previewCardObserver = null;\nlet savedPresetPreviewObserver = null;\nconst savedPresetPreviewQueue = new Map();\nlet pendingResolution = engine.gridSize;\nlet resolutionCommitTimer = null;\nconst RESOLUTION_COMMIT_DELAY_MS = 90;\nconst urgentPreviewIds = new Set();\nlet urgentPreviewScheduled = false;\nconst previewWorkQueue = [];\nlet previewWorkScheduled = false;\n\nfunction scheduleIdlePreviewWork(callback) {\n if (typeof window.requestIdleCallback === \"function\") {\n window.requestIdleCallback(callback, { timeout: 240 });\n } else {\n window.setTimeout(callback, 16);\n }\n}\n\nfunction runPreviewWorkSlice(deadline) {\n if (previewsDisposed) {\n previewWorkQueue.length = 0;\n previewWorkScheduled = false;\n return;\n }\n const task = previewWorkQueue.shift();\n try {\n task?.(deadline);\n } finally {\n if (previewWorkQueue.length) scheduleIdlePreviewWork(runPreviewWorkSlice);\n else previewWorkScheduled = false;\n }\n}\n\nfunction enqueuePreviewWork(task, { priority = false } = {}) {\n if (typeof task !== \"function\" || previewsDisposed) return;\n if (priority) previewWorkQueue.unshift(task);\n else previewWorkQueue.push(task);\n if (previewWorkScheduled) return;\n previewWorkScheduled = true;\n scheduleIdlePreviewWork(runPreviewWorkSlice);\n}\n\nfunction setPreviewResolution(preview, resolution) {\n if (preview.gridSize !== resolution) preview.setResolution(resolution);\n if (preview.canvas?.dataset) {\n preview.canvas.dataset.gridSize = String(resolution);\n }\n}\n\nfunction currentPreviewProgress() {\n const value = Number(elements.progressRange?.value);\n return Number.isFinite(value)\n ? Math.min(1, Math.max(0, value / 100))\n : 0.48;\n}\n\nfunction previewId(preview) {\n return preview?.previewSpriteId || preview?.currentRecipe?.id || null;\n}\n\nfunction previewShouldAnimate(id) {\n return (\n !reduceMotion.matches &&\n shouldAnimateThumbnail(id, {\n selectedId: selectedSpriteId,\n mainRunning: engine.running,\n hoveredIds: hoveredPreviewIds,\n focusedIds: focusedPreviewIds,\n })\n );\n}\n\nfunction freezePreview(preview) {\n return renderThumbnailRestFrame(preview, {\n progress: currentPreviewProgress(),\n });\n}\n\nfunction freezePreviewIfInactive(id) {\n const preview = previewById.get(id);\n if (preview && !previewShouldAnimate(id)) freezePreview(preview);\n}\n\nfunction prioritizedPreviewQueue() {\n const queue = [];\n const seen = new Set();\n const add = (preview) => {\n if (!preview || seen.has(preview)) return;\n seen.add(preview);\n queue.push(preview);\n };\n add(previewById.get(selectedSpriteId));\n for (const id of hoveredPreviewIds) add(previewById.get(id));\n for (const id of focusedPreviewIds) add(previewById.get(id));\n for (const preview of previewEngines) add(preview);\n return queue;\n}\n\nfunction schedulePreviewResolutionSync(value = engine.gridSize) {\n const resolution = Math.round(Number(value) || engine.gridSize);\n const version = ++previewResolutionVersion;\n const queue = prioritizedPreviewQueue();\n const updateNext = () => {\n if (version !== previewResolutionVersion) return;\n const preview = queue.shift();\n if (!preview) return;\n setPreviewResolution(preview, resolution);\n if (!previewShouldAnimate(previewId(preview))) freezePreview(preview);\n if (queue.length) enqueuePreviewWork(updateNext);\n };\n enqueuePreviewWork(updateNext);\n}\n\nfunction cancelPendingResolutionCommit() {\n if (resolutionCommitTimer !== null) {\n window.clearTimeout(resolutionCommitTimer);\n resolutionCommitTimer = null;\n }\n}\n\nfunction commitPendingResolution() {\n cancelPendingResolutionCommit();\n const value = Math.round(Number(pendingResolution) || engine.gridSize);\n if (engine.gridSize !== value) engine.setResolution(value);\n applyPreviewMode(previewMode);\n scheduleStudioMetadataUpdate();\n return value;\n}\n\nfunction scheduleResolutionCommit(value) {\n pendingResolution = Math.round(Number(value) || engine.gridSize);\n cancelPendingResolutionCommit();\n resolutionCommitTimer = window.setTimeout(\n commitPendingResolution,\n RESOLUTION_COMMIT_DELAY_MS,\n );\n}\n\nfunction schedulePreviewRefresh() {\n const version = ++previewRefreshVersion;\n const queue = prioritizedPreviewQueue().filter((preview) => {\n return !previewShouldAnimate(previewId(preview));\n });\n const refreshNext = () => {\n if (version !== previewRefreshVersion) return;\n const preview = queue.shift();\n if (!preview) return;\n if (!previewShouldAnimate(previewId(preview))) freezePreview(preview);\n if (queue.length) enqueuePreviewWork(refreshNext);\n };\n enqueuePreviewWork(refreshNext);\n}\n\nfunction applyPreviewMode(mode) {\n const details = previewModeDetails(mode, engine.gridSize);\n previewMode = normalizePreviewMode(details.mode);\n const actual = previewMode === \"actual\";\n\n if (engine.options.previewMode !== previewMode) {\n engine.setOptions({ previewMode });\n }\n elements.canvasFrame?.classList.toggle(\"is-actual-preview\", actual);\n elements.canvasFrame?.style.setProperty(\n \"--preview-grid-size\",\n `${details.resolution}px`,\n );\n elements.previewFitButton?.setAttribute(\"aria-pressed\", String(!actual));\n elements.previewActualButton?.setAttribute(\"aria-pressed\", String(actual));\n\n if (elements.previewActualButton) {\n elements.previewActualButton.title = actual\n ? `Actual size: ${details.resolution}×${details.resolution}px. Each grid cell uses one CSS pixel.`\n : `Show actual size at ${details.resolution}×${details.resolution}px, with one CSS pixel per grid cell.`;\n }\n if (elements.frameRenderBadge) {\n elements.frameRenderBadge.textContent = actual ? \"1:1 render\" : \"Live render\";\n }\n if (elements.canvasHint) {\n elements.canvasHint.textContent = actual\n ? `${details.resolution}×${details.resolution}px · one cell = one CSS pixel`\n : \"Move · press · drop a glyph\";\n }\n}\n\nconst lightPalette = {\n background: \"#e5e6e2\",\n off: \"#c7cbc8\",\n ink: \"#17191e\",\n variants: {\n neutral: { accent: \"#464c52\", glow: \"#8b9096\" },\n info: { accent: \"#50565b\", glow: \"#8a8f94\" },\n success: { accent: \"#303438\", glow: \"#777b7e\" },\n warning: { accent: \"#5b5a56\", glow: \"#92908a\" },\n danger: { accent: \"#3f4245\", glow: \"#838689\" },\n celebration: { accent: \"#585d63\", glow: \"#95999e\" },\n },\n};\n\nconst darkPalette = {\n background: \"#070708\",\n off: \"#1f2022\",\n ink: \"#f2f1ee\",\n variants: {\n neutral: { accent: \"#c9c8c4\", glow: \"#8b8d90\" },\n info: { accent: \"#b7babd\", glow: \"#777b80\" },\n success: { accent: \"#d6d6d0\", glow: \"#959792\" },\n warning: { accent: \"#b9b6af\", glow: \"#82807b\" },\n danger: { accent: \"#c5c4c1\", glow: \"#8d8c89\" },\n celebration: { accent: \"#eeeeea\", glow: \"#aaa9a5\" },\n },\n};\n\nfunction studioPaletteOverride() {\n const palette = {\n ...(isLight ? lightPalette : darkPalette),\n ...customNonErrorPalette,\n };\n return palette;\n}\n\nfunction setGlowControlEnabled(enabled) {\n const isEnabled = Boolean(enabled);\n if (elements.glowEnabledInput) elements.glowEnabledInput.checked = isEnabled;\n if (elements.glowEnabledLabel) {\n elements.glowEnabledLabel.textContent = isEnabled ? \"Color\" : \"None\";\n }\n if (elements.glowColorInput) elements.glowColorInput.disabled = !isEnabled;\n elements.glowColorControl?.classList.toggle(\"is-glow-disabled\", !isEnabled);\n\n const output = elements.glowColorInput?.parentElement?.querySelector(\"output\");\n if (output) {\n output.textContent = isEnabled\n ? elements.glowColorInput.value.toUpperCase()\n : \"NONE\";\n }\n updateControlTip(elements.glowEnabledInput);\n updateControlTip(elements.glowColorInput);\n}\n\nfunction normalizedOrbBackgroundColor(value) {\n if (typeof value !== \"string\") return null;\n const color = value.trim();\n return color && color.toLowerCase() !== \"transparent\" ? color : null;\n}\n\nfunction normalizedOrbBackgroundMode(value, color = null) {\n if (value === \"none\") return \"none\";\n if (value === \"pixelated\" || value === \"solid\") return value;\n return normalizedOrbBackgroundColor(color) ? \"solid\" : \"none\";\n}\n\nfunction setOrbBackgroundControlState(\n color = engine.options.orbBackgroundColor,\n available = supportsGestaltBoundary(engine.currentRecipe),\n mode = engine.options.orbBackgroundMode,\n) {\n const activeColor = normalizedOrbBackgroundColor(color);\n const activeMode = normalizedOrbBackgroundMode(mode, activeColor);\n const enabled = Boolean(activeColor) && activeMode !== \"none\";\n if (activeColor) orbBackgroundColorChoice = activeColor;\n if (activeMode !== \"none\") orbBackgroundModeChoice = activeMode;\n\n if (elements.orbBackgroundControl) {\n elements.orbBackgroundControl.dataset.available = String(available);\n elements.orbBackgroundControl.dataset.enabled = String(enabled);\n }\n if (elements.orbBackgroundEnabledInput) {\n elements.orbBackgroundEnabledInput.checked = enabled;\n elements.orbBackgroundEnabledInput.disabled = !available;\n }\n if (elements.orbBackgroundEnabledLabel) {\n elements.orbBackgroundEnabledLabel.textContent = enabled ? \"On\" : \"None\";\n }\n if (elements.orbBackgroundModeSelect) {\n elements.orbBackgroundModeSelect.value =\n activeMode === \"none\" ? orbBackgroundModeChoice : activeMode;\n elements.orbBackgroundModeSelect.disabled = !available || !enabled;\n }\n if (elements.orbBackgroundColorInput) {\n if (/^#[\\da-f]{6}$/i.test(activeColor || \"\")) {\n elements.orbBackgroundColorInput.value = activeColor;\n } else if (/^#[\\da-f]{6}$/i.test(orbBackgroundColorChoice)) {\n elements.orbBackgroundColorInput.value = orbBackgroundColorChoice;\n }\n elements.orbBackgroundColorInput.disabled = !available || !enabled;\n const output = elements.orbBackgroundColorInput.parentElement?.querySelector(\n \"output\",\n );\n if (output) output.textContent = enabled ? activeColor.toUpperCase() : \"NONE\";\n }\n if (elements.orbBackgroundAvailability) {\n elements.orbBackgroundAvailability.textContent = available\n ? enabled\n ? activeMode === \"pixelated\"\n ? \"Grid-aligned disk behind the orb pixels\"\n : \"Solid circle behind the orb pixels\"\n : \"Optional color behind the orb pixels\"\n : \"Available for Ready and ambient orbs · choice stays saved\";\n }\n updateControlTip(elements.orbBackgroundEnabledInput);\n updateControlTip(elements.orbBackgroundModeSelect);\n updateControlTip(elements.orbBackgroundColorInput);\n}\n\nfunction syncOrbAppearanceToPreviews({ refresh = true } = {}) {\n for (const preview of previewEngines) {\n preview.options.orbBoundary = engine.options.orbBoundary;\n preview.options.orbBackgroundColor = engine.options.orbBackgroundColor;\n preview.options.orbBackgroundMode = engine.options.orbBackgroundMode;\n }\n if (refresh) schedulePreviewRefresh();\n}\n\nfunction updatePaletteControls(palette) {\n for (const input of elements.paletteInputs) {\n const key = input.dataset.paletteColor;\n const value = palette?.[key];\n if (/^#[\\da-f]{6}$/i.test(value || \"\")) input.value = value;\n const output = input.parentElement?.querySelector(\"output\");\n if (key === \"glow\") {\n const enabled = String(value).trim().toLowerCase() !== \"transparent\";\n setGlowControlEnabled(enabled);\n } else if (output) {\n output.textContent = input.value.toUpperCase();\n }\n updateControlTip(input);\n }\n}\n\nfunction authoredPaletteForControls(recipe) {\n const palette = recipe?.palette || {};\n return {\n background: palette.background,\n off: palette.off || palette.shadow,\n ink: palette.ink,\n accent: palette.accent,\n glow: palette.glow || palette.accent,\n };\n}\n\nfunction nonErrorPaletteForProtectedPreview(palette) {\n if (!palette || typeof palette !== \"object\") return palette;\n return {\n ...palette,\n ...(palette.variants?.neutral || {}),\n };\n}\n\nfunction applyStudioPalette({\n refreshPreviews = true,\n immediatePreviewRefresh = false,\n} = {}) {\n const palette = studioPaletteOverride();\n engine.setNonErrorPalette(palette);\n heroEngine?.setNonErrorPalette(palette);\n for (const preview of previewEngines) {\n preview.setNonErrorPalette(palette, { render: false });\n }\n if (immediatePreviewRefresh) {\n for (const preview of previewEngines) freezePreview(preview);\n } else if (refreshPreviews) {\n schedulePreviewRefresh();\n }\n if (engine.currentRecipe.id !== \"status.error\") {\n updatePaletteControls(engine.exportConfig().palette);\n } else if (palette) {\n updatePaletteControls(nonErrorPaletteForProtectedPreview(palette));\n } else {\n updatePaletteControls(authoredPaletteForControls(spriteById.get(\"ai.idle\")));\n }\n updateWorkflowState();\n updateCode();\n}\n\nlet paletteUpdateFrame = null;\nfunction scheduleStudioPaletteUpdate() {\n if (paletteUpdateFrame !== null) return;\n paletteUpdateFrame = requestAnimationFrame(() => {\n paletteUpdateFrame = null;\n applyStudioPalette({ refreshPreviews: false });\n });\n}\n\nfunction flushStudioPaletteUpdate() {\n if (paletteUpdateFrame !== null) {\n cancelAnimationFrame(paletteUpdateFrame);\n paletteUpdateFrame = null;\n }\n applyStudioPalette();\n}\n\nlet studioMetadataFrame = null;\nfunction scheduleStudioMetadataUpdate() {\n if (studioMetadataFrame !== null) return;\n studioMetadataFrame = requestAnimationFrame(() => {\n studioMetadataFrame = null;\n updateWorkflowState();\n updateCode();\n });\n}\n\nfunction recipePixelSwitch(recipe) {\n const pixelSwitch = recipe?.pixelSwitch;\n return typeof pixelSwitch === \"string\"\n ? pixelSwitch\n : pixelSwitch?.name ||\n pixelSwitch?.type ||\n pixelSwitch?.mode ||\n \"ordered-dither\";\n}\n\nfunction normalizeStructuredOption(value, values, keys, canonicalKey) {\n if (typeof value === \"string\") return values.includes(value) ? value : null;\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;\n const name = keys.map((key) => value[key]).find((entry) => values.includes(entry));\n if (!name) return null;\n return { ...cloneData(value), [canonicalKey]: name };\n}\n\nfunction normalizeTransitionOption(value) {\n return normalizeStructuredOption(\n value,\n TRANSITIONS,\n [\"name\", \"type\", \"enter\"],\n \"name\",\n );\n}\n\nfunction normalizePixelSwitchOption(value) {\n return normalizeStructuredOption(\n value,\n PIXEL_SWITCHES,\n [\"mode\", \"name\", \"type\"],\n \"mode\",\n );\n}\n\nfunction transitionOptionName(value) {\n const normalized = normalizeTransitionOption(value);\n return typeof normalized === \"string\" ? normalized : normalized?.name || null;\n}\n\nfunction pixelSwitchOptionMode(value) {\n const normalized = normalizePixelSwitchOption(value);\n return typeof normalized === \"string\" ? normalized : normalized?.mode || null;\n}\n\nfunction sameStudioValue(left, right) {\n return JSON.stringify(left) === JSON.stringify(right);\n}\n\nfunction formatCodeValue(value, indent = \" \") {\n const serialized = JSON.stringify(value, null, 2);\n return serialized ? serialized.replaceAll(\"\\n\", `\\n${indent}`) : \"null\";\n}\n\nfunction recipeDescription(recipe) {\n return (\n recipe?.semantic?.description ||\n recipe?.semantic?.meaning ||\n recipe?.description ||\n \"A deterministic semantic sprite composed from a crisp mask or field orb, procedural texture, and persistent pixel gate.\"\n );\n}\n\nfunction recipeLabel(recipe) {\n return (\n recipe?.labels?.default ||\n recipe?.semantic?.label ||\n recipe?.label ||\n humanize(recipe?.id || \"state\")\n );\n}\n\nfunction showWorkflowStatus(message, { announce = true, tone = \"neutral\" } = {}) {\n window.clearTimeout(workflowStatusTimer);\n if (elements.workflowStatus) {\n elements.workflowStatus.textContent = message;\n elements.workflowStatus.dataset.tone = tone;\n }\n if (announce && elements.liveRegion) elements.liveRegion.textContent = message;\n workflowStatusTimer = window.setTimeout(() => {\n if (!elements.workflowStatus) return;\n elements.workflowStatus.textContent = \"Ready to customize\";\n delete elements.workflowStatus.dataset.tone;\n }, 4200);\n}\n\nfunction setScopeBadge(element, text, kind = \"recipe\") {\n if (!element) return;\n element.textContent = text;\n element.dataset.scope = kind;\n}\n\nfunction buildStatePicker(query = \"\") {\n const select = elements.statePickerSelect;\n if (!select) return;\n const normalized = query.trim().toLowerCase();\n const matches = sprites.filter((recipe) => {\n if (!normalized) return true;\n return [\n recipe.id,\n recipeLabel(recipe),\n recipe.semantic?.category,\n recipeDescription(recipe),\n ]\n .filter(Boolean)\n .some((value) => String(value).toLowerCase().includes(normalized));\n });\n\n const selectedMatches = matches.some((recipe) => recipe.id === selectedSpriteId);\n const groups = new Map();\n for (const recipe of matches) {\n const category = humanize(recipe.semantic?.category || \"Other\");\n if (!groups.has(category)) groups.set(category, []);\n groups.get(category).push(recipe);\n }\n\n select.textContent = \"\";\n if (!selectedMatches && spriteById.has(selectedSpriteId)) {\n const currentGroup = document.createElement(\"optgroup\");\n currentGroup.label = \"Current state\";\n const current = spriteById.get(selectedSpriteId);\n const option = document.createElement(\"option\");\n option.value = current.id;\n option.textContent = recipeLabel(current);\n currentGroup.append(option);\n select.append(currentGroup);\n }\n for (const [category, recipes] of groups) {\n const group = document.createElement(\"optgroup\");\n group.label = category;\n for (const recipe of recipes) {\n const option = document.createElement(\"option\");\n option.value = recipe.id;\n option.textContent = recipeLabel(recipe);\n group.append(option);\n }\n select.append(group);\n }\n\n if (!select.options.length) {\n const option = document.createElement(\"option\");\n option.textContent = \"No matching states\";\n option.disabled = true;\n select.append(option);\n select.disabled = true;\n } else {\n select.disabled = false;\n select.value = selectedSpriteId;\n }\n if (elements.statePickerStatus) {\n elements.statePickerStatus.textContent = normalized\n ? `${matches.length} matching ${matches.length === 1 ? \"state\" : \"states\"}`\n : `${sprites.length} states available in ${groups.size} categories`;\n }\n}\n\nfunction cloneData(value) {\n if (typeof structuredClone === \"function\") return structuredClone(value);\n return JSON.parse(JSON.stringify(value));\n}\n\nfunction captureStudioState() {\n return {\n sprite: engine.currentRecipe.id,\n fieldOverride: engine.fieldOverride || null,\n transitionOverride: transitionOverridden\n ? normalizeTransitionOption(transitionOverrideValue)\n : null,\n pixelShape: engine.options.pixelShape || elements.pixelShapeSelect?.value || null,\n pixelShapeOverridden,\n pixelSwitch: pixelSwitchOverridden\n ? normalizePixelSwitchOption(engine.options.pixelSwitch)\n : null,\n pixelSwitchOverridden,\n orbBoundary:\n engine.options.orbBoundary === \"gestalt\" ? \"gestalt\" : \"defined\",\n orbBackgroundColor:\n normalizedOrbBackgroundColor(engine.options.orbBackgroundColor),\n orbBackgroundMode: normalizedOrbBackgroundMode(\n engine.options.orbBackgroundMode,\n engine.options.orbBackgroundColor,\n ),\n orbBackgroundColorChoice,\n orbBackgroundModeChoice,\n gridSize: engine.gridSize,\n speed: Number(engine.options.speed),\n density: Number(engine.options.density),\n seed: engine.seed,\n progress: Number(engine.progress),\n palette: { ...customNonErrorPalette },\n savedGlowOverride,\n hadSavedGlowOverride,\n previewMode,\n };\n}\n\nfunction changedSettingCount(state, baseline = baselineStudioState) {\n if (!baseline) return 0;\n let count = 0;\n if (state.fieldOverride !== baseline.fieldOverride) count += 1;\n if (!sameStudioValue(state.transitionOverride, baseline.transitionOverride)) count += 1;\n if (state.pixelShape !== baseline.pixelShape) count += 1;\n if (\n state.pixelSwitchOverridden !== baseline.pixelSwitchOverridden ||\n !sameStudioValue(state.pixelSwitch, baseline.pixelSwitch)\n ) {\n count += 1;\n }\n if (state.orbBoundary !== baseline.orbBoundary) count += 1;\n if (\n state.orbBackgroundColor !== baseline.orbBackgroundColor ||\n state.orbBackgroundMode !== baseline.orbBackgroundMode\n ) {\n count += 1;\n }\n if (state.gridSize !== baseline.gridSize) count += 1;\n if (state.speed !== baseline.speed) count += 1;\n if (state.density !== baseline.density) count += 1;\n if (state.seed !== baseline.seed) count += 1;\n if (JSON.stringify(state.palette) !== JSON.stringify(baseline.palette)) count += 1;\n return count;\n}\n\nfunction updateWorkflowState() {\n const state = captureStudioState();\n const changes = changedSettingCount(state);\n const motionCustom = Boolean(\n baselineStudioState &&\n (state.gridSize !== baselineStudioState.gridSize ||\n state.speed !== baselineStudioState.speed ||\n state.density !== baselineStudioState.density),\n );\n const pixelShapeCustom = Boolean(\n baselineStudioState && state.pixelShape !== baselineStudioState.pixelShape,\n );\n\n setScopeBadge(\n elements.fieldScopeBadge,\n state.fieldOverride ? \"Custom\" : \"Recipe\",\n state.fieldOverride ? \"custom\" : \"recipe\",\n );\n setScopeBadge(\n elements.transitionScopeBadge,\n state.transitionOverride ? \"Custom\" : \"Recipe\",\n state.transitionOverride ? \"custom\" : \"recipe\",\n );\n setScopeBadge(\n elements.pixelShapeScopeBadge,\n pixelShapeCustom ? \"Custom · carries\" : \"Carries\",\n pixelShapeCustom ? \"custom\" : \"carry\",\n );\n setScopeBadge(\n elements.pixelSwitchScopeBadge,\n state.pixelSwitchOverridden ? \"Custom · carries\" : \"Recipe\",\n state.pixelSwitchOverridden ? \"custom\" : \"recipe\",\n );\n const boundaryAvailable = supportsGestaltBoundary(engine.currentRecipe);\n setScopeBadge(\n elements.orbBoundaryScopeBadge,\n boundaryAvailable ? \"Carries\" : \"Orb only\",\n boundaryAvailable\n ? state.orbBoundary === \"gestalt\"\n ? \"custom\"\n : \"carry\"\n : \"recipe\",\n );\n setScopeBadge(\n elements.orbBackgroundScopeBadge,\n boundaryAvailable\n ? state.orbBackgroundMode !== \"none\" && state.orbBackgroundColor\n ? `${state.orbBackgroundMode === \"pixelated\" ? \"Pixels\" : \"Solid\"} · carries`\n : \"None\"\n : \"Orb only\",\n boundaryAvailable\n ? state.orbBackgroundMode !== \"none\" && state.orbBackgroundColor\n ? \"custom\"\n : \"carry\"\n : \"recipe\",\n );\n setScopeBadge(\n elements.paletteScopeBadge,\n Object.keys(state.palette).length ? \"Custom · carries\" : \"Theme\",\n Object.keys(state.palette).length ? \"custom\" : \"carry\",\n );\n setScopeBadge(\n elements.motionScopeBadge,\n motionCustom ? \"Custom\" : \"Recipe\",\n motionCustom ? \"custom\" : \"recipe\",\n );\n setScopeBadge(\n elements.overrideSummary,\n changes ? `${changes} custom ${changes === 1 ? \"change\" : \"changes\"}` : \"Recipe base\",\n changes ? \"custom\" : \"recipe\",\n );\n if (elements.resetOverridesButton) {\n elements.resetOverridesButton.disabled = changes === 0;\n }\n if (elements.undoStudioButton) {\n elements.undoStudioButton.disabled = undoStack.length === 0;\n const latest = undoStack.at(-1);\n elements.undoStudioButton.title = latest\n ? `Undo: ${latest.label}`\n : \"Nothing to undo\";\n }\n}\n\nfunction pushUndoSnapshot(label) {\n if (isRestoringStudioState) return;\n undoStack.push({ label, state: cloneData(captureStudioState()) });\n if (undoStack.length > 24) undoStack.shift();\n updateWorkflowState();\n}\n\nfunction beginMutation(key, label) {\n if (activeMutations.has(key)) return;\n activeMutations.add(key);\n pushUndoSnapshot(label);\n}\n\nfunction endMutation(key) {\n activeMutations.delete(key);\n}\n\nfunction syncStudioInputs(state) {\n if (elements.fieldSelect) {\n elements.fieldSelect.value = state.fieldOverride || \"__recipe__\";\n }\n if (elements.transitionSelect) {\n elements.transitionSelect.value =\n transitionOptionName(state.transitionOverride) || \"__recipe__\";\n }\n if (elements.pixelShapeSelect && state.pixelShape) {\n elements.pixelShapeSelect.value = state.pixelShape;\n }\n if (elements.pixelSwitchSelect) {\n const recipe = spriteById.get(state.sprite);\n elements.pixelSwitchSelect.value = state.pixelSwitchOverridden\n ? pixelSwitchOptionMode(state.pixelSwitch)\n : recipePixelSwitch(recipe);\n }\n if (elements.orbBoundaryInput) {\n elements.orbBoundaryInput.checked = state.orbBoundary === \"gestalt\";\n }\n orbBackgroundColorChoice =\n state.orbBackgroundColorChoice ||\n state.orbBackgroundColor ||\n orbBackgroundColorChoice;\n orbBackgroundModeChoice = normalizedOrbBackgroundMode(\n state.orbBackgroundModeChoice,\n state.orbBackgroundColorChoice,\n );\n if (orbBackgroundModeChoice === \"none\") orbBackgroundModeChoice = \"solid\";\n setOrbBackgroundControlState(\n state.orbBackgroundColor,\n supportsGestaltBoundary(spriteById.get(state.sprite)),\n state.orbBackgroundMode,\n );\n if (elements.resolutionRange) elements.resolutionRange.value = String(state.gridSize);\n if (elements.resolutionValue) {\n elements.resolutionValue.textContent = `${state.gridSize}×${state.gridSize}`;\n }\n if (elements.speedRange) elements.speedRange.value = String(state.speed);\n if (elements.speedValue) elements.speedValue.textContent = `${formatNumber(state.speed)}×`;\n if (elements.densityRange) elements.densityRange.value = String(state.density);\n if (elements.densityValue) {\n elements.densityValue.textContent = formatNumber(state.density);\n }\n if (elements.seedInput) elements.seedInput.value = state.seed;\n if (elements.progressRange) {\n elements.progressRange.value = String(Math.round(state.progress * 100));\n }\n if (elements.progressValue) {\n elements.progressValue.textContent = `${Math.round(state.progress * 100)}%`;\n }\n}\n\nfunction restoreStudioState(state, { announce = null } = {}) {\n if (!state || !spriteById.has(state.sprite)) return;\n isRestoringStudioState = true;\n try {\n stopAutopilot();\n cancelPendingResolutionCommit();\n pendingResolution = state.gridSize;\n activeMutations.clear();\n transitionOverrideValue = normalizeTransitionOption(state.transitionOverride);\n transitionOverridden = Boolean(transitionOverrideValue);\n pixelShapeOverridden = Boolean(state.pixelShapeOverridden);\n const pixelSwitchOverride = normalizePixelSwitchOption(state.pixelSwitch);\n pixelSwitchOverridden = Boolean(\n state.pixelSwitchOverridden && pixelSwitchOverride,\n );\n customNonErrorPalette = { ...(state.palette || {}) };\n savedGlowOverride = state.savedGlowOverride ?? null;\n hadSavedGlowOverride = Boolean(state.hadSavedGlowOverride);\n orbBackgroundColorChoice =\n state.orbBackgroundColorChoice ||\n state.orbBackgroundColor ||\n orbBackgroundColorChoice;\n orbBackgroundModeChoice = normalizedOrbBackgroundMode(\n state.orbBackgroundModeChoice,\n state.orbBackgroundColorChoice,\n );\n if (orbBackgroundModeChoice === \"none\") orbBackgroundModeChoice = \"solid\";\n\n engine.setOptions({\n pixelShape: pixelShapeOverridden ? state.pixelShape : null,\n pixelSwitch: pixelSwitchOverridden ? pixelSwitchOverride : null,\n transition: transitionOverrideValue,\n speed: state.speed,\n density: state.density,\n orbBoundary: state.orbBoundary === \"gestalt\" ? \"gestalt\" : \"defined\",\n orbBackgroundColor:\n normalizedOrbBackgroundColor(state.orbBackgroundColor),\n orbBackgroundMode: normalizedOrbBackgroundMode(\n state.orbBackgroundMode,\n state.orbBackgroundColor,\n ),\n });\n engine.setResolution(state.gridSize);\n engine.setSeed(state.seed);\n engine.setSprite(state.sprite, {\n transition: \"instant\",\n immediate: true,\n preservePhase: true,\n useRecipeFields: !state.fieldOverride,\n });\n if (state.fieldOverride) engine.setField(state.fieldOverride);\n engine.setProgress(state.progress);\n syncStudioInputs({\n ...state,\n transitionOverride: transitionOverrideValue,\n pixelSwitch: pixelSwitchOverride,\n pixelSwitchOverridden,\n });\n applyPreviewMode(state.previewMode || previewMode);\n updateStateUI(spriteById.get(state.sprite));\n schedulePreviewResolutionSync(state.gridSize);\n applyStudioPalette();\n syncOrbAppearanceToPreviews();\n buildStatePicker(elements.stateSearchInput?.value || \"\");\n updateWorkflowState();\n updateCode();\n } finally {\n isRestoringStudioState = false;\n }\n if (announce) showWorkflowStatus(announce, { tone: \"success\" });\n}\n\nfunction resetStudioOverrides() {\n if (!baselineStudioState) return;\n pushUndoSnapshot(\"reset custom changes\");\n const reset = {\n ...cloneData(baselineStudioState),\n sprite: selectedSpriteId,\n previewMode,\n };\n restoreStudioState(reset, { announce: \"Custom changes reset to the recipe base.\" });\n}\n\nfunction undoStudioChange() {\n const entry = undoStack.pop();\n if (!entry) return;\n restoreStudioState(entry.state, { announce: `Undid ${entry.label}.` });\n updateWorkflowState();\n}\n\nfunction portableRecipe() {\n if (!builtInSpriteIds.has(engine.currentRecipe.id)) {\n throw new Error(\n \"Imported artwork is not embedded in recipe JSON. Export an image, or reopen the artwork separately.\",\n );\n }\n const config = engine.exportConfig();\n const transitionOverride = transitionOverridden\n ? normalizeTransitionOption(transitionOverrideValue)\n : null;\n const pixelSwitchOverride = pixelSwitchOverridden\n ? normalizePixelSwitchOption(engine.options.pixelSwitch)\n : null;\n return {\n ...config,\n transition: transitionOverride || config.transition,\n pixelSwitch: pixelSwitchOverride || config.pixelSwitch,\n package: STUDIO_PACKAGE,\n version: STUDIO_VERSION,\n studio: {\n schema: 1,\n fieldOverride: engine.fieldOverride || null,\n transitionOverride,\n pixelShapeCarries: true,\n pixelSwitchOverridden,\n pixelSwitchOverride,\n orbBackgroundColorChoice,\n orbBackgroundModeChoice,\n customNonErrorPalette: { ...customNonErrorPalette },\n },\n };\n}\n\nfunction normalizePalette(value) {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const palette = {};\n for (const key of [\"background\", \"off\", \"ink\", \"accent\", \"glow\"]) {\n const color = value[key];\n if (typeof color === \"string\" && SAFE_COLOR.test(color.trim())) {\n palette[key] = color.trim();\n }\n }\n return palette;\n}\n\nfunction normalizeImportedRecipe(source) {\n if (!source || typeof source !== \"object\" || Array.isArray(source)) {\n throw new TypeError(\"Recipe JSON must contain one object.\");\n }\n if (source.package && source.package !== STUDIO_PACKAGE) {\n throw new TypeError(`This file targets ${source.package}, not Orby.`);\n }\n if (typeof source.version !== \"string\") {\n throw new TypeError(\"Recipe version is missing.\");\n }\n const major = Number(source.version.split(\".\")[0]);\n if (major !== 5) {\n throw new RangeError(`Recipe version ${source.version} is not compatible with v5.`);\n }\n if (!spriteById.has(source.sprite)) {\n throw new RangeError(`Unknown recipe state: ${source.sprite || \"missing\"}.`);\n }\n\n const numberInRange = (value, fallback, minimum, maximum) => {\n const number = Number(value);\n return Number.isFinite(number)\n ? Math.min(maximum, Math.max(minimum, number))\n : fallback;\n };\n const studio = source.studio && typeof source.studio === \"object\" ? source.studio : {};\n const fieldOverride =\n source.fieldMode === \"override\" && FIELD_IDS.includes(source.field)\n ? source.field\n : FIELD_IDS.includes(studio.fieldOverride)\n ? studio.fieldOverride\n : null;\n const transitionOverride = normalizeTransitionOption(\n Object.hasOwn(studio, \"transitionOverride\")\n ? studio.transitionOverride\n : source.transition,\n );\n const pixelShape = PIXEL_SHAPES.includes(source.pixelShape)\n ? source.pixelShape\n : baselineStudioState?.pixelShape || \"disc\";\n const pixelShapeOverridden = Object.hasOwn(studio, \"pixelShapeCarries\")\n ? Boolean(studio.pixelShapeCarries && PIXEL_SHAPES.includes(source.pixelShape))\n : PIXEL_SHAPES.includes(source.pixelShape);\n const pixelSwitch = normalizePixelSwitchOption(\n Object.hasOwn(studio, \"pixelSwitchOverride\")\n ? studio.pixelSwitchOverride\n : source.pixelSwitch,\n );\n const paletteSource = studio.customNonErrorPalette || source.nonErrorPalette;\n const importedOrbBackgroundColor =\n typeof source.orbBackgroundColor === \"string\" &&\n SAFE_COLOR.test(source.orbBackgroundColor.trim()) &&\n source.orbBackgroundColor.trim().toLowerCase() !== \"transparent\"\n ? source.orbBackgroundColor.trim()\n : null;\n const importedOrbBackgroundMode = [\"none\", \"solid\", \"pixelated\"].includes(\n source.orbBackgroundMode,\n )\n ? source.orbBackgroundMode\n : importedOrbBackgroundColor\n ? \"solid\"\n : \"none\";\n const importedOrbBackgroundColorChoice =\n typeof studio.orbBackgroundColorChoice === \"string\" &&\n SAFE_COLOR.test(studio.orbBackgroundColorChoice.trim())\n ? studio.orbBackgroundColorChoice.trim()\n : importedOrbBackgroundColor || \"#262626\";\n const importedOrbBackgroundModeChoice =\n studio.orbBackgroundModeChoice === \"pixelated\" ||\n studio.orbBackgroundModeChoice === \"solid\"\n ? studio.orbBackgroundModeChoice\n : importedOrbBackgroundMode === \"pixelated\"\n ? \"pixelated\"\n : \"solid\";\n\n return {\n package: STUDIO_PACKAGE,\n version: source.version,\n sprite: source.sprite,\n seed: String(source.seed || \"joan-v5\").slice(0, 160),\n gridSize: Math.round(numberInRange(source.gridSize, 68, 12, 96)),\n speed: numberInRange(source.speed, 1, 0.1, 5),\n density: numberInRange(source.density, 1, 0.35, 1.6),\n progress: numberInRange(source.progress, 0, 0, 1),\n fieldOverride,\n transitionOverride,\n pixelShape,\n pixelShapeOverridden,\n pixelSwitch,\n pixelSwitchOverridden: Object.hasOwn(studio, \"pixelSwitchOverridden\")\n ? Boolean(studio.pixelSwitchOverridden && pixelSwitch)\n : Boolean(pixelSwitch),\n orbBoundary: source.orbBoundary === \"gestalt\" ? \"gestalt\" : \"defined\",\n orbBackgroundColor: importedOrbBackgroundColor,\n orbBackgroundMode: importedOrbBackgroundMode,\n orbBackgroundColorChoice: importedOrbBackgroundColorChoice,\n orbBackgroundModeChoice: importedOrbBackgroundModeChoice,\n palette: normalizePalette(paletteSource),\n savedGlowOverride: null,\n hadSavedGlowOverride: false,\n previewMode,\n };\n}\n\nfunction presetCreatedAtFromId(id) {\n const match = /^preset-(\\d{10,})-/.exec(String(id || \"\"));\n const timestamp = Number(match?.[1]);\n if (!Number.isFinite(timestamp)) return null;\n const date = new Date(timestamp);\n return Number.isNaN(date.getTime()) ? null : date.toISOString();\n}\n\nfunction normalizeStoredPreset(item, index = 0) {\n if (!item || typeof item !== \"object\" || !item.recipe) return null;\n const id =\n typeof item.id === \"string\" && item.id.trim()\n ? item.id.trim().slice(0, 160)\n : `preset-migrated-${index}`;\n const candidateDate =\n typeof item.createdAt === \"string\" ? new Date(item.createdAt) : null;\n const createdAt =\n candidateDate && !Number.isNaN(candidateDate.getTime())\n ? candidateDate.toISOString()\n : presetCreatedAtFromId(id);\n return {\n id,\n label: String(item.label || \"Saved preset\").slice(0, 72),\n createdAt,\n recipe: item.recipe,\n };\n}\n\nfunction presetStateLabel(preset) {\n const recipe = spriteById.get(preset?.recipe?.sprite);\n return recipe ? recipeLabel(recipe) : humanize(preset?.recipe?.sprite || \"Saved state\");\n}\n\nfunction presetTitle(preset) {\n return String(preset?.label || presetStateLabel(preset)).trim();\n}\n\nfunction presetDateLabel(preset) {\n if (!preset?.createdAt) return \"Saved locally\";\n const date = new Date(preset.createdAt);\n if (Number.isNaN(date.getTime())) return \"Saved locally\";\n return new Intl.DateTimeFormat(undefined, {\n month: \"short\",\n day: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n }).format(date);\n}\n\nfunction paintSavedPresetPreview(canvas, preset) {\n const spriteId = preset?.recipe?.sprite;\n if (!canvas || !spriteById.has(spriteId)) return;\n const previewKey = preset.id;\n const queued = savedPresetPreviewQueue.get(previewKey);\n if (queued) {\n queued.push(canvas);\n return;\n }\n const canvases = [canvas];\n savedPresetPreviewQueue.set(previewKey, canvases);\n enqueuePreviewWork(() => {\n savedPresetPreviewQueue.delete(previewKey);\n if (previewsDisposed) return;\n let preview = null;\n try {\n const state = normalizeImportedRecipe(preset.recipe);\n const source = document.createElement(\"canvas\");\n source.width = 50;\n source.height = 50;\n const options = {\n sprite: state.sprite,\n seed: state.seed,\n gridSize: state.gridSize,\n speed: state.speed,\n density: state.density,\n progress: state.progress,\n pixelShape: state.pixelShapeOverridden ? state.pixelShape : null,\n pixelSwitch: state.pixelSwitchOverridden ? state.pixelSwitch : null,\n transition: null,\n orbBoundary: state.orbBoundary,\n orbBackgroundColor: state.orbBackgroundColor,\n orbBackgroundMode: state.orbBackgroundMode,\n nonErrorPalette: {\n ...studioPaletteOverride(),\n ...state.palette,\n },\n autoplay: false,\n autoResize: false,\n interactive: false,\n background: true,\n offPixels: false,\n quality: \"low\",\n dprMax: 1,\n reducedMotion: true,\n };\n if (state.fieldOverride) options.field = state.fieldOverride;\n preview = new JoanGlyphEngine(source, options);\n renderThumbnailRestFrame(preview, { progress: state.progress });\n for (const target of canvases) {\n if (!target.isConnected) continue;\n const context = target.getContext(\"2d\");\n if (!context) continue;\n context.clearRect(0, 0, target.width, target.height);\n context.imageSmoothingEnabled = false;\n context.drawImage(source, 0, 0, target.width, target.height);\n target.dataset.previewState = \"ready\";\n }\n } catch {\n // A legacy preset that no longer validates still gets the authored state\n // thumbnail instead of leaving a permanently empty tile.\n const fallback = ensurePreviewEngine(spriteId);\n const source = previewDescriptors.get(spriteId)?.canvas;\n if (!fallback || !source) return;\n if (!previewShouldAnimate(spriteId)) freezePreview(fallback);\n for (const target of canvases) {\n if (!target.isConnected) continue;\n const context = target.getContext(\"2d\");\n if (!context) continue;\n context.clearRect(0, 0, target.width, target.height);\n context.imageSmoothingEnabled = false;\n context.drawImage(source, 0, 0, target.width, target.height);\n target.dataset.previewState = \"ready\";\n }\n } finally {\n preview?.destroy();\n }\n });\n}\n\nfunction observeSavedPresetPreview(canvas, preset) {\n if (typeof IntersectionObserver !== \"function\") {\n paintSavedPresetPreview(canvas, preset);\n return;\n }\n if (!savedPresetPreviewObserver) {\n savedPresetPreviewObserver = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n savedPresetPreviewObserver.unobserve(entry.target);\n const current = recentPresets.find(\n (item) => item.id === entry.target.dataset.presetId,\n );\n if (current) paintSavedPresetPreview(entry.target, current);\n }\n },\n {\n root: elements.savedPresetGallery,\n rootMargin: \"120px 0px\",\n threshold: 0.01,\n },\n );\n }\n canvas.dataset.presetId = preset.id;\n savedPresetPreviewObserver.observe(canvas);\n}\n\nfunction renderRecentPresets() {\n const hasPresets = recentPresets.length > 0;\n const select = elements.recentPresetSelect;\n if (select) {\n select.textContent = \"\";\n const placeholder = document.createElement(\"option\");\n placeholder.value = \"\";\n placeholder.textContent = hasPresets ? \"Quick open preset\" : \"No saved presets\";\n select.append(placeholder);\n for (const preset of recentPresets) {\n const option = document.createElement(\"option\");\n option.value = preset.id;\n option.textContent = `${presetTitle(preset)} · ${presetDateLabel(preset)}`;\n select.append(option);\n }\n select.disabled = !hasPresets;\n }\n\n if (elements.savedPresetCount) {\n const count = recentPresets.length;\n elements.savedPresetCount.textContent = `${count} preset${count === 1 ? \"\" : \"s\"}`;\n }\n if (elements.savedPresetEmpty) elements.savedPresetEmpty.hidden = hasPresets;\n if (!elements.savedPresetGallery) return;\n\n savedPresetPreviewObserver?.disconnect();\n elements.savedPresetGallery.textContent = \"\";\n elements.savedPresetGallery.hidden = !hasPresets;\n if (!hasPresets) return;\n\n const fragment = document.createDocumentFragment();\n const previews = [];\n for (const preset of recentPresets) {\n const stateLabel = presetStateLabel(preset);\n const titleLabel = presetTitle(preset);\n const accessibleIdentity =\n titleLabel === stateLabel ? stateLabel : `${titleLabel}, ${stateLabel}`;\n const dateLabel = presetDateLabel(preset);\n const savedDescription = preset.createdAt ? `saved ${dateLabel}` : dateLabel;\n const resolution = Math.round(Number(preset.recipe?.gridSize) || 68);\n const item = document.createElement(\"article\");\n item.className = \"saved-preset-card\";\n item.dataset.presetId = preset.id;\n item.setAttribute(\"role\", \"listitem\");\n\n const openButton = document.createElement(\"button\");\n openButton.type = \"button\";\n openButton.className = \"saved-preset-card__open\";\n openButton.dataset.openPreset = preset.id;\n openButton.setAttribute(\n \"aria-label\",\n `Open ${accessibleIdentity}, ${savedDescription}`,\n );\n\n const previewWrap = document.createElement(\"span\");\n previewWrap.className = \"saved-preset-card__preview\";\n const canvas = document.createElement(\"canvas\");\n canvas.width = 50;\n canvas.height = 50;\n canvas.dataset.previewState = \"pending\";\n canvas.setAttribute(\"aria-hidden\", \"true\");\n previewWrap.append(canvas);\n\n const copy = document.createElement(\"span\");\n copy.className = \"saved-preset-card__copy\";\n const label = document.createElement(\"strong\");\n label.textContent = titleLabel;\n const state = document.createElement(\"small\");\n state.textContent =\n titleLabel === stateLabel\n ? preset.recipe?.sprite || stateLabel\n : `${stateLabel} · ${preset.recipe?.sprite || \"custom\"}`;\n const metadata = document.createElement(\"span\");\n metadata.className = \"saved-preset-card__metadata\";\n metadata.textContent = `${resolution}×${resolution} · ${dateLabel}`;\n copy.append(label, state, metadata);\n openButton.append(previewWrap, copy);\n\n const deleteButton = document.createElement(\"button\");\n deleteButton.type = \"button\";\n deleteButton.className = \"saved-preset-card__delete\";\n deleteButton.dataset.deletePreset = preset.id;\n deleteButton.textContent = \"Delete\";\n deleteButton.setAttribute(\n \"aria-label\",\n `Delete ${accessibleIdentity}, ${savedDescription}`,\n );\n\n item.append(openButton, deleteButton);\n fragment.append(item);\n previews.push([canvas, preset]);\n }\n elements.savedPresetGallery.append(fragment);\n for (const [canvas, preset] of previews) observeSavedPresetPreview(canvas, preset);\n}\n\nfunction persistRecentPresets() {\n try {\n localStorage.setItem(RECENT_PRESET_KEY, JSON.stringify(recentPresets));\n } catch {\n showWorkflowStatus(\"Preset changes apply to this session only.\", {\n tone: \"warning\",\n });\n }\n renderRecentPresets();\n}\n\nfunction loadRecentPresets() {\n let stored = [];\n try {\n const parsed = JSON.parse(localStorage.getItem(RECENT_PRESET_KEY) || \"[]\");\n stored = Array.isArray(parsed) ? parsed : [];\n } catch {\n stored = [];\n }\n recentPresets = stored\n .map((item, index) => normalizeStoredPreset(item, index))\n .filter(Boolean)\n .slice(0, MAX_RECENT_PRESETS);\n if (\n JSON.stringify(stored.slice(0, MAX_RECENT_PRESETS)) !==\n JSON.stringify(recentPresets)\n ) {\n try {\n localStorage.setItem(RECENT_PRESET_KEY, JSON.stringify(recentPresets));\n } catch {\n // Keep migrated presets available for this session when storage is read-only.\n }\n }\n renderRecentPresets();\n}\n\nfunction rememberPreset(recipe, label) {\n const createdAt = new Date().toISOString();\n const id = `preset-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;\n recentPresets = [\n {\n id,\n label: String(label || \"Saved preset\").slice(0, 72),\n createdAt,\n recipe,\n },\n ...recentPresets,\n ].slice(0, MAX_RECENT_PRESETS);\n persistRecentPresets();\n return id;\n}\n\nfunction openRecentPreset(id) {\n const preset = recentPresets.find((item) => item.id === id);\n if (!preset) return;\n try {\n applyPortableRecipe(preset.recipe, { label: preset.label });\n } catch (error) {\n showWorkflowStatus(error?.message || \"The saved preset could not be opened.\", {\n tone: \"danger\",\n });\n }\n}\n\nfunction deleteRecentPreset(id) {\n const index = recentPresets.findIndex((item) => item.id === id);\n if (index < 0) return;\n const [preset] = recentPresets.splice(index, 1);\n deletedPreset = { preset, index };\n persistRecentPresets();\n if (elements.deletedPresetMessage) {\n elements.deletedPresetMessage.textContent = `${presetTitle(preset)} deleted.`;\n }\n if (elements.deletedPresetNotice) elements.deletedPresetNotice.hidden = false;\n showWorkflowStatus(`${presetTitle(preset)} deleted. Undo is available below.`, {\n tone: \"warning\",\n });\n window.requestAnimationFrame(() => elements.undoDeletePresetButton?.focus());\n}\n\nfunction undoDeletedPreset() {\n if (!deletedPreset) return;\n const { preset, index } = deletedPreset;\n recentPresets.splice(Math.min(index, recentPresets.length), 0, preset);\n recentPresets = recentPresets.slice(0, MAX_RECENT_PRESETS);\n deletedPreset = null;\n persistRecentPresets();\n if (elements.deletedPresetNotice) elements.deletedPresetNotice.hidden = true;\n showWorkflowStatus(`${presetTitle(preset)} restored.`, { tone: \"success\" });\n window.requestAnimationFrame(() => {\n const restoredButton = [\n ...(elements.savedPresetGallery?.querySelectorAll(\"[data-open-preset]\") || []),\n ].find((button) => button.dataset.openPreset === preset.id);\n restoredButton?.focus();\n });\n}\n\nfunction saveCurrentPreset() {\n let recipe;\n try {\n recipe = portableRecipe();\n } catch (error) {\n showWorkflowStatus(error.message, { tone: \"danger\" });\n return;\n }\n const date = new Intl.DateTimeFormat(undefined, {\n month: \"short\",\n day: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n }).format(new Date());\n const label = recipeLabel(engine.currentRecipe);\n rememberPreset(recipe, label);\n showWorkflowStatus(`Saved ${label} · ${date} to recent presets.`, {\n tone: \"success\",\n });\n}\n\nfunction applyPortableRecipe(source, { label = \"recipe\", remember = false } = {}) {\n const normalized = normalizeImportedRecipe(source);\n pushUndoSnapshot(`open ${label}`);\n restoreStudioState(normalized, {\n announce: `Opened ${label}. ${recipeLabel(spriteById.get(normalized.sprite))} is on stage.`,\n });\n if (remember) rememberPreset(source, label);\n}\n\nasync function openRecipeFile(file) {\n if (!file) return;\n if (file.size > 1024 * 1024) {\n throw new RangeError(\"Recipe files must be smaller than 1 MB.\");\n }\n const source = JSON.parse(await file.text());\n const label = file.name.replace(/\\.joan\\.json$|\\.json$/i, \"\") || \"recipe\";\n applyPortableRecipe(source, { label, remember: true });\n}\n\nfunction updateCode() {\n if (!elements.codeSnippet) return;\n const config = engine.exportConfig();\n const paletteLine = config.nonErrorPalette\n ? `\\n nonErrorPalette: ${formatCodeValue(config.nonErrorPalette, \" \")},`\n : \"\";\n const fieldLine =\n config.fieldMode === \"override\" ? `\\n field: \"${config.field}\",` : \"\";\n const transitionValue = transitionOverridden\n ? normalizeTransitionOption(transitionOverrideValue)\n : null;\n const transitionLine = transitionValue\n ? `\\n transition: ${formatCodeValue(transitionValue)},`\n : \"\";\n elements.codeSnippet.textContent = `import { createGlyph } from \"@joan/procedural-glyph-engine\";\nimport { playStateSequence } from \"@joan/procedural-glyph-engine/state-sequence\";\n\nconst glyph = createGlyph(\"#ai-glyph\", {\n sprite: \"${config.sprite}\",\n seed: \"${config.seed.replaceAll('\"', '\\\\\"')}\",\n gridSize: ${config.gridSize},\n speed: ${formatNumber(config.speed)},\n density: ${formatNumber(config.density)},${paletteLine}${fieldLine}${transitionLine}\n orbBoundary: \"${config.orbBoundary}\",\n orbBackgroundColor: ${JSON.stringify(config.orbBackgroundColor)},\n orbBackgroundMode: \"${config.orbBackgroundMode}\",\n pixelShape: \"${config.pixelShape}\",\n pixelSwitch: ${formatCodeValue(config.pixelSwitch)},\n interactive: true\n});\n\nconst playback = playStateSequence(glyph, [\n { sprite: \"${config.sprite}\", holdMs: 5_000 },\n { sprite: \"status.success\", transition: \"path-draw\" }\n]);\n\nawait playback.finished;`;\n}\n\nfunction updateStateUI(recipe) {\n if (!recipe) return;\n const label = recipeLabel(recipe);\n const category = recipe.semantic?.category || recipe.category || \"semantic\";\n const previousSelectedId = selectedSpriteId;\n selectedSpriteId = recipe.id;\n ensurePreviewEngine(recipe.id);\n if (previousSelectedId !== selectedSpriteId) {\n freezePreviewIfInactive(previousSelectedId);\n }\n if (elements.stateTitle) elements.stateTitle.textContent = label;\n if (elements.stateDescription) {\n elements.stateDescription.textContent = recipeDescription(recipe);\n }\n if (elements.stateCategory) elements.stateCategory.textContent = humanize(category);\n if (elements.currentStateValue) elements.currentStateValue.textContent = recipe.id;\n const libraryHasSelection = spriteButtons.has(recipe.id);\n for (const [id, button] of spriteButtons) {\n const selected = id === recipe.id;\n button.classList.toggle(\"is-selected\", selected);\n button.setAttribute(\"aria-checked\", String(selected));\n button.tabIndex = selected || (!libraryHasSelection && id === sprites[0]?.id) ? 0 : -1;\n }\n for (const button of document.querySelectorAll(\"[data-quick-sprite]\")) {\n const selected = button.dataset.quickSprite === recipe.id;\n button.classList.toggle(\"is-selected\", selected);\n button.setAttribute(\"aria-pressed\", String(selected));\n }\n\n if (elements.fieldSelect) {\n elements.fieldSelect.value = engine.fieldOverride || \"__recipe__\";\n }\n if (elements.transitionSelect && !transitionOverridden) {\n elements.transitionSelect.value = \"__recipe__\";\n }\n if (elements.pixelShapeSelect && !pixelShapeOverridden) {\n const shapeAliases = { circle: \"disc\", \"rounded-square\": \"square\" };\n const sourceShape =\n recipe.pixelShape || recipe.render?.pixelShape || recipe.pixel?.shape;\n const shape = shapeAliases[sourceShape] || sourceShape;\n if (PIXEL_SHAPES.includes(shape)) elements.pixelShapeSelect.value = shape;\n }\n if (elements.pixelSwitchSelect && !pixelSwitchOverridden) {\n const pixelSwitch = recipePixelSwitch(recipe);\n if (PIXEL_SWITCHES.includes(pixelSwitch)) {\n elements.pixelSwitchSelect.value = pixelSwitch;\n }\n }\n updateControlTip(elements.fieldSelect);\n updateControlTip(elements.transitionSelect);\n updateControlTip(elements.pixelShapeSelect);\n updateControlTip(elements.pixelSwitchSelect);\n\n const boundaryAvailable = supportsGestaltBoundary(recipe);\n if (elements.orbBoundaryControl) {\n elements.orbBoundaryControl.dataset.available = String(boundaryAvailable);\n }\n if (elements.orbBoundaryInput) {\n elements.orbBoundaryInput.disabled = !boundaryAvailable;\n elements.orbBoundaryInput.checked =\n engine.options.orbBoundary === \"gestalt\";\n }\n if (elements.orbBoundaryAvailability) {\n elements.orbBoundaryAvailability.textContent = boundaryAvailable\n ? engine.options.orbBoundary === \"gestalt\"\n ? \"Circle implied by clustered edge pixels\"\n : \"Continuous authored rim\"\n : \"Available for Ready and ambient orbs · choice stays saved\";\n }\n updateControlTip(elements.orbBoundaryInput);\n setOrbBackgroundControlState(\n engine.options.orbBackgroundColor,\n boundaryAvailable,\n );\n\n const isErrorState = recipe.id === \"status.error\";\n elements.nonErrorPaletteSection?.classList.toggle(\n \"is-error-state\",\n isErrorState,\n );\n if (!isErrorState) updatePaletteControls(engine.exportConfig().palette);\n\n const supportsProgress =\n recipe.id === \"ai.progress\" || recipe.id === \"transfer.active\";\n elements.progressControl?.classList.toggle(\"is-visible\", supportsProgress);\n elements.progressControl?.setAttribute(\"aria-hidden\", String(!supportsProgress));\n if (elements.statePickerSelect) elements.statePickerSelect.value = recipe.id;\n buildStatePicker(elements.stateSearchInput?.value || \"\");\n updateWorkflowState();\n updateCode();\n}\n\nfunction selectSprite(id, options = {}) {\n const recipe = spriteById.get(id);\n if (!recipe) return;\n if (options.recordUndo !== false && recipe.id !== selectedSpriteId) {\n pushUndoSnapshot(`select ${recipeLabel(recipe)}`);\n }\n engine.setSprite(id, {\n transition:\n options.transition ||\n (transitionOverridden ? transitionOverrideValue : undefined),\n duration: options.duration,\n preservePhase: options.preservePhase,\n });\n updateStateUI(recipe);\n}\n\nfunction ensurePreviewEngine(id) {\n if (previewsDisposed) return null;\n const existing = previewById.get(id);\n if (existing) {\n previewCardObserver?.unobserve(spriteButtons.get(id));\n return existing;\n }\n const descriptor = previewDescriptors.get(id);\n if (!descriptor) return null;\n const { canvas, index, recipe } = descriptor;\n const preview = new JoanGlyphEngine(canvas, {\n sprite: createThumbnailRecipe(recipe),\n seed: `preview-${index}`,\n gridSize: engine.gridSize,\n progress: currentPreviewProgress(),\n autoplay: false,\n autoResize: false,\n interactive: false,\n background: true,\n offPixels: false,\n quality: \"low\",\n dprMax: 1,\n fps: 12,\n // The constructor paints the authored representative pose once. Restore\n // the user's motion preference immediately afterward for active cards.\n reducedMotion: true,\n nonErrorPalette: studioPaletteOverride(),\n orbBoundary: engine.options.orbBoundary,\n orbBackgroundColor: engine.options.orbBackgroundColor,\n orbBackgroundMode: engine.options.orbBackgroundMode,\n });\n preview.previewSpriteId = recipe.id;\n preview.reducedMotion = reduceMotion.matches;\n previewEngines.push(preview);\n previewById.set(recipe.id, preview);\n previewCardObserver?.unobserve(spriteButtons.get(recipe.id));\n setPreviewResolution(preview, engine.gridSize);\n if (preview.canvas?.dataset) preview.canvas.dataset.previewState = \"ready\";\n return preview;\n}\n\nfunction scheduleDeferredPreviewConstruction() {\n // IntersectionObserver constructs only cards approaching the viewport. The\n // idle sweep remains as a compatibility fallback for older embedded hosts.\n if (typeof IntersectionObserver === \"function\") return;\n const pendingIds = sprites\n .map((recipe) => recipe.id)\n .filter((id) => id !== selectedSpriteId);\n const buildNext = (deadline) => {\n if (previewsDisposed) return;\n const minimumBatch = 1;\n const maximumBatch = 1;\n let built = 0;\n\n while (pendingIds.length && built < maximumBatch) {\n const id = pendingIds.shift();\n if (!id || previewById.has(id)) continue;\n ensurePreviewEngine(id);\n built += 1;\n\n const remaining =\n typeof deadline?.timeRemaining === \"function\"\n ? deadline.timeRemaining()\n : 0;\n if (built >= minimumBatch && remaining < 6) break;\n }\n\n if (pendingIds.length) enqueuePreviewWork(buildNext);\n };\n if (pendingIds.length) enqueuePreviewWork(buildNext);\n}\n\nfunction flushUrgentPreviewConstruction() {\n urgentPreviewScheduled = false;\n const id = urgentPreviewIds.values().next().value;\n if (id) urgentPreviewIds.delete(id);\n if (id && !previewById.has(id)) ensurePreviewEngine(id);\n if (urgentPreviewIds.size && !previewsDisposed) {\n scheduleUrgentPreviewConstruction();\n }\n}\n\nfunction scheduleUrgentPreviewConstruction() {\n if (urgentPreviewScheduled || !urgentPreviewIds.size || previewsDisposed) return;\n urgentPreviewScheduled = true;\n enqueuePreviewWork(flushUrgentPreviewConstruction, { priority: true });\n}\n\nfunction prioritizePreviewConstruction(ids) {\n for (const id of ids) {\n if (id && !previewById.has(id)) urgentPreviewIds.add(id);\n }\n scheduleUrgentPreviewConstruction();\n}\n\nfunction startPreviewCardObserver() {\n if (typeof IntersectionObserver !== \"function\") return;\n previewCardObserver = new IntersectionObserver(\n (entries) => {\n const visibleIds = [];\n for (const entry of entries) {\n const id = entry.target.dataset.sprite;\n if (entry.isIntersecting) visibleIds.push(id);\n else urgentPreviewIds.delete(id);\n }\n prioritizePreviewConstruction(visibleIds);\n },\n { rootMargin: \"600px 0px\", threshold: 0.01 },\n );\n for (const [id, button] of spriteButtons) {\n if (!previewById.has(id)) previewCardObserver.observe(button);\n }\n}\n\nfunction buildSpriteLibrary() {\n if (!elements.spriteGrid) return;\n const fragment = document.createDocumentFragment();\n sprites.forEach((recipe, index) => {\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.className = \"sprite-card\";\n button.dataset.sprite = recipe.id;\n button.setAttribute(\"role\", \"radio\");\n button.setAttribute(\"aria-checked\", String(recipe.id === selectedSpriteId));\n button.tabIndex = recipe.id === selectedSpriteId ? 0 : -1;\n button.setAttribute(\n \"aria-label\",\n `${\n recipe.labels?.default ||\n recipe.semantic?.label ||\n recipe.label ||\n humanize(recipe.id)\n }: ${recipeDescription(recipe)}`,\n );\n\n const previewWrap = document.createElement(\"span\");\n previewWrap.className = \"sprite-preview\";\n const canvas = document.createElement(\"canvas\");\n canvas.width = 64;\n canvas.height = 64;\n canvas.dataset.previewState = \"pending\";\n canvas.setAttribute(\"aria-hidden\", \"true\");\n previewWrap.append(canvas);\n\n const copy = document.createElement(\"span\");\n copy.className = \"sprite-copy\";\n const label = document.createElement(\"strong\");\n label.textContent =\n recipe.labels?.default ||\n recipe.semantic?.label ||\n recipe.label ||\n humanize(recipe.id);\n const idLabel = document.createElement(\"small\");\n idLabel.textContent = recipe.id;\n copy.append(label, idLabel);\n button.append(previewWrap, copy);\n button.addEventListener(\"pointerenter\", () => {\n hoveredPreviewIds.add(recipe.id);\n ensurePreviewEngine(recipe.id);\n });\n button.addEventListener(\"pointerleave\", () => {\n hoveredPreviewIds.delete(recipe.id);\n freezePreviewIfInactive(recipe.id);\n });\n button.addEventListener(\"focus\", () => {\n if (!button.matches(\":focus-visible\")) return;\n focusedPreviewIds.add(recipe.id);\n ensurePreviewEngine(recipe.id);\n });\n button.addEventListener(\"blur\", () => {\n focusedPreviewIds.delete(recipe.id);\n freezePreviewIfInactive(recipe.id);\n });\n button.addEventListener(\"click\", () => selectSprite(recipe.id));\n button.addEventListener(\"keydown\", (event) => {\n const keys = [\"ArrowRight\", \"ArrowDown\", \"ArrowLeft\", \"ArrowUp\", \"Home\", \"End\"];\n if (!keys.includes(event.key)) return;\n event.preventDefault();\n const direction = event.key === \"ArrowRight\" || event.key === \"ArrowDown\" ? 1 : -1;\n let nextIndex = index;\n if (event.key === \"Home\") nextIndex = 0;\n else if (event.key === \"End\") nextIndex = sprites.length - 1;\n else nextIndex = (index + direction + sprites.length) % sprites.length;\n const next = spriteButtons.get(sprites[nextIndex].id);\n selectSprite(sprites[nextIndex].id);\n next?.focus();\n });\n fragment.append(button);\n spriteButtons.set(recipe.id, button);\n previewDescriptors.set(recipe.id, { canvas, index, recipe });\n });\n elements.spriteGrid.append(fragment);\n ensurePreviewEngine(selectedSpriteId);\n startPreviewCardObserver();\n scheduleDeferredPreviewConstruction();\n}\n\nsetTheme(isLight, { persist: false });\nheroEngine = elements.heroCanvas\n ? new JoanGlyphEngine(elements.heroCanvas, {\n sprite: \"ai.idle\",\n seed: \"world-class-ai\",\n gridSize: 68,\n speed: 1,\n density: 1,\n interactive: false,\n background: false,\n offPixels: true,\n fps: 30,\n quality: \"auto\",\n dprMax: 1.5,\n nonErrorPalette: studioPaletteOverride(),\n })\n : null;\nbuildSpriteLibrary();\nupdateStateUI(engine.currentRecipe);\napplyPreviewMode(previewMode);\nconst initialPixelShape = elements.pixelShapeSelect?.value;\nif (PIXEL_SHAPES.includes(initialPixelShape)) {\n engine.setPixelShape(initialPixelShape);\n pixelShapeOverridden = true;\n}\nengine.setProgress((Number(elements.progressRange?.value) || 0) / 100);\nbaselineStudioState = cloneData(captureStudioState());\nbuildStatePicker();\nloadRecentPresets();\nupdateWorkflowState();\nupdateCode();\n\nlet previewLast = 0;\nfunction animatePreviews(timestamp) {\n requestAnimationFrame(animatePreviews);\n if (reduceMotion.matches || document.hidden || timestamp - previewLast < 90) return;\n const previewDt = previewLast\n ? Math.min(0.12, (timestamp - previewLast) / 1000)\n : 1 / 12;\n previewLast = timestamp;\n for (const preview of previewEngines) {\n const id = previewId(preview);\n if (!preview.visible || !previewShouldAnimate(id)) continue;\n if (preview.gridSize !== engine.gridSize) {\n setPreviewResolution(preview, engine.gridSize);\n }\n preview.renderFrame(timestamp / 1000, previewDt);\n }\n}\nrequestAnimationFrame(animatePreviews);\n\nfunction updatePlayButton() {\n if (!elements.playButton) return;\n elements.playButton.textContent = engine.running ? \"Pause motion\" : \"Play motion\";\n elements.playButton.setAttribute(\"aria-pressed\", String(engine.running));\n}\nupdatePlayButton();\n\nfunction stopAutopilot() {\n if (autopilotTimer !== null) window.clearInterval(autopilotTimer);\n autopilotTimer = null;\n elements.autopilotButton?.classList.remove(\"is-active\");\n elements.autopilotButton?.setAttribute(\"aria-pressed\", \"false\");\n if (elements.autopilotButton) {\n elements.autopilotButton.textContent = \"Auto-play AI states\";\n elements.autopilotButton.title =\n \"Automatically cycles through 10 common AI states every 4 seconds. Click again to stop.\";\n }\n}\n\nconst story = [\n \"ai.idle\",\n \"ai.listening\",\n \"ai.thinking\",\n \"ai.thinking-deep\",\n \"ai.still-working\",\n \"ai.searching\",\n \"ai.tool-use\",\n \"ai.generating\",\n \"ai.speaking\",\n \"status.success\",\n];\n\nfunction toggleAutopilot() {\n if (autopilotTimer !== null) {\n stopAutopilot();\n return;\n }\n autopilotIndex = Math.max(0, story.indexOf(selectedSpriteId));\n selectSprite(story[autopilotIndex], { duration: 0.5, recordUndo: false });\n autopilotTimer = window.setInterval(() => {\n autopilotIndex = (autopilotIndex + 1) % story.length;\n selectSprite(story[autopilotIndex], { duration: 0.5, recordUndo: false });\n if (story[autopilotIndex] === \"ai.generating\") {\n engine.signal(\"token\", { energy: 0.8 });\n }\n if (story[autopilotIndex] === \"status.success\") {\n engine.signal(\"complete\", { energy: 1 });\n }\n }, 4200);\n elements.autopilotButton?.classList.add(\"is-active\");\n elements.autopilotButton?.setAttribute(\"aria-pressed\", \"true\");\n if (elements.autopilotButton) {\n elements.autopilotButton.textContent = \"Stop AI state auto-play\";\n elements.autopilotButton.title = \"Stop the automatic AI state sequence.\";\n }\n}\n\nfunction setTheme(light, { persist = true } = {}) {\n isLight = Boolean(light);\n const theme = isLight ? \"light\" : \"dark\";\n document.documentElement.dataset.theme = theme;\n document.documentElement.style.colorScheme = theme;\n applyStudioPalette({ immediatePreviewRefresh: true });\n if (elements.themeButton) {\n const nextThemeLabel = isLight ? \"Dark mode\" : \"Light mode\";\n elements.themeButton.textContent = nextThemeLabel;\n elements.themeButton.setAttribute(\n \"aria-label\",\n `Switch to ${nextThemeLabel}`,\n );\n elements.themeButton.setAttribute(\"aria-checked\", String(isLight));\n elements.themeButton.title = `Switch to ${nextThemeLabel}`;\n }\n const themeColor = document.querySelector('meta[name=\"theme-color\"]');\n if (themeColor) themeColor.content = isLight ? \"#eeece6\" : \"#050506\";\n if (persist) {\n try {\n window.localStorage.setItem(THEME_STORAGE_KEY, theme);\n } catch {\n // Storage can be unavailable in private or embedded contexts.\n }\n }\n updateWorkflowState();\n}\n\nfunction randomSeed() {\n pushUndoSnapshot(\"generate a new seed\");\n const bytes = new Uint32Array(2);\n crypto.getRandomValues(bytes);\n const value = `joan-${bytes[0].toString(36)}-${bytes[1].toString(36)}`;\n if (elements.seedInput) elements.seedInput.value = value;\n engine.setSeed(value);\n engine.signal(\"reseed\", { energy: 0.6 });\n updateWorkflowState();\n updateCode();\n}\n\nasync function copyCode() {\n const code = elements.codeSnippet?.textContent || \"\";\n try {\n await navigator.clipboard.writeText(code);\n } catch {\n const range = document.createRange();\n range.selectNodeContents(elements.codeSnippet);\n const selection = window.getSelection();\n selection.removeAllRanges();\n selection.addRange(range);\n document.execCommand(\"copy\");\n selection.removeAllRanges();\n }\n const old = elements.copyCodeButton?.textContent;\n if (elements.copyCodeButton) elements.copyCodeButton.textContent = \"Copied\";\n showWorkflowStatus(\"Integration code copied.\", { tone: \"success\" });\n window.setTimeout(() => {\n if (elements.copyCodeButton) elements.copyCodeButton.textContent = old || \"Copy\";\n }, 1400);\n}\n\nfunction downloadJSON(\n filename = `${engine.currentRecipe.id.replaceAll(\".\", \"-\")}.joan.json`,\n) {\n const data = JSON.stringify(portableRecipe(), null, 2);\n const blob = new Blob([`${data}\\n`], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement(\"a\");\n anchor.href = url;\n anchor.download = filename;\n anchor.click();\n URL.revokeObjectURL(url);\n return blob;\n}\n\nasync function downloadStudioPNG(\n filename = `${engine.currentRecipe.id.replaceAll(\".\", \"-\")}.png`,\n { background = true } = {},\n) {\n const previousMode = previewMode;\n const previousBackground = engine.options.background;\n\n engine.setOptions({ previewMode: \"fit\", background });\n engine.paint();\n try {\n return await engine.downloadPNG(filename);\n } finally {\n engine.setOptions({\n previewMode: previousMode,\n background: previousBackground,\n });\n engine.paint();\n }\n}\n\nconst EXPORT_FORMATS = Object.freeze({\n png: {\n extension: \".png\",\n label: \"PNG\",\n motion: \"Current frame\",\n },\n svg: {\n extension: \".svg\",\n label: \"SVG\",\n motion: \"Current frame\",\n },\n \"animated-svg\": {\n extension: \".animated.svg\",\n label: \"Animated SVG\",\n motion: \"2.4 s · 12 fps · 48×48 max grid\",\n },\n config: {\n extension: \".joan.json\",\n label: \"Orby recipe\",\n motion: \"Deterministic parameters\",\n },\n});\n\nfunction exportBaseName() {\n return engine.currentRecipe.id.replaceAll(\".\", \"-\");\n}\n\nfunction defaultExportFilename(format) {\n return `${exportBaseName()}${EXPORT_FORMATS[format]?.extension || \".png\"}`;\n}\n\nfunction sanitizeExportFilename(value, format) {\n const meta = EXPORT_FORMATS[format] || EXPORT_FORMATS.png;\n let filename = String(value || \"\")\n .trim()\n .replace(/[\\\\/:*?\"<>|]+/g, \"-\")\n .replace(/^\\.+/, \"\");\n if (!filename) filename = defaultExportFilename(format);\n const knownExtension = [...Object.values(EXPORT_FORMATS)]\n .sort((left, right) => right.extension.length - left.extension.length)\n .find(({ extension }) => filename.toLowerCase().endsWith(extension));\n if (knownExtension && knownExtension.extension !== meta.extension) {\n filename = filename.slice(0, -knownExtension.extension.length);\n }\n if (!filename.toLowerCase().endsWith(meta.extension)) filename += meta.extension;\n return filename.slice(0, 128);\n}\n\nfunction renderExportPreview() {\n const canvas = elements.exportPreviewCanvas;\n const context = canvas?.getContext(\"2d\");\n if (!canvas || !context) return;\n const includeBackground = elements.exportBackgroundInput?.checked !== false;\n const previousMode = engine.options.previewMode;\n const previousBackground = engine.options.background;\n try {\n engine.setOptions({ previewMode: \"fit\", background: includeBackground });\n context.clearRect(0, 0, canvas.width, canvas.height);\n context.imageSmoothingEnabled = false;\n context.drawImage(engine.canvas, 0, 0, canvas.width, canvas.height);\n } finally {\n engine.setOptions({\n previewMode: previousMode,\n background: previousBackground,\n });\n }\n canvas.closest(\".export-preview__frame\")?.classList.toggle(\n \"is-transparent\",\n !includeBackground,\n );\n}\n\nfunction updateExportPreflight({ resetFilename = false } = {}) {\n const format = elements.exportFormatSelect?.value || \"png\";\n const meta = EXPORT_FORMATS[format] || EXPORT_FORMATS.png;\n const vector = format === \"svg\" || format === \"animated-svg\";\n const recipe = format === \"config\";\n const size = Number(elements.exportSizeSelect?.value) || 512;\n const includeBackground = elements.exportBackgroundInput?.checked !== false;\n const recipePortable = builtInSpriteIds.has(engine.currentRecipe.id);\n\n if (elements.exportSizeSelect) elements.exportSizeSelect.disabled = !vector;\n if (elements.exportBackgroundInput) elements.exportBackgroundInput.disabled = recipe;\n if (elements.exportFilenameInput && resetFilename) {\n elements.exportFilenameInput.value = defaultExportFilename(format);\n }\n if (elements.exportDimensionsValue) {\n elements.exportDimensionsValue.textContent = recipe\n ? \"Portable JSON\"\n : format === \"png\"\n ? `${engine.canvas.width}×${engine.canvas.height} px`\n : `${size}×${size} px`;\n }\n if (elements.exportCompositionValue) {\n elements.exportCompositionValue.textContent = recipe\n ? \"Parameters only\"\n : \"Fit · full frame\";\n }\n if (elements.exportMotionValue) elements.exportMotionValue.textContent = meta.motion;\n if (elements.exportBackgroundValue) {\n elements.exportBackgroundValue.textContent = recipe\n ? \"Stored in palette\"\n : includeBackground\n ? \"Included\"\n : \"Transparent\";\n }\n if (elements.exportPreviewNote) {\n elements.exportPreviewNote.textContent = recipe\n ? recipePortable\n ? \"The recipe stores deterministic controls and can be reopened in this studio.\"\n : \"Imported artwork is not embedded in JSON. Export an image or reopen the artwork separately.\"\n : \"Export uses the fitted full-frame composition. Fit and 1:1 only change the inspection view.\";\n }\n if (elements.confirmExportButton) {\n elements.confirmExportButton.textContent = `Export ${meta.label}`;\n elements.confirmExportButton.disabled = recipe && !recipePortable;\n }\n renderExportPreview();\n}\n\nfunction openExportPreflight(format = \"png\") {\n if (!EXPORT_FORMATS[format] || !elements.exportDialog) return;\n exportReturnFocus = document.activeElement;\n if (elements.exportFormatSelect) elements.exportFormatSelect.value = format;\n updateExportPreflight({ resetFilename: true });\n if (typeof elements.exportDialog.showModal === \"function\") {\n elements.exportDialog.showModal();\n } else {\n elements.exportDialog.setAttribute(\"open\", \"\");\n }\n elements.exportFormatSelect?.focus();\n}\n\nfunction closeExportPreflight() {\n if (!elements.exportDialog?.open) return;\n if (typeof elements.exportDialog.close === \"function\") elements.exportDialog.close();\n else elements.exportDialog.removeAttribute(\"open\");\n if (exportReturnFocus?.isConnected) exportReturnFocus.focus();\n exportReturnFocus = null;\n}\n\nasync function performPreflightExport() {\n const format = elements.exportFormatSelect?.value || \"png\";\n const meta = EXPORT_FORMATS[format] || EXPORT_FORMATS.png;\n const filename = sanitizeExportFilename(\n elements.exportFilenameInput?.value,\n format,\n );\n const size = Number(elements.exportSizeSelect?.value) || 512;\n const background = elements.exportBackgroundInput?.checked !== false;\n const button = elements.confirmExportButton;\n if (button) {\n button.disabled = true;\n button.textContent = format === \"animated-svg\" ? \"Rendering frames…\" : \"Preparing…\";\n }\n try {\n if (format === \"png\") {\n await downloadStudioPNG(filename, { background });\n } else if (format === \"svg\") {\n await engine.downloadSVG(filename, { size, background });\n } else if (format === \"animated-svg\") {\n await engine.downloadAnimatedSVG(filename, {\n size,\n background,\n duration: 2.4,\n fps: 12,\n });\n } else {\n downloadJSON(filename);\n }\n closeExportPreflight();\n showWorkflowStatus(`Exported ${filename}.`, { tone: \"success\" });\n } catch (error) {\n showWorkflowStatus(error?.message || `${meta.label} export failed.`, {\n tone: \"danger\",\n });\n } finally {\n if (button) {\n button.disabled = format === \"config\" && !builtInSpriteIds.has(engine.currentRecipe.id);\n button.textContent = `Export ${meta.label}`;\n }\n }\n}\n\nfunction simulateAudio() {\n if (audioTimer !== null) {\n window.clearInterval(audioTimer);\n audioTimer = null;\n }\n selectSprite(\"ai.speaking\");\n const started = performance.now();\n audioTimer = window.setInterval(() => {\n const elapsed = (performance.now() - started) / 1000;\n const level =\n (0.2 + 0.8 * Math.abs(Math.sin(elapsed * 8.7) * Math.sin(elapsed * 3.1 + 0.4))) *\n Math.max(0, 1 - elapsed / 2.6);\n engine.setAudioLevel(level);\n if (elapsed > 2.6) {\n window.clearInterval(audioTimer);\n audioTimer = null;\n engine.setAudioLevel(0);\n }\n }, 45);\n}\n\nelements.playButton?.addEventListener(\"click\", () => {\n engine.toggle();\n updatePlayButton();\n});\nelements.autopilotButton?.addEventListener(\"click\", toggleAutopilot);\nelements.themeButton?.addEventListener(\"click\", () => setTheme(!isLight));\nelements.randomizeButton?.addEventListener(\"click\", randomSeed);\nfor (const input of elements.paletteInputs) {\n input.addEventListener(\"input\", (event) => {\n const key = event.currentTarget.dataset.paletteColor;\n beginMutation(`palette-${key}`, `change ${key} color`);\n customNonErrorPalette = {\n ...customNonErrorPalette,\n [key]: event.currentTarget.value,\n };\n scheduleStudioPaletteUpdate();\n });\n input.addEventListener(\"change\", () => {\n endMutation(`palette-${input.dataset.paletteColor}`);\n flushStudioPaletteUpdate();\n });\n}\nelements.glowEnabledInput?.addEventListener(\"change\", (event) => {\n pushUndoSnapshot(event.currentTarget.checked ? \"enable glow\" : \"disable glow\");\n const enabled = event.currentTarget.checked;\n const nextPalette = { ...customNonErrorPalette };\n\n if (enabled) {\n if (hadSavedGlowOverride) nextPalette.glow = savedGlowOverride;\n else delete nextPalette.glow;\n savedGlowOverride = null;\n hadSavedGlowOverride = false;\n } else {\n const currentGlow = nextPalette.glow;\n hadSavedGlowOverride =\n typeof currentGlow === \"string\" &&\n currentGlow.trim().toLowerCase() !== \"transparent\";\n savedGlowOverride = hadSavedGlowOverride ? currentGlow : null;\n nextPalette.glow = \"transparent\";\n }\n\n customNonErrorPalette = nextPalette;\n applyStudioPalette();\n});\nelements.resetPaletteButton?.addEventListener(\"click\", () => {\n if (!Object.keys(customNonErrorPalette).length) return;\n pushUndoSnapshot(\"reset custom colors\");\n customNonErrorPalette = {};\n savedGlowOverride = null;\n hadSavedGlowOverride = false;\n applyStudioPalette();\n});\nelements.copyCodeButton?.addEventListener(\"click\", copyCode);\nelements.openExportButton?.addEventListener(\"click\", () => openExportPreflight(\"png\"));\nelements.exportConfigButton?.addEventListener(\"click\", () =>\n openExportPreflight(\"config\"),\n);\nelements.exportPngButton?.addEventListener(\"click\", () => openExportPreflight(\"png\"));\nelements.exportSvgButton?.addEventListener(\"click\", () => openExportPreflight(\"svg\"));\nelements.exportAnimatedSvgButton?.addEventListener(\"click\", () =>\n openExportPreflight(\"animated-svg\"),\n);\nelements.undoStudioButton?.addEventListener(\"click\", undoStudioChange);\nelements.resetOverridesButton?.addEventListener(\"click\", resetStudioOverrides);\nelements.savePresetButton?.addEventListener(\"click\", saveCurrentPreset);\nelements.savePresetGalleryButton?.addEventListener(\"click\", saveCurrentPreset);\n\nelements.fieldSelect?.addEventListener(\"change\", (event) => {\n pushUndoSnapshot(\"change the noise field\");\n if (event.target.value === \"__recipe__\") engine.useRecipeFields();\n else engine.setField(event.target.value);\n updateWorkflowState();\n updateCode();\n});\nelements.transitionSelect?.addEventListener(\"change\", (event) => {\n pushUndoSnapshot(\"change the transition\");\n if (event.target.value === \"__recipe__\") {\n transitionOverridden = false;\n transitionOverrideValue = null;\n engine.setOptions({ transition: null });\n } else {\n transitionOverridden = true;\n transitionOverrideValue = event.target.value;\n engine.setOptions({ transition: transitionOverrideValue });\n engine.setSprite(engine.currentRecipe, {\n transition: transitionOverrideValue,\n duration: 0.55,\n });\n }\n updateWorkflowState();\n updateCode();\n});\nelements.pixelShapeSelect?.addEventListener(\"change\", (event) => {\n pushUndoSnapshot(\"change pixel geometry\");\n pixelShapeOverridden = true;\n engine.setPixelShape(event.target.value);\n updateWorkflowState();\n updateCode();\n});\nelements.pixelSwitchSelect?.addEventListener(\"change\", (event) => {\n pushUndoSnapshot(\"change the switch system\");\n pixelSwitchOverridden = true;\n engine.setPixelSwitch(event.target.value);\n updateWorkflowState();\n updateCode();\n});\nelements.orbBoundaryInput?.addEventListener(\"change\", (event) => {\n pushUndoSnapshot(\n event.currentTarget.checked\n ? \"use a Gestalt orb boundary\"\n : \"use a defined orb boundary\",\n );\n engine.setOptions({\n orbBoundary: event.currentTarget.checked ? \"gestalt\" : \"defined\",\n });\n syncOrbAppearanceToPreviews();\n updateStateUI(engine.currentRecipe);\n});\nelements.orbBackgroundEnabledInput?.addEventListener(\"change\", (event) => {\n pushUndoSnapshot(\n event.currentTarget.checked\n ? \"enable the orb background\"\n : \"remove the orb background\",\n );\n if (event.currentTarget.checked) {\n const selectedColor =\n normalizedOrbBackgroundColor(orbBackgroundColorChoice) ||\n elements.orbBackgroundColorInput?.value ||\n \"#262626\";\n orbBackgroundColorChoice = selectedColor;\n engine.setOptions({\n orbBackgroundColor: selectedColor,\n orbBackgroundMode: orbBackgroundModeChoice,\n });\n } else {\n const activeColor = normalizedOrbBackgroundColor(\n engine.options.orbBackgroundColor,\n );\n if (activeColor) orbBackgroundColorChoice = activeColor;\n const activeMode = normalizedOrbBackgroundMode(\n engine.options.orbBackgroundMode,\n activeColor,\n );\n if (activeMode !== \"none\") orbBackgroundModeChoice = activeMode;\n engine.setOptions({ orbBackgroundColor: null, orbBackgroundMode: \"none\" });\n }\n syncOrbAppearanceToPreviews();\n setOrbBackgroundControlState();\n updateWorkflowState();\n updateCode();\n});\nelements.orbBackgroundModeSelect?.addEventListener(\"change\", (event) => {\n const mode = normalizedOrbBackgroundMode(event.currentTarget.value);\n if (mode === \"none\") return;\n pushUndoSnapshot(\n mode === \"pixelated\"\n ? \"use a pixelated orb background\"\n : \"use a solid orb background\",\n );\n orbBackgroundModeChoice = mode;\n engine.setOptions({ orbBackgroundMode: mode });\n syncOrbAppearanceToPreviews();\n setOrbBackgroundControlState();\n updateWorkflowState();\n updateCode();\n});\nelements.orbBackgroundColorInput?.addEventListener(\"input\", (event) => {\n beginMutation(\"orb-background-color\", \"change the orb background color\");\n orbBackgroundColorChoice = event.currentTarget.value;\n engine.options.orbBackgroundColor = orbBackgroundColorChoice;\n engine.paint();\n syncOrbAppearanceToPreviews();\n setOrbBackgroundControlState(\n orbBackgroundColorChoice,\n true,\n orbBackgroundModeChoice,\n );\n scheduleStudioMetadataUpdate();\n});\nelements.orbBackgroundColorInput?.addEventListener(\"change\", () => {\n engine.setOptions({\n orbBackgroundColor: orbBackgroundColorChoice,\n orbBackgroundMode: orbBackgroundModeChoice,\n });\n syncOrbAppearanceToPreviews();\n endMutation(\"orb-background-color\");\n updateWorkflowState();\n updateCode();\n});\nelements.resolutionRange?.addEventListener(\"input\", (event) => {\n beginMutation(\"resolution\", \"change resolution\");\n const value = Number(event.target.value);\n if (elements.resolutionValue) elements.resolutionValue.textContent = `${value}×${value}`;\n scheduleResolutionCommit(value);\n});\nelements.resolutionRange?.addEventListener(\"change\", () => {\n const value = commitPendingResolution();\n endMutation(\"resolution\");\n schedulePreviewResolutionSync(value);\n});\nelements.speedRange?.addEventListener(\"input\", (event) => {\n beginMutation(\"speed\", \"change velocity\");\n const value = Number(event.target.value);\n if (elements.speedValue) elements.speedValue.textContent = `${formatNumber(value)}×`;\n engine.setOptions({ speed: value });\n scheduleStudioMetadataUpdate();\n});\nelements.speedRange?.addEventListener(\"change\", () => endMutation(\"speed\"));\nelements.densityRange?.addEventListener(\"input\", (event) => {\n beginMutation(\"density\", \"change density\");\n const value = Number(event.target.value);\n if (elements.densityValue) elements.densityValue.textContent = formatNumber(value);\n engine.setOptions({ density: value });\n scheduleStudioMetadataUpdate();\n});\nelements.densityRange?.addEventListener(\"change\", () => endMutation(\"density\"));\nelements.seedInput?.addEventListener(\"change\", (event) => {\n pushUndoSnapshot(\"change the seed\");\n engine.setSeed(event.target.value.trim() || \"joan-v5\");\n elements.seedInput.value = engine.seed;\n updateWorkflowState();\n updateCode();\n});\nelements.progressRange?.addEventListener(\"input\", (event) => {\n beginMutation(\"progress\", \"change semantic progress\");\n const value = Number(event.target.value) / 100;\n engine.setProgress(value);\n for (const id of [\"ai.progress\", \"transfer.active\"]) {\n const preview = previewById.get(id);\n if (!preview) continue;\n preview.setProgress(value);\n if (!previewShouldAnimate(id)) freezePreview(preview);\n }\n if (elements.progressValue) elements.progressValue.textContent = `${Math.round(value * 100)}%`;\n});\nelements.progressRange?.addEventListener(\"change\", () => endMutation(\"progress\"));\nelements.signalTokenButton?.addEventListener(\"click\", () => {\n selectSprite(\"ai.generating\");\n engine.signal(\"token\", { energy: 0.9 });\n});\nelements.signalSearchButton?.addEventListener(\"click\", () => {\n selectSprite(\"ai.searching\");\n engine.signal(\"search.hit\", {\n energy: 0.9,\n x: -0.5 + Math.random(),\n y: -0.5 + Math.random(),\n });\n});\nelements.signalAudioButton?.addEventListener(\"click\", simulateAudio);\nelements.previewFitButton?.addEventListener(\"click\", () => applyPreviewMode(\"fit\"));\nelements.previewActualButton?.addEventListener(\"click\", () =>\n applyPreviewMode(\"actual\"),\n);\n\nelements.stateSearchInput?.addEventListener(\"input\", (event) => {\n buildStatePicker(event.currentTarget.value);\n});\nelements.stateSearchInput?.addEventListener(\"keydown\", (event) => {\n if (event.key !== \"Escape\" || !event.currentTarget.value) return;\n event.preventDefault();\n event.currentTarget.value = \"\";\n buildStatePicker();\n});\nelements.statePickerSelect?.addEventListener(\"change\", (event) => {\n if (spriteById.has(event.target.value)) selectSprite(event.target.value);\n});\nelements.recentPresetSelect?.addEventListener(\"change\", (event) => {\n openRecentPreset(event.target.value);\n event.target.value = \"\";\n});\nelements.savedPresetGallery?.addEventListener(\"click\", (event) => {\n const deleteButton = event.target.closest(\"[data-delete-preset]\");\n if (deleteButton) {\n deleteRecentPreset(deleteButton.dataset.deletePreset);\n return;\n }\n const openButton = event.target.closest(\"[data-open-preset]\");\n if (openButton) openRecentPreset(openButton.dataset.openPreset);\n});\nelements.undoDeletePresetButton?.addEventListener(\"click\", undoDeletedPreset);\n\nelements.exportFormatSelect?.addEventListener(\"change\", () =>\n updateExportPreflight({ resetFilename: true }),\n);\nelements.exportSizeSelect?.addEventListener(\"change\", () => updateExportPreflight());\nelements.exportBackgroundInput?.addEventListener(\"change\", () =>\n updateExportPreflight(),\n);\nelements.exportForm?.addEventListener(\"submit\", (event) => {\n event.preventDefault();\n void performPreflightExport();\n});\nelements.closeExportButton?.addEventListener(\"click\", closeExportPreflight);\nelements.cancelExportButton?.addEventListener(\"click\", closeExportPreflight);\nelements.exportDialog?.addEventListener(\"click\", (event) => {\n if (event.target === elements.exportDialog) closeExportPreflight();\n});\nelements.exportDialog?.addEventListener(\"close\", () => {\n if (exportReturnFocus?.isConnected) exportReturnFocus.focus();\n exportReturnFocus = null;\n});\n\nelements.openRecipeButton?.addEventListener(\"click\", () => elements.recipeFile?.click());\nelements.recipeFile?.addEventListener(\"change\", async (event) => {\n const [file] = event.target.files || [];\n if (!file) return;\n elements.openRecipeButton.disabled = true;\n elements.openRecipeButton.textContent = \"Opening…\";\n try {\n await openRecipeFile(file);\n } catch (error) {\n showWorkflowStatus(error?.message || \"The recipe could not be opened.\", {\n tone: \"danger\",\n });\n } finally {\n elements.openRecipeButton.disabled = false;\n elements.openRecipeButton.textContent = \"Open recipe\";\n event.target.value = \"\";\n }\n});\n\nelements.importButton?.addEventListener(\"click\", () => elements.glyphFile?.click());\nelements.glyphFile?.addEventListener(\"change\", async (event) => {\n const [file] = event.target.files || [];\n if (!file) return;\n elements.importButton.disabled = true;\n elements.importButton.textContent = \"Rasterizing…\";\n try {\n pushUndoSnapshot(\"import artwork\");\n await engine.loadGlyphFile(file, {\n id: \"custom.product-glyph\",\n label: file.name.replace(/\\.[^.]+$/, \"\"),\n });\n spriteById.set(engine.currentRecipe.id, engine.currentRecipe);\n updateStateUI(engine.currentRecipe);\n showWorkflowStatus(`Imported ${file.name}.`, { tone: \"success\" });\n } catch (error) {\n showWorkflowStatus(error.message, { tone: \"danger\" });\n } finally {\n elements.importButton.disabled = false;\n elements.importButton.textContent = \"Import artwork\";\n event.target.value = \"\";\n }\n});\n\nfor (const button of document.querySelectorAll(\"[data-quick-sprite]\")) {\n button.addEventListener(\"click\", () => selectSprite(button.dataset.quickSprite));\n}\n\nelements.canvas?.addEventListener(\"dragover\", (event) => {\n if (!event.dataTransfer?.types.includes(\"Files\")) return;\n event.preventDefault();\n elements.canvas.closest(\".canvas-frame\")?.classList.add(\"is-dragging\");\n});\nelements.canvas?.addEventListener(\"dragleave\", () => {\n elements.canvas.closest(\".canvas-frame\")?.classList.remove(\"is-dragging\");\n});\nelements.canvas?.addEventListener(\"drop\", async (event) => {\n event.preventDefault();\n elements.canvas.closest(\".canvas-frame\")?.classList.remove(\"is-dragging\");\n const [file] = event.dataTransfer?.files || [];\n if (file) {\n pushUndoSnapshot(\"import artwork\");\n await engine.loadGlyphFile(file, {\n id: \"custom.product-glyph\",\n label: file.name.replace(/\\.[^.]+$/, \"\"),\n });\n spriteById.set(engine.currentRecipe.id, engine.currentRecipe);\n updateStateUI(engine.currentRecipe);\n showWorkflowStatus(`Imported ${file.name}.`, { tone: \"success\" });\n }\n});\n\nengine.addEventListener(\"stats\", (event) => {\n if (elements.fpsValue) elements.fpsValue.textContent = String(event.detail.fps || \"—\");\n if (elements.activeValue) {\n elements.activeValue.textContent = event.detail.activePixels.toLocaleString();\n }\n});\nengine.addEventListener(\"play\", updatePlayButton);\nengine.addEventListener(\"pause\", () => {\n updatePlayButton();\n freezePreviewIfInactive(selectedSpriteId);\n});\nengine.addEventListener(\"timelinecomplete\", (event) => {\n const recipe = spriteById.get(event.detail?.to);\n if (recipe) updateStateUI(recipe);\n});\n\ndocument.addEventListener(\"keydown\", (event) => {\n if (event.defaultPrevented || elements.exportDialog?.open) return;\n const tag = event.target?.tagName;\n if ([\"INPUT\", \"SELECT\", \"TEXTAREA\", \"BUTTON\", \"CANVAS\"].includes(tag)) return;\n if (event.code === \"Space\") {\n event.preventDefault();\n engine.toggle();\n updatePlayButton();\n } else if (event.key === \"ArrowRight\" || event.key === \"ArrowLeft\") {\n const index = Math.max(0, sprites.findIndex((sprite) => sprite.id === selectedSpriteId));\n const direction = event.key === \"ArrowRight\" ? 1 : -1;\n selectSprite(sprites[(index + direction + sprites.length) % sprites.length].id);\n } else if (event.key.toLowerCase() === \"r\") {\n randomSeed();\n } else if (event.key.toLowerCase() === \"a\") {\n toggleAutopilot();\n }\n});\n\nreduceMotion.addEventListener(\"change\", () => {\n previewLast = 0;\n for (const preview of previewEngines) {\n preview.reducedMotion = reduceMotion.matches;\n }\n schedulePreviewRefresh();\n});\n\nwindow.addEventListener(\"beforeunload\", () => {\n stopAutopilot();\n cancelPendingResolutionCommit();\n if (audioTimer !== null) window.clearInterval(audioTimer);\n previewsDisposed = true;\n previewWorkQueue.length = 0;\n previewCardObserver?.disconnect();\n savedPresetPreviewObserver?.disconnect();\n heroEngine?.destroy();\n engine.destroy();\n for (const preview of previewEngines) preview.destroy();\n});\n"
.replaceAll("./joan-engine.js", engineUrl)
.replaceAll("./fields.js", fieldsUrl)
.replaceAll("./preview-layout.js", previewLayoutUrl)
.replaceAll("./preview-thumbnails.js", previewThumbnailsUrl)
.replaceAll("./control-help.js", controlHelpUrl);
const studioUrl = createModuleUrl(studioSource);
await import(studioUrl);
</script>
</body>
</html>