@pixerate/flair 0.24.0 → 0.26.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 (69) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +1 -0
  3. package/dist/catalog/index.js +2 -2
  4. package/dist/{chunk-WAMA45G3.js → chunk-5USSDEQL.js} +77 -21
  5. package/dist/chunk-5USSDEQL.js.map +1 -0
  6. package/dist/{chunk-GTIAOYOK.js → chunk-74ZLCZOG.js} +13 -10
  7. package/dist/chunk-74ZLCZOG.js.map +1 -0
  8. package/dist/{chunk-NGKNI3QX.js → chunk-CK6EV37I.js} +19 -495
  9. package/dist/chunk-CK6EV37I.js.map +1 -0
  10. package/dist/chunk-DBTLCXKY.js +332 -0
  11. package/dist/chunk-DBTLCXKY.js.map +1 -0
  12. package/dist/chunk-G2B5L234.js +414 -0
  13. package/dist/chunk-G2B5L234.js.map +1 -0
  14. package/dist/{chunk-VP6IEWIK.js → chunk-H6L67KHI.js} +1 -1
  15. package/dist/chunk-H6VDFBIO.js +498 -0
  16. package/dist/chunk-H6VDFBIO.js.map +1 -0
  17. package/dist/{chunk-OQ4EWZ3B.js → chunk-HVXWNGTW.js} +83 -6
  18. package/dist/{chunk-OQ4EWZ3B.js.map → chunk-HVXWNGTW.js.map} +1 -1
  19. package/dist/chunk-JL6TI6II.js +724 -0
  20. package/dist/chunk-JL6TI6II.js.map +1 -0
  21. package/dist/{chunk-OHW7UG6D.js → chunk-ORAFCHX3.js} +26 -2
  22. package/dist/{chunk-OHW7UG6D.js.map → chunk-ORAFCHX3.js.map} +1 -1
  23. package/dist/{chunk-ARKC7LDW.js → chunk-RDU63CDO.js} +2 -331
  24. package/dist/chunk-RDU63CDO.js.map +1 -0
  25. package/dist/{chunk-LVPIIWPP.js → chunk-STKEKXCK.js} +18 -8
  26. package/dist/chunk-STKEKXCK.js.map +1 -0
  27. package/dist/chunk-UCZ43ARE.js +902 -0
  28. package/dist/chunk-UCZ43ARE.js.map +1 -0
  29. package/dist/chunk-YBGRXVAE.js +407 -0
  30. package/dist/chunk-YBGRXVAE.js.map +1 -0
  31. package/dist/cli.js +74 -11
  32. package/dist/cli.js.map +1 -1
  33. package/dist/editor/index.d.ts +1 -1
  34. package/dist/editor/index.js +14 -12
  35. package/dist/editor/index.js.map +1 -1
  36. package/dist/effect-GGH3OSOT.js +47 -0
  37. package/dist/embed/index.d.ts +215 -0
  38. package/dist/embed/index.js +21 -0
  39. package/dist/{export-DH2HMACO.js → export-QVW474KM.js} +3 -3
  40. package/dist/export-QVW474KM.js.map +1 -0
  41. package/dist/frames-VEcqa40g.d.ts +161 -0
  42. package/dist/index.d.ts +47 -164
  43. package/dist/index.js +102 -80
  44. package/dist/index.js.map +1 -1
  45. package/dist/node.d.ts +167 -7
  46. package/dist/node.js +25 -3
  47. package/dist/react/index.d.ts +33 -2
  48. package/dist/react/index.js +54 -9
  49. package/dist/react/index.js.map +1 -1
  50. package/dist/recorder-CDPPTJMG.js +31 -0
  51. package/dist/recorder-CDPPTJMG.js.map +1 -0
  52. package/dist/{renderer-C2P1qGzP.d.ts → renderer-teaa98RE.d.ts} +1 -1
  53. package/dist/svelte/index.d.ts +33 -4
  54. package/dist/svelte/index.js +51 -9
  55. package/dist/svelte/index.js.map +1 -1
  56. package/dist/vector/index.d.ts +52 -1
  57. package/dist/vector/index.js +8 -4
  58. package/package.json +1 -1
  59. package/dist/chunk-ARKC7LDW.js.map +0 -1
  60. package/dist/chunk-GTIAOYOK.js.map +0 -1
  61. package/dist/chunk-LVPIIWPP.js.map +0 -1
  62. package/dist/chunk-NGKNI3QX.js.map +0 -1
  63. package/dist/chunk-UP6QQVRS.js +0 -331
  64. package/dist/chunk-UP6QQVRS.js.map +0 -1
  65. package/dist/chunk-WAMA45G3.js.map +0 -1
  66. package/dist/recorder-HKFFHQES.js +0 -10
  67. /package/dist/{chunk-VP6IEWIK.js.map → chunk-H6L67KHI.js.map} +0 -0
  68. /package/dist/{export-DH2HMACO.js.map → effect-GGH3OSOT.js.map} +0 -0
  69. /package/dist/{recorder-HKFFHQES.js.map → embed/index.js.map} +0 -0
@@ -1,393 +1,35 @@
1
1
  import {
2
- CAMERA_MS,
3
2
  CAMERA_STEP_MS,
3
+ CAMERA_TRANSITIONS,
4
4
  EFFECT_TYPES,
5
5
  MAX_TILT,
6
6
  cameraAt,
7
7
  cameraTrack,
8
8
  sceneStage
9
- } from "./chunk-WAMA45G3.js";
9
+ } from "./chunk-5USSDEQL.js";
10
+ import {
11
+ ShaderFrames
12
+ } from "./chunk-H6VDFBIO.js";
10
13
  import {
11
14
  HOVER_SECONDS,
12
15
  MAX_CLICKS,
13
16
  MAX_TRAIL,
14
- PointerTracker,
15
17
  SVG_EDGE,
16
- ShaderCompileError,
17
18
  TRAIL_SECONDS,
18
19
  TextureCache,
19
20
  VERTEX_SOURCE,
20
- clicksUniform,
21
21
  compileShader,
22
22
  createContext,
23
23
  drawFrame,
24
24
  resetGlCaches,
25
- trailUniform,
26
25
  wrapSource
27
- } from "./chunk-ARKC7LDW.js";
26
+ } from "./chunk-RDU63CDO.js";
28
27
  import {
29
28
  controlValue,
30
29
  imageUrls,
31
30
  parseControls
32
31
  } from "./chunk-VYOMXE6I.js";
33
32
 
34
- // src/shader/renderer.ts
35
- var ShaderRenderer = class {
36
- canvas;
37
- gl;
38
- alpha;
39
- shader = null;
40
- textures = null;
41
- /** Live sources by image control, kept to set again on a restored context. */
42
- live = /* @__PURE__ */ new Map();
43
- controls = [];
44
- values;
45
- source = "";
46
- raf = 0;
47
- lastNow = 0;
48
- frame = 0;
49
- fpsFrames = 0;
50
- fpsSince = 0;
51
- ownsCanvas = false;
52
- resizeObserver;
53
- intersectionObserver;
54
- wasPlayingBeforeHidden = false;
55
- hasEnteredViewport = false;
56
- mouse = [0, 0, 0, 0];
57
- /** `iPointer` and `iClicks`. */
58
- pointer = new PointerTracker();
59
- disposed = false;
60
- /** The latest source's status. */
61
- status = { ok: true, errors: [] };
62
- /** `iTime`, in seconds. Use `seek` to change it. */
63
- time = 0;
64
- /** Playback speed: 1 is real time. */
65
- speed = 1;
66
- /** Device pixels per CSS pixel; lower renders faster. Use `setPixelRatio` to change it. */
67
- pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
68
- playing = true;
69
- fps = 0;
70
- onStats;
71
- /** Called with `true` when the GL context is lost and `false` once it is restored and recompiled. */
72
- onContextLost;
73
- /** Called when an image control's URL (or live video) cannot be loaded; the control shows black. */
74
- onImageError;
75
- /**
76
- * Creates a canvas in `target` (with `className` or options), or attaches to an existing
77
- * `HTMLCanvasElement`, and starts the animation loop. Size the canvas with CSS.
78
- */
79
- constructor(target, options) {
80
- const opts = typeof options === "string" ? { className: options } : options ?? {};
81
- let canvas;
82
- if (target instanceof HTMLCanvasElement || "tagName" in target && target.tagName === "CANVAS") {
83
- canvas = target;
84
- if (opts.className) canvas.className = opts.className;
85
- } else {
86
- canvas = document.createElement("canvas");
87
- if (opts.className) canvas.className = opts.className;
88
- target.appendChild(canvas);
89
- this.ownsCanvas = true;
90
- }
91
- this.canvas = canvas;
92
- this.alpha = Boolean(opts.alpha);
93
- this.gl = createContext(canvas, true, this.alpha);
94
- this.textures = this.gl && this.textureCache(this.gl);
95
- this.resizeObserver = new ResizeObserver(() => this.resize());
96
- this.resizeObserver.observe(canvas);
97
- const needsObserver = (opts.pauseWhenHidden || opts.playOnEnter) && typeof IntersectionObserver !== "undefined";
98
- if (needsObserver) {
99
- this.intersectionObserver = new IntersectionObserver(([entry]) => {
100
- if (!entry) return;
101
- if (entry.isIntersecting) {
102
- if (opts.playOnEnter && !this.hasEnteredViewport) {
103
- this.hasEnteredViewport = true;
104
- if (opts.autoplay ?? true) {
105
- this.play();
106
- }
107
- } else if (this.wasPlayingBeforeHidden) {
108
- this.wasPlayingBeforeHidden = false;
109
- this.play();
110
- }
111
- } else {
112
- if (this.playing && opts.pauseWhenHidden) {
113
- this.wasPlayingBeforeHidden = true;
114
- this.pause();
115
- }
116
- }
117
- });
118
- this.intersectionObserver.observe(canvas);
119
- }
120
- const shouldAutoplay = (opts.autoplay ?? true) && !opts.playOnEnter;
121
- this.playing = shouldAutoplay;
122
- canvas.addEventListener("webglcontextlost", this.onLost);
123
- canvas.addEventListener("webglcontextrestored", this.onRestored);
124
- canvas.addEventListener("pointerdown", this.onPointerDown);
125
- canvas.addEventListener("pointermove", this.onPointerMove);
126
- canvas.addEventListener("pointerenter", this.onPointerEnter);
127
- canvas.addEventListener("pointerleave", this.onPointerLeave);
128
- window.addEventListener("pointerup", this.onPointerUp);
129
- this.resize();
130
- this.raf = requestAnimationFrame(this.tick);
131
- }
132
- /** False when the browser has no WebGL2. */
133
- get supported() {
134
- return this.gl !== null;
135
- }
136
- /** Compiles a new source; returns its status. Keeps the previous program when it fails. */
137
- setSource(source) {
138
- this.source = source;
139
- const controls = parseControls(source);
140
- if (!this.gl) {
141
- this.status = {
142
- ok: false,
143
- errors: [{ line: 0, message: "WebGL2 is not available in this browser." }]
144
- };
145
- return this.status;
146
- }
147
- const result = compileShader(this.gl, source, controls);
148
- if (result.ok) {
149
- if (this.shader) this.gl.deleteProgram(this.shader.program);
150
- this.shader = result.shader;
151
- this.controls = controls;
152
- this.status = { ok: true, errors: [] };
153
- } else {
154
- this.status = { ok: false, errors: result.errors };
155
- }
156
- this.syncTextures();
157
- if (!this.playing) this.render();
158
- return this.status;
159
- }
160
- /**
161
- * Sets control values (see `controlValue`); a paused preview redraws.
162
- * Images load in the background and show once they arrive: await
163
- * `ready()` before a `capture` that needs them.
164
- */
165
- setValues(values) {
166
- this.values = values;
167
- this.syncTextures();
168
- if (!this.playing) this.render();
169
- }
170
- /**
171
- * Shows a `<video>` (or a canvas) in the image control `name` (the
172
- * uniform's name, such as `uImage`) in place of its URL, frame by frame;
173
- * `null` goes back to the URL. The preview follows the video even while
174
- * the shader's own clock is paused, and the video plays, seeks and loops
175
- * on its own: the renderer never controls it. A video from another site
176
- * needs `crossOrigin = "anonymous"` and CORS, or `onImageError` hears why
177
- * it shows black. The value stays as it is, so documents, presets and
178
- * agents keep seeing the URL.
179
- */
180
- setLiveSource(name, source) {
181
- if (source) this.live.set(name, source);
182
- else this.live.delete(name);
183
- this.textures?.setLive(name, source);
184
- if (!this.playing) this.render();
185
- }
186
- /**
187
- * Settles once the images the current controls show have loaded, and
188
- * every live video has a frame. Rejects with an {@link ImageLoadError}
189
- * when one cannot be (it shows black).
190
- */
191
- async ready() {
192
- await Promise.all([
193
- this.textures?.ready(imageUrls(this.controls, this.values)),
194
- this.textures?.liveReady()
195
- ]);
196
- }
197
- play() {
198
- this.playing = true;
199
- this.emit();
200
- }
201
- pause() {
202
- this.playing = false;
203
- this.emit();
204
- }
205
- /** Back to `iTime` 0 and frame 0. */
206
- restart() {
207
- this.time = 0;
208
- this.pointer.clear();
209
- this.frame = 0;
210
- this.render();
211
- this.emit();
212
- }
213
- /** Jumps to `time` seconds (not below 0) and draws it. */
214
- seek(time) {
215
- if (time < this.time) this.pointer.clear();
216
- this.time = Math.max(0, time);
217
- this.render();
218
- this.emit();
219
- }
220
- setPixelRatio(ratio) {
221
- this.pixelRatio = ratio;
222
- this.resize();
223
- }
224
- /**
225
- * Captures the current frame (optionally at another time) as a data URL
226
- * (JPEG unless `type` says PNG), at most `maxEdge` pixels on the long side
227
- * (default 1024). Playback time is left as it was. Null when nothing has
228
- * compiled yet.
229
- */
230
- capture(options = {}) {
231
- if (!this.gl || !this.shader) return null;
232
- const saved = this.time;
233
- if (options.time !== void 0) this.time = Math.max(0, options.time);
234
- this.render();
235
- const { width, height } = this.canvas;
236
- const scale = Math.min(1, (options.maxEdge ?? 1024) / Math.max(width, height));
237
- const out = document.createElement("canvas");
238
- out.width = Math.max(1, Math.round(width * scale));
239
- out.height = Math.max(1, Math.round(height * scale));
240
- out.getContext("2d").drawImage(this.canvas, 0, 0, out.width, out.height);
241
- this.time = saved;
242
- if (options.time !== void 0) this.render();
243
- return out.toDataURL(options.type ?? "image/jpeg", 0.85);
244
- }
245
- /** Stops the loop, releases the context and removes the canvas. The renderer cannot be used again. */
246
- dispose() {
247
- this.disposed = true;
248
- cancelAnimationFrame(this.raf);
249
- this.resizeObserver.disconnect();
250
- this.intersectionObserver?.disconnect();
251
- this.canvas.removeEventListener("webglcontextlost", this.onLost);
252
- this.canvas.removeEventListener("webglcontextrestored", this.onRestored);
253
- this.canvas.removeEventListener("pointerdown", this.onPointerDown);
254
- this.canvas.removeEventListener("pointermove", this.onPointerMove);
255
- this.canvas.removeEventListener("pointerenter", this.onPointerEnter);
256
- this.canvas.removeEventListener("pointerleave", this.onPointerLeave);
257
- window.removeEventListener("pointerup", this.onPointerUp);
258
- if (this.gl && this.shader) this.gl.deleteProgram(this.shader.program);
259
- this.textures?.dispose();
260
- this.textures = null;
261
- this.gl?.getExtension("WEBGL_lose_context")?.loseContext();
262
- this.gl = null;
263
- if (this.ownsCanvas) this.canvas.remove();
264
- }
265
- resize() {
266
- const w = Math.max(1, Math.round(this.canvas.clientWidth * this.pixelRatio));
267
- const h = Math.max(1, Math.round(this.canvas.clientHeight * this.pixelRatio));
268
- if (this.canvas.width !== w || this.canvas.height !== h) {
269
- this.canvas.width = w;
270
- this.canvas.height = h;
271
- this.render();
272
- }
273
- }
274
- render(delta = 0) {
275
- if (!this.gl || !this.shader || this.gl.isContextLost()) return;
276
- drawFrame(
277
- this.gl,
278
- this.shader,
279
- this.controls,
280
- this.values,
281
- {
282
- width: this.canvas.width,
283
- height: this.canvas.height,
284
- time: this.time,
285
- delta,
286
- frame: this.frame,
287
- mouse: this.mouse,
288
- pointer: this.pointer.pointer(this.time),
289
- clicks: this.pointer.clicksAt(this.time),
290
- trail: this.pointer.trailAt(this.time)
291
- },
292
- this.textures ?? void 0
293
- );
294
- }
295
- textureCache(gl) {
296
- const cache = new TextureCache(gl, (_url, error) => {
297
- if (error) this.onImageError?.(error);
298
- else if (!this.playing) this.render();
299
- });
300
- for (const [name, source] of this.live) cache.setLive(name, source);
301
- return cache;
302
- }
303
- /** Starts loading the images now shown, and frees the ones no longer shown. */
304
- syncTextures() {
305
- if (!this.textures) return;
306
- const urls = imageUrls(this.controls, this.values);
307
- this.textures.retain(urls);
308
- for (const url of urls) this.textures.request(url);
309
- }
310
- tick = (now) => {
311
- if (this.disposed) return;
312
- this.raf = requestAnimationFrame(this.tick);
313
- const delta = this.lastNow ? Math.min((now - this.lastNow) / 1e3, 0.1) : 0;
314
- this.lastNow = now;
315
- if (this.playing) {
316
- this.time += delta * this.speed;
317
- this.frame += 1;
318
- this.render(delta * this.speed);
319
- this.fpsFrames += 1;
320
- } else if (this.textures?.liveChanged()) {
321
- this.render();
322
- }
323
- if (now - this.fpsSince >= 500) {
324
- this.fps = this.playing ? Math.round(this.fpsFrames * 1e3 / (now - this.fpsSince || 1)) : 0;
325
- this.fpsFrames = 0;
326
- this.fpsSince = now;
327
- this.emit();
328
- }
329
- };
330
- emit() {
331
- this.onStats?.({
332
- time: this.time,
333
- fps: this.fps,
334
- playing: this.playing,
335
- width: this.canvas.width,
336
- height: this.canvas.height
337
- });
338
- }
339
- /** Pointer position in shader pixels (origin bottom-left, like gl_FragCoord). */
340
- toPixels(e) {
341
- const rect = this.canvas.getBoundingClientRect();
342
- return [
343
- (e.clientX - rect.left) * (this.canvas.width / rect.width),
344
- (rect.bottom - e.clientY) * (this.canvas.height / rect.height)
345
- ];
346
- }
347
- onPointerDown = (e) => {
348
- const [x, y] = this.toPixels(e);
349
- this.mouse = [x, y, x, y];
350
- this.pointer.down(x, y, this.time);
351
- if (!this.playing) this.render();
352
- };
353
- onPointerMove = (e) => {
354
- const [x, y] = this.toPixels(e);
355
- this.pointer.move(x, y, this.time);
356
- if (this.mouse[2] <= 0 || e.buttons === 0) {
357
- if (!this.playing) this.render();
358
- return;
359
- }
360
- this.mouse = [x, y, this.mouse[2], this.mouse[3]];
361
- if (!this.playing) this.render();
362
- };
363
- onPointerEnter = (e) => {
364
- this.pointer.move(...this.toPixels(e), this.time);
365
- this.pointer.hover(true, this.time);
366
- };
367
- onPointerLeave = () => {
368
- this.pointer.hover(false, this.time);
369
- };
370
- onPointerUp = () => {
371
- this.pointer.up(this.time);
372
- if (this.mouse[2] > 0)
373
- this.mouse = [this.mouse[0], this.mouse[1], -this.mouse[2], -Math.abs(this.mouse[3])];
374
- };
375
- onLost = (e) => {
376
- e.preventDefault();
377
- this.shader = null;
378
- this.textures?.dispose();
379
- this.textures = null;
380
- this.onContextLost?.(true);
381
- };
382
- onRestored = () => {
383
- resetGlCaches();
384
- this.gl = createContext(this.canvas, true, this.alpha);
385
- this.textures = this.gl && this.textureCache(this.gl);
386
- this.setSource(this.source);
387
- this.onContextLost?.(false);
388
- };
389
- };
390
-
391
33
  // src/shader/thumbnails.ts
392
34
  var ThumbnailPool = class {
393
35
  renderWidth;
@@ -623,126 +265,6 @@ function registerThumbnail(canvas, source, values, onStatus) {
623
265
  return sharedThumbnailPool().register(canvas, source, values, onStatus);
624
266
  }
625
267
 
626
- // src/shader/frames.ts
627
- var ShaderFrames = class {
628
- canvas;
629
- width;
630
- height;
631
- controls;
632
- gl;
633
- shader;
634
- textures;
635
- values;
636
- rendered = 0;
637
- lastTime = null;
638
- pixels;
639
- constructor(options) {
640
- const { source, values, width, height } = options;
641
- this.width = Math.max(1, Math.round(width));
642
- this.height = Math.max(1, Math.round(height));
643
- this.canvas = options.canvas ?? document.createElement("canvas");
644
- this.canvas.width = this.width;
645
- this.canvas.height = this.height;
646
- this.values = values;
647
- const gl = createContext(this.canvas, true, Boolean(options.alpha));
648
- if (!gl) throw new Error("WebGL2 is not available in this browser.");
649
- this.gl = gl;
650
- this.controls = parseControls(source);
651
- const compiled = compileShader(gl, source, this.controls);
652
- if (!compiled.ok) {
653
- this.dispose();
654
- throw new ShaderCompileError(compiled.errors);
655
- }
656
- this.shader = compiled.shader;
657
- this.textures = new TextureCache(gl);
658
- this.syncTextures();
659
- this.pixels = new Uint8Array(this.width * this.height * 4);
660
- }
661
- /** Changes control values for the frames that follow. */
662
- setValues(values) {
663
- this.values = values;
664
- this.syncTextures();
665
- }
666
- /**
667
- * Draws a `<video>` (or a canvas) in the image control `name` instead of
668
- * its URL, at the frame it is on when `render` runs; `null` goes back to
669
- * the URL. See `ShaderRenderer.setLiveSource`.
670
- */
671
- setLiveSource(name, source) {
672
- this.textures.setLive(name, source);
673
- }
674
- /**
675
- * Settles once every image the controls show has loaded, and every live
676
- * video has a frame. Rejects with an {@link ImageLoadError} when one
677
- * cannot be.
678
- */
679
- async ready() {
680
- await Promise.all([this.textures.ready(imageUrls(this.controls, this.values)), this.textures.liveReady()]);
681
- }
682
- /** Draws the frame at `time` seconds onto `canvas`. */
683
- render(time, options = {}) {
684
- const gl = this.context();
685
- const delta = options.delta ?? (this.lastTime === null ? 0 : time - this.lastTime);
686
- drawFrame(
687
- gl,
688
- this.shader,
689
- this.controls,
690
- this.values,
691
- {
692
- width: this.width,
693
- height: this.height,
694
- time,
695
- delta,
696
- frame: options.frame ?? this.rendered,
697
- mouse: options.mouse ?? [0, 0, 0, 0],
698
- pointer: options.pointer,
699
- clicks: options.clicks && clicksUniform(options.clicks, time),
700
- trail: options.trail && trailUniform(options.trail, time)
701
- },
702
- this.textures
703
- );
704
- this.rendered += 1;
705
- this.lastTime = time;
706
- }
707
- /**
708
- * The last rendered frame as RGBA bytes, rows top down (as in `ImageData`;
709
- * WebGL itself reads bottom up). Pass `out` to reuse a buffer of
710
- * `width * height * 4` bytes.
711
- */
712
- readPixels(out) {
713
- const gl = this.context();
714
- const { width, height } = this;
715
- const target = out ?? new Uint8Array(width * height * 4);
716
- if (target.length !== width * height * 4) {
717
- throw new RangeError(`readPixels needs a buffer of ${width * height * 4} bytes.`);
718
- }
719
- gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, this.pixels);
720
- const row = width * 4;
721
- for (let y = 0; y < height; y++) {
722
- target.set(this.pixels.subarray((height - 1 - y) * row, (height - y) * row), y * row);
723
- }
724
- return target;
725
- }
726
- /** Releases the GL context now rather than at garbage collection (browsers cap live contexts). */
727
- dispose() {
728
- if (!this.gl) return;
729
- if (this.shader) this.gl.deleteProgram(this.shader.program);
730
- this.textures?.dispose();
731
- this.gl.getExtension("WEBGL_lose_context")?.loseContext();
732
- this.gl = null;
733
- }
734
- syncTextures() {
735
- const urls = imageUrls(this.controls, this.values);
736
- this.textures.retain(urls);
737
- for (const url of urls) this.textures.request(url);
738
- }
739
- context() {
740
- if (!this.gl) throw new Error("These frames have been disposed.");
741
- if (this.gl.isContextLost()) throw new Error("The graphics context was lost.");
742
- return this.gl;
743
- }
744
- };
745
-
746
268
  // src/shader/gif.ts
747
269
  var yieldToBrowser = () => new Promise((resolve) => setTimeout(resolve, 0));
748
270
  async function recordGif(options) {
@@ -1229,6 +751,10 @@ function validateScene(scene) {
1229
751
  say(shotNum, "Camera perspective is the lens distance as a multiple of the stage width, 0.3 or more (1.2 by default).");
1230
752
  if (cam.motionBlur !== void 0 && !(typeof cam.motionBlur === "number" && cam.motionBlur >= 0 && cam.motionBlur <= 3))
1231
753
  say(shotNum, "Camera motionBlur is 0 (none) to 3 (a whip); 1 is natural.");
754
+ if (cam.transition !== void 0 && !CAMERA_TRANSITIONS.includes(cam.transition))
755
+ say(shotNum, `Camera transition is one of ${CAMERA_TRANSITIONS.map((t) => `"${t}"`).join(", ")}; leave it out to move.`);
756
+ else if (idx === 0 && (cam.transition === "whip" || cam.transition === "zoom"))
757
+ say(shotNum, `The first shot has no framing to ${cam.transition} from, so its camera starts in place; move the transition to a later shot or leave it out.`);
1232
758
  }
1233
759
  if (shot.hold !== void 0 && shot.hold < 0) {
1234
760
  say(shotNum, "Shot hold cannot be negative.");
@@ -1282,13 +808,13 @@ function planShots(scene, stage) {
1282
808
  const steps = Math.round(entry.moveMs / CAMERA_STEP_MS);
1283
809
  const move = Array.from({ length: Math.max(0, steps - 1) }, (_, i) => cameraAt(entry, (i + 1) * CAMERA_STEP_MS, stage));
1284
810
  move.push(entry.to);
1285
- return { move, elements: entry.elements };
811
+ return { move, moveMs: entry.moveMs, elements: entry.elements };
1286
812
  });
1287
813
  }
1288
814
  function standaloneSceneHtml(scene) {
1289
815
  const stage = sceneStage(scene);
1290
816
  const serializedScene = JSON.stringify(scene).replace(/</g, "\\u003c");
1291
- const serializedPlan = JSON.stringify({ stage, cameraMs: CAMERA_MS, shots: planShots(scene, stage) }).replace(/</g, "\\u003c");
817
+ const serializedPlan = JSON.stringify({ stage, shots: planShots(scene, stage) }).replace(/</g, "\\u003c");
1292
818
  const title = scene.title || scene.name;
1293
819
  return `<!DOCTYPE html>
1294
820
  <html lang="en">
@@ -1550,8 +1076,8 @@ function standaloneSceneHtml(scene) {
1550
1076
 
1551
1077
  // The camera at time 'now': a step of the current shot's move, or where it settled.
1552
1078
  function cameraNow(now) {
1553
- const move = plan.shots[currentShotIdx].move;
1554
- const u = Math.min(1, (now - moveFrom) / plan.cameraMs);
1079
+ const { move, moveMs } = plan.shots[currentShotIdx];
1080
+ const u = moveMs > 0 ? Math.min(1, (now - moveFrom) / moveMs) : 1;
1555
1081
  return move[Math.min(move.length - 1, Math.floor(u * move.length))];
1556
1082
  }
1557
1083
 
@@ -1603,10 +1129,10 @@ function standaloneSceneHtml(scene) {
1603
1129
 
1604
1130
  // Playing moves on to the next shot once this one's move and hold are over.
1605
1131
  const shot = scene.shots[currentShotIdx];
1606
- if (isPlaying && now - shotStartTime > plan.cameraMs + (shot.hold ?? 800) && currentShotIdx < scene.shots.length - 1) {
1132
+ if (isPlaying && now - shotStartTime > plan.shots[currentShotIdx].moveMs + (shot.hold ?? 800) && currentShotIdx < scene.shots.length - 1) {
1607
1133
  goTo(currentShotIdx + 1, true);
1608
1134
  }
1609
- if (isPlaying || now - moveFrom < plan.cameraMs) {
1135
+ if (isPlaying || now - moveFrom < plan.shots[currentShotIdx].moveMs) {
1610
1136
  requestAnimationFrame(draw);
1611
1137
  }
1612
1138
  }
@@ -1615,7 +1141,7 @@ function standaloneSceneHtml(scene) {
1615
1141
  function goTo(idx, animate) {
1616
1142
  currentShotIdx = idx;
1617
1143
  shotStartTime = performance.now();
1618
- moveFrom = animate ? shotStartTime : shotStartTime - plan.cameraMs;
1144
+ moveFrom = animate ? shotStartTime : shotStartTime - plan.shots[idx].moveMs;
1619
1145
  updateShotUI();
1620
1146
  requestAnimationFrame(draw);
1621
1147
  }
@@ -1625,7 +1151,7 @@ function standaloneSceneHtml(scene) {
1625
1151
  playBtn.textContent = isPlaying ? 'Pause' : 'Play';
1626
1152
  // The current shot counts as settled, so its hold runs from now.
1627
1153
  if (isPlaying) {
1628
- shotStartTime = performance.now() - plan.cameraMs;
1154
+ shotStartTime = performance.now() - plan.shots[currentShotIdx].moveMs;
1629
1155
  requestAnimationFrame(draw);
1630
1156
  }
1631
1157
  });
@@ -1649,11 +1175,9 @@ function escapeHtml2(str) {
1649
1175
  }
1650
1176
 
1651
1177
  export {
1652
- ShaderRenderer,
1653
1178
  ThumbnailPool,
1654
1179
  sharedThumbnailPool,
1655
1180
  registerThumbnail,
1656
- ShaderFrames,
1657
1181
  recordGif,
1658
1182
  standaloneHtml,
1659
1183
  ALL_BLEND_MODES,
@@ -1662,4 +1186,4 @@ export {
1662
1186
  validateScene,
1663
1187
  standaloneSceneHtml
1664
1188
  };
1665
- //# sourceMappingURL=chunk-NGKNI3QX.js.map
1189
+ //# sourceMappingURL=chunk-CK6EV37I.js.map