@dopaminefx/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/engine/color.d.ts +71 -0
  2. package/dist/engine/color.d.ts.map +1 -0
  3. package/dist/engine/color.js +107 -0
  4. package/dist/engine/color.js.map +1 -0
  5. package/dist/engine/context.d.ts +54 -0
  6. package/dist/engine/context.d.ts.map +1 -0
  7. package/dist/engine/context.js +0 -0
  8. package/dist/engine/context.js.map +1 -0
  9. package/dist/engine/gl.d.ts +9 -0
  10. package/dist/engine/gl.d.ts.map +1 -0
  11. package/dist/engine/gl.js +39 -0
  12. package/dist/engine/gl.js.map +1 -0
  13. package/dist/engine/look/glsl.d.ts +95 -0
  14. package/dist/engine/look/glsl.d.ts.map +1 -0
  15. package/dist/engine/look/glsl.js +171 -0
  16. package/dist/engine/look/glsl.js.map +1 -0
  17. package/dist/engine/look/particles.glsl.d.ts +21 -0
  18. package/dist/engine/look/particles.glsl.d.ts.map +1 -0
  19. package/dist/engine/look/particles.glsl.js +44 -0
  20. package/dist/engine/look/particles.glsl.js.map +1 -0
  21. package/dist/engine/sdf.d.ts +77 -0
  22. package/dist/engine/sdf.d.ts.map +1 -0
  23. package/dist/engine/sdf.js +255 -0
  24. package/dist/engine/sdf.js.map +1 -0
  25. package/dist/engine/seed.d.ts +10 -0
  26. package/dist/engine/seed.d.ts.map +1 -0
  27. package/dist/engine/seed.js +20 -0
  28. package/dist/engine/seed.js.map +1 -0
  29. package/dist/engine/shadow.d.ts +41 -0
  30. package/dist/engine/shadow.d.ts.map +1 -0
  31. package/dist/engine/shadow.js +39 -0
  32. package/dist/engine/shadow.js.map +1 -0
  33. package/dist/engine/tempo.d.ts +33 -0
  34. package/dist/engine/tempo.d.ts.map +1 -0
  35. package/dist/engine/tempo.js +51 -0
  36. package/dist/engine/tempo.js.map +1 -0
  37. package/dist/framework/conductor.d.ts +100 -0
  38. package/dist/framework/conductor.d.ts.map +1 -0
  39. package/dist/framework/conductor.js +493 -0
  40. package/dist/framework/conductor.js.map +1 -0
  41. package/dist/framework/content.d.ts +67 -0
  42. package/dist/framework/content.d.ts.map +1 -0
  43. package/dist/framework/content.js +72 -0
  44. package/dist/framework/content.js.map +1 -0
  45. package/dist/framework/dope-pass.d.ts +131 -0
  46. package/dist/framework/dope-pass.d.ts.map +1 -0
  47. package/dist/framework/dope-pass.js +346 -0
  48. package/dist/framework/dope-pass.js.map +1 -0
  49. package/dist/framework/dope-zip.d.ts +22 -0
  50. package/dist/framework/dope-zip.d.ts.map +1 -0
  51. package/dist/framework/dope-zip.js +116 -0
  52. package/dist/framework/dope-zip.js.map +1 -0
  53. package/dist/framework/effect.d.ts +128 -0
  54. package/dist/framework/effect.d.ts.map +1 -0
  55. package/dist/framework/effect.js +19 -0
  56. package/dist/framework/effect.js.map +1 -0
  57. package/dist/framework/frame-expr.d.ts +124 -0
  58. package/dist/framework/frame-expr.d.ts.map +1 -0
  59. package/dist/framework/frame-expr.js +135 -0
  60. package/dist/framework/frame-expr.js.map +1 -0
  61. package/dist/framework/load-effect.d.ts +77 -0
  62. package/dist/framework/load-effect.d.ts.map +1 -0
  63. package/dist/framework/load-effect.js +135 -0
  64. package/dist/framework/load-effect.js.map +1 -0
  65. package/dist/framework/loader.d.ts +309 -0
  66. package/dist/framework/loader.d.ts.map +1 -0
  67. package/dist/framework/loader.js +266 -0
  68. package/dist/framework/loader.js.map +1 -0
  69. package/dist/framework/mood-registry.d.ts +58 -0
  70. package/dist/framework/mood-registry.d.ts.map +1 -0
  71. package/dist/framework/mood-registry.js +58 -0
  72. package/dist/framework/mood-registry.js.map +1 -0
  73. package/dist/framework/panel-runner.d.ts +96 -0
  74. package/dist/framework/panel-runner.d.ts.map +1 -0
  75. package/dist/framework/panel-runner.js +137 -0
  76. package/dist/framework/panel-runner.js.map +1 -0
  77. package/dist/framework/pass-common.d.ts +97 -0
  78. package/dist/framework/pass-common.d.ts.map +1 -0
  79. package/dist/framework/pass-common.js +178 -0
  80. package/dist/framework/pass-common.js.map +1 -0
  81. package/dist/framework/pass-runner.d.ts +183 -0
  82. package/dist/framework/pass-runner.d.ts.map +1 -0
  83. package/dist/framework/pass-runner.js +212 -0
  84. package/dist/framework/pass-runner.js.map +1 -0
  85. package/dist/framework/programs.d.ts +54 -0
  86. package/dist/framework/programs.d.ts.map +1 -0
  87. package/dist/framework/programs.js +33 -0
  88. package/dist/framework/programs.js.map +1 -0
  89. package/dist/framework/registry.d.ts +29 -0
  90. package/dist/framework/registry.d.ts.map +1 -0
  91. package/dist/framework/registry.js +38 -0
  92. package/dist/framework/registry.js.map +1 -0
  93. package/dist/framework/runtime.d.ts +19 -0
  94. package/dist/framework/runtime.d.ts.map +1 -0
  95. package/dist/framework/runtime.js +37 -0
  96. package/dist/framework/runtime.js.map +1 -0
  97. package/dist/index.d.ts +63 -0
  98. package/dist/index.d.ts.map +1 -0
  99. package/dist/index.js +126 -0
  100. package/dist/index.js.map +1 -0
  101. package/dist/overlay.d.ts +46 -0
  102. package/dist/overlay.d.ts.map +1 -0
  103. package/dist/overlay.js +79 -0
  104. package/dist/overlay.js.map +1 -0
  105. package/dist/types.d.ts +68 -0
  106. package/dist/types.d.ts.map +1 -0
  107. package/dist/types.js +10 -0
  108. package/dist/types.js.map +1 -0
  109. package/package.json +37 -0
  110. package/src/engine/color.ts +154 -0
  111. package/src/engine/context.ts +0 -0
  112. package/src/engine/gl.ts +46 -0
  113. package/src/engine/look/glsl.ts +183 -0
  114. package/src/engine/look/particles.glsl.ts +44 -0
  115. package/src/engine/sdf.ts +298 -0
  116. package/src/engine/seed.ts +23 -0
  117. package/src/engine/shadow.ts +66 -0
  118. package/src/engine/tempo.ts +54 -0
  119. package/src/framework/conductor.ts +604 -0
  120. package/src/framework/content.ts +113 -0
  121. package/src/framework/dope-pass.ts +432 -0
  122. package/src/framework/dope-zip.ts +125 -0
  123. package/src/framework/effect.ts +127 -0
  124. package/src/framework/frame-expr.ts +217 -0
  125. package/src/framework/load-effect.ts +204 -0
  126. package/src/framework/loader.ts +502 -0
  127. package/src/framework/mood-registry.ts +87 -0
  128. package/src/framework/panel-runner.ts +233 -0
  129. package/src/framework/pass-common.ts +222 -0
  130. package/src/framework/pass-runner.ts +391 -0
  131. package/src/framework/programs.ts +62 -0
  132. package/src/framework/registry.ts +44 -0
  133. package/src/framework/runtime.ts +38 -0
  134. package/src/index.ts +227 -0
  135. package/src/overlay.ts +109 -0
  136. package/src/types.ts +63 -0
@@ -0,0 +1,266 @@
1
+ /**
2
+ * `.dope` effect loader.
3
+ *
4
+ * Parses a `.dope` JSON document (per docs/effect-format.md) and evaluates its
5
+ * `controls → render.params` mapping grammar, the OKLCH golden-angle palette,
6
+ * and the per-mood baseline table into the SAME flat render-param bag the engine
7
+ * consumes. Shader bodies stay referenced GLSL (the format references them); the
8
+ * loader is NOT a GLSL transpiler.
9
+ *
10
+ * The single load-bearing invariant (the correctness anchor): the PRNG is
11
+ * consumed in the SAME order as the legacy `resolve*Params` — `buildPalette`
12
+ * draws the base hue first (one `rng()` inside it), then the per-fire scatter
13
+ * (`rng() * 1000`). So a pinned seed reproduces the legacy output byte-for-byte;
14
+ * a vitest asserts this across a mood × intensity × whimsy × seed grid.
15
+ *
16
+ * The grammar is intentionally tiny + non-Turing-complete (no loops, no user
17
+ * functions) so it is safe to evaluate from an untrusted file and trivial to
18
+ * port to Swift for the Metal backend.
19
+ */
20
+ import { buildPalette, oklchToLinearSrgb } from "../engine/color.js";
21
+ import { mulberry32 } from "../engine/seed.js";
22
+ import { NPR_TIME_STEP_MS } from "../engine/tempo.js";
23
+ const clamp01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
24
+ const lerp = (a, b, t) => a + (b - a) * clamp01(t);
25
+ /** Read a named outline from a doc's geometry, or undefined. */
26
+ export function getOutline(doc, name) {
27
+ return doc.geometry?.outlines?.[name];
28
+ }
29
+ /**
30
+ * The mood a doc degrades to when asked for one it doesn't declare.
31
+ *
32
+ * Each effect declares its OWN moods (the success trio declares
33
+ * serene/celebratory/electric; the fail effect declares try-again/error/denied),
34
+ * so there is no single hardcoded fallback mood that exists for every effect.
35
+ * An effect's own default is, in order of preference:
36
+ * 1. its `controls.mood.default`, if that mood actually has a baseline, else
37
+ * 2. the first mood key in its `baselines` table.
38
+ * This keeps EVERY declared mood resolvable and makes an unknown mood degrade to
39
+ * the effect's own sensible default instead of throwing on a missing baseline.
40
+ */
41
+ export function defaultMoodKey(doc) {
42
+ const controls = doc.controls;
43
+ const declared = controls?.mood?.default;
44
+ if (typeof declared === "string" && doc.baselines[declared])
45
+ return declared;
46
+ const first = Object.keys(doc.baselines)[0];
47
+ if (!first)
48
+ throw new Error("dope: document has no baselines to resolve a mood against");
49
+ return first;
50
+ }
51
+ /**
52
+ * Resolve a requested mood name against a doc to a key the doc actually declares:
53
+ * the requested mood if it has a baseline, otherwise the doc's own default mood
54
+ * ({@link defaultMoodKey}). Returned key is used for BOTH the baseline table and
55
+ * the palette `perMood` register, so they always agree.
56
+ */
57
+ function resolveMoodKey(doc, mood) {
58
+ return doc.baselines[mood] ? mood : defaultMoodKey(doc);
59
+ }
60
+ /** Evaluate a grammar node to a number. Pure; matches mood.ts arithmetic. */
61
+ export function evalExpr(node, ctx) {
62
+ if (typeof node === "number")
63
+ return node;
64
+ if ("const" in node)
65
+ return node.const;
66
+ if ("control" in node)
67
+ return clamp01(ctx.controls[node.control] ?? 0);
68
+ if ("baseline" in node) {
69
+ const v = ctx.baseline[node.baseline];
70
+ if (v === undefined)
71
+ throw new Error(`dope: unknown baseline "${node.baseline}"`);
72
+ return v;
73
+ }
74
+ if ("lerp" in node) {
75
+ const [c, a, b] = node.lerp;
76
+ return lerp(a, b, ctx.controls[c] ?? 0);
77
+ }
78
+ if ("mul" in node)
79
+ return node.mul.reduce((p, n) => p * evalExpr(n, ctx), 1);
80
+ if ("add" in node)
81
+ return node.add.reduce((p, n) => p + evalExpr(n, ctx), 0);
82
+ if ("sub" in node) {
83
+ const parts = node.sub.map((n) => evalExpr(n, ctx));
84
+ return parts.slice(1).reduce((p, n) => p - n, parts[0] ?? 0);
85
+ }
86
+ if ("round" in node)
87
+ return Math.round(evalExpr(node.round, ctx));
88
+ if ("floor" in node)
89
+ return Math.floor(evalExpr(node.floor, ctx));
90
+ if ("mix" in node) {
91
+ const [a, b, c] = node.mix;
92
+ const va = evalExpr(a, ctx);
93
+ const vb = evalExpr(b, ctx);
94
+ return va + (vb - va) * clamp01(ctx.controls[c] ?? 0);
95
+ }
96
+ if ("max" in node)
97
+ return Math.max(...node.max.map((n) => evalExpr(n, ctx)));
98
+ if ("min" in node)
99
+ return Math.min(...node.min.map((n) => evalExpr(n, ctx)));
100
+ throw new Error(`dope: unknown expr node ${JSON.stringify(node)}`);
101
+ }
102
+ /** Apply a param spec's post-clamp flags. */
103
+ function applyFlags(v, spec, consts) {
104
+ if (spec.clamp01)
105
+ v = clamp01(v);
106
+ if (spec.clampMax)
107
+ v = Math.min(v, consts[spec.clampMax] ?? Infinity);
108
+ if (spec.clampMin)
109
+ v = Math.max(v, consts[spec.clampMin] ?? -Infinity);
110
+ return v;
111
+ }
112
+ /**
113
+ * Resolve a `.dope` doc + a feeling into the flat render-param bag (palette,
114
+ * style, durationMs, seed, scatter seed, and every `render.params` entry). The
115
+ * `scatterKey` is the name the legacy code gave the per-fire scatter offset
116
+ * (`moteSeed` / `inkSeed` / `comicSeed`) so the output keys match exactly.
117
+ *
118
+ * RNG order (the parity anchor): baseHue via buildPalette FIRST, then the
119
+ * scatter `rng()*1000` — identical to `resolve*Params`.
120
+ */
121
+ export function resolveDopeParams(doc, input, consts, scatterKey,
122
+ /**
123
+ * Host theme override: three explicit OKLCH stops that REPLACE the generated
124
+ * golden-angle palette (a pinned brand palette). The base-hue rng() is still
125
+ * consumed first, so the per-fire scatter offset stays identical to the
126
+ * generated path — pinning the palette never shifts the mote/spray layout.
127
+ */
128
+ paletteOverride) {
129
+ const i = clamp01(input.intensity);
130
+ const w = clamp01(input.whimsy);
131
+ // Degrade an undeclared mood to THIS effect's own default mood (not a hardcoded
132
+ // "celebratory", which the fail effect — and any non-success effect — has no
133
+ // baseline for). Declared moods always resolve to themselves, so parity holds.
134
+ const moodKey = resolveMoodKey(doc, input.mood);
135
+ const baseline = doc.baselines[moodKey];
136
+ const rng = mulberry32(input.seed);
137
+ const ctx = {
138
+ controls: { intensity: i, whimsy: w },
139
+ baseline,
140
+ consts,
141
+ };
142
+ const out = {
143
+ seed: input.seed,
144
+ style: w,
145
+ };
146
+ // durationMs (tempo)
147
+ if (doc.tempo.durationMs) {
148
+ out.durationMs = applyFlags(evalExpr(doc.tempo.durationMs.from, ctx), doc.tempo.durationMs, consts);
149
+ }
150
+ // render.params
151
+ for (const [name, spec] of Object.entries(doc.render.params)) {
152
+ if (name === "style")
153
+ continue; // style is the raw whimsy control, set above
154
+ out[name] = applyFlags(evalExpr(spec.from, ctx), spec, consts);
155
+ }
156
+ // Palette FIRST (consumes one rng() for the base hue inside buildPalette),
157
+ // matching the engine's call order exactly.
158
+ // Same resolved key as the baseline lookup, so palette + baselines always agree
159
+ // (an undeclared mood uses the effect's own default for both).
160
+ const reg = doc.palette.perMood[moodKey] ?? doc.palette.perMood[defaultMoodKey(doc)];
161
+ const chroma = evalExpr(doc.palette.chroma.from, { ...ctx, baseline: reg });
162
+ const generated = buildPalette(rng, {
163
+ lightness: reg.lightness,
164
+ chroma,
165
+ hueCenter: reg.hueCenter,
166
+ hueRange: reg.hueRange,
167
+ hueSpread: doc.palette.hueSpread,
168
+ });
169
+ // A host palette override REPLACES the generated stops (the base-hue rng() above
170
+ // was still consumed, so scatter parity holds), pinning a brand palette.
171
+ out.palette = paletteOverride ? paletteOverride.map(oklchToLinearSrgb) : generated;
172
+ // THEN the per-fire scatter offset (same rng() * 1000 as the engine).
173
+ out[scatterKey] = rng() * 1000;
174
+ return out;
175
+ }
176
+ // A `.dope` must be SELF-CONTAINED — it may inline assets (e.g. `data:` URIs) or
177
+ // reference bundled programs/assets by key or by a path RELATIVE to the package
178
+ // (resolved inside a `.dope` zip), but it must never point at the network or an
179
+ // absolute filesystem path. This keeps every effect portable and offline.
180
+ const REMOTE_REF_RE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i; // http(s)://, ftp://, //host
181
+ const ABS_PATH_RE = /^(?:\/|[A-Za-z]:[\\/])/; // /etc/..., C:\...
182
+ function assertStandalone(node, path = "$") {
183
+ if (typeof node === "string") {
184
+ if (REMOTE_REF_RE.test(node) || ABS_PATH_RE.test(node)) {
185
+ throw new Error(`dope: external asset reference is not allowed — a .dope must be ` +
186
+ `self-contained (inline or bundle assets). Offending value at ${path}: "${node}"`);
187
+ }
188
+ return;
189
+ }
190
+ if (Array.isArray(node)) {
191
+ node.forEach((v, i) => assertStandalone(v, `${path}[${i}]`));
192
+ }
193
+ else if (node && typeof node === "object") {
194
+ for (const [k, v] of Object.entries(node))
195
+ assertStandalone(v, `${path}.${k}`);
196
+ }
197
+ }
198
+ // Tolerance for the loop whole-multiple checks (the step 1000/12 is not exactly
199
+ // representable, so an exact `% === 0` would be float-fragile).
200
+ const LOOP_EPS = 1e-6;
201
+ const isWhole = (x) => Math.abs(x - Math.round(x)) < LOOP_EPS;
202
+ /**
203
+ * Validate a `tempo.loop` block against the doc's baselines: the period must be
204
+ * positive, must tile the "animate on twos" grid (unless `snapAligned` is
205
+ * false), and every per-mood baseline `durationMs` must be a whole number of
206
+ * periods — the seam guarantee, moved from per-effect convention into the
207
+ * parser (identical on every platform).
208
+ */
209
+ function assertValidLoop(loop, baselines) {
210
+ const p = loop.periodMs;
211
+ if (typeof p !== "number" || !Number.isFinite(p) || p <= 0) {
212
+ throw new Error(`dope: tempo.loop.periodMs must be a positive number (got ${JSON.stringify(p)})`);
213
+ }
214
+ if (loop.snapAligned !== false && !isWhole(p / NPR_TIME_STEP_MS)) {
215
+ throw new Error(`dope: tempo.loop.periodMs (${p}) is not a whole number of animate-on-twos steps (1000/12 ms)`);
216
+ }
217
+ for (const [mood, row] of Object.entries(baselines)) {
218
+ const d = row.durationMs;
219
+ if (d !== undefined && !isWhole(d / p)) {
220
+ throw new Error(`dope: baselines.${mood}.durationMs (${d}) is not a whole number of tempo.loop periods (${p} ms)`);
221
+ }
222
+ }
223
+ }
224
+ /**
225
+ * Parse + validate a `.dope` document from a JSON string or already-parsed
226
+ * object. Rejects a wrong/absent magic or major version, any external
227
+ * (remote / absolute-path) asset reference — a `.dope` must be self-contained —
228
+ * and a `tempo.loop` that breaks the seam invariants.
229
+ * (A fuller JSON-Schema validation lives in CI against effect-format.schema.json.)
230
+ */
231
+ export function parseDope(src) {
232
+ const doc = (typeof src === "string" ? JSON.parse(src) : src);
233
+ if (doc.fmt !== "dopamine-effect") {
234
+ throw new Error(`dope: not a Dopamine effect document (fmt="${doc.fmt}")`);
235
+ }
236
+ const major = Number(doc.v?.split(".")[0]);
237
+ if (!Number.isFinite(major) || major > 1) {
238
+ throw new Error(`dope: unsupported format version "${doc.v}"`);
239
+ }
240
+ if (!doc.render?.params || !doc.palette?.perMood || !doc.baselines) {
241
+ throw new Error("dope: document missing render.params / palette.perMood / baselines");
242
+ }
243
+ if (doc.tempo?.loop)
244
+ assertValidLoop(doc.tempo.loop, doc.baselines);
245
+ if (doc.render?.panel)
246
+ assertValidPanel(doc.render.panel, doc.binding);
247
+ assertStandalone(doc);
248
+ return doc;
249
+ }
250
+ /**
251
+ * `render.panel` invariants: the panel sampler must be one of the declared
252
+ * `binding.samplers` (the panel is a texture binding like any other — one
253
+ * source of truth for the uniform list), and the unit must be a non-negative
254
+ * integer.
255
+ */
256
+ function assertValidPanel(panel, binding) {
257
+ const samplers = (binding?.samplers ?? []).map((s) => (typeof s === "string" ? s : s.web));
258
+ if (!panel.sampler || !samplers.includes(panel.sampler)) {
259
+ throw new Error(`dope: render.panel.sampler "${panel.sampler}" is not a declared binding.samplers entry`);
260
+ }
261
+ const unit = panel.texture ?? 0;
262
+ if (!Number.isInteger(unit) || unit < 0) {
263
+ throw new Error(`dope: render.panel.texture must be a non-negative integer (got ${unit})`);
264
+ }
265
+ }
266
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/framework/loader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Mood registry — the shared, effect-agnostic "emotional register" layer.
3
+ *
4
+ * A mood describes a *feeling baseline* (how warm, how energetic, how bright) in
5
+ * effect-neutral terms. Every effect reads the same resolved mood, so adding a
6
+ * mood ("triumphant", "focused", a brand mood) lights up across *all* effects at
7
+ * once — no per-effect edits.
8
+ *
9
+ * Why a register, not a full param table: the three built-in effects share their
10
+ * *color identity* per mood (the same `hueCenter`/`hueRange` — a serene blue, a
11
+ * celebratory roam, a hot electric band) but each effect has its own spatial /
12
+ * motion / typographic baselines (a bloom radius means nothing to a comic word).
13
+ * So the registry owns the shared register + a normalized `energy`, and each
14
+ * effect's `mood.ts` baseline table keys off the mood name (built-ins keep their
15
+ * exact tuned values; an unfamiliar mood is derived from the register + energy
16
+ * so it still renders sensibly everywhere). This keeps built-in output
17
+ * byte-identical to the legacy `resolve*Params` while making moods extensible.
18
+ */
19
+ /** Effect-neutral description of a mood's shared color register + energy. */
20
+ export interface MoodSpec {
21
+ /** Preferred hue center in degrees (arousal rises blue→green→red). */
22
+ hueCenter: number;
23
+ /** Width of the random hue band around the center, in degrees. */
24
+ hueRange: number;
25
+ /** Perceptual lightness reference for palettes, 0..1. */
26
+ lightness: number;
27
+ /** Base chroma (colorfulness) reference for palettes, ~0..0.4. */
28
+ chroma: number;
29
+ /**
30
+ * Normalized energy 0..1 (serene → electric). Effects use this to derive a
31
+ * baseline for a mood they have no tuned table entry for (faster, denser,
32
+ * harder slams toward 1). Built-in effects ignore it for their built-in moods.
33
+ */
34
+ energy: number;
35
+ }
36
+ /** A mood resolved for use: its spec plus the name it was registered under. */
37
+ export interface ResolvedMood extends MoodSpec {
38
+ readonly name: string;
39
+ }
40
+ /** The mood used when none is given or an unknown one is requested. */
41
+ export declare const DEFAULT_MOOD = "celebratory";
42
+ /**
43
+ * Register (or override) a mood. Returns the name so it can be used inline.
44
+ *
45
+ * ```ts
46
+ * registerMood("triumphant", { hueCenter: 280, hueRange: 160,
47
+ * lightness: 0.8, chroma: 0.22, energy: 0.9 });
48
+ * await celebrate({ mood: "triumphant" }); // now works for ALL effects
49
+ * ```
50
+ */
51
+ export declare function registerMood(name: string, spec: MoodSpec): string;
52
+ /** Look up a mood, falling back to the default. Always returns a usable mood. */
53
+ export declare function resolveMood(name: string | undefined): ResolvedMood;
54
+ /** Whether a mood name is currently registered. */
55
+ export declare function hasMood(name: string): boolean;
56
+ /** Names of all registered moods (built-in + custom). */
57
+ export declare function moodNames(): string[];
58
+ //# sourceMappingURL=mood-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mood-registry.d.ts","sourceRoot":"","sources":["../../src/framework/mood-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,QAAQ;IACvB,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAa,SAAQ,QAAQ;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAeD,uEAAuE;AACvE,eAAO,MAAM,YAAY,gBAAgB,CAAC;AAE1C;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAGjE;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,CAGlE;AAED,mDAAmD;AACnD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE7C;AAED,yDAAyD;AACzD,wBAAgB,SAAS,IAAI,MAAM,EAAE,CAEpC"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Mood registry — the shared, effect-agnostic "emotional register" layer.
3
+ *
4
+ * A mood describes a *feeling baseline* (how warm, how energetic, how bright) in
5
+ * effect-neutral terms. Every effect reads the same resolved mood, so adding a
6
+ * mood ("triumphant", "focused", a brand mood) lights up across *all* effects at
7
+ * once — no per-effect edits.
8
+ *
9
+ * Why a register, not a full param table: the three built-in effects share their
10
+ * *color identity* per mood (the same `hueCenter`/`hueRange` — a serene blue, a
11
+ * celebratory roam, a hot electric band) but each effect has its own spatial /
12
+ * motion / typographic baselines (a bloom radius means nothing to a comic word).
13
+ * So the registry owns the shared register + a normalized `energy`, and each
14
+ * effect's `mood.ts` baseline table keys off the mood name (built-ins keep their
15
+ * exact tuned values; an unfamiliar mood is derived from the register + energy
16
+ * so it still renders sensibly everywhere). This keeps built-in output
17
+ * byte-identical to the legacy `resolve*Params` while making moods extensible.
18
+ */
19
+ /**
20
+ * The three built-in moods. The register values mirror the per-mood columns
21
+ * shared by all three effects' baseline tables in `mood.ts`; `energy` orders
22
+ * them serene(0) → celebratory(0.5) → electric(1).
23
+ */
24
+ const BUILTIN_MOODS = {
25
+ serene: { hueCenter: 230, hueRange: 120, lightness: 0.83, chroma: 0.1, energy: 0.0 },
26
+ celebratory: { hueCenter: 50, hueRange: 320, lightness: 0.81, chroma: 0.17, energy: 0.5 },
27
+ electric: { hueCenter: 35, hueRange: 150, lightness: 0.79, chroma: 0.24, energy: 1.0 },
28
+ };
29
+ const moods = new Map(Object.entries(BUILTIN_MOODS));
30
+ /** The mood used when none is given or an unknown one is requested. */
31
+ export const DEFAULT_MOOD = "celebratory";
32
+ /**
33
+ * Register (or override) a mood. Returns the name so it can be used inline.
34
+ *
35
+ * ```ts
36
+ * registerMood("triumphant", { hueCenter: 280, hueRange: 160,
37
+ * lightness: 0.8, chroma: 0.22, energy: 0.9 });
38
+ * await celebrate({ mood: "triumphant" }); // now works for ALL effects
39
+ * ```
40
+ */
41
+ export function registerMood(name, spec) {
42
+ moods.set(name, spec);
43
+ return name;
44
+ }
45
+ /** Look up a mood, falling back to the default. Always returns a usable mood. */
46
+ export function resolveMood(name) {
47
+ const key = name && moods.has(name) ? name : DEFAULT_MOOD;
48
+ return { name: key, ...moods.get(key) };
49
+ }
50
+ /** Whether a mood name is currently registered. */
51
+ export function hasMood(name) {
52
+ return moods.has(name);
53
+ }
54
+ /** Names of all registered moods (built-in + custom). */
55
+ export function moodNames() {
56
+ return [...moods.keys()];
57
+ }
58
+ //# sourceMappingURL=mood-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mood-registry.js","sourceRoot":"","sources":["../../src/framework/mood-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAyBH;;;;GAIG;AACH,MAAM,aAAa,GAA6B;IAC9C,MAAM,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE;IACpF,WAAW,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;IACzF,QAAQ,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;CACvF,CAAC;AAEF,MAAM,KAAK,GAAG,IAAI,GAAG,CAAmB,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;AAEvE,uEAAuE;AACvE,MAAM,CAAC,MAAM,YAAY,GAAG,aAAa,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,IAAc;IACvD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACtB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,WAAW,CAAC,IAAwB;IAClD,MAAM,GAAG,GAAG,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC;IAC1D,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAE,EAAE,CAAC;AAC3C,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,SAAS;IACvB,OAAO,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Generic Canvas2D "panel" runner — the shared backbone for HYBRID effects whose
3
+ * per-frame content is drawn with Canvas2D (vector / text / shape) and then lit
4
+ * by a fragment shader (Comic Impact's hand-lettered word + jagged starburst +
5
+ * ink, lit into Ben-Day halftone + action lines + flash).
6
+ *
7
+ * It owns ALL the renderer/texture/upload/shadow plumbing that a Canvas2D-panel
8
+ * effect would otherwise hand-wire:
9
+ * • the single offscreen panel canvas + its 2D context,
10
+ * • resizing the panel to track the live GL canvas each frame,
11
+ * • the per-frame draw → `texImage2D` UPLOAD into BOTH the light + shadow
12
+ * contexts (the panel pixels change every frame, so — unlike a static SDF
13
+ * aux — they are re-uploaded each frame), with the FLIP_Y + non-premultiplied
14
+ * channel-encoding convention the panel shaders expect,
15
+ * • the standard shader uniforms (resolution, center, life, time, style,
16
+ * palette, the shadow-pass uniforms via shadowGeometry) + scalar render.params
17
+ * auto-bound by name convention,
18
+ * • the light + (optional) shadow pass via the program-cached contexts, and
19
+ * • disposing the per-fire panel textures.
20
+ *
21
+ * What stays per-effect (the honest boundary): the GLSL, a small `draw()` panel
22
+ * program (the Canvas2D draw — genuinely code-shaped vector/text logic stays JS),
23
+ * and a tiny config naming the shader's uniforms + the per-frame timing.
24
+ */
25
+ import type { EffectContext, EffectInstance } from "./effect.js";
26
+ import type { PassParams } from "./pass-runner.js";
27
+ /** Per-frame timing context for a panel effect's draw + frame hooks. */
28
+ export interface PanelFrameInfo {
29
+ /** Raw elapsed time since start, ms (panels don't snap "on twos"). */
30
+ elapsedMs: number;
31
+ /** Normalized life 0..1 (elapsedMs / durationMs, clamped). */
32
+ life: number;
33
+ /** Device-pixel ratio the panel is rendered at. */
34
+ dpr: number;
35
+ /**
36
+ * Targeted element CENTRE in panel device px (canvas space, y-down). The
37
+ * centrepiece is drawn here instead of the canvas centre. Defaults to the
38
+ * canvas centre.
39
+ */
40
+ centerPx: {
41
+ x: number;
42
+ y: number;
43
+ };
44
+ /**
45
+ * Targeted element SIZE in device px. The centrepiece is sized to this box.
46
+ * Defaults to the full canvas.
47
+ */
48
+ targetPx: {
49
+ width: number;
50
+ height: number;
51
+ };
52
+ }
53
+ /** A registered "panel program": the Canvas2D draw for one frame. */
54
+ export type PanelDraw<P extends PassParams = PassParams> = (panelCtx: CanvasRenderingContext2D, width: number, height: number, params: P, info: PanelFrameInfo) => void;
55
+ /** Config for one Canvas2D-panel effect. */
56
+ export interface PanelConfig<P extends PassParams = PassParams> {
57
+ /** Vertex + fragment GLSL (the per-effect look). */
58
+ vertex: string;
59
+ fragment: string;
60
+ /** Every uniform name the shader reads. */
61
+ uniforms: readonly string[];
62
+ /** Sampler uniform for the uploaded panel (bound to TEXTURE0). Default "uPanel". */
63
+ panelSampler?: string;
64
+ /**
65
+ * Explicit `param name → uniform name` overrides for the scalar auto-binding;
66
+ * map to `null` to skip a param that isn't a uniform (e.g. a scatter seed).
67
+ */
68
+ bindings?: Record<string, string | null>;
69
+ /** The shadow occluder "height" as a fraction of min canvas dim. */
70
+ shadowHeightFrac: number | ((params: P) => number);
71
+ /** The Canvas2D panel program (draws one frame). */
72
+ draw: PanelDraw<P>;
73
+ /**
74
+ * Compute the genuinely effect-specific TIME-VARYING uniforms for a frame
75
+ * (presence, flash, …). Returns a map of uniform name → float; `amp` (a
76
+ * well-known key) feeds shadowGeometry.
77
+ */
78
+ frame(info: PanelFrameInfo, params: P): {
79
+ amp: number;
80
+ } & Record<string, number>;
81
+ /**
82
+ * Extra per-pass scalar uniforms that depend on the live canvas / dpr but are
83
+ * not plain params (e.g. `uDotSize = dotSize * dpr`, `uInkBoost`). Computed per
84
+ * pass.
85
+ */
86
+ passUniforms?(canvas: HTMLCanvasElement, params: P, dpr: number, targetPx: {
87
+ width: number;
88
+ height: number;
89
+ }): Record<string, number>;
90
+ }
91
+ /**
92
+ * Build a drawable {@link EffectInstance} for a Canvas2D-panel effect from its
93
+ * config + resolved params + the runtime context.
94
+ */
95
+ export declare function createPanelInstance<P extends PassParams>(config: PanelConfig<P>, params: P, ctx: EffectContext): EffectInstance;
96
+ //# sourceMappingURL=panel-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panel-runner.d.ts","sourceRoot":"","sources":["../../src/framework/panel-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAcnD,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC;;;OAGG;IACH,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAED,qEAAqE;AACrE,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,IAAI,CACzD,QAAQ,EAAE,wBAAwB,EAClC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,CAAC,EACT,IAAI,EAAE,cAAc,KACjB,IAAI,CAAC;AAEV,4CAA4C;AAC5C,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU;IAC5D,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,oFAAoF;IACpF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,oEAAoE;IACpE,gBAAgB,EAAE,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC,CAAC;IACnD,oDAAoD;IACpD,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC,GAAG;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjF;;;;OAIG;IACH,YAAY,CAAC,CACX,MAAM,EAAE,iBAAiB,EACzB,MAAM,EAAE,CAAC,EACT,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAC1C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3B;AAQD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,UAAU,EACtD,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EACtB,MAAM,EAAE,CAAC,EACT,GAAG,EAAE,aAAa,GACjB,cAAc,CA6GhB"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Generic Canvas2D "panel" runner — the shared backbone for HYBRID effects whose
3
+ * per-frame content is drawn with Canvas2D (vector / text / shape) and then lit
4
+ * by a fragment shader (Comic Impact's hand-lettered word + jagged starburst +
5
+ * ink, lit into Ben-Day halftone + action lines + flash).
6
+ *
7
+ * It owns ALL the renderer/texture/upload/shadow plumbing that a Canvas2D-panel
8
+ * effect would otherwise hand-wire:
9
+ * • the single offscreen panel canvas + its 2D context,
10
+ * • resizing the panel to track the live GL canvas each frame,
11
+ * • the per-frame draw → `texImage2D` UPLOAD into BOTH the light + shadow
12
+ * contexts (the panel pixels change every frame, so — unlike a static SDF
13
+ * aux — they are re-uploaded each frame), with the FLIP_Y + non-premultiplied
14
+ * channel-encoding convention the panel shaders expect,
15
+ * • the standard shader uniforms (resolution, center, life, time, style,
16
+ * palette, the shadow-pass uniforms via shadowGeometry) + scalar render.params
17
+ * auto-bound by name convention,
18
+ * • the light + (optional) shadow pass via the program-cached contexts, and
19
+ * • disposing the per-fire panel textures.
20
+ *
21
+ * What stays per-effect (the honest boundary): the GLSL, a small `draw()` panel
22
+ * program (the Canvas2D draw — genuinely code-shaped vector/text logic stays JS),
23
+ * and a tiny config naming the shader's uniforms + the per-frame timing.
24
+ */
25
+ import { STANDARD_COMMON, applyFloatMap, beginProgram, bindFrameUniforms, bindPalette, bindScalars, bindShadowGeometry, bindTarget, compositeLightFragment, computeScalarBinds, } from "./pass-common.js";
26
+ // The panel runner adds `uCenter` (the panel composites around screen center)
27
+ // to the shared standard set — and binds the SAME anchor under the pass-runner's
28
+ // `uOrigin` name, so a panel shader may use either spelling (the single-source
29
+ // GLSL→MSL path maps `uOrigin` onto the packed struct's `origin` field).
30
+ const STANDARD = ["uCenter", "uOrigin", ...STANDARD_COMMON];
31
+ /**
32
+ * Build a drawable {@link EffectInstance} for a Canvas2D-panel effect from its
33
+ * config + resolved params + the runtime context.
34
+ */
35
+ export function createPanelInstance(config, params, ctx) {
36
+ const pal = params.palette;
37
+ const dpr = ctx.dpr;
38
+ const sampler = config.panelSampler ?? "uPanel";
39
+ const allUniforms = [...new Set([...STANDARD, sampler, ...config.uniforms])];
40
+ // Backdrop-aware mode: the light pass emits premultiplied light (source-over,
41
+ // visible on any surface); the shadow pass keeps the opaque multiply fragment.
42
+ const lightFragment = ctx.composite?.premultiplied
43
+ ? compositeLightFragment(config.fragment)
44
+ : config.fragment;
45
+ // The numeric params that auto-bind to a uniform.
46
+ const scalarBinds = computeScalarBinds(params, config.bindings ?? {});
47
+ // One offscreen Canvas2D panel, shared by both passes (drawn once per frame).
48
+ const panel = document.createElement("canvas");
49
+ const pctx = panel.getContext("2d", { alpha: true });
50
+ const lightTex = ctx.light.gl.createTexture();
51
+ const shadowTex = ctx.shadow ? ctx.shadow.gl.createTexture() : null;
52
+ for (const glc of [ctx.light, ctx.shadow]) {
53
+ if (!glc)
54
+ continue;
55
+ const { gl } = glc;
56
+ gl.bindTexture(gl.TEXTURE_2D, glc === ctx.light ? lightTex : shadowTex);
57
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
58
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
59
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
60
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
61
+ }
62
+ const heightFrac = () => typeof config.shadowHeightFrac === "function" ? config.shadowHeightFrac(params) : config.shadowHeightFrac;
63
+ const drawPass = (glc, tex, info, frameUniforms, isShadow) => {
64
+ const { gl } = glc;
65
+ const c = glc.canvas;
66
+ const { u } = beginProgram(glc, config.vertex, isShadow ? config.fragment : lightFragment, allUniforms);
67
+ // Upload the freshly-drawn panel (changes every frame).
68
+ gl.activeTexture(gl.TEXTURE0);
69
+ gl.bindTexture(gl.TEXTURE_2D, tex);
70
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
71
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
72
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, panel);
73
+ if (u[sampler])
74
+ gl.uniform1i(u[sampler], 0);
75
+ // Extra per-pass scalar uniforms (dpr-scaled etc.).
76
+ applyFloatMap(gl, u, config.passUniforms?.(c, params, dpr, info.targetPx));
77
+ // Standard uniforms.
78
+ gl.uniform2f(u.uResolution, c.width, c.height);
79
+ bindTarget(gl, u, c, ctx.targetSize, dpr);
80
+ // uCenter is the impact/heart centre the PROCEDURAL parts radiate from (comic's
81
+ // action lines, heartburst's bloom). It must match where the panel centrepiece
82
+ // lands — the anchor — NOT the canvas centre, or they split (centrepiece on the
83
+ // target, lines/glow stuck at screen centre). `frag` here is vUv*uResolution
84
+ // (y-up, matching the flipped panel texture), so flip the anchor's y exactly as
85
+ // the pure-shader runner does for uOrigin.
86
+ gl.uniform2f(u.uCenter, ctx.anchor.x * dpr, c.height - ctx.anchor.y * dpr);
87
+ if (u.uOrigin)
88
+ gl.uniform2f(u.uOrigin, ctx.anchor.x * dpr, c.height - ctx.anchor.y * dpr);
89
+ gl.uniform1f(u.uLife, info.life);
90
+ gl.uniform1f(u.uTimeS, info.elapsedMs / 1000);
91
+ gl.uniform1f(u.uStyle, params.style);
92
+ bindPalette(gl, u, pal);
93
+ bindScalars(gl, u, params, scalarBinds);
94
+ bindFrameUniforms(gl, u, frameUniforms);
95
+ gl.uniform1f(u.uShadow, isShadow ? 1 : 0);
96
+ if (isShadow)
97
+ bindShadowGeometry(gl, u, c, heightFrac(), frameUniforms.amp, params.style);
98
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
99
+ };
100
+ let disposed = false;
101
+ return {
102
+ durationMs: params.durationMs,
103
+ renderAt(elapsedMs) {
104
+ if (disposed)
105
+ return;
106
+ const c = ctx.light.canvas;
107
+ if (panel.width !== c.width || panel.height !== c.height) {
108
+ panel.width = c.width;
109
+ panel.height = c.height;
110
+ }
111
+ const life = Math.min(Math.max(elapsedMs, 0) / params.durationMs, 1);
112
+ // The targeted element box → panel device px (y-down canvas space). Defaults
113
+ // to the canvas centre + full canvas, reproducing the old screen-centred pose.
114
+ const centerPx = { x: ctx.anchor.x * dpr, y: ctx.anchor.y * dpr };
115
+ const targetPx = ctx.targetSize
116
+ ? { width: ctx.targetSize.width * dpr, height: ctx.targetSize.height * dpr }
117
+ : { width: c.width, height: c.height };
118
+ const info = { elapsedMs, life, dpr, centerPx, targetPx };
119
+ const frameUniforms = config.frame(info, params);
120
+ // Draw the shared offscreen panel once, then composite into each pass.
121
+ config.draw(pctx, c.width, c.height, params, info);
122
+ if (ctx.shadow && shadowTex)
123
+ drawPass(ctx.shadow, shadowTex, info, frameUniforms, true);
124
+ drawPass(ctx.light, lightTex, info, frameUniforms, false);
125
+ },
126
+ dispose() {
127
+ if (disposed)
128
+ return;
129
+ disposed = true;
130
+ if (lightTex)
131
+ ctx.light.gl.deleteTexture(lightTex);
132
+ if (shadowTex && ctx.shadow)
133
+ ctx.shadow.gl.deleteTexture(shadowTex);
134
+ },
135
+ };
136
+ }
137
+ //# sourceMappingURL=panel-runner.js.map