@pixerate/flair 0.0.0-stage → 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 (53) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +254 -2
  4. package/dist/catalog/index.d.ts +74 -0
  5. package/dist/catalog/index.js +47 -0
  6. package/dist/catalog/index.js.map +1 -0
  7. package/dist/chunk-36DTNSHU.js +160 -0
  8. package/dist/chunk-36DTNSHU.js.map +1 -0
  9. package/dist/chunk-3FAUZOGL.js +331 -0
  10. package/dist/chunk-3FAUZOGL.js.map +1 -0
  11. package/dist/chunk-CFCLT2XL.js +1 -0
  12. package/dist/chunk-CFCLT2XL.js.map +1 -0
  13. package/dist/chunk-EHDJ3CPJ.js +327 -0
  14. package/dist/chunk-EHDJ3CPJ.js.map +1 -0
  15. package/dist/chunk-G5FNZDTA.js +536 -0
  16. package/dist/chunk-G5FNZDTA.js.map +1 -0
  17. package/dist/chunk-HKXOHRCD.js +763 -0
  18. package/dist/chunk-HKXOHRCD.js.map +1 -0
  19. package/dist/chunk-J55UBNAT.js +164 -0
  20. package/dist/chunk-J55UBNAT.js.map +1 -0
  21. package/dist/chunk-JWZCPMFG.js +2074 -0
  22. package/dist/chunk-JWZCPMFG.js.map +1 -0
  23. package/dist/chunk-MWVTQFV5.js +1894 -0
  24. package/dist/chunk-MWVTQFV5.js.map +1 -0
  25. package/dist/chunk-UQQNZSYL.js +594 -0
  26. package/dist/chunk-UQQNZSYL.js.map +1 -0
  27. package/dist/chunk-WMZQCUFJ.js +73 -0
  28. package/dist/chunk-WMZQCUFJ.js.map +1 -0
  29. package/dist/cli.d.ts +1 -0
  30. package/dist/cli.js +331 -0
  31. package/dist/cli.js.map +1 -0
  32. package/dist/clock-BhdXWH-n.d.ts +47 -0
  33. package/dist/controls-DdXMbovi.d.ts +97 -0
  34. package/dist/editor/index.d.ts +413 -0
  35. package/dist/editor/index.js +1009 -0
  36. package/dist/editor/index.js.map +1 -0
  37. package/dist/export-FEBPEEO7.js +22 -0
  38. package/dist/export-FEBPEEO7.js.map +1 -0
  39. package/dist/index.d.ts +1431 -0
  40. package/dist/index.js +1263 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/motion-D0TVv2CF.d.ts +148 -0
  43. package/dist/node.d.ts +24 -0
  44. package/dist/node.js +10 -0
  45. package/dist/node.js.map +1 -0
  46. package/dist/recorder-WJTWMEQN.js +10 -0
  47. package/dist/recorder-WJTWMEQN.js.map +1 -0
  48. package/dist/renderer-Oyynz-fs.d.ts +293 -0
  49. package/dist/types-CkKkavvB.d.ts +71 -0
  50. package/dist/vector/index.d.ts +83 -0
  51. package/dist/vector/index.js +39 -0
  52. package/dist/vector/index.js.map +1 -0
  53. package/package.json +96 -4
@@ -0,0 +1,2074 @@
1
+ import {
2
+ SVG_EDGE,
3
+ ShaderCompileError,
4
+ TextureCache,
5
+ VERTEX_SOURCE,
6
+ compileShader,
7
+ createContext,
8
+ drawFrame,
9
+ resetGlCaches,
10
+ wrapSource
11
+ } from "./chunk-HKXOHRCD.js";
12
+ import {
13
+ ManualClock
14
+ } from "./chunk-WMZQCUFJ.js";
15
+ import {
16
+ CAMERA_MS,
17
+ DEFAULT_STAGE,
18
+ areaOf,
19
+ between,
20
+ boundsOf,
21
+ calculateFramingRect,
22
+ easeInOutCubic,
23
+ frame,
24
+ isDeleteCommand,
25
+ isElementCommand,
26
+ isUpdateCommand,
27
+ overlapArea,
28
+ viewAt,
29
+ visibleRect
30
+ } from "./chunk-36DTNSHU.js";
31
+ import {
32
+ controlValue,
33
+ imageUrls,
34
+ parseControls
35
+ } from "./chunk-J55UBNAT.js";
36
+
37
+ // src/shader/renderer.ts
38
+ var ShaderRenderer = class {
39
+ canvas;
40
+ gl;
41
+ shader = null;
42
+ textures = null;
43
+ controls = [];
44
+ values;
45
+ source = "";
46
+ raf = 0;
47
+ lastNow = 0;
48
+ frame = 0;
49
+ fpsFrames = 0;
50
+ fpsSince = 0;
51
+ resizeObserver;
52
+ mouse = [0, 0, 0, 0];
53
+ disposed = false;
54
+ /** The latest source's status. */
55
+ status = { ok: true, errors: [] };
56
+ /** `iTime`, in seconds. Use `seek` to change it. */
57
+ time = 0;
58
+ /** Playback speed: 1 is real time. */
59
+ speed = 1;
60
+ /** Device pixels per CSS pixel; lower renders faster. Use `setPixelRatio` to change it. */
61
+ pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
62
+ playing = true;
63
+ fps = 0;
64
+ onStats;
65
+ /** Called with `true` when the GL context is lost and `false` once it is restored and recompiled. */
66
+ onContextLost;
67
+ /** Called when an image control's URL cannot be loaded; the control shows black. */
68
+ onImageError;
69
+ /** Creates a canvas in `host` (with `className`) and starts the animation loop. Size the canvas with CSS. */
70
+ constructor(host, className = "") {
71
+ const canvas = document.createElement("canvas");
72
+ canvas.className = className;
73
+ host.appendChild(canvas);
74
+ this.canvas = canvas;
75
+ this.gl = createContext(canvas, true);
76
+ this.textures = this.gl && this.textureCache(this.gl);
77
+ this.resizeObserver = new ResizeObserver(() => this.resize());
78
+ this.resizeObserver.observe(canvas);
79
+ canvas.addEventListener("webglcontextlost", this.onLost);
80
+ canvas.addEventListener("webglcontextrestored", this.onRestored);
81
+ canvas.addEventListener("pointerdown", this.onPointerDown);
82
+ canvas.addEventListener("pointermove", this.onPointerMove);
83
+ window.addEventListener("pointerup", this.onPointerUp);
84
+ this.resize();
85
+ this.raf = requestAnimationFrame(this.tick);
86
+ }
87
+ /** False when the browser has no WebGL2. */
88
+ get supported() {
89
+ return this.gl !== null;
90
+ }
91
+ /** Compiles a new source; returns its status. Keeps the previous program when it fails. */
92
+ setSource(source) {
93
+ this.source = source;
94
+ const controls = parseControls(source);
95
+ if (!this.gl) {
96
+ this.status = {
97
+ ok: false,
98
+ errors: [{ line: 0, message: "WebGL2 is not available in this browser." }]
99
+ };
100
+ return this.status;
101
+ }
102
+ const result = compileShader(this.gl, source, controls);
103
+ if (result.ok) {
104
+ if (this.shader) this.gl.deleteProgram(this.shader.program);
105
+ this.shader = result.shader;
106
+ this.controls = controls;
107
+ this.status = { ok: true, errors: [] };
108
+ } else {
109
+ this.status = { ok: false, errors: result.errors };
110
+ }
111
+ this.syncTextures();
112
+ if (!this.playing) this.render();
113
+ return this.status;
114
+ }
115
+ /**
116
+ * Sets control values (see `controlValue`); a paused preview redraws.
117
+ * Images load in the background and show once they arrive: await
118
+ * `ready()` before a `capture` that needs them.
119
+ */
120
+ setValues(values) {
121
+ this.values = values;
122
+ this.syncTextures();
123
+ if (!this.playing) this.render();
124
+ }
125
+ /**
126
+ * Settles once the images the current controls show have loaded. Rejects
127
+ * with an {@link ImageLoadError} when one cannot be (it shows black).
128
+ */
129
+ async ready() {
130
+ await this.textures?.ready(imageUrls(this.controls, this.values));
131
+ }
132
+ play() {
133
+ this.playing = true;
134
+ this.emit();
135
+ }
136
+ pause() {
137
+ this.playing = false;
138
+ this.emit();
139
+ }
140
+ /** Back to `iTime` 0 and frame 0. */
141
+ restart() {
142
+ this.time = 0;
143
+ this.frame = 0;
144
+ this.render();
145
+ this.emit();
146
+ }
147
+ /** Jumps to `time` seconds (not below 0) and draws it. */
148
+ seek(time) {
149
+ this.time = Math.max(0, time);
150
+ this.render();
151
+ this.emit();
152
+ }
153
+ setPixelRatio(ratio) {
154
+ this.pixelRatio = ratio;
155
+ this.resize();
156
+ }
157
+ /**
158
+ * Captures the current frame (optionally at another time) as a data URL
159
+ * (JPEG unless `type` says PNG), at most `maxEdge` pixels on the long side
160
+ * (default 1024). Playback time is left as it was. Null when nothing has
161
+ * compiled yet.
162
+ */
163
+ capture(options = {}) {
164
+ if (!this.gl || !this.shader) return null;
165
+ const saved = this.time;
166
+ if (options.time !== void 0) this.time = Math.max(0, options.time);
167
+ this.render();
168
+ const { width, height } = this.canvas;
169
+ const scale = Math.min(1, (options.maxEdge ?? 1024) / Math.max(width, height));
170
+ const out = document.createElement("canvas");
171
+ out.width = Math.max(1, Math.round(width * scale));
172
+ out.height = Math.max(1, Math.round(height * scale));
173
+ out.getContext("2d").drawImage(this.canvas, 0, 0, out.width, out.height);
174
+ this.time = saved;
175
+ if (options.time !== void 0) this.render();
176
+ return out.toDataURL(options.type ?? "image/jpeg", 0.85);
177
+ }
178
+ /** Stops the loop, releases the context and removes the canvas. The renderer cannot be used again. */
179
+ dispose() {
180
+ this.disposed = true;
181
+ cancelAnimationFrame(this.raf);
182
+ this.resizeObserver.disconnect();
183
+ this.canvas.removeEventListener("webglcontextlost", this.onLost);
184
+ this.canvas.removeEventListener("webglcontextrestored", this.onRestored);
185
+ this.canvas.removeEventListener("pointerdown", this.onPointerDown);
186
+ this.canvas.removeEventListener("pointermove", this.onPointerMove);
187
+ window.removeEventListener("pointerup", this.onPointerUp);
188
+ if (this.gl && this.shader) this.gl.deleteProgram(this.shader.program);
189
+ this.textures?.dispose();
190
+ this.textures = null;
191
+ this.gl?.getExtension("WEBGL_lose_context")?.loseContext();
192
+ this.gl = null;
193
+ this.canvas.remove();
194
+ }
195
+ resize() {
196
+ const w = Math.max(1, Math.round(this.canvas.clientWidth * this.pixelRatio));
197
+ const h = Math.max(1, Math.round(this.canvas.clientHeight * this.pixelRatio));
198
+ if (this.canvas.width !== w || this.canvas.height !== h) {
199
+ this.canvas.width = w;
200
+ this.canvas.height = h;
201
+ this.render();
202
+ }
203
+ }
204
+ render(delta = 0) {
205
+ if (!this.gl || !this.shader || this.gl.isContextLost()) return;
206
+ drawFrame(
207
+ this.gl,
208
+ this.shader,
209
+ this.controls,
210
+ this.values,
211
+ {
212
+ width: this.canvas.width,
213
+ height: this.canvas.height,
214
+ time: this.time,
215
+ delta,
216
+ frame: this.frame,
217
+ mouse: this.mouse
218
+ },
219
+ this.textures ?? void 0
220
+ );
221
+ }
222
+ textureCache(gl) {
223
+ return new TextureCache(gl, (_url, error) => {
224
+ if (error) this.onImageError?.(error);
225
+ else if (!this.playing) this.render();
226
+ });
227
+ }
228
+ /** Starts loading the images now shown, and frees the ones no longer shown. */
229
+ syncTextures() {
230
+ if (!this.textures) return;
231
+ const urls = imageUrls(this.controls, this.values);
232
+ this.textures.retain(urls);
233
+ for (const url of urls) this.textures.request(url);
234
+ }
235
+ tick = (now) => {
236
+ if (this.disposed) return;
237
+ this.raf = requestAnimationFrame(this.tick);
238
+ const delta = this.lastNow ? Math.min((now - this.lastNow) / 1e3, 0.1) : 0;
239
+ this.lastNow = now;
240
+ if (this.playing) {
241
+ this.time += delta * this.speed;
242
+ this.frame += 1;
243
+ this.render(delta * this.speed);
244
+ this.fpsFrames += 1;
245
+ }
246
+ if (now - this.fpsSince >= 500) {
247
+ this.fps = this.playing ? Math.round(this.fpsFrames * 1e3 / (now - this.fpsSince || 1)) : 0;
248
+ this.fpsFrames = 0;
249
+ this.fpsSince = now;
250
+ this.emit();
251
+ }
252
+ };
253
+ emit() {
254
+ this.onStats?.({
255
+ time: this.time,
256
+ fps: this.fps,
257
+ playing: this.playing,
258
+ width: this.canvas.width,
259
+ height: this.canvas.height
260
+ });
261
+ }
262
+ /** Pointer position in shader pixels (origin bottom-left, like gl_FragCoord). */
263
+ toPixels(e) {
264
+ const rect = this.canvas.getBoundingClientRect();
265
+ return [
266
+ (e.clientX - rect.left) * (this.canvas.width / rect.width),
267
+ (rect.bottom - e.clientY) * (this.canvas.height / rect.height)
268
+ ];
269
+ }
270
+ onPointerDown = (e) => {
271
+ const [x, y] = this.toPixels(e);
272
+ this.mouse = [x, y, x, y];
273
+ if (!this.playing) this.render();
274
+ };
275
+ onPointerMove = (e) => {
276
+ if (this.mouse[2] <= 0 || e.buttons === 0) return;
277
+ const [x, y] = this.toPixels(e);
278
+ this.mouse = [x, y, this.mouse[2], this.mouse[3]];
279
+ if (!this.playing) this.render();
280
+ };
281
+ onPointerUp = () => {
282
+ if (this.mouse[2] > 0)
283
+ this.mouse = [this.mouse[0], this.mouse[1], -this.mouse[2], -Math.abs(this.mouse[3])];
284
+ };
285
+ onLost = (e) => {
286
+ e.preventDefault();
287
+ this.shader = null;
288
+ this.textures?.dispose();
289
+ this.textures = null;
290
+ this.onContextLost?.(true);
291
+ };
292
+ onRestored = () => {
293
+ resetGlCaches();
294
+ this.gl = createContext(this.canvas, true);
295
+ this.textures = this.gl && this.textureCache(this.gl);
296
+ this.setSource(this.source);
297
+ this.onContextLost?.(false);
298
+ };
299
+ };
300
+
301
+ // src/shader/thumbnails.ts
302
+ var ThumbnailPool = class {
303
+ renderWidth;
304
+ renderHeight;
305
+ interval;
306
+ fps;
307
+ stillTime;
308
+ reducedMotion;
309
+ entries = /* @__PURE__ */ new Set();
310
+ byCanvas = /* @__PURE__ */ new WeakMap();
311
+ observer;
312
+ glCanvas = null;
313
+ gl = null;
314
+ textures = null;
315
+ raf = 0;
316
+ last = 0;
317
+ start = null;
318
+ constructor(options = {}) {
319
+ this.renderWidth = options.renderWidth ?? 480;
320
+ this.renderHeight = options.renderHeight ?? 270;
321
+ this.fps = options.fps ?? 30;
322
+ this.interval = 1e3 / this.fps;
323
+ this.stillTime = options.stillTime ?? 2;
324
+ this.reducedMotion = options.reducedMotion ?? (typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches);
325
+ }
326
+ /**
327
+ * Starts drawing `source` into `canvas`, which should be sized with CSS; its
328
+ * drawing buffer follows its CSS size and the device pixel ratio (up to 2).
329
+ * `onStatus` hears whether the source compiled (and `false` without WebGL2).
330
+ */
331
+ register(canvas, source, values, onStatus) {
332
+ const observer = this.intersection();
333
+ const entry = {
334
+ canvas,
335
+ ctx: canvas.getContext("2d"),
336
+ source,
337
+ values,
338
+ controls: [],
339
+ shader: null,
340
+ failed: false,
341
+ dirty: true,
342
+ visible: observer === null,
343
+ hovered: false,
344
+ onStatus
345
+ };
346
+ this.entries.add(entry);
347
+ this.byCanvas.set(canvas, entry);
348
+ observer?.observe(canvas);
349
+ if (!this.context()) onStatus?.(false);
350
+ this.schedule();
351
+ return {
352
+ update: (nextSource, nextValues) => {
353
+ if (nextSource !== entry.source) {
354
+ this.release(entry);
355
+ entry.source = nextSource;
356
+ entry.failed = false;
357
+ }
358
+ entry.values = nextValues;
359
+ entry.dirty = true;
360
+ this.retainTextures();
361
+ this.schedule();
362
+ },
363
+ setHovered: (hovered) => {
364
+ entry.hovered = hovered;
365
+ entry.dirty = true;
366
+ this.schedule();
367
+ },
368
+ dispose: () => {
369
+ observer?.unobserve(canvas);
370
+ this.entries.delete(entry);
371
+ this.byCanvas.delete(canvas);
372
+ this.release(entry);
373
+ this.retainTextures();
374
+ }
375
+ };
376
+ }
377
+ /** The number of registered thumbnails. */
378
+ get size() {
379
+ return this.entries.size;
380
+ }
381
+ intersection() {
382
+ if (this.observer !== void 0) return this.observer;
383
+ this.observer = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver(
384
+ (records) => {
385
+ for (const r of records) {
386
+ const entry = this.byCanvas.get(r.target);
387
+ if (!entry) continue;
388
+ entry.visible = r.isIntersecting;
389
+ if (entry.visible) entry.dirty = true;
390
+ }
391
+ this.schedule();
392
+ },
393
+ { rootMargin: "100px" }
394
+ );
395
+ return this.observer;
396
+ }
397
+ context() {
398
+ if (this.gl && !this.gl.isContextLost()) return this.gl;
399
+ const canvas = document.createElement("canvas");
400
+ canvas.width = this.renderWidth;
401
+ canvas.height = this.renderHeight;
402
+ canvas.addEventListener("webglcontextlost", (e) => {
403
+ e.preventDefault();
404
+ for (const entry of this.entries) entry.shader = null;
405
+ this.textures?.dispose();
406
+ });
407
+ canvas.addEventListener("webglcontextrestored", () => {
408
+ resetGlCaches();
409
+ if (this.gl) this.textures = this.textureCache(this.gl);
410
+ for (const entry of this.entries) entry.dirty = true;
411
+ this.schedule();
412
+ });
413
+ resetGlCaches();
414
+ this.glCanvas = canvas;
415
+ this.gl = createContext(canvas);
416
+ this.textures = this.gl && this.textureCache(this.gl);
417
+ return this.gl;
418
+ }
419
+ textureCache(gl) {
420
+ return new TextureCache(gl, (url) => {
421
+ for (const entry of this.entries) {
422
+ if (imageUrls(entry.controls, entry.values).includes(url)) entry.dirty = true;
423
+ }
424
+ this.schedule();
425
+ });
426
+ }
427
+ /** Frees the textures no thumbnail shows any more. */
428
+ retainTextures() {
429
+ this.textures?.retain(
430
+ [...this.entries].flatMap((entry) => imageUrls(entry.controls, entry.values))
431
+ );
432
+ }
433
+ release(entry) {
434
+ if (entry.shader) this.gl?.deleteProgram(entry.shader.program);
435
+ entry.shader = null;
436
+ }
437
+ compile(entry, gl) {
438
+ entry.controls = parseControls(entry.source);
439
+ const result = compileShader(gl, entry.source, entry.controls);
440
+ entry.shader = result.ok ? result.shader : null;
441
+ entry.failed = !result.ok;
442
+ entry.onStatus?.(result.ok);
443
+ }
444
+ renderEntry(entry, gl, time) {
445
+ if (!entry.shader && !entry.failed) this.compile(entry, gl);
446
+ if (!entry.shader) return;
447
+ sizeCanvas(entry.canvas);
448
+ const aspect = entry.canvas.width / entry.canvas.height;
449
+ const w = Math.min(this.renderWidth, Math.round(this.renderHeight * aspect));
450
+ const h = Math.min(this.renderHeight, Math.round(w / aspect));
451
+ drawFrame(
452
+ gl,
453
+ entry.shader,
454
+ entry.controls,
455
+ entry.values,
456
+ {
457
+ width: w,
458
+ height: h,
459
+ time,
460
+ delta: this.interval / 1e3,
461
+ frame: Math.floor(time * this.fps),
462
+ mouse: [0, 0, 0, 0]
463
+ },
464
+ this.textures ?? void 0
465
+ );
466
+ entry.ctx.drawImage(
467
+ this.glCanvas,
468
+ 0,
469
+ this.renderHeight - h,
470
+ w,
471
+ h,
472
+ 0,
473
+ 0,
474
+ entry.canvas.width,
475
+ entry.canvas.height
476
+ );
477
+ entry.dirty = false;
478
+ }
479
+ schedule() {
480
+ if (!this.raf && this.entries.size) this.raf = requestAnimationFrame(this.tick);
481
+ }
482
+ tick = (now) => {
483
+ this.raf = 0;
484
+ const gl = this.context();
485
+ if (!gl) return;
486
+ this.start ??= now;
487
+ const animate = now - this.last >= this.interval;
488
+ const time = (now - this.start) / 1e3;
489
+ let keepGoing = false;
490
+ for (const entry of this.entries) {
491
+ if (!entry.visible) continue;
492
+ const moving = !this.reducedMotion || entry.hovered;
493
+ if (entry.dirty || moving && animate) {
494
+ this.renderEntry(entry, gl, moving ? time : this.stillTime);
495
+ }
496
+ if (moving && entry.shader) keepGoing = true;
497
+ }
498
+ if (animate) this.last = now;
499
+ if (keepGoing) this.schedule();
500
+ };
501
+ };
502
+ function sizeCanvas(canvas) {
503
+ const ratio = Math.min(window.devicePixelRatio || 1, 2);
504
+ const w = Math.max(1, Math.round(canvas.clientWidth * ratio));
505
+ const h = Math.max(1, Math.round(canvas.clientHeight * ratio));
506
+ if (canvas.width !== w || canvas.height !== h) {
507
+ canvas.width = w;
508
+ canvas.height = h;
509
+ }
510
+ }
511
+ var shared = null;
512
+ function sharedThumbnailPool() {
513
+ shared ??= new ThumbnailPool();
514
+ return shared;
515
+ }
516
+ function registerThumbnail(canvas, source, values, onStatus) {
517
+ return sharedThumbnailPool().register(canvas, source, values, onStatus);
518
+ }
519
+
520
+ // src/shader/frames.ts
521
+ var ShaderFrames = class {
522
+ canvas;
523
+ width;
524
+ height;
525
+ controls;
526
+ gl;
527
+ shader;
528
+ textures;
529
+ values;
530
+ rendered = 0;
531
+ lastTime = null;
532
+ pixels;
533
+ constructor(options) {
534
+ const { source, values, width, height } = options;
535
+ this.width = Math.max(1, Math.round(width));
536
+ this.height = Math.max(1, Math.round(height));
537
+ this.canvas = options.canvas ?? document.createElement("canvas");
538
+ this.canvas.width = this.width;
539
+ this.canvas.height = this.height;
540
+ this.values = values;
541
+ const gl = createContext(this.canvas, true);
542
+ if (!gl) throw new Error("WebGL2 is not available in this browser.");
543
+ this.gl = gl;
544
+ this.controls = parseControls(source);
545
+ const compiled = compileShader(gl, source, this.controls);
546
+ if (!compiled.ok) {
547
+ this.dispose();
548
+ throw new ShaderCompileError(compiled.errors);
549
+ }
550
+ this.shader = compiled.shader;
551
+ this.textures = new TextureCache(gl);
552
+ this.syncTextures();
553
+ this.pixels = new Uint8Array(this.width * this.height * 4);
554
+ }
555
+ /** Changes control values for the frames that follow. */
556
+ setValues(values) {
557
+ this.values = values;
558
+ this.syncTextures();
559
+ }
560
+ /**
561
+ * Settles once every image the controls show has loaded. Rejects with an
562
+ * {@link ImageLoadError} when one cannot be.
563
+ */
564
+ async ready() {
565
+ await this.textures.ready(imageUrls(this.controls, this.values));
566
+ }
567
+ /** Draws the frame at `time` seconds onto `canvas`. */
568
+ render(time, options = {}) {
569
+ const gl = this.context();
570
+ const delta = options.delta ?? (this.lastTime === null ? 0 : time - this.lastTime);
571
+ drawFrame(
572
+ gl,
573
+ this.shader,
574
+ this.controls,
575
+ this.values,
576
+ {
577
+ width: this.width,
578
+ height: this.height,
579
+ time,
580
+ delta,
581
+ frame: options.frame ?? this.rendered,
582
+ mouse: options.mouse ?? [0, 0, 0, 0]
583
+ },
584
+ this.textures
585
+ );
586
+ this.rendered += 1;
587
+ this.lastTime = time;
588
+ }
589
+ /**
590
+ * The last rendered frame as RGBA bytes, rows top down (as in `ImageData`;
591
+ * WebGL itself reads bottom up). Pass `out` to reuse a buffer of
592
+ * `width * height * 4` bytes.
593
+ */
594
+ readPixels(out) {
595
+ const gl = this.context();
596
+ const { width, height } = this;
597
+ const target = out ?? new Uint8Array(width * height * 4);
598
+ if (target.length !== width * height * 4) {
599
+ throw new RangeError(`readPixels needs a buffer of ${width * height * 4} bytes.`);
600
+ }
601
+ gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, this.pixels);
602
+ const row = width * 4;
603
+ for (let y = 0; y < height; y++) {
604
+ target.set(this.pixels.subarray((height - 1 - y) * row, (height - y) * row), y * row);
605
+ }
606
+ return target;
607
+ }
608
+ /** Releases the GL context now rather than at garbage collection (browsers cap live contexts). */
609
+ dispose() {
610
+ if (!this.gl) return;
611
+ if (this.shader) this.gl.deleteProgram(this.shader.program);
612
+ this.textures?.dispose();
613
+ this.gl.getExtension("WEBGL_lose_context")?.loseContext();
614
+ this.gl = null;
615
+ }
616
+ syncTextures() {
617
+ const urls = imageUrls(this.controls, this.values);
618
+ this.textures.retain(urls);
619
+ for (const url of urls) this.textures.request(url);
620
+ }
621
+ context() {
622
+ if (!this.gl) throw new Error("These frames have been disposed.");
623
+ if (this.gl.isContextLost()) throw new Error("The graphics context was lost.");
624
+ return this.gl;
625
+ }
626
+ };
627
+
628
+ // src/shader/gif.ts
629
+ var yieldToBrowser = () => new Promise((resolve) => setTimeout(resolve, 0));
630
+ async function recordGif(options) {
631
+ const { source, values, width, height, fps, duration, start = 0, onProgress, signal } = options;
632
+ const { GIFEncoder, quantize, applyPalette } = await import("gifenc");
633
+ const frames = new ShaderFrames({ source, values, width, height });
634
+ try {
635
+ await frames.ready();
636
+ const total = Math.max(1, Math.round(duration * fps));
637
+ const delay = 1e3 / fps;
638
+ const rgba = new Uint8Array(frames.width * frames.height * 4);
639
+ const gif = GIFEncoder();
640
+ for (let i = 0; i < total; i++) {
641
+ if (signal?.aborted) throw new DOMException("Cancelled", "AbortError");
642
+ frames.render(start + i / fps, { frame: i, delta: 1 / fps });
643
+ frames.readPixels(rgba);
644
+ const palette = quantize(rgba, 256);
645
+ gif.writeFrame(applyPalette(rgba, palette), frames.width, frames.height, {
646
+ palette,
647
+ delay,
648
+ ...i === 0 ? { repeat: 0 } : {}
649
+ });
650
+ onProgress?.(i + 1, total);
651
+ if (i % 2 === 1) await yieldToBrowser();
652
+ }
653
+ gif.finish();
654
+ return new Blob([gif.bytes()], { type: "image/gif" });
655
+ } finally {
656
+ frames.dispose();
657
+ }
658
+ }
659
+
660
+ // src/shader/standalone.ts
661
+ var escapeHtml = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
662
+ var scriptJson = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
663
+ function standaloneHtml(title, source, values, options = {}) {
664
+ const controls = parseControls(source).map((c) => ({
665
+ name: c.name,
666
+ type: c.type,
667
+ value: controlValue(c, values),
668
+ ...c.texture ? { texture: c.texture } : {}
669
+ }));
670
+ const credit = (options.credit ?? "Exported with Flair.").replace(/\n/g, " ");
671
+ return `<!doctype html>
672
+ <html lang="en">
673
+ <head>
674
+ <meta charset="utf-8">
675
+ <meta name="viewport" content="width=device-width, initial-scale=1">
676
+ <title>${escapeHtml(title)}</title>
677
+ <style>html,body{margin:0;height:100%;background:#000}canvas{display:block;width:100%;height:100%}</style>
678
+ </head>
679
+ <body>
680
+ <canvas id="c"></canvas>
681
+ <script>
682
+ // ${credit.replace(/</g, "\\u003c")} Shadertoy-style fragment shader on WebGL2.
683
+ const VERTEX = ${scriptJson(VERTEX_SOURCE)};
684
+ const FRAGMENT = ${scriptJson(wrapSource(source))};
685
+ const CONTROLS = ${scriptJson(controls)};
686
+ const canvas = document.getElementById("c");
687
+ const gl = canvas.getContext("webgl2");
688
+ if (!gl) document.body.textContent = "WebGL2 is not available.";
689
+ function compile(type, src) {
690
+ const s = gl.createShader(type);
691
+ gl.shaderSource(s, src);
692
+ gl.compileShader(s);
693
+ if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
694
+ return s;
695
+ }
696
+ const program = gl.createProgram();
697
+ gl.attachShader(program, compile(gl.VERTEX_SHADER, VERTEX));
698
+ gl.attachShader(program, compile(gl.FRAGMENT_SHADER, FRAGMENT));
699
+ gl.linkProgram(program);
700
+ gl.useProgram(program);
701
+ const u = (n) => gl.getUniformLocation(program, n);
702
+ let unit = 0;
703
+ // An SVG decodes at its own, often icon, size: give it ${SVG_EDGE} px on its long edge (and a
704
+ // viewBox, so the drawing scales) to keep it sharp.
705
+ async function sizedSvg(url) {
706
+ const doc = new DOMParser().parseFromString(await (await fetch(url)).text(), "image/svg+xml");
707
+ const svg = doc.documentElement;
708
+ const len = (n) => { const m = (svg.getAttribute(n) || "").trim().match(/^([\\d.]+)(px)?$/); return m && +m[1] > 0 ? +m[1] : 0; };
709
+ const box = (svg.getAttribute("viewBox") || "").trim().split(/[\\s,]+/).map(Number);
710
+ const hasBox = box.length === 4 && box[2] > 0 && box[3] > 0;
711
+ const w = len("width") || (hasBox ? box[2] : 300), h = len("height") || (hasBox ? box[3] : 150);
712
+ const scale = ${SVG_EDGE} / Math.max(w, h);
713
+ if (!hasBox) svg.setAttribute("viewBox", "0 0 " + w + " " + h);
714
+ svg.setAttribute("width", Math.max(1, Math.round(w * scale)));
715
+ svg.setAttribute("height", Math.max(1, Math.round(h * scale)));
716
+ return URL.createObjectURL(new Blob([new XMLSerializer().serializeToString(svg)], { type: "image/svg+xml" }));
717
+ }
718
+ function image(loc, url, texture) {
719
+ const i = unit++, t = gl.createTexture();
720
+ const bind = () => { gl.activeTexture(gl.TEXTURE0 + i); gl.bindTexture(gl.TEXTURE_2D, t); };
721
+ const filter = { mipmap: gl.LINEAR_MIPMAP_LINEAR, linear: gl.LINEAR, nearest: gl.NEAREST }[texture.filter];
722
+ const wrap = { repeat: gl.REPEAT, clamp: gl.CLAMP_TO_EDGE, mirror: gl.MIRRORED_REPEAT }[texture.wrap];
723
+ bind();
724
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 255]));
725
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
726
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, texture.filter === "nearest" ? gl.NEAREST : gl.LINEAR);
727
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
728
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
729
+ gl.uniform1i(loc, i);
730
+ if (!url) return;
731
+ const img = new Image();
732
+ if (!/^(data|blob):/i.test(url)) img.crossOrigin = "anonymous";
733
+ img.onload = () => {
734
+ bind();
735
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
736
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
737
+ gl.generateMipmap(gl.TEXTURE_2D);
738
+ };
739
+ if (/^data:image\\/svg\\+xml[;,]/i.test(url) || /^(?!data:)[^?#]*\\.svg([?#]|$)/i.test(url)) {
740
+ sizedSvg(url).then((src) => { img.src = src; }, () => {});
741
+ } else img.src = url;
742
+ }
743
+ for (const c of CONTROLS) {
744
+ const loc = u(c.name);
745
+ const v = c.value;
746
+ if (c.type === "float") gl.uniform1f(loc, v);
747
+ else if (c.type === "int" || c.type === "bool") gl.uniform1i(loc, Number(v));
748
+ else if (c.type === "sampler2D") image(loc, v, c.texture);
749
+ else gl["uniform" + v.length + "fv"](loc, v);
750
+ }
751
+ let mouse = [0, 0, 0, 0];
752
+ const px = (e) => { const r = canvas.getBoundingClientRect(); return [(e.clientX - r.left) * canvas.width / r.width, (r.bottom - e.clientY) * canvas.height / r.height]; };
753
+ canvas.addEventListener("pointerdown", (e) => { const [x, y] = px(e); mouse = [x, y, x, y]; });
754
+ canvas.addEventListener("pointermove", (e) => { if (e.buttons && mouse[2] > 0) { const [x, y] = px(e); mouse = [x, y, mouse[2], mouse[3]]; } });
755
+ addEventListener("pointerup", () => { if (mouse[2] > 0) mouse = [mouse[0], mouse[1], -mouse[2], -Math.abs(mouse[3])]; });
756
+ let start = performance.now(), last = start, frame = 0;
757
+ function draw(now) {
758
+ const dpr = Math.min(devicePixelRatio || 1, 2);
759
+ const w = Math.round(canvas.clientWidth * dpr), h = Math.round(canvas.clientHeight * dpr);
760
+ if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
761
+ gl.viewport(0, 0, w, h);
762
+ gl.uniform3f(u("iResolution"), w, h, 1);
763
+ gl.uniform1f(u("iTime"), (now - start) / 1000);
764
+ gl.uniform1f(u("iTimeDelta"), (now - last) / 1000);
765
+ gl.uniform1i(u("iFrame"), frame++);
766
+ gl.uniform4f(u("iMouse"), mouse[0], mouse[1], mouse[2], mouse[3]);
767
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
768
+ last = now;
769
+ requestAnimationFrame(draw);
770
+ }
771
+ requestAnimationFrame(draw);
772
+ </script>
773
+ </body>
774
+ </html>
775
+ `;
776
+ }
777
+
778
+ // src/shader/blend.ts
779
+ var ALL_BLEND_MODES = [
780
+ "normal",
781
+ "dissolve",
782
+ "dancingDissolve",
783
+ "darken",
784
+ "multiply",
785
+ "colorBurn",
786
+ "classicColorBurn",
787
+ "linearBurn",
788
+ "darkerColor",
789
+ "add",
790
+ "lighten",
791
+ "screen",
792
+ "colorDodge",
793
+ "classicColorDodge",
794
+ "linearDodge",
795
+ "lighterColor",
796
+ "overlay",
797
+ "softLight",
798
+ "hardLight",
799
+ "linearLight",
800
+ "vividLight",
801
+ "pinLight",
802
+ "hardMix",
803
+ "difference",
804
+ "classicDifference",
805
+ "exclusion",
806
+ "subtract",
807
+ "divide",
808
+ "hue",
809
+ "saturation",
810
+ "color",
811
+ "luminosity",
812
+ "stencilAlpha",
813
+ "stencilLuma",
814
+ "silhouetteAlpha",
815
+ "silhouetteLuma",
816
+ "alphaAdd",
817
+ "luminescentPremul"
818
+ ];
819
+ function lum(r, g, b) {
820
+ return 0.3 * r + 0.59 * g + 0.11 * b;
821
+ }
822
+ function sat(r, g, b) {
823
+ return Math.max(r, g, b) - Math.min(r, g, b);
824
+ }
825
+ function clipColor(r, g, b) {
826
+ const l = lum(r, g, b);
827
+ const n = Math.min(r, g, b);
828
+ const x = Math.max(r, g, b);
829
+ let cr = r;
830
+ let cg = g;
831
+ let cb = b;
832
+ if (n < 0) {
833
+ const denom = Math.max(1e-9, l - n);
834
+ cr = l + (cr - l) * l / denom;
835
+ cg = l + (cg - l) * l / denom;
836
+ cb = l + (cb - l) * l / denom;
837
+ }
838
+ if (x > 1) {
839
+ const denom = Math.max(1e-9, x - l);
840
+ cr = l + (cr - l) * (1 - l) / denom;
841
+ cg = l + (cg - l) * (1 - l) / denom;
842
+ cb = l + (cb - l) * (1 - l) / denom;
843
+ }
844
+ return [cr, cg, cb];
845
+ }
846
+ function setLum(r, g, b, l) {
847
+ const d = l - lum(r, g, b);
848
+ return clipColor(r + d, g + d, b + d);
849
+ }
850
+ function setSat(r, g, b, s) {
851
+ const mx = Math.max(r, g, b);
852
+ const mn = Math.min(r, g, b);
853
+ if (mx - mn <= 1e-9) return [0, 0, 0];
854
+ const f = (v) => (v - mn) * s / (mx - mn);
855
+ return [f(r), f(g), f(b)];
856
+ }
857
+ function burn(cb, cs) {
858
+ if (cb >= 1) return 1;
859
+ if (cs <= 0) return 0;
860
+ return 1 - Math.min(1, (1 - cb) / cs);
861
+ }
862
+ function dodge(cb, cs) {
863
+ if (cb <= 0) return 0;
864
+ if (cs >= 1) return 1;
865
+ return Math.min(1, cb / (1 - cs));
866
+ }
867
+ function hardLight(cb, cs) {
868
+ if (cs <= 0.5) {
869
+ return cb * 2 * cs;
870
+ }
871
+ const s = 2 * cs - 1;
872
+ return cb + s - cb * s;
873
+ }
874
+ function softLight(cb, cs) {
875
+ if (cs <= 0.5) {
876
+ return cb - (1 - 2 * cs) * cb * (1 - cb);
877
+ }
878
+ const d = cb <= 0.25 ? ((16 * cb - 12) * cb + 4) * cb : Math.sqrt(Math.max(0, cb));
879
+ return cb + (2 * cs - 1) * (d - cb);
880
+ }
881
+ function vividLight(cb, cs) {
882
+ return cs <= 0.5 ? burn(cb, 2 * cs) : dodge(cb, 2 * (cs - 0.5));
883
+ }
884
+ function separableBlend(mode, cb, cs) {
885
+ switch (mode) {
886
+ case "darken":
887
+ return Math.min(cb, cs);
888
+ case "multiply":
889
+ return cb * cs;
890
+ case "colorBurn":
891
+ return burn(cb, cs);
892
+ case "classicColorBurn":
893
+ return cs <= 0 ? 0 : 1 - (1 - cb) / cs;
894
+ case "linearBurn":
895
+ return Math.max(0, cb + cs - 1);
896
+ case "add":
897
+ case "linearDodge":
898
+ return cb + cs;
899
+ case "lighten":
900
+ return Math.max(cb, cs);
901
+ case "screen":
902
+ return cb + cs - cb * cs;
903
+ case "colorDodge":
904
+ return dodge(cb, cs);
905
+ case "classicColorDodge":
906
+ return cs >= 1 ? 1 : Math.min(1, cb / (1 - cs));
907
+ case "overlay":
908
+ return hardLight(cs, cb);
909
+ case "softLight":
910
+ return softLight(cb, cs);
911
+ case "hardLight":
912
+ return hardLight(cb, cs);
913
+ case "linearLight":
914
+ return Math.min(1, Math.max(0, cb + 2 * cs - 1));
915
+ case "vividLight":
916
+ return vividLight(cb, cs);
917
+ case "pinLight":
918
+ return cs <= 0.5 ? Math.min(cb, 2 * cs) : Math.max(cb, 2 * cs - 1);
919
+ case "hardMix":
920
+ return cb + cs >= 1 ? 1 : 0;
921
+ case "difference":
922
+ case "classicDifference":
923
+ return Math.abs(cb - cs);
924
+ case "exclusion":
925
+ return cb + cs - 2 * cb * cs;
926
+ case "subtract":
927
+ return Math.max(0, cb - cs);
928
+ case "divide":
929
+ return cs <= 0 ? cb > 0 ? 1 : 0 : cb / cs;
930
+ default:
931
+ return cs;
932
+ }
933
+ }
934
+ function blendRgba(mode, dst, src, noise = 0) {
935
+ const sa = src[3];
936
+ const da = dst[3];
937
+ switch (mode) {
938
+ case "normal": {
939
+ const k = 1 - sa;
940
+ return [src[0] + dst[0] * k, src[1] + dst[1] * k, src[2] + dst[2] * k, sa + da * k];
941
+ }
942
+ case "dissolve":
943
+ case "dancingDissolve": {
944
+ if (sa <= 0 || noise >= sa) return [...dst];
945
+ const inv = 1 / sa;
946
+ return [src[0] * inv, src[1] * inv, src[2] * inv, 1];
947
+ }
948
+ case "stencilAlpha":
949
+ return [dst[0] * sa, dst[1] * sa, dst[2] * sa, dst[3] * sa];
950
+ case "silhouetteAlpha": {
951
+ const k = 1 - sa;
952
+ return [dst[0] * k, dst[1] * k, dst[2] * k, dst[3] * k];
953
+ }
954
+ case "stencilLuma":
955
+ case "silhouetteLuma": {
956
+ const l = lum(src[0], src[1], src[2]);
957
+ const k = mode === "stencilLuma" ? l : 1 - l;
958
+ return [dst[0] * k, dst[1] * k, dst[2] * k, dst[3] * k];
959
+ }
960
+ case "alphaAdd": {
961
+ const k = 1 - sa;
962
+ return [src[0] + dst[0] * k, src[1] + dst[1] * k, src[2] + dst[2] * k, Math.min(1, sa + da)];
963
+ }
964
+ case "luminescentPremul": {
965
+ const k = 1 - sa;
966
+ return [src[0] + dst[0] * k, src[1] + dst[1] * k, src[2] + dst[2] * k, sa + da - sa * da];
967
+ }
968
+ }
969
+ if (sa <= 0) return [...dst];
970
+ if (da <= 0) return [...src];
971
+ const cs = [src[0] / sa, src[1] / sa, src[2] / sa];
972
+ const cb = [dst[0] / da, dst[1] / da, dst[2] / da];
973
+ let b;
974
+ switch (mode) {
975
+ case "hue":
976
+ b = setLum(...setSat(cs[0], cs[1], cs[2], sat(...cb)), lum(...cb));
977
+ break;
978
+ case "saturation":
979
+ b = setLum(...setSat(cb[0], cb[1], cb[2], sat(...cs)), lum(...cb));
980
+ break;
981
+ case "color":
982
+ b = setLum(cs[0], cs[1], cs[2], lum(...cb));
983
+ break;
984
+ case "luminosity":
985
+ b = setLum(cb[0], cb[1], cb[2], lum(...cs));
986
+ break;
987
+ case "darkerColor":
988
+ b = lum(...cs) < lum(...cb) ? cs : cb;
989
+ break;
990
+ case "lighterColor":
991
+ b = lum(...cs) > lum(...cb) ? cs : cb;
992
+ break;
993
+ default:
994
+ b = [
995
+ separableBlend(mode, cb[0], cs[0]),
996
+ separableBlend(mode, cb[1], cs[1]),
997
+ separableBlend(mode, cb[2], cs[2])
998
+ ];
999
+ break;
1000
+ }
1001
+ const ao = sa + da - sa * da;
1002
+ return [
1003
+ (1 - da) * src[0] + (1 - sa) * dst[0] + sa * da * b[0],
1004
+ (1 - da) * src[1] + (1 - sa) * dst[1] + sa * da * b[1],
1005
+ (1 - da) * src[2] + (1 - sa) * dst[2] + sa * da * b[2],
1006
+ ao
1007
+ ];
1008
+ }
1009
+ function getGlslBlendFunction(mode) {
1010
+ switch (mode) {
1011
+ case "multiply":
1012
+ return `vec4 blend(vec4 dst, vec4 src) {
1013
+ return vec4(src.rgb * dst.rgb * src.a + dst.rgb * (1.0 - src.a), src.a + dst.a * (1.0 - src.a));
1014
+ }`;
1015
+ case "screen":
1016
+ return `vec4 blend(vec4 dst, vec4 src) {
1017
+ vec3 res = src.rgb + dst.rgb - src.rgb * dst.rgb;
1018
+ return vec4(mix(dst.rgb, res, src.a), src.a + dst.a * (1.0 - src.a));
1019
+ }`;
1020
+ case "add":
1021
+ case "linearDodge":
1022
+ return `vec4 blend(vec4 dst, vec4 src) {
1023
+ return vec4(min(vec3(1.0), dst.rgb + src.rgb * src.a), min(1.0, dst.a + src.a));
1024
+ }`;
1025
+ case "overlay":
1026
+ return `vec4 blend(vec4 dst, vec4 src) {
1027
+ vec3 res = mix(2.0 * dst.rgb * src.rgb, 1.0 - 2.0 * (1.0 - dst.rgb) * (1.0 - src.rgb), step(vec3(0.5), dst.rgb));
1028
+ return vec4(mix(dst.rgb, res, src.a), src.a + dst.a * (1.0 - src.a));
1029
+ }`;
1030
+ case "difference":
1031
+ return `vec4 blend(vec4 dst, vec4 src) {
1032
+ return vec4(mix(dst.rgb, abs(dst.rgb - src.rgb), src.a), src.a + dst.a * (1.0 - src.a));
1033
+ }`;
1034
+ case "normal":
1035
+ default:
1036
+ return `vec4 blend(vec4 dst, vec4 src) {
1037
+ return src + dst * (1.0 - src.a);
1038
+ }`;
1039
+ }
1040
+ }
1041
+
1042
+ // src/time/noise.ts
1043
+ var GOLDEN_GAMMA = 0x9e3779b97f4a7c15n;
1044
+ var MULT1 = 0xbf58476d1ce4e5b9n;
1045
+ var MULT2 = 0x94d049bb133111ebn;
1046
+ var MASK64 = 0xffffffffffffffffn;
1047
+ var BASE_MIX = 0x2545f4914f6cdd1dn;
1048
+ var POW2_53 = 9007199254740992;
1049
+ function hash64(x) {
1050
+ let v = x + GOLDEN_GAMMA & MASK64;
1051
+ v = (v ^ v >> 30n) * MULT1 & MASK64;
1052
+ v = (v ^ v >> 27n) * MULT2 & MASK64;
1053
+ return (v ^ v >> 31n) & MASK64;
1054
+ }
1055
+ function mixSeeds(parts) {
1056
+ let h = BASE_MIX;
1057
+ for (const p of parts) {
1058
+ const val = typeof p === "bigint" ? p : BigInt(Math.floor(p));
1059
+ h = hash64(h ^ hash64(val & MASK64));
1060
+ }
1061
+ return h;
1062
+ }
1063
+ function unitFloat(seed) {
1064
+ const s = typeof seed === "bigint" ? seed : BigInt(Math.floor(seed));
1065
+ const hashed = hash64(s);
1066
+ const shifted = Number(hashed >> 11n);
1067
+ return shifted / POW2_53;
1068
+ }
1069
+ function quinticFade(t) {
1070
+ return t * t * t * (t * (t * 6 - 15) + 10);
1071
+ }
1072
+ function lattice(seed, i) {
1073
+ const mixed = seed ^ hash64(i & MASK64);
1074
+ return unitFloat(mixed) * 2 - 1;
1075
+ }
1076
+ function noise1d(seed, x) {
1077
+ if (!Number.isFinite(x)) return 0;
1078
+ const s = typeof seed === "bigint" ? seed : BigInt(Math.floor(seed));
1079
+ const floorX = Math.floor(x);
1080
+ const u = quinticFade(x - floorX);
1081
+ const i = BigInt(Math.floor(floorX));
1082
+ const a = lattice(s, i);
1083
+ const b = lattice(s, i + 1n);
1084
+ return a + (b - a) * u;
1085
+ }
1086
+ function noise3d(x, y, z, seed = 0) {
1087
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
1088
+ return 0;
1089
+ }
1090
+ const s = typeof seed === "bigint" ? seed : BigInt(Math.floor(seed));
1091
+ const fx = Math.floor(x);
1092
+ const fy = Math.floor(y);
1093
+ const fz = Math.floor(z);
1094
+ const u = quinticFade(x - fx);
1095
+ const v = quinticFade(y - fy);
1096
+ const w = quinticFade(z - fz);
1097
+ const ix = BigInt(fx);
1098
+ const iy = BigInt(fy);
1099
+ const iz = BigInt(fz);
1100
+ const g = (dx, dy, dz) => {
1101
+ const combined = mixSeeds([s, ix + dx, iy + dy, iz + dz]);
1102
+ return unitFloat(combined) * 2 - 1;
1103
+ };
1104
+ const lerp = (a, b, t) => a + (b - a) * t;
1105
+ const x00 = lerp(g(0n, 0n, 0n), g(1n, 0n, 0n), u);
1106
+ const x10 = lerp(g(0n, 1n, 0n), g(1n, 1n, 0n), u);
1107
+ const x01 = lerp(g(0n, 0n, 1n), g(1n, 0n, 1n), u);
1108
+ const x11 = lerp(g(0n, 1n, 1n), g(1n, 1n, 1n), u);
1109
+ const y0 = lerp(x00, x10, v);
1110
+ const y1 = lerp(x01, x11, v);
1111
+ return lerp(y0, y1, w);
1112
+ }
1113
+ function fractalWiggle(freq, amp, octaves = 3, ampMult = 0.5, t = 0, seed = 42, dimension = 0) {
1114
+ const oct = Math.max(1, Math.min(16, Math.floor(octaves)));
1115
+ let currentAmp = amp;
1116
+ let currentFreq = freq;
1117
+ let sum = 0;
1118
+ const baseSeed = BigInt(seed);
1119
+ for (let k = 0; k < oct; k++) {
1120
+ const s = mixSeeds([baseSeed, BigInt(dimension), BigInt(k)]);
1121
+ const phaseOffset = unitFloat(s) * 1e3;
1122
+ sum += currentAmp * noise1d(s, currentFreq * t + phaseOffset);
1123
+ currentAmp *= ampMult;
1124
+ currentFreq *= 2;
1125
+ }
1126
+ return sum;
1127
+ }
1128
+
1129
+ // src/scene/effects.ts
1130
+ var EFFECT_TYPES = [
1131
+ "gradient",
1132
+ "flow",
1133
+ "glow",
1134
+ "spotlight",
1135
+ "shimmer",
1136
+ "ripple",
1137
+ "timer",
1138
+ "counter",
1139
+ "highlight",
1140
+ "blur",
1141
+ "burst",
1142
+ "media"
1143
+ ];
1144
+ var EFFECT_FADE_MS = 300;
1145
+ function effectGone(running, now) {
1146
+ if (running.until !== void 0) {
1147
+ return now >= running.until + EFFECT_FADE_MS;
1148
+ }
1149
+ if (running.effect.duration !== void 0) {
1150
+ return now >= running.from + running.effect.duration + EFFECT_FADE_MS;
1151
+ }
1152
+ return false;
1153
+ }
1154
+ function effectOpacity(running, now) {
1155
+ if (now < running.from) return 0;
1156
+ const inFade = Math.min(1, (now - running.from) / 150);
1157
+ if (running.until !== void 0) {
1158
+ const out = Math.max(0, 1 - (now - running.until) / EFFECT_FADE_MS);
1159
+ return inFade * out;
1160
+ }
1161
+ if (running.effect.duration !== void 0) {
1162
+ const remaining = running.from + running.effect.duration - now;
1163
+ if (remaining < 0) {
1164
+ const out = Math.max(0, 1 + remaining / EFFECT_FADE_MS);
1165
+ return inFade * out;
1166
+ }
1167
+ }
1168
+ return inFade;
1169
+ }
1170
+
1171
+ // src/scene/check.ts
1172
+ var NO_CHECK = { framing: [], overlaps: [], edges: [], underTitle: [] };
1173
+ var SMALL_TEXT_PX = 24;
1174
+ function titleBox(title, stage) {
1175
+ const width = Math.min(stage.width, title.length * 20 + 64);
1176
+ return { x: (stage.width - width) / 2, y: 24, width, height: 60 };
1177
+ }
1178
+ function touchedIds(shot) {
1179
+ const ids = /* @__PURE__ */ new Set();
1180
+ for (const cmd of shot.run ?? []) {
1181
+ if (isElementCommand(cmd)) {
1182
+ ids.add(cmd.element.id);
1183
+ } else if ("id" in cmd && typeof cmd.id === "string") {
1184
+ ids.add(cmd.id);
1185
+ }
1186
+ }
1187
+ return [...ids];
1188
+ }
1189
+ var IN_FRAME = 0.95;
1190
+ function checkShot(shot, elements, view, stage, title) {
1191
+ const frameRect = visibleRect(view, stage);
1192
+ const touched = new Set(touchedIds(shot));
1193
+ const framing = [];
1194
+ const edges = [];
1195
+ const overlaps = [];
1196
+ const activeElements = elements.filter((e) => !e.isDeleted && (e.opacity === void 0 || e.opacity > 0));
1197
+ for (const el of activeElements) {
1198
+ const box = boundsOf(el);
1199
+ const overlap = overlapArea(box, frameRect);
1200
+ const boxArea = areaOf(box);
1201
+ if (touched.has(el.id)) {
1202
+ if (overlap === 0) {
1203
+ framing.push({ id: el.id, how: "outside" });
1204
+ } else if (boxArea > 0 && overlap / boxArea < IN_FRAME) {
1205
+ framing.push({ id: el.id, how: "cut" });
1206
+ }
1207
+ } else if (overlap > 0 && boxArea > 0 && overlap / boxArea < IN_FRAME) {
1208
+ edges.push(el.id);
1209
+ }
1210
+ }
1211
+ const touchedList = activeElements.filter((e) => touched.has(e.id));
1212
+ for (let i = 0; i < touchedList.length; i++) {
1213
+ for (let j = i + 1; j < touchedList.length; j++) {
1214
+ const a = touchedList[i];
1215
+ const b = touchedList[j];
1216
+ const aBox = boundsOf(a);
1217
+ const bBox = boundsOf(b);
1218
+ const ol = overlapArea(aBox, bBox);
1219
+ const minArea = Math.min(areaOf(aBox), areaOf(bBox));
1220
+ if (minArea > 0 && ol / minArea > 0.3) {
1221
+ overlaps.push([a.id, b.id]);
1222
+ }
1223
+ }
1224
+ }
1225
+ const smallTexts = [];
1226
+ let smallestText;
1227
+ for (const el of activeElements) {
1228
+ if (el.text && el.fontSize) {
1229
+ const px = Math.round(el.fontSize * view.zoom);
1230
+ const item = { id: el.id, text: el.text, px };
1231
+ if (!smallestText || px < smallestText.px) {
1232
+ smallestText = item;
1233
+ }
1234
+ if (px < SMALL_TEXT_PX) {
1235
+ smallTexts.push(item);
1236
+ }
1237
+ }
1238
+ }
1239
+ const underTitle = [];
1240
+ if (title) {
1241
+ const tBox = titleBox(title, stage);
1242
+ const sceneTBox = {
1243
+ x: tBox.x / view.zoom - view.scrollX,
1244
+ y: tBox.y / view.zoom - view.scrollY,
1245
+ width: tBox.width / view.zoom,
1246
+ height: tBox.height / view.zoom
1247
+ };
1248
+ for (const el of activeElements) {
1249
+ if (overlapArea(boundsOf(el), sceneTBox) > 0) {
1250
+ underTitle.push(el.id);
1251
+ }
1252
+ }
1253
+ }
1254
+ return {
1255
+ framing,
1256
+ overlaps,
1257
+ smallestText,
1258
+ smallTexts: smallTexts.length ? smallTexts : void 0,
1259
+ edges,
1260
+ underTitle
1261
+ };
1262
+ }
1263
+
1264
+ // src/scene/validate.ts
1265
+ var LONG_CAPTION = 120;
1266
+ var LONG_TITLE = 60;
1267
+ var MAX_EFFECTS_PER_SHOT = 5;
1268
+ function validateScene(scene) {
1269
+ const issues = [];
1270
+ const say = (shot, message) => issues.push({ shot, message });
1271
+ if (!scene.name || typeof scene.name !== "string") {
1272
+ say(0, "Scene needs a string name.");
1273
+ } else if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(scene.name)) {
1274
+ say(0, `Scene name "${scene.name}" should be lowercase words and dashes.`);
1275
+ }
1276
+ if (!Array.isArray(scene.shots) || scene.shots.length === 0) {
1277
+ say(0, "Scene needs at least one shot.");
1278
+ return issues;
1279
+ }
1280
+ const mediaKeys = new Set(Object.keys(scene.media ?? {}));
1281
+ const effectIds = /* @__PURE__ */ new Set();
1282
+ if (scene.media) {
1283
+ for (const [id, m] of Object.entries(scene.media)) {
1284
+ if (!m || typeof m !== "object") {
1285
+ say(0, `Media "${id}" must be an object.`);
1286
+ } else if (!m.type) {
1287
+ say(0, `Media "${id}" needs a type.`);
1288
+ } else if (m.type === "shader" && !m.source) {
1289
+ say(0, `Shader media "${id}" needs a source string.`);
1290
+ }
1291
+ }
1292
+ }
1293
+ for (const [idx, shot] of scene.shots.entries()) {
1294
+ const shotNum = idx + 1;
1295
+ if (shot.camera) {
1296
+ if ("frame" in shot.camera) {
1297
+ if (!Array.isArray(shot.camera.frame) || shot.camera.frame.length === 0) {
1298
+ say(shotNum, "Dynamic camera framing requires a non-empty frame array of element IDs.");
1299
+ }
1300
+ } else if (shot.camera.width <= 0 || shot.camera.height <= 0) {
1301
+ say(shotNum, "Camera rectangle width and height must be positive.");
1302
+ }
1303
+ }
1304
+ if (shot.hold !== void 0 && shot.hold < 0) {
1305
+ say(shotNum, "Shot hold cannot be negative.");
1306
+ }
1307
+ if (shot.caption && shot.caption.length > LONG_CAPTION) {
1308
+ say(shotNum, `Caption is long (${shot.caption.length} chars); consider breaking into multiple shots.`);
1309
+ }
1310
+ if (shot.title && shot.title.length > LONG_TITLE) {
1311
+ say(shotNum, `Title is long (${shot.title.length} chars); titles should be concise headings.`);
1312
+ }
1313
+ const effects = shot.effects ?? [];
1314
+ if (effects.length > MAX_EFFECTS_PER_SHOT) {
1315
+ say(shotNum, `Shot starts ${effects.length} effects at once; consider keeping effects focused.`);
1316
+ }
1317
+ for (const eff of effects) {
1318
+ if (!eff.id || typeof eff.id !== "string") {
1319
+ say(shotNum, "Every effect needs a unique string id.");
1320
+ continue;
1321
+ }
1322
+ if (effectIds.has(eff.id)) {
1323
+ say(shotNum, `Duplicate effect id "${eff.id}".`);
1324
+ }
1325
+ effectIds.add(eff.id);
1326
+ if (!EFFECT_TYPES.includes(eff.type)) {
1327
+ say(shotNum, `Unknown effect type "${eff.type}".`);
1328
+ }
1329
+ if (eff.type === "media") {
1330
+ if (!eff.media) {
1331
+ say(shotNum, `Media effect "${eff.id}" needs a media id.`);
1332
+ } else if (!mediaKeys.has(eff.media)) {
1333
+ say(shotNum, `Media effect "${eff.id}" references "${eff.media}" which is not defined in scene.media.`);
1334
+ }
1335
+ }
1336
+ if (eff.type === "flow" && !eff.along) {
1337
+ say(shotNum, `Flow effect "${eff.id}" needs an "along" element id.`);
1338
+ }
1339
+ if (["glow", "ripple", "spotlight", "shimmer", "timer", "blur", "burst"].includes(eff.type)) {
1340
+ const withIds = eff;
1341
+ if (!Array.isArray(withIds.ids) || withIds.ids.length === 0) {
1342
+ say(shotNum, `${eff.type} effect "${eff.id}" needs a non-empty ids array.`);
1343
+ }
1344
+ }
1345
+ }
1346
+ }
1347
+ return issues;
1348
+ }
1349
+
1350
+ // src/scene/report.ts
1351
+ function reviewScene(scene, logs, options = {}) {
1352
+ const warnings = [];
1353
+ const notes = [];
1354
+ const minTextPx = options.minTextPx ?? 18;
1355
+ const totalMs = logs.length > 0 ? logs[logs.length - 1].end : 0;
1356
+ const totalSec = (totalMs / 1e3).toFixed(1);
1357
+ if (scene.target) {
1358
+ const [minS, maxS] = scene.target;
1359
+ const durS = totalMs / 1e3;
1360
+ if (durS < minS) {
1361
+ warnings.push(`Scene is ${totalSec}s, shorter than target minimum (${minS}s).`);
1362
+ } else if (durS > maxS) {
1363
+ warnings.push(`Scene is ${totalSec}s, longer than target maximum (${maxS}s).`);
1364
+ }
1365
+ }
1366
+ for (const log of logs) {
1367
+ const shotPrefix = `Shot ${log.index}:`;
1368
+ if (log.failed.length > 0) {
1369
+ warnings.push(`${shotPrefix} ${log.failed.length} command(s) failed: ${log.failed.join(", ")}`);
1370
+ }
1371
+ for (const f of log.framing) {
1372
+ warnings.push(`${shotPrefix} element "${f.id}" is ${f.how} of the camera frame.`);
1373
+ }
1374
+ for (const [a, b] of log.overlaps) {
1375
+ notes.push(`${shotPrefix} elements "${a}" and "${b}" overlap significantly.`);
1376
+ }
1377
+ if (log.smallTexts) {
1378
+ for (const t of log.smallTexts) {
1379
+ if (t.px < minTextPx) {
1380
+ warnings.push(`${shotPrefix} text "${t.text}" is only ${t.px}px on screen (less than ${minTextPx}px minimum).`);
1381
+ }
1382
+ }
1383
+ }
1384
+ for (const id of log.underTitle) {
1385
+ warnings.push(`${shotPrefix} element "${id}" is partially covered by the title bar.`);
1386
+ }
1387
+ }
1388
+ const lines = [
1389
+ `# Scene Preview Review: ${scene.title || scene.name}`,
1390
+ `**Total Duration:** ${totalSec}s (${logs.length} shots)`,
1391
+ ""
1392
+ ];
1393
+ if (warnings.length > 0) {
1394
+ lines.push("### \u26A0\uFE0F Warnings to Address:");
1395
+ for (const w of warnings) lines.push(`- ${w}`);
1396
+ lines.push("");
1397
+ } else {
1398
+ lines.push("\u2705 No critical layout or framing warnings.");
1399
+ lines.push("");
1400
+ }
1401
+ if (notes.length > 0) {
1402
+ lines.push("### \u2139\uFE0F Layout Notes:");
1403
+ for (const n of notes) lines.push(`- ${n}`);
1404
+ lines.push("");
1405
+ }
1406
+ lines.push("### Shot Breakdown");
1407
+ lines.push("| Shot | Start | End | Duration | Title / Caption |");
1408
+ lines.push("|:---:|:---:|:---:|:---:|:---|");
1409
+ for (const log of logs) {
1410
+ const dur = ((log.end - log.start) / 1e3).toFixed(1);
1411
+ const text = log.title || log.caption || "\u2014";
1412
+ lines.push(`| ${log.index} | ${(log.start / 1e3).toFixed(1)}s | ${(log.end / 1e3).toFixed(1)}s | ${dur}s | ${text.slice(0, 40)} |`);
1413
+ }
1414
+ return {
1415
+ totalMs,
1416
+ warnings,
1417
+ notes,
1418
+ markdown: lines.join("\n")
1419
+ };
1420
+ }
1421
+
1422
+ // src/scene/behavior.ts
1423
+ function evaluateWriggle(t, freq, amount, octaves = 3, seed = 42) {
1424
+ return fractalWiggle(freq, amount, octaves, 0.5, t, seed, 0);
1425
+ }
1426
+ function evaluateBehavior(behavior, elapsedSec, durationSec, baseValue = 0) {
1427
+ const t = Math.max(0, elapsedSec);
1428
+ switch (behavior.type) {
1429
+ case "oscillate": {
1430
+ const freq = behavior.frequency;
1431
+ const phase = behavior.phase ?? 0;
1432
+ const angle = 2 * Math.PI * freq * t + phase;
1433
+ let waveVal = 0;
1434
+ switch (behavior.wave) {
1435
+ case "square":
1436
+ waveVal = Math.sin(angle) >= 0 ? 1 : -1;
1437
+ break;
1438
+ case "triangle":
1439
+ waveVal = 2 / Math.PI * Math.asin(Math.sin(angle));
1440
+ break;
1441
+ case "sawtooth": {
1442
+ const norm = angle / (2 * Math.PI) % 1;
1443
+ waveVal = 2 * (norm < 0 ? norm + 1 : norm) - 1;
1444
+ break;
1445
+ }
1446
+ case "sine":
1447
+ default:
1448
+ waveVal = Math.sin(angle);
1449
+ break;
1450
+ }
1451
+ const offset = waveVal * behavior.amplitude;
1452
+ return behavior.center !== void 0 ? behavior.center + offset : baseValue + offset;
1453
+ }
1454
+ case "ramp": {
1455
+ const from = behavior.from ?? baseValue;
1456
+ const to = behavior.to;
1457
+ const p = durationSec > 0 ? Math.min(1, Math.max(0, t / durationSec)) : 1;
1458
+ let eased = p;
1459
+ switch (behavior.ease) {
1460
+ case "easeIn":
1461
+ eased = p * p;
1462
+ break;
1463
+ case "easeOut":
1464
+ eased = p * (2 - p);
1465
+ break;
1466
+ case "easeInOut":
1467
+ eased = p < 0.5 ? 2 * p * p : -1 + (4 - 2 * p) * p;
1468
+ break;
1469
+ case "linear":
1470
+ default:
1471
+ eased = p;
1472
+ break;
1473
+ }
1474
+ return from + (to - from) * eased;
1475
+ }
1476
+ case "rate": {
1477
+ return baseValue + behavior.rate * t;
1478
+ }
1479
+ case "wriggle": {
1480
+ const wander = evaluateWriggle(
1481
+ t,
1482
+ behavior.frequency,
1483
+ behavior.amount,
1484
+ behavior.octaves ?? 3,
1485
+ behavior.seed ?? 42
1486
+ );
1487
+ return baseValue + wander;
1488
+ }
1489
+ case "overshoot": {
1490
+ const from = behavior.startValue ?? baseValue;
1491
+ const target = behavior.targetValue;
1492
+ const cycles = behavior.cycles ?? 3;
1493
+ const damping = behavior.damping ?? 4;
1494
+ if (durationSec <= 0) return target;
1495
+ const p = Math.min(1, t / durationSec);
1496
+ if (p >= 1) return target;
1497
+ const envelope = Math.exp(-damping * p);
1498
+ const oscillation = Math.cos(cycles * Math.PI * p);
1499
+ const progress = 1 - envelope * oscillation;
1500
+ return from + (target - from) * progress;
1501
+ }
1502
+ case "throw": {
1503
+ const v0 = behavior.initialVelocity;
1504
+ const k = Math.max(1e-3, behavior.friction ?? 2);
1505
+ const displacement = v0 / k * (1 - Math.exp(-k * t));
1506
+ return baseValue + displacement;
1507
+ }
1508
+ }
1509
+ }
1510
+
1511
+ // src/scene/player.ts
1512
+ var MemorySceneBoard = class {
1513
+ view;
1514
+ elMap = /* @__PURE__ */ new Map();
1515
+ currentTitle = "";
1516
+ currentCaption = "";
1517
+ runningEffects = [];
1518
+ constructor(stage = DEFAULT_STAGE) {
1519
+ this.view = viewAt(stage.width / 2, stage.height / 2, 1, stage);
1520
+ }
1521
+ getView() {
1522
+ return this.view;
1523
+ }
1524
+ setView(view) {
1525
+ this.view = view;
1526
+ }
1527
+ elements() {
1528
+ return Array.from(this.elMap.values());
1529
+ }
1530
+ runCommand(cmd) {
1531
+ if (isElementCommand(cmd)) {
1532
+ this.elMap.set(cmd.element.id, { ...cmd.element });
1533
+ } else if (isUpdateCommand(cmd)) {
1534
+ const existing = this.elMap.get(cmd.id);
1535
+ if (existing) {
1536
+ this.elMap.set(cmd.id, { ...existing, ...cmd.props });
1537
+ }
1538
+ } else if (isDeleteCommand(cmd)) {
1539
+ this.elMap.delete(cmd.id);
1540
+ } else if (cmd.type === "clear") {
1541
+ this.elMap.clear();
1542
+ }
1543
+ }
1544
+ setTitle(text) {
1545
+ this.currentTitle = text;
1546
+ }
1547
+ setCaption(text) {
1548
+ this.currentCaption = text;
1549
+ }
1550
+ setEffects(effects) {
1551
+ this.runningEffects = effects;
1552
+ }
1553
+ };
1554
+ async function playScene(scene, board, clock, options = {}) {
1555
+ const stage = {
1556
+ width: scene.stage?.width ?? DEFAULT_STAGE.width,
1557
+ height: scene.stage?.height ?? DEFAULT_STAGE.height,
1558
+ caption: scene.stage?.caption ?? DEFAULT_STAGE.caption
1559
+ };
1560
+ const shots = [];
1561
+ const begin = clock.now();
1562
+ let runningEffects = [];
1563
+ const endsAt = /* @__PURE__ */ new Map();
1564
+ const updateEffects = (next) => {
1565
+ runningEffects = next.filter((r) => !effectGone(r, clock.now()));
1566
+ board.setEffects?.(runningEffects);
1567
+ };
1568
+ for (const [idx, shot] of scene.shots.entries()) {
1569
+ const shotStart = clock.now() - begin;
1570
+ const failedCmds = [];
1571
+ const stopIds = new Set(shot.stopEffects ?? []);
1572
+ if (stopIds.size > 0 || runningEffects.some((r) => endsAt.get(r) === idx)) {
1573
+ const now = clock.now();
1574
+ updateEffects(
1575
+ runningEffects.map(
1576
+ (r) => r.until === void 0 && (stopIds.has(r.effect.id) || endsAt.get(r) === idx) ? { ...r, until: now } : r
1577
+ )
1578
+ );
1579
+ }
1580
+ if (shot.camera) {
1581
+ let targetRect;
1582
+ if ("frame" in shot.camera) {
1583
+ const frameIds = new Set(shot.camera.frame);
1584
+ const targetElements = board.elements().filter((el) => frameIds.has(el.id));
1585
+ if (targetElements.length === 0 && shot.run) {
1586
+ for (const cmd of shot.run) {
1587
+ if (isElementCommand(cmd) && frameIds.has(cmd.element.id)) {
1588
+ targetElements.push(cmd.element);
1589
+ }
1590
+ }
1591
+ }
1592
+ targetRect = calculateFramingRect(targetElements, stage, {
1593
+ padding: shot.camera.padding,
1594
+ minWidth: shot.camera.minWidth,
1595
+ minHeight: shot.camera.minHeight
1596
+ });
1597
+ } else {
1598
+ targetRect = shot.camera;
1599
+ }
1600
+ const targetView = frame(targetRect, stage);
1601
+ if (idx === 0) {
1602
+ board.setView(targetView);
1603
+ } else {
1604
+ const fromView = board.getView();
1605
+ const steps = 15;
1606
+ const stepMs = CAMERA_MS / steps;
1607
+ for (let s = 1; s <= steps; s++) {
1608
+ const t = easeInOutCubic(s / steps);
1609
+ board.setView(between(fromView, targetView, t, stage));
1610
+ await clock.sleep(stepMs);
1611
+ }
1612
+ }
1613
+ }
1614
+ if (shot.title !== void 0) {
1615
+ board.setTitle?.(shot.title, 1);
1616
+ }
1617
+ if (shot.caption !== void 0) {
1618
+ board.setCaption?.(shot.caption);
1619
+ }
1620
+ const startEffects = (shot.effects ?? []).filter((e) => e.at === "start");
1621
+ if (startEffects.length > 0) {
1622
+ const now = clock.now();
1623
+ const added = startEffects.map((effect) => ({ effect, from: now }));
1624
+ for (const r of added) if (r.effect.shots) endsAt.set(r, idx + r.effect.shots);
1625
+ updateEffects([...runningEffects, ...added]);
1626
+ }
1627
+ for (const cmd of shot.run ?? []) {
1628
+ try {
1629
+ await board.runCommand(cmd);
1630
+ } catch (err) {
1631
+ failedCmds.push(err.message || String(err));
1632
+ }
1633
+ }
1634
+ const drawnEffects = (shot.effects ?? []).filter((e) => e.at !== "start");
1635
+ if (drawnEffects.length > 0) {
1636
+ const now = clock.now();
1637
+ const added = drawnEffects.map((effect) => ({ effect, from: now }));
1638
+ for (const r of added) if (r.effect.shots) endsAt.set(r, idx + r.effect.shots);
1639
+ updateEffects([...runningEffects, ...added]);
1640
+ }
1641
+ const drawnTime = clock.now() - begin;
1642
+ const check = options.check ? checkShot(shot, board.elements(), board.getView(), stage, shot.title) : { framing: [], overlaps: [], edges: [], underTitle: [] };
1643
+ const holdMs = shot.hold ?? 800;
1644
+ const behaviors = shot.behaviors ?? [];
1645
+ if (behaviors.length > 0) {
1646
+ const initialProps = /* @__PURE__ */ new Map();
1647
+ for (const b of behaviors) {
1648
+ const key = `${b.target}:${b.property}`;
1649
+ const el = board.elements().find((e) => e.id === b.target);
1650
+ if (el && typeof el[b.property] === "number") {
1651
+ initialProps.set(key, el[b.property]);
1652
+ } else {
1653
+ initialProps.set(key, 0);
1654
+ }
1655
+ }
1656
+ const stepMs = 50;
1657
+ const steps = Math.max(1, Math.round(holdMs / stepMs));
1658
+ const actualStepMs = holdMs / steps;
1659
+ const durationSec = holdMs / 1e3;
1660
+ for (let s = 1; s <= steps; s++) {
1661
+ await clock.sleep(actualStepMs);
1662
+ const elapsedSec = s * actualStepMs / 1e3;
1663
+ for (const b of behaviors) {
1664
+ const baseVal = initialProps.get(`${b.target}:${b.property}`) ?? 0;
1665
+ const nextVal = evaluateBehavior(b, elapsedSec, durationSec, baseVal);
1666
+ board.runCommand({
1667
+ type: "update",
1668
+ id: b.target,
1669
+ props: { [b.property]: nextVal }
1670
+ });
1671
+ }
1672
+ }
1673
+ } else {
1674
+ await clock.sleep(holdMs);
1675
+ }
1676
+ const shotEnd = clock.now() - begin;
1677
+ const log = {
1678
+ ...check,
1679
+ index: idx + 1,
1680
+ start: shotStart,
1681
+ drawn: drawnTime,
1682
+ end: shotEnd,
1683
+ caption: shot.caption,
1684
+ title: shot.title,
1685
+ failed: failedCmds,
1686
+ effects: runningEffects.map((r) => r.effect.id)
1687
+ };
1688
+ shots.push(log);
1689
+ if (options.afterShot) {
1690
+ await options.afterShot(log);
1691
+ }
1692
+ }
1693
+ const duration = clock.now() - begin;
1694
+ const ok = shots.every((s) => s.failed.length === 0 && s.framing.every((f) => f.how !== "outside"));
1695
+ return { ok, duration, shots };
1696
+ }
1697
+ async function simulateScene(scene, options = {}) {
1698
+ const clock = new ManualClock();
1699
+ const board = new MemorySceneBoard();
1700
+ let done = false;
1701
+ let playError = null;
1702
+ const playPromise = playScene(scene, board, clock, { check: true }).then((res) => {
1703
+ done = true;
1704
+ return res;
1705
+ }).catch((err) => {
1706
+ done = true;
1707
+ playError = err;
1708
+ throw err;
1709
+ });
1710
+ for (let i = 0; i < 5e3 && !done; i++) {
1711
+ await clock.advance(50);
1712
+ }
1713
+ if (playError) throw playError;
1714
+ const result = await playPromise;
1715
+ const review = reviewScene(scene, result.shots, {
1716
+ target: scene.target,
1717
+ minTextPx: options.minTextPx
1718
+ });
1719
+ return { result, review };
1720
+ }
1721
+
1722
+ // src/scene/standalone.ts
1723
+ function standaloneSceneHtml(scene) {
1724
+ const serializedScene = JSON.stringify(scene).replace(/</g, "\\u003c");
1725
+ const title = scene.title || scene.name;
1726
+ return `<!DOCTYPE html>
1727
+ <html lang="en">
1728
+ <head>
1729
+ <meta charset="utf-8">
1730
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1731
+ <title>${escapeHtml2(title)} - Flair</title>
1732
+ <style>
1733
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1734
+ body {
1735
+ background: #0f1117;
1736
+ color: #e2e8f0;
1737
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
1738
+ min-height: 100vh;
1739
+ display: flex;
1740
+ flex-direction: column;
1741
+ align-items: center;
1742
+ justify-content: center;
1743
+ padding: 16px;
1744
+ user-select: none;
1745
+ }
1746
+ .player-container {
1747
+ position: relative;
1748
+ width: 100%;
1749
+ max-width: 1200px;
1750
+ aspect-ratio: 16 / 9;
1751
+ background: #000;
1752
+ border-radius: 12px;
1753
+ overflow: hidden;
1754
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.1);
1755
+ display: flex;
1756
+ flex-direction: column;
1757
+ }
1758
+ .viewport {
1759
+ position: relative;
1760
+ flex: 1;
1761
+ width: 100%;
1762
+ height: 100%;
1763
+ overflow: hidden;
1764
+ }
1765
+ canvas.layer {
1766
+ position: absolute;
1767
+ top: 0;
1768
+ left: 0;
1769
+ width: 100%;
1770
+ height: 100%;
1771
+ }
1772
+ .overlay-title {
1773
+ position: absolute;
1774
+ top: 24px;
1775
+ left: 50%;
1776
+ transform: translateX(-50%);
1777
+ background: rgba(15, 17, 23, 0.85);
1778
+ backdrop-filter: blur(8px);
1779
+ border: 1px solid rgba(255, 255, 255, 0.15);
1780
+ padding: 8px 24px;
1781
+ border-radius: 9999px;
1782
+ font-size: 1.15rem;
1783
+ font-weight: 600;
1784
+ letter-spacing: 0.02em;
1785
+ color: #fff;
1786
+ pointer-events: none;
1787
+ transition: opacity 0.3s ease;
1788
+ opacity: 0;
1789
+ }
1790
+ .overlay-caption {
1791
+ position: absolute;
1792
+ bottom: 64px;
1793
+ left: 50%;
1794
+ transform: translateX(-50%);
1795
+ background: rgba(15, 17, 23, 0.88);
1796
+ backdrop-filter: blur(8px);
1797
+ border: 1px solid rgba(255, 255, 255, 0.15);
1798
+ padding: 10px 20px;
1799
+ border-radius: 8px;
1800
+ font-size: 1.1rem;
1801
+ color: #f1f5f9;
1802
+ max-width: 80%;
1803
+ text-align: center;
1804
+ pointer-events: none;
1805
+ transition: opacity 0.3s ease;
1806
+ opacity: 0;
1807
+ }
1808
+ .controls-bar {
1809
+ position: absolute;
1810
+ bottom: 0;
1811
+ left: 0;
1812
+ right: 0;
1813
+ height: 52px;
1814
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.9), transparent);
1815
+ display: flex;
1816
+ align-items: center;
1817
+ padding: 0 16px;
1818
+ gap: 12px;
1819
+ z-index: 10;
1820
+ }
1821
+ .btn {
1822
+ background: rgba(255, 255, 255, 0.15);
1823
+ border: none;
1824
+ color: #fff;
1825
+ padding: 6px 14px;
1826
+ border-radius: 6px;
1827
+ cursor: pointer;
1828
+ font-size: 0.9rem;
1829
+ font-weight: 500;
1830
+ transition: background 0.15s;
1831
+ }
1832
+ .btn:hover { background: rgba(255, 255, 255, 0.25); }
1833
+ .timeline {
1834
+ flex: 1;
1835
+ height: 6px;
1836
+ background: rgba(255, 255, 255, 0.2);
1837
+ border-radius: 3px;
1838
+ cursor: pointer;
1839
+ position: relative;
1840
+ }
1841
+ .timeline-progress {
1842
+ position: absolute;
1843
+ top: 0;
1844
+ left: 0;
1845
+ height: 100%;
1846
+ background: #3b82f6;
1847
+ border-radius: 3px;
1848
+ width: 0%;
1849
+ }
1850
+ .shot-badge {
1851
+ font-size: 0.85rem;
1852
+ color: #94a3b8;
1853
+ font-variant-numeric: tabular-nums;
1854
+ }
1855
+ </style>
1856
+ </head>
1857
+ <body>
1858
+ <div class="player-container">
1859
+ <div class="viewport" id="viewport">
1860
+ <canvas id="shaderCanvas" class="layer"></canvas>
1861
+ <canvas id="sceneCanvas" class="layer"></canvas>
1862
+ <div id="titleEl" class="overlay-title"></div>
1863
+ <div id="captionEl" class="overlay-caption"></div>
1864
+ </div>
1865
+ <div class="controls-bar">
1866
+ <button id="playBtn" class="btn">Play</button>
1867
+ <button id="prevBtn" class="btn">\u23EE</button>
1868
+ <button id="nextBtn" class="btn">\u23ED</button>
1869
+ <div id="timeline" class="timeline">
1870
+ <div id="progress" class="timeline-progress"></div>
1871
+ </div>
1872
+ <div id="shotBadge" class="shot-badge">Shot 1 / 1</div>
1873
+ </div>
1874
+ </div>
1875
+
1876
+ <script>
1877
+ const scene = ${serializedScene};
1878
+ const sceneCanvas = document.getElementById('sceneCanvas');
1879
+ const shaderCanvas = document.getElementById('shaderCanvas');
1880
+ const ctx = sceneCanvas.getContext('2d');
1881
+ const titleEl = document.getElementById('titleEl');
1882
+ const captionEl = document.getElementById('captionEl');
1883
+ const playBtn = document.getElementById('playBtn');
1884
+ const prevBtn = document.getElementById('prevBtn');
1885
+ const nextBtn = document.getElementById('nextBtn');
1886
+ const progressEl = document.getElementById('progress');
1887
+ const shotBadge = document.getElementById('shotBadge');
1888
+
1889
+ let currentShotIdx = 0;
1890
+ let isPlaying = false;
1891
+ let shotStartTime = 0;
1892
+ let gl = null;
1893
+ let shaderProgram = null;
1894
+
1895
+ function resize() {
1896
+ const rect = sceneCanvas.parentElement.getBoundingClientRect();
1897
+ const dpr = window.devicePixelRatio || 1;
1898
+ sceneCanvas.width = rect.width * dpr;
1899
+ sceneCanvas.height = rect.height * dpr;
1900
+ shaderCanvas.width = rect.width * dpr;
1901
+ shaderCanvas.height = rect.height * dpr;
1902
+ }
1903
+ window.addEventListener('resize', resize);
1904
+ resize();
1905
+
1906
+ // Setup WebGL if shader media is present
1907
+ const firstShaderKey = Object.keys(scene.media || {}).find(k => scene.media[k].type === 'shader');
1908
+ if (firstShaderKey && scene.media[firstShaderKey].source) {
1909
+ try {
1910
+ gl = shaderCanvas.getContext('webgl2');
1911
+ if (gl) {
1912
+ const vs = gl.createShader(gl.VERTEX_SHADER);
1913
+ gl.shaderSource(vs, '#version 300 es\\nin vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); }');
1914
+ gl.compileShader(vs);
1915
+
1916
+ const fs = gl.createShader(gl.FRAGMENT_SHADER);
1917
+ const fullFrag = '#version 300 es\\nprecision highp float;\\nout vec4 fragColor;\\nuniform vec2 u_resolution;\\nuniform float u_time;\\n' + scene.media[firstShaderKey].source + '\\nvoid main() { mainImage(fragColor, gl_FragCoord.xy); }';
1918
+ gl.shaderSource(fs, fullFrag);
1919
+ gl.compileShader(fs);
1920
+
1921
+ shaderProgram = gl.createProgram();
1922
+ gl.attachShader(shaderProgram, vs);
1923
+ gl.attachShader(shaderProgram, fs);
1924
+ gl.linkProgram(shaderProgram);
1925
+
1926
+ const quad = gl.createBuffer();
1927
+ gl.bindBuffer(gl.ARRAY_BUFFER, quad);
1928
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);
1929
+ const pos = gl.getAttribLocation(shaderProgram, 'position');
1930
+ gl.enableVertexAttribArray(pos);
1931
+ gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
1932
+ }
1933
+ } catch (e) {
1934
+ console.warn('Shader setup failed:', e);
1935
+ }
1936
+ }
1937
+
1938
+ function renderShader(timeSeconds) {
1939
+ if (!gl || !shaderProgram) return;
1940
+ gl.viewport(0, 0, shaderCanvas.width, shaderCanvas.height);
1941
+ gl.useProgram(shaderProgram);
1942
+ const resLoc = gl.getUniformLocation(shaderProgram, 'u_resolution');
1943
+ const timeLoc = gl.getUniformLocation(shaderProgram, 'u_time');
1944
+ if (resLoc) gl.uniform2f(resLoc, shaderCanvas.width, shaderCanvas.height);
1945
+ if (timeLoc) gl.uniform1f(timeLoc, timeSeconds);
1946
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
1947
+ }
1948
+
1949
+ function updateShotUI() {
1950
+ const shot = scene.shots[currentShotIdx];
1951
+ if (!shot) return;
1952
+
1953
+ shotBadge.textContent = 'Shot ' + (currentShotIdx + 1) + ' / ' + scene.shots.length;
1954
+
1955
+ if (shot.title) {
1956
+ titleEl.textContent = shot.title;
1957
+ titleEl.style.opacity = '1';
1958
+ } else {
1959
+ titleEl.style.opacity = '0';
1960
+ }
1961
+
1962
+ if (shot.caption) {
1963
+ captionEl.textContent = shot.caption;
1964
+ captionEl.style.opacity = '1';
1965
+ } else {
1966
+ captionEl.style.opacity = '0';
1967
+ }
1968
+
1969
+ const pct = (currentShotIdx / scene.shots.length) * 100;
1970
+ progressEl.style.width = pct + '%';
1971
+ }
1972
+
1973
+ function draw() {
1974
+ const now = performance.now() / 1000;
1975
+ renderShader(now);
1976
+
1977
+ ctx.clearRect(0, 0, sceneCanvas.width, sceneCanvas.height);
1978
+ ctx.save();
1979
+ const shot = scene.shots[currentShotIdx];
1980
+
1981
+ // Draw shot elements
1982
+ if (shot && shot.run) {
1983
+ for (const cmd of shot.run) {
1984
+ if (cmd.type === 'element' && cmd.element) {
1985
+ const el = cmd.element;
1986
+ ctx.fillStyle = el.backgroundColor || 'rgba(59, 130, 246, 0.4)';
1987
+ ctx.strokeStyle = el.strokeColor || '#3b82f6';
1988
+ ctx.lineWidth = 3;
1989
+ ctx.strokeRect(el.x, el.y, el.width, el.height);
1990
+ ctx.fillRect(el.x, el.y, el.width, el.height);
1991
+
1992
+ if (el.text) {
1993
+ ctx.fillStyle = '#fff';
1994
+ ctx.font = '24px sans-serif';
1995
+ ctx.fillText(el.text, el.x + 16, el.y + 40);
1996
+ }
1997
+ }
1998
+ }
1999
+ }
2000
+ ctx.restore();
2001
+
2002
+ if (isPlaying) {
2003
+ requestAnimationFrame(draw);
2004
+ }
2005
+ }
2006
+
2007
+ playBtn.addEventListener('click', () => {
2008
+ isPlaying = !isPlaying;
2009
+ playBtn.textContent = isPlaying ? 'Pause' : 'Play';
2010
+ if (isPlaying) requestAnimationFrame(draw);
2011
+ });
2012
+
2013
+ prevBtn.addEventListener('click', () => {
2014
+ if (currentShotIdx > 0) {
2015
+ currentShotIdx--;
2016
+ updateShotUI();
2017
+ draw();
2018
+ }
2019
+ });
2020
+
2021
+ nextBtn.addEventListener('click', () => {
2022
+ if (currentShotIdx < scene.shots.length - 1) {
2023
+ currentShotIdx++;
2024
+ updateShotUI();
2025
+ draw();
2026
+ }
2027
+ });
2028
+
2029
+ updateShotUI();
2030
+ draw();
2031
+ </script>
2032
+ </body>
2033
+ </html>`;
2034
+ }
2035
+ function escapeHtml2(str) {
2036
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2037
+ }
2038
+
2039
+ export {
2040
+ ShaderRenderer,
2041
+ ThumbnailPool,
2042
+ sharedThumbnailPool,
2043
+ registerThumbnail,
2044
+ ShaderFrames,
2045
+ recordGif,
2046
+ standaloneHtml,
2047
+ ALL_BLEND_MODES,
2048
+ blendRgba,
2049
+ getGlslBlendFunction,
2050
+ hash64,
2051
+ mixSeeds,
2052
+ unitFloat,
2053
+ quinticFade,
2054
+ noise1d,
2055
+ noise3d,
2056
+ fractalWiggle,
2057
+ EFFECT_TYPES,
2058
+ EFFECT_FADE_MS,
2059
+ effectGone,
2060
+ effectOpacity,
2061
+ NO_CHECK,
2062
+ SMALL_TEXT_PX,
2063
+ titleBox,
2064
+ touchedIds,
2065
+ checkShot,
2066
+ validateScene,
2067
+ reviewScene,
2068
+ evaluateBehavior,
2069
+ MemorySceneBoard,
2070
+ playScene,
2071
+ simulateScene,
2072
+ standaloneSceneHtml
2073
+ };
2074
+ //# sourceMappingURL=chunk-JWZCPMFG.js.map