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
5593 lines
738 KiB
HTML
5593 lines
738 KiB
HTML
<!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 16–18 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(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\nfunction escapeXmlAttribute(value) {\n return escapeXml(value)\n .replaceAll(\"\\t\", \"	\")\n .replaceAll(\"\\n\", \" \")\n .replaceAll(\"\\r\", \" \");\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 state’s 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 cell’s 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 theme’s 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>
|