pixi-effects 0.28.2 → 0.30.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 (93) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +11 -10
  3. package/ai/chat-template.html +5 -5
  4. package/ai/tools/check.mjs +18 -3
  5. package/ai/tools/lookup.mjs +208 -0
  6. package/ai/tools/pixi-effects.mjs +16 -5
  7. package/ai/tools/render.mjs +14 -9
  8. package/ai/tools/view.mjs +6 -2
  9. package/dist/Base-CB1_FPZD.d.cts +60 -0
  10. package/dist/Base-Ne74ztNl.d.ts +60 -0
  11. package/dist/Composition-MUKR3FYD.cjs +16 -0
  12. package/dist/{Composition-K6B3APM5.cjs.map → Composition-MUKR3FYD.cjs.map} +1 -1
  13. package/dist/Composition-RJGA7KDU.js +7 -0
  14. package/dist/{Composition-H7UNR5F5.js.map → Composition-RJGA7KDU.js.map} +1 -1
  15. package/dist/Controller.cjs +73 -45
  16. package/dist/Controller.cjs.map +1 -1
  17. package/dist/Controller.d.cts +16 -22
  18. package/dist/Controller.d.ts +16 -22
  19. package/dist/Controller.js +74 -40
  20. package/dist/Controller.js.map +1 -1
  21. package/dist/{Movie-CHcM8HNB.d.ts → Movie-C2rZFkrO.d.ts} +227 -119
  22. package/dist/{Movie-Cx5ozUGb.d.cts → Movie-DdnpLpw1.d.cts} +227 -119
  23. package/dist/{blendModes-HDOP6R7T.cjs → blendModes-ETMYWATF.cjs} +5 -5
  24. package/dist/{blendModes-HDOP6R7T.cjs.map → blendModes-ETMYWATF.cjs.map} +1 -1
  25. package/dist/{blendModes-PAVTW3BO.js → blendModes-RIVZXL5A.js} +4 -4
  26. package/dist/{blendModes-PAVTW3BO.js.map → blendModes-RIVZXL5A.js.map} +1 -1
  27. package/dist/{chunk-P4GY3Z4G.cjs → chunk-2DXPTKQZ.cjs} +6 -6
  28. package/dist/chunk-2DXPTKQZ.cjs.map +1 -0
  29. package/dist/{chunk-TYWFKCPY.js → chunk-4KB3GUEK.js} +5 -5
  30. package/dist/chunk-4KB3GUEK.js.map +1 -0
  31. package/dist/{chunk-5MZWXVTT.js → chunk-A3Q3SDSH.js} +35 -28
  32. package/dist/chunk-A3Q3SDSH.js.map +1 -0
  33. package/dist/chunk-CRQFDY7E.js +4182 -0
  34. package/dist/chunk-CRQFDY7E.js.map +1 -0
  35. package/dist/chunk-HREBQAY4.cjs +4206 -0
  36. package/dist/chunk-HREBQAY4.cjs.map +1 -0
  37. package/dist/{chunk-ZMAD3KJU.cjs → chunk-LYOKH7KE.cjs} +22 -3
  38. package/dist/chunk-LYOKH7KE.cjs.map +1 -0
  39. package/dist/{chunk-45QKLD6M.js → chunk-RCVZX2JP.js} +22 -3
  40. package/dist/chunk-RCVZX2JP.js.map +1 -0
  41. package/dist/{chunk-KBH33IQX.cjs → chunk-WHNADXXG.cjs} +121 -114
  42. package/dist/chunk-WHNADXXG.cjs.map +1 -0
  43. package/dist/{chunk-7VN2KI3U.js → chunk-ZAXYPXKW.js} +14 -6
  44. package/dist/chunk-ZAXYPXKW.js.map +1 -0
  45. package/dist/{chunk-MJW7KILM.cjs → chunk-ZVUO325O.cjs} +32 -24
  46. package/dist/chunk-ZVUO325O.cjs.map +1 -0
  47. package/dist/index.cjs +106 -4482
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.cts +15 -60
  50. package/dist/index.d.ts +15 -60
  51. package/dist/index.js +6 -4457
  52. package/dist/index.js.map +1 -1
  53. package/dist/loader.css +1 -1
  54. package/dist/{Presenter.cjs → present.cjs} +370 -31
  55. package/dist/present.cjs.map +1 -0
  56. package/dist/present.d.cts +278 -0
  57. package/dist/present.d.ts +278 -0
  58. package/dist/{Presenter.js → present.js} +358 -32
  59. package/dist/present.js.map +1 -0
  60. package/dist/three.cjs +6 -6
  61. package/dist/three.d.cts +2 -2
  62. package/dist/three.d.ts +2 -2
  63. package/dist/three.js +2 -2
  64. package/dist/{types-DpK1vwcl.d.ts → types-BzIB65Vk.d.cts} +11 -265
  65. package/dist/{types-DpK1vwcl.d.cts → types-BzIB65Vk.d.ts} +11 -265
  66. package/docs/api.md +44 -77
  67. package/docs/dsl.md +16 -14
  68. package/llms-full.txt +121 -117
  69. package/llms.txt +3 -2
  70. package/package.json +5 -5
  71. package/skills/pixi-effects/SKILL.md +7 -7
  72. package/skills/pixi-effects/reference/cheatsheet.md +9 -9
  73. package/skills/pixi-effects/reference/direction.md +2 -0
  74. package/skills/pixi-effects/reference/pitfalls.md +6 -6
  75. package/skills/pixi-effects/reference/quickstart.md +29 -0
  76. package/skills/pixi-effects/reference/recipes.md +4 -4
  77. package/skills/pixi-effects/template.html +7 -7
  78. package/dist/Base-C6f0fE8j.d.ts +0 -109
  79. package/dist/Base-DI1FCGcH.d.cts +0 -109
  80. package/dist/Composition-H7UNR5F5.js +0 -7
  81. package/dist/Composition-K6B3APM5.cjs +0 -16
  82. package/dist/Presenter.cjs.map +0 -1
  83. package/dist/Presenter.d.cts +0 -122
  84. package/dist/Presenter.d.ts +0 -122
  85. package/dist/Presenter.js.map +0 -1
  86. package/dist/chunk-45QKLD6M.js.map +0 -1
  87. package/dist/chunk-5MZWXVTT.js.map +0 -1
  88. package/dist/chunk-7VN2KI3U.js.map +0 -1
  89. package/dist/chunk-KBH33IQX.cjs.map +0 -1
  90. package/dist/chunk-MJW7KILM.cjs.map +0 -1
  91. package/dist/chunk-P4GY3Z4G.cjs.map +0 -1
  92. package/dist/chunk-TYWFKCPY.js.map +0 -1
  93. package/dist/chunk-ZMAD3KJU.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,4459 +1,8 @@
1
- import { CompositionSequence, maskSourceOf } from './chunk-5MZWXVTT.js';
2
- import { createTimeline, lengthen, ensureFilterLibrary, checkEase, parseEase, homeDistance, DEFAULT_FOV, random, noise, flattenSvgPath, rand, DEG, timeRemapOf, contentLength, resolveAt } from './chunk-7VN2KI3U.js';
3
- export { Sequence, noise, rand, random, registerSequenceType } from './chunk-7VN2KI3U.js';
4
- import { usesAdvancedBlend, enableAdvancedBlend } from './chunk-TYWFKCPY.js';
5
- import { collectPropKeys } from './chunk-45QKLD6M.js';
6
- import { warnUnknownOptions, suggestName } from './chunk-BXOCEZ4U.js';
7
- import { extensions, CullerPlugin, Application, Container, Rectangle, Culler, CanvasTextMetrics, TextStyle, Assets, ExtensionType, Filter, Texture, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
8
- import { Output, BufferTarget, CanvasSource, AudioBufferSource, Input, ALL_FORMATS, BlobSource, WavOutputFormat, Conversion, VideoSampleSink, OggOutputFormat, QUALITY_VERY_HIGH, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_VERY_LOW, MkvOutputFormat, WebMOutputFormat, MovOutputFormat, Mp4OutputFormat, canEncodeVideo, getFirstEncodableAudioCodec } from 'mediabunny';
9
-
10
- var registered = false;
11
- function ensureLoadersRegistered(audioContext) {
12
- if (registered) return;
13
- registered = true;
14
- extensions.add({
15
- extension: { type: ExtensionType.LoadParser, name: "pixi-effects-audio" },
16
- test: (url) => /\.(mp3|wav|ogg)$/i.test(url),
17
- async load(url) {
18
- const res = await fetch(url);
19
- if (!res.ok) throw new Error(`pixi-effects: failed to load ${url}: ${res.statusText}`);
20
- const buffer = await res.arrayBuffer();
21
- const audioBuffer = await audioContext.decodeAudioData(buffer);
22
- return { audioBuffer, duration: audioBuffer.duration };
23
- }
24
- });
25
- extensions.add({
26
- extension: { type: ExtensionType.LoadParser, name: "pixi-effects-video", priority: 1 },
27
- test: (url) => /\.(mp4|webm|mov|mkv)$/i.test(url),
28
- async load(url) {
29
- const blob = await fetch(url).then((r) => r.blob());
30
- const input = new Input({ source: new BlobSource(blob), formats: ALL_FORMATS });
31
- const videoTrack = await input.getPrimaryVideoTrack();
32
- if (!videoTrack || !await videoTrack.canDecode()) {
33
- throw new Error("pixi-effects: video track cannot be decoded");
34
- }
35
- const audioTrack = await input.getPrimaryAudioTrack();
36
- const videoDuration = await videoTrack.computeDuration();
37
- const audioDuration = audioTrack ? await audioTrack.computeDuration() : 0;
38
- let audioBuffer = null;
39
- if (audioTrack) {
40
- try {
41
- const out = new Output({ format: new WavOutputFormat(), target: new BufferTarget() });
42
- const conv = await Conversion.init({ input, output: out, video: { discard: true } });
43
- await conv.execute();
44
- audioBuffer = await audioContext.decodeAudioData(out.target.buffer);
45
- } catch (e) {
46
- console.warn("pixi-effects: audio extraction failed:", e);
47
- }
48
- }
49
- return {
50
- videoTrack,
51
- audioBuffer,
52
- videoDuration,
53
- audioDuration,
54
- duration: Math.max(videoDuration, audioDuration),
55
- sink: new VideoSampleSink(videoTrack)
56
- };
57
- }
58
- });
59
- }
60
- async function loadAssetBundle(assets, audioContext, onProgress) {
61
- ensureLoadersRegistered(audioContext);
62
- const bundle = assets.map((a) => ({ alias: a.name, src: a.src }));
63
- Assets.addBundle("pixi-effects", bundle);
64
- return await Assets.loadBundle("pixi-effects", onProgress);
65
- }
66
-
67
- // src/core/audioRemap.ts
68
- function gainAt(a, u) {
69
- let prevT = a.start, prevV = a.initialVolume ?? 1;
70
- if (u <= prevT) return prevV;
71
- for (const kf of a.volumeKeyframes ?? []) {
72
- if (u <= kf.time) return kf.time === prevT ? kf.value : prevV + (kf.value - prevV) * ((u - prevT) / (kf.time - prevT));
73
- prevT = kf.time;
74
- prevV = kf.value;
75
- }
76
- return prevV;
77
- }
78
- var CELL = 1 / 1e3;
79
- function resampleThrough(a, left, right, srcRate, outRate, total) {
80
- const nCell = Math.ceil(total / CELL) + 2;
81
- const U = new Float64Array(nCell);
82
- let first = -1, last = -1;
83
- for (let k = 0; k < nCell; k++) {
84
- const u = a.warp ? a.warp(k * CELL) : k * CELL;
85
- U[k] = u;
86
- if (Number.isFinite(u) && u >= a.start && u < a.end) {
87
- if (first < 0) first = k;
88
- last = k;
89
- }
90
- }
91
- if (first < 0) return null;
92
- const lo = Math.max(0, (first - 1) * CELL), hi = Math.min(total, (last + 2) * CELL);
93
- const n = Math.max(1, Math.ceil((hi - lo) * outRate));
94
- const L = new Float32Array(n), R = new Float32Array(n);
95
- const dur = left.length / srcRate;
96
- let prevPos = NaN;
97
- for (let i = 0; i < n; i++) {
98
- const T = lo + i / outRate;
99
- const x = T / CELL, k = Math.min(Math.floor(x), nCell - 2), f = x - k;
100
- const u0 = U[k], u1 = U[k + 1];
101
- if (!(Number.isFinite(u0) && Number.isFinite(u1))) {
102
- prevPos = NaN;
103
- continue;
104
- }
105
- const u = u0 * (1 - f) + u1 * f;
106
- if (u < a.start || u >= a.end) {
107
- prevPos = NaN;
108
- continue;
109
- }
110
- const raw = a.sourceMap ? a.sourceMap(u) : u - a.start;
111
- let pos = raw;
112
- if (a.loop) pos = (pos % dur + dur) % dur;
113
- const rate = Number.isNaN(prevPos) ? 1 : Math.abs(raw - prevPos) * outRate;
114
- prevPos = raw;
115
- if (pos < 0 || pos >= dur) continue;
116
- const g = gainAt(a, u) * Math.min(1, rate / 0.02);
117
- const sp = pos * srcRate, j = Math.floor(sp), fr = sp - j, j1 = Math.min(j + 1, left.length - 1);
118
- L[i] = (left[j] * (1 - fr) + left[j1] * fr) * g;
119
- R[i] = (right[j] * (1 - fr) + right[j1] * fr) * g;
120
- }
121
- return { L, R, at: lo };
122
- }
123
- function playSpan(a, total) {
124
- if (!a.warp) return { start: a.start, end: a.end };
125
- let first = -1, last = -1;
126
- const n = Math.ceil(total / CELL) + 1;
127
- for (let k = 0; k <= n; k++) {
128
- const u = a.warp(k * CELL);
129
- if (Number.isFinite(u) && u >= a.start && u < a.end) {
130
- if (first < 0) first = k;
131
- last = k;
132
- }
133
- }
134
- return first < 0 ? { start: total, end: total } : { start: first * CELL, end: (last + 1) * CELL };
135
- }
136
- function clockReach(a, total) {
137
- let reach = -Infinity;
138
- const n = Math.ceil(total / CELL) + 1;
139
- for (let k = 0; k <= n; k++) {
140
- const u = a.warp ? a.warp(k * CELL) : k * CELL;
141
- if (Number.isFinite(u) && u > reach) reach = u;
142
- }
143
- return reach;
144
- }
145
-
146
- // src/core/AudioMixer.ts
147
- async function mixdown(audios, totalDuration, sampleRate = 44100, pace) {
148
- if (audios.length === 0) return null;
149
- const weight = (a) => a.synth ? Math.max(1, a.end - a.start) : 0.2;
150
- const all = audios.reduce((s, a) => s + weight(a), 0) + 0.5;
151
- let made = 0;
152
- const ctx = new OfflineAudioContext(2, Math.ceil(sampleRate * totalDuration), sampleRate);
153
- const synthesised = /* @__PURE__ */ new Map();
154
- const bufferOf = async (a, inner) => {
155
- if (a.buffer) return a.buffer;
156
- if (!a.synth) return null;
157
- let buf = synthesised.get(a.synth.key);
158
- if (!buf) {
159
- const [left, right] = a.synth.renderAsync ? await a.synth.renderAsync(sampleRate, inner) : a.synth.render(sampleRate);
160
- buf = ctx.createBuffer(2, left.length, sampleRate);
161
- buf.getChannelData(0).set(left);
162
- buf.getChannelData(1).set(right);
163
- synthesised.set(a.synth.key, buf);
164
- }
165
- return buf;
166
- };
167
- for (const a of audios) {
168
- const w = weight(a), base = made;
169
- const buffer = await bufferOf(a, pace ? (f) => pace((base + w * f) / all) : void 0);
170
- made += w;
171
- if (pace) await pace(made / all);
172
- if (!buffer) continue;
173
- if (a.warp || a.sourceMap) {
174
- const r = resampleThrough(a, buffer.getChannelData(0), buffer.numberOfChannels > 1 ? buffer.getChannelData(1) : buffer.getChannelData(0), buffer.sampleRate, sampleRate, totalDuration);
175
- if (!r) continue;
176
- const out = ctx.createBuffer(2, r.L.length, sampleRate);
177
- out.getChannelData(0).set(r.L);
178
- out.getChannelData(1).set(r.R);
179
- const rendered = ctx.createBufferSource();
180
- rendered.buffer = out;
181
- rendered.connect(ctx.destination);
182
- rendered.start(r.at);
183
- continue;
184
- }
185
- const begin = Math.max(0, a.start);
186
- const skipped = begin - a.start;
187
- if (skipped > 0) {
188
- console.warn(
189
- `pixi-effects: ${a.layer ?? "an audio layer"} starts at ${a.start.toFixed(2)}s, before the movie starts, so its first ${skipped.toFixed(2)}s are cut off. Start it at 0 or later.`
190
- );
191
- }
192
- const src = ctx.createBufferSource();
193
- src.buffer = buffer;
194
- src.loop = !!a.loop;
195
- const gain = ctx.createGain();
196
- gain.gain.setValueAtTime(a.initialVolume ?? 1, begin);
197
- for (const kf of a.volumeKeyframes ?? []) {
198
- gain.gain.linearRampToValueAtTime(kf.value, Math.max(0, kf.time));
199
- }
200
- src.connect(gain).connect(ctx.destination);
201
- src.start(begin, skipped);
202
- src.stop(Math.max(begin, a.end));
203
- }
204
- return await ctx.startRendering();
205
- }
206
- var KNEE = 0.9;
207
- function limitMix(buffer, audios) {
208
- let peak = 0;
209
- let peakIndex = 0;
210
- for (let ch = 0; ch < buffer.numberOfChannels; ch++) {
211
- const d = buffer.getChannelData(ch);
212
- for (let i = 0; i < d.length; i++) {
213
- const x = Math.abs(d[i]);
214
- if (x > peak) {
215
- peak = x;
216
- peakIndex = i;
217
- }
218
- if (x > KNEE) d[i] = Math.sign(d[i]) * (KNEE + (1 - KNEE) * Math.tanh((x - KNEE) / (1 - KNEE)));
219
- }
220
- }
221
- const peakAt = peakIndex / buffer.sampleRate;
222
- if (peak > 1) {
223
- const playing = audios.map((a) => ({ a, ...playSpan(a, buffer.duration) })).filter((s) => s.start <= peakAt && peakAt < s.end).map((s) => s.a.layer ?? "an audio layer");
224
- console.warn(
225
- `pixi-effects: the audio mix peaks at ${peak.toFixed(2)} (${(20 * Math.log10(peak)).toFixed(1)} dBFS) at ${peakAt.toFixed(2)}s, so it was limited (it would distort). Playing there: ${[...new Set(playing)].join(", ")}. Lower their volume.`
226
- );
227
- }
228
- return { peak, peakAt };
229
- }
230
-
231
- // src/core/spectrum.ts
232
- function fft(re, im) {
233
- const n = re.length;
234
- for (let i = 1, j = 0; i < n; i++) {
235
- let bit = n >> 1;
236
- for (; j & bit; bit >>= 1) j ^= bit;
237
- j ^= bit;
238
- if (i < j) {
239
- [re[i], re[j]] = [re[j], re[i]];
240
- [im[i], im[j]] = [im[j], im[i]];
241
- }
242
- }
243
- for (let len = 2; len <= n; len <<= 1) {
244
- const ang = -2 * Math.PI / len;
245
- for (let i = 0; i < n; i += len) {
246
- for (let k = 0; k < len / 2; k++) {
247
- const wr = Math.cos(ang * k), wi = Math.sin(ang * k);
248
- const a = i + k, b = a + len / 2;
249
- const xr = re[b] * wr - im[b] * wi, xi = re[b] * wi + im[b] * wr;
250
- re[b] = re[a] - xr;
251
- im[b] = im[a] - xi;
252
- re[a] = re[a] + xr;
253
- im[a] = im[a] + xi;
254
- }
255
- }
256
- }
257
- }
258
- var FRAME = 2048;
259
- function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
260
- let num3 = 0;
261
- let den = 0;
262
- const re = new Float64Array(FRAME);
263
- const im = new Float64Array(FRAME);
264
- const end = Math.min(to, samples.length);
265
- for (let start = Math.max(0, from); start < end; start += FRAME / 2) {
266
- for (let i = 0; i < FRAME; i++) {
267
- const k = start + i;
268
- const w = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / (FRAME - 1));
269
- re[i] = k < end ? samples[k] * w : 0;
270
- im[i] = 0;
271
- }
272
- fft(re, im);
273
- for (let b = 1; b < FRAME / 2; b++) {
274
- const power = re[b] * re[b] + im[b] * im[b];
275
- num3 += power * (b * sampleRate / FRAME);
276
- den += power;
277
- }
278
- }
279
- return den > 1e-9 ? Math.round(num3 / den / 10) * 10 : 0;
280
- }
281
-
282
- // src/audio/loudness.ts
283
- var LOUDNESS_NOTES = { quietBelow: -24, loudAbove: -9, headroomDb: -1 };
284
- function kWeighting(fs) {
285
- const f0 = 1681.974450955533, G = 3.999843853973347, Q = 0.7071752369554196;
286
- const K = Math.tan(Math.PI * f0 / fs), Vh = 10 ** (G / 20), Vb = Vh ** 0.4996667741545416;
287
- const a0 = 1 + K / Q + K * K;
288
- const shelf = {
289
- b: [(Vh + Vb * K / Q + K * K) / a0, 2 * (K * K - Vh) / a0, (Vh - Vb * K / Q + K * K) / a0],
290
- a: [1, 2 * (K * K - 1) / a0, (1 - K / Q + K * K) / a0]
291
- };
292
- const f1 = 38.13547087602444, Q1 = 0.5003270373238773, K1 = Math.tan(Math.PI * f1 / fs), a1 = 1 + K1 / Q1 + K1 * K1;
293
- const high = { b: [1, -2, 1], a: [1, 2 * (K1 * K1 - 1) / a1, (1 - K1 / Q1 + K1 * K1) / a1] };
294
- return [shelf, high];
295
- }
296
- function filter(x, stages) {
297
- let cur = x;
298
- for (const { b, a } of stages) {
299
- const out = new Float32Array(cur.length);
300
- let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
301
- for (let i = 0; i < cur.length; i++) {
302
- const xi = cur[i], y = b[0] * xi + b[1] * x1 + b[2] * x2 - a[1] * y1 - a[2] * y2;
303
- x2 = x1;
304
- x1 = xi;
305
- y2 = y1;
306
- y1 = y;
307
- out[i] = y;
308
- }
309
- cur = out;
310
- }
311
- return cur;
312
- }
313
- var toDb = (x) => Math.round(200 * Math.log10(Math.max(x, 1e-6))) / 10;
314
- var TP_HALF = 12;
315
- var TP_BLOCK = 32;
316
- var TP_KERNELS = [0.25, 0.5, 0.75].map((p) => Float64Array.from({ length: 2 * TP_HALF }, (_, j) => {
317
- const k = j - TP_HALF + 1, x = k - p, s = x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
318
- return s * (0.5 + 0.5 * Math.cos(Math.PI * x / (TP_HALF + 1)));
319
- }));
320
- var TP_GAIN = Math.max(...TP_KERNELS.map((k) => k.reduce((a, v) => a + Math.abs(v), 0)));
321
- function truePeak(chans) {
322
- let peak = 0;
323
- for (const c of chans) {
324
- const n = c.length;
325
- const blocks = Math.ceil(n / TP_BLOCK), blockMax = new Float32Array(blocks + 2);
326
- for (let i = 0; i < n; i++) {
327
- const a = Math.abs(c[i]), b = (i / TP_BLOCK | 0) + 1;
328
- if (a > blockMax[b]) blockMax[b] = a;
329
- if (a > peak) peak = a;
330
- }
331
- const padded = new Float32Array(n + 2 * TP_HALF);
332
- padded.set(c, TP_HALF);
333
- for (let i = 0; i < n; i++) {
334
- const b = (i / TP_BLOCK | 0) + 1;
335
- const local = Math.max(blockMax[b - 1], blockMax[b], blockMax[b + 1]);
336
- if (local * TP_GAIN <= peak) continue;
337
- for (let pi = 0; pi < 3; pi++) {
338
- const k = TP_KERNELS[pi];
339
- let v = 0;
340
- for (let j = 0; j < 2 * TP_HALF; j++) v += padded[i + 1 + j] * k[j];
341
- const a = Math.abs(v);
342
- if (a > peak) peak = a;
343
- }
344
- }
345
- }
346
- return peak;
347
- }
348
- function measureLoudness(channels, sampleRate, opts = {}) {
349
- let samplePeak = 0, clipped = 0;
350
- for (const c of channels) for (let i = 0; i < c.length; i++) {
351
- const a = Math.abs(c[i]);
352
- if (a > samplePeak) samplePeak = a;
353
- if (a >= 1) clipped++;
354
- }
355
- const stages = kWeighting(sampleRate);
356
- const weighted = channels.map((c) => filter(c, stages));
357
- const block = Math.round(0.4 * sampleRate), hop = Math.round(0.1 * sampleRate);
358
- const energies = [];
359
- for (let s = 0; s + block <= (weighted[0]?.length ?? 0); s += hop) {
360
- let sum = 0;
361
- for (const w of weighted) {
362
- let z = 0;
363
- for (let i = s; i < s + block; i++) z += w[i] * w[i];
364
- sum += z / block;
365
- }
366
- energies.push(sum);
367
- }
368
- const lufs = (e) => -0.691 + 10 * Math.log10(e);
369
- const absolute = energies.filter((e) => e > 0 && lufs(e) > -70);
370
- let integrated = null;
371
- if (absolute.length) {
372
- const relative = lufs(absolute.reduce((a, b) => a + b, 0) / absolute.length) - 10;
373
- const gated = absolute.filter((e) => lufs(e) > relative);
374
- if (gated.length) integrated = Math.round(10 * lufs(gated.reduce((a, b) => a + b, 0) / gated.length)) / 10;
375
- }
376
- return { integratedLufs: integrated, truePeakDb: opts.truePeak === false ? null : toDb(truePeak(channels)), samplePeakDb: toDb(samplePeak), clippedSamples: clipped };
377
- }
378
- function findSilences(channels, sampleRate, opts = {}) {
379
- const min = Math.round((opts.minLength ?? 1) * sampleRate), limit = 10 ** ((opts.thresholdDb ?? -60) / 20);
380
- const n = channels[0]?.length ?? 0, out = [];
381
- let start = -1;
382
- const quiet = (i) => channels.every((c) => Math.abs(c[i]) < limit);
383
- for (let i = 0; i <= n; i++) {
384
- const q = i < n && quiet(i);
385
- if (q && start < 0) start = i;
386
- if (!q && start >= 0) {
387
- if (i - start >= min) out.push({ from: Math.round(start / sampleRate * 1e3) / 1e3, to: Math.round(i / sampleRate * 1e3) / 1e3 });
388
- start = -1;
389
- }
390
- }
391
- return out;
392
- }
393
-
394
- // src/core/inspectAudio.ts
395
- var INAUDIBLE_DB = -50;
396
- var db = (x) => Math.round(10 * Math.max(-120, 20 * Math.log10(x))) / 10;
397
- var mono = (chans) => {
398
- if (chans.length === 1) return chans[0];
399
- const out = new Float32Array(chans[0].length);
400
- for (const d of chans) for (let i = 0; i < out.length; i++) out[i] = out[i] + d[i] / chans.length;
401
- return out;
402
- };
403
- function measure(chans, sampleRate) {
404
- let peak = 0;
405
- let at = 0;
406
- for (const d of chans) for (let i = 0; i < d.length; i++) if (Math.abs(d[i]) > peak) {
407
- peak = Math.abs(d[i]);
408
- at = i;
409
- }
410
- return {
411
- length: Math.round(chans[0].length / sampleRate * 1e3) / 1e3,
412
- peakDb: db(peak),
413
- loudestAt: Math.round(at / sampleRate * 1e3) / 1e3,
414
- brightnessHz: spectralCentroid(mono(chans), sampleRate)
415
- };
416
- }
417
- function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
418
- const warped = /* @__PURE__ */ new Map();
419
- sources = sources.map((s) => {
420
- if (!s.warp) return s;
421
- const moved = { ...s, ...playSpan(s, movieDuration) };
422
- warped.set(moved, { localEnd: s.end, reach: clockReach(s, movieDuration) });
423
- return moved;
424
- });
425
- if (!mix) {
426
- return {
427
- duration: movieDuration,
428
- sampleRate: 0,
429
- peakDb: -120,
430
- peakAt: 0,
431
- sources: [],
432
- windows: [],
433
- loudness: { integratedLufs: null, truePeakDb: -120, clippedSamples: 0, silences: [] },
434
- scenes: [],
435
- cues: [],
436
- notes: [],
437
- issues: ['no audio: the movie has no audio layers (sound effects need no files: { type: "audio", sfx: "pop", at: 1 })']
438
- };
439
- }
440
- const sr = mix.sampleRate;
441
- const chans = Array.from({ length: mix.numberOfChannels }, (_, c) => mix.getChannelData(c));
442
- const mixMono = mono(chans);
443
- const peakIn = (t0, t1) => {
444
- let p = 0;
445
- const i1 = Math.min(mix.length, Math.ceil(t1 * sr));
446
- for (const d of chans) for (let i = Math.max(0, Math.floor(t0 * sr)); i < i1; i++) p = Math.max(p, Math.abs(d[i]));
447
- return p;
448
- };
449
- const win = opts.window && opts.window > 0 ? Math.max(0.01, opts.window) : 0.1;
450
- const windows2 = [];
451
- for (let t = 0; t < movieDuration - 1e-9; t += win) {
452
- const i0 = Math.floor(t * sr);
453
- const i1 = Math.min(mix.length, Math.floor((t + win) * sr));
454
- let sum = 0;
455
- let peak = 0;
456
- for (const d of chans) {
457
- for (let i = i0; i < i1; i++) {
458
- sum += d[i] * d[i];
459
- peak = Math.max(peak, Math.abs(d[i]));
460
- }
461
- }
462
- const n = Math.max(1, (i1 - i0) * chans.length);
463
- windows2.push({ t: Math.round(t * 1e3) / 1e3, rmsDb: db(Math.sqrt(sum / n)), peakDb: db(peak), brightnessHz: spectralCentroid(mixMono, sr, i0, i1) });
464
- }
465
- const issues = [];
466
- if (stats && stats.peak > 1) {
467
- issues.push(`the mix peaks at ${db(stats.peak)} dBFS at ${stats.peakAt.toFixed(2)}s and was limited \u2014 lower the volume of the layers playing there`);
468
- }
469
- const rendered = /* @__PURE__ */ new Map();
470
- const soundOf = (s) => {
471
- if (s.synth) {
472
- let m = rendered.get(s.synth.key);
473
- if (!m) rendered.set(s.synth.key, m = measure(s.synth.render(sr), sr));
474
- return m;
475
- }
476
- const b = s.buffer;
477
- return measure(Array.from({ length: b.numberOfChannels }, (_, c) => b.getChannelData(c)), b.sampleRate);
478
- };
479
- const list = [...sources].sort((a, b) => a.start - b.start).map((s) => {
480
- const layer = s.layer ?? "an audio layer";
481
- const source = s.source ?? "audio";
482
- const peakDb = db(peakIn(s.start, Math.min(s.end, movieDuration)));
483
- const sound = soundOf(s);
484
- const topVolume = Math.max(s.initialVolume ?? 1, ...(s.volumeKeyframes ?? []).map((k) => k.value));
485
- const ownDb = db(Math.pow(10, sound.peakDb / 20) * Math.max(0, topVolume));
486
- if (s.start >= movieDuration) {
487
- issues.push(`${layer} (${source}) starts at ${s.start.toFixed(2)}s, after the movie ends (${movieDuration}s)`);
488
- } else if (ownDb < INAUDIBLE_DB || peakDb < INAUDIBLE_DB) {
489
- issues.push(`${layer} (${source}) is inaudible from ${s.start.toFixed(2)}s to ${s.end.toFixed(2)}s (its own peak ${ownDb} dBFS at volume ${topVolume}; mix peak there ${peakDb} dBFS) \u2014 check its volume`);
490
- }
491
- const clock = warped.get(s);
492
- if (s.synth && s.start < movieDuration) {
493
- if (clock) {
494
- if (clock.reach < clock.localEnd - 1e-3) {
495
- issues.push(`${layer} (${source}) is cut off: its composition's clock only reaches ${clock.reach.toFixed(2)}s (the sound is ${clock.localEnd.toFixed(2)}s long), because the clock stops or the movie ends first \u2014 give the layer a shorter duration, or let the clock run on`);
496
- }
497
- } else if (s.end > movieDuration + 1e-6) {
498
- issues.push(`${layer} (${source}) is cut off by the end of the movie at ${movieDuration}s (it runs to ${s.end.toFixed(2)}s)`);
499
- }
500
- }
501
- return { layer, source, start: s.start, end: s.end, peakDb, sound, ...s.music ? { music: s.music } : {} };
502
- });
503
- const whole = measureLoudness(chans, sr);
504
- const silences = findSilences(chans, sr, { minLength: 1 });
505
- if (whole.clippedSamples > 0) issues.push(`${whole.clippedSamples} sample(s) of the mix are at full scale (clipped) \u2014 lower the volume of the layers that play loudest`);
506
- if (whole.truePeakDb > 0) issues.push(`the mix's true peak is ${whole.truePeakDb} dBTP: it will distort when the video is encoded \u2014 lower the volume`);
507
- const notes = [];
508
- if (whole.integratedLufs !== null) {
509
- if (whole.integratedLufs < LOUDNESS_NOTES.quietBelow) notes.push(`the mix is quiet for web video: ${whole.integratedLufs} LUFS (typical \u221214 to \u221216); raise the volume if that is not on purpose`);
510
- else if (whole.integratedLufs > LOUDNESS_NOTES.loudAbove) notes.push(`the mix is loud: ${whole.integratedLufs} LUFS (typical web video \u221214 to \u221216)`);
511
- }
512
- if (whole.truePeakDb > LOUDNESS_NOTES.headroomDb && whole.truePeakDb <= 0) notes.push(`the true peak is ${whole.truePeakDb} dBTP: little headroom left for encoding (keep it below ${LOUDNESS_NOTES.headroomDb} dBTP)`);
513
- for (const q of silences.slice(0, 3)) notes.push(`the mix is silent from ${q.from} s to ${q.to} s (${Math.round((q.to - q.from) * 10) / 10} s)`);
514
- if (silences.length > 3) notes.push(`and ${silences.length - 3} more silent stretch(es) (loudness.silences lists them all)`);
515
- const sceneList = (opts.scenes && opts.scenes.length ? opts.scenes : [{ name: "movie", start: 0, end: movieDuration }]).map((sc) => {
516
- const a = Math.max(0, Math.round(sc.start * sr)), b = Math.min(mix.length, Math.round(sc.end * sr));
517
- const part = chans.map((c) => c.subarray(a, Math.max(a, b)));
518
- const m = measureLoudness(part, sr, { truePeak: false });
519
- return { name: sc.name, from: sc.start, to: sc.end, lufs: m.integratedLufs, peakDb: m.samplePeakDb, silent: m.samplePeakDb < -60 };
520
- });
521
- return {
522
- duration: movieDuration,
523
- sampleRate: sr,
524
- peakDb: db(stats?.peak ?? peakIn(0, movieDuration)),
525
- peakAt: stats?.peakAt ?? 0,
526
- sources: list,
527
- windows: windows2,
528
- issues,
529
- loudness: { integratedLufs: whole.integratedLufs, truePeakDb: whole.truePeakDb ?? -120, clippedSamples: whole.clippedSamples, silences },
530
- scenes: sceneList,
531
- cues: list.map((s) => ({ t: s.start, name: `${s.layer} (${s.source})` })),
532
- notes
533
- };
534
- }
535
-
536
- // src/text/fonts.ts
537
- var GENERIC = /* @__PURE__ */ new Set(["serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", "ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded", "emoji", "math", "fangsong", "-apple-system", "blinkmacsystemfont"]);
538
- function fontAvailable(family2) {
539
- const f = family2.trim().replace(/^["']|["']$/g, "");
540
- if (!f || GENERIC.has(f.toLowerCase())) return true;
541
- const g = document.createElement("canvas").getContext("2d");
542
- if (!g) return true;
543
- const sample = "mmmmmmmmmmlliWWWW 0123 \u3042\u3044\u3046";
544
- const width = (stack) => {
545
- g.font = `72px ${stack}`;
546
- return g.measureText(sample).width;
547
- };
548
- const q = JSON.stringify(f);
549
- return width(`${q}, monospace`) !== width("monospace") || width(`${q}, serif`) !== width("serif");
550
- }
551
-
552
- // src/core/inspectFonts.ts
553
- function inspectFonts(root) {
554
- const failed = /* @__PURE__ */ new Set();
555
- if (typeof document !== "undefined" && document.fonts) document.fonts.forEach((face) => {
556
- if (face.status === "error") failed.add(face.family.replace(/^["']|["']$/g, ""));
557
- });
558
- const missing = [];
559
- const used = /* @__PURE__ */ new Set();
560
- const walk2 = (seq, index) => {
561
- const spec = seq.spec;
562
- if (spec.type === "text") {
563
- const raw = spec.style?.fontFamily;
564
- const family2 = Array.isArray(raw) ? raw.join(", ") : typeof raw === "string" ? raw : "";
565
- const list = family2.split(",").map((f) => f.trim()).filter(Boolean);
566
- for (const f of list) used.add(f.replace(/^["']|["']$/g, "").toLowerCase());
567
- if (list.length && !list.some(fontAvailable) && !list.some((f) => failed.has(f.replace(/^["']|["']$/g, "")))) missing.push({ layer: spec.name ?? `text#${index}`, family: family2 });
568
- }
569
- if (seq.maskSequence) walk2(seq.maskSequence, index);
570
- (seq._children ?? []).forEach((child, i) => walk2(child, i));
571
- };
572
- walk2(root, 0);
573
- const all = [...failed];
574
- return { missing, failed: all.filter((f) => used.has(f.toLowerCase())), failedUnused: all.filter((f) => !used.has(f.toLowerCase())) };
575
- }
576
-
577
- // src/core/scenes.ts
578
- function namedScenes(rows) {
579
- return rows.filter((r) => !r.local && r.depth === 0 && r.type === "composition" && !/[#×]/.test(r.name) && r.end - r.start >= 1).map((r) => ({ name: r.name, start: r.start, end: r.end }));
580
- }
581
-
582
- // src/core/timeFilters.ts
583
- function collectTimeFilters(seq, out = [], clock = null) {
584
- for (const f of seq.filters ?? []) {
585
- if (typeof f.setTime === "function") {
586
- const tf = f;
587
- out.push(clock ? { setTime: () => tf.setTime(clock()) } : tf);
588
- }
589
- }
590
- if (seq.maskSequence) collectTimeFilters(seq.maskSequence, out, clock);
591
- const own = seq.localClock?.() ?? clock;
592
- for (const child of seq._children ?? []) collectTimeFilters(child, out, own);
593
- return out;
594
- }
595
-
596
- // src/core/review.ts
597
- function sampleFrames({ totalFrames, frameRate, scenes = [], step = 0.25, cap = 240 }) {
598
- const last = totalFrames;
599
- const clamp2 = (f) => Math.max(0, Math.min(last, Math.round(f)));
600
- const edges = /* @__PURE__ */ new Set([0, last]);
601
- for (const s of scenes) {
602
- edges.add(clamp2(s.start * frameRate));
603
- edges.add(clamp2(s.end * frameRate - 1));
604
- }
605
- const room = Math.max(0, cap - edges.size);
606
- const seconds = totalFrames / frameRate;
607
- const wanted = Math.floor(seconds / step) + 1;
608
- const stride = wanted <= room ? step : seconds / Math.max(1, room);
609
- const frames = new Set(edges);
610
- for (let t = stride; frames.size < cap && t < seconds; t += stride) frames.add(clamp2(t * frameRate));
611
- return [...frames].sort((a, b) => a - b);
612
- }
613
- var AT_HELP = "use seconds (3.5), a percentage (50%), a frame (f120) or a layer's start / mid / end (name@end)";
614
- function resolveAtList(list, ctx) {
615
- const { frameRate: fps, totalFrames, duration, rows } = ctx;
616
- const out = [], used = /* @__PURE__ */ new Set();
617
- const named = rows.filter((r) => !r.local).flatMap((r) => r.parts && r.partNames ? r.partNames.map((name, i) => ({ name, start: r.parts[i].start, end: r.parts[i].end })) : [r]);
618
- for (const raw of String(list).split(",").map((s) => s.trim()).filter(Boolean)) {
619
- let frame, label, m;
620
- if (m = raw.match(/^(.+)@(start|mid|end)$/)) {
621
- const row = named.find((r) => r.name === m[1]);
622
- const inside = row ? void 0 : rows.find((r) => r.local && r.name === m[1]);
623
- if (inside) throw new Error(`--at "${raw}": layer "${m[1]}" is inside a time-remapped composition (${inside.local}), so its times are that composition's local seconds, not the movie's; name the composition (${inside.local.replace(/ (×.*|time keyframes)$/, "")}@${m[2]}) or give seconds`);
624
- if (!row) throw new Error(`--at "${raw}": no layer named "${m[1]}" (names: ${named.slice(0, 40).map((r) => r.name).join(", ")}${named.length > 40 ? ", \u2026" : ""})`);
625
- const t = m[2] === "start" ? row.start : m[2] === "mid" ? (row.start + row.end) / 2 : row.end - 1 / fps;
626
- frame = Math.round(t * fps);
627
- label = `${m[1]}-${m[2]}`;
628
- } else if (m = raw.match(/^(\d+(?:\.\d+)?)%$/)) {
629
- frame = Math.round(Number(m[1]) / 100 * totalFrames);
630
- label = `${m[1]}pct`;
631
- } else if (m = raw.match(/^f(\d+)$/i)) {
632
- frame = Number(m[1]);
633
- label = `f${m[1]}`;
634
- } else if (/^\d+(\.\d+)?$/.test(raw)) {
635
- frame = Math.round(Number(raw) * fps);
636
- label = `${Number(raw).toFixed(2)}s`;
637
- } else throw new Error(`--at: cannot read "${raw}": ${AT_HELP}`);
638
- if (frame > totalFrames || frame < 0) throw new Error(`--at "${raw}" is past the end: the movie is ${duration} s (${totalFrames} frames)`);
639
- label = label.replace(/[\\/:*?"<>|]+/g, "-");
640
- let unique = label, n = 2;
641
- while (used.has(unique)) unique = `${label}-${n++}`;
642
- used.add(unique);
643
- out.push({ label: unique, frame });
644
- }
645
- return out;
646
- }
647
- function groupIssues(perFrame) {
648
- const groups = /* @__PURE__ */ new Map();
649
- for (const { frame, issues } of perFrame) {
650
- for (const message of issues) {
651
- const key = message.replace(/-?\d+(\.\d+)?/g, "#");
652
- const g = groups.get(key);
653
- if (g) {
654
- g.count++;
655
- g.lastFrame = frame;
656
- } else groups.set(key, { message, count: 1, firstFrame: frame, lastFrame: frame });
657
- }
658
- }
659
- return [...groups.values()];
660
- }
661
- function splitIssues(groups, strict = false) {
662
- if (strict) return { problems: groups, review: [] };
663
- const overlap = (g) => /\boverlap\b/.test(g.message);
664
- return { problems: groups.filter((g) => !overlap(g)), review: groups.filter(overlap) };
665
- }
666
- async function reviewMovie(movie, opts = {}) {
667
- const rows = movie.timelineData().rows;
668
- const at = opts.at ? resolveAtList(opts.at, { frameRate: movie.frameRate, totalFrames: movie.totalFrames, duration: movie.duration, rows }) : [];
669
- const frames = sampleFrames({ totalFrames: movie.totalFrames, frameRate: movie.frameRate, scenes: namedScenes(rows) });
670
- const perFrame = [];
671
- for (const frame of frames) {
672
- const r = await movie.inspect(frame, { layers: "none" });
673
- if (r.issues.length) perFrame.push({ frame, issues: r.issues });
674
- }
675
- const grouped = splitIssues(groupIssues(perFrame), !!opts.strict);
676
- const fonts = movie.inspectFonts();
677
- for (const f of fonts.failedUnused) grouped.review.push({ message: `web font "${f}" failed to load, but no text layer uses it (a broken or unused @font-face)`, count: 1, firstFrame: 0, lastFrame: 0 });
678
- for (const m of fonts.missing) {
679
- (opts.strict ? grouped.problems : grouped.review).push({ message: `layer "${m.layer}": none of the fonts "${m.family}" is available here (it is drawn in a fallback font)`, count: 1, firstFrame: 0, lastFrame: 0 });
680
- }
681
- return { frames: frames.length, problems: grouped.problems, review: grouped.review, fonts, audio: movie.audioBuffer ? movie.inspectAudio() : null, at };
682
- }
683
-
684
- // src/core/onion.ts
685
- function onionTimes(from, to, count) {
686
- if (count <= 1) return [from];
687
- return Array.from({ length: count }, (_, i) => Math.round((from + (to - from) * i / (count - 1)) * 1e6) / 1e6);
688
- }
689
- function onionAlphas(count, ramp = 0.25) {
690
- const weights = Array.from({ length: count }, (_, i) => count === 1 ? 1 : ramp + (1 - ramp) * (i / (count - 1)));
691
- let sum = 0;
692
- return weights.map((w) => {
693
- sum += w;
694
- return w / sum;
695
- });
696
- }
697
-
698
- // src/core/motionBlur.ts
699
- var DEFAULTS = { samples: 8, shutter: 0.5 };
700
- function resolveMotionBlur(spec, where) {
701
- if (spec === void 0 || spec === null || spec === false) return null;
702
- const o = spec === true ? {} : typeof spec === "number" ? { samples: spec } : spec;
703
- if (typeof spec === "object") warnUnknownOptions(`${where} motionBlur`, o, ["samples", "shutter"]);
704
- const samples = o.samples ?? DEFAULTS.samples, shutter = o.shutter ?? DEFAULTS.shutter;
705
- if (!(Number.isInteger(samples) && samples >= 2 && samples <= 64)) throw new Error(`pixi-effects: ${where}: motionBlur samples must be a whole number from 2 to 64, got ${samples}`);
706
- if (!(Number.isFinite(shutter) && shutter > 0 && shutter <= 1)) throw new Error(`pixi-effects: ${where}: motionBlur shutter must be a fraction of the frame time above 0 and up to 1 (0.5 is a 180\xB0 shutter), got ${shutter}`);
707
- return { samples, shutter };
708
- }
709
- function blurTimes(frame, frameRate, mb, duration) {
710
- const t = frame / frameRate, width = mb.shutter / frameRate;
711
- return Array.from({ length: mb.samples }, (_, k) => Math.min(Math.max(t + ((k + 0.5) / mb.samples - 0.5) * width, 0), duration));
712
- }
713
-
714
- // src/core/renderRange.ts
715
- function resolveRange(range2, ctx) {
716
- const { frameRate: fps, totalFrames, duration } = ctx;
717
- if (range2 === void 0) return { fromFrame: 0, toFrame: totalFrames, fromSec: 0, toSec: duration, partial: false };
718
- let from, to;
719
- if (typeof range2 === "string") {
720
- const rows = (ctx.rows ?? []).filter((r) => r.depth === 0 && !r.local).flatMap((r) => r.parts && r.partNames ? r.partNames.map((name, i) => ({ name, start: r.parts[i].start, end: r.parts[i].end })) : [r]);
721
- const row = rows.find((r) => r.name === range2);
722
- if (!row) throw new Error(`pixi-effects: render({ range: "${range2}" }): no layer named "${range2}" at the top level (names: ${rows.slice(0, 40).map((r) => r.name).join(", ") || "none"}${rows.length > 40 ? ", \u2026" : ""})`);
723
- from = row.start;
724
- to = row.end;
725
- } else {
726
- if (!Array.isArray(range2) || range2.length !== 2 || !range2.every((n) => typeof n === "number" && Number.isFinite(n))) {
727
- throw new Error("pixi-effects: render({ range }) must be [from, to] in seconds (numbers) or the name of a top-level layer");
728
- }
729
- [from, to] = range2;
730
- }
731
- if (from < 0 || to <= from) throw new Error(`pixi-effects: render({ range: [${from}, ${to}] }): a range must start before it ends (from >= 0, from < to)`);
732
- if (to > duration + 0.5 / fps) throw new Error(`pixi-effects: render({ range: [${from}, ${to}] }) goes past the end: the movie is ${duration} s long`);
733
- const fromFrame = Math.round(from * fps);
734
- const toFrame = Math.min(Math.round(to * fps), totalFrames);
735
- if (toFrame <= fromFrame) throw new Error(`pixi-effects: render({ range: [${from}, ${to}] }) is shorter than one frame (${(1 / fps).toFixed(4)} s at ${fps} fps)`);
736
- const partial = fromFrame > 0 || toFrame < totalFrames;
737
- return { fromFrame, toFrame, fromSec: fromFrame / fps, toSec: toFrame / fps, partial };
738
- }
739
- function sliceChannels(channels, sampleRate, fromSec, toSec, opts = {}) {
740
- const fade = Math.max(0, Math.round((opts.fade ?? 0.01) * sampleRate));
741
- const from = Math.round(fromSec * sampleRate), len = Math.max(1, Math.round(toSec * sampleRate) - from);
742
- return channels.map((src) => {
743
- const out = new Float32Array(len);
744
- for (let i = 0; i < len; i++) {
745
- const j = from + i;
746
- if (j >= 0 && j < src.length) out[i] = src[j];
747
- }
748
- const f = Math.min(fade, len >> 1);
749
- if (opts.fadeIn !== false) for (let i = 0; i < f; i++) out[i] = out[i] * (i / f);
750
- if (opts.fadeOut !== false) for (let i = 0; i < f; i++) out[len - 1 - i] = out[len - 1 - i] * (i / f);
751
- return out;
752
- });
753
- }
754
- function resolveRenderOptions(o) {
755
- const draft = !!o.draft;
756
- const scale = o.scale ?? (draft ? 0.5 : 1);
757
- if (!(typeof scale === "number" && scale > 0 && scale <= 1)) throw new Error(`pixi-effects: render({ scale }): scale must be above 0 and at most 1 (got ${scale})`);
758
- return {
759
- scale,
760
- bitrate: o.video?.bitrate ?? (draft ? "low" : void 0),
761
- motionBlur: o.motionBlur !== void 0 ? o.motionBlur : draft ? false : void 0
762
- };
763
- }
764
-
765
- // src/core/renderOptions.ts
766
- var QUALITY_NAMES = ["very-low", "low", "medium", "high", "very-high"];
767
- function parseBitrate(v) {
768
- if (v === void 0 || v === null) return {};
769
- if (typeof v === "string" && QUALITY_NAMES.includes(v)) return { quality: v };
770
- if (typeof v === "number" && Number.isFinite(v) && v > 0) return { bps: Math.round(v) };
771
- const m = typeof v === "string" ? /^\s*(\d+(?:\.\d+)?)\s*([kKmM]?)\s*$/.exec(v) : null;
772
- if (m) {
773
- const n = Number(m[1]) * { "": 1, k: 1e3, m: 1e6 }[m[2].toLowerCase()];
774
- if (n > 0) return { bps: Math.round(n) };
775
- }
776
- throw new Error(`pixi-effects: bitrate ${JSON.stringify(v)}: use a quality (${QUALITY_NAMES.join(", ")}) or bits a second (8000000, '8M', '800k')`);
777
- }
778
- var AUDIO_FORMATS = ["wav", "ogg"];
779
- var isAudioFormat = (f) => AUDIO_FORMATS.includes(f);
780
- var VIDEO_CODEC_PREFERENCE = {
781
- mp4: ["avc", "hevc", "vp9", "av1"],
782
- mov: ["avc", "hevc"],
783
- webm: ["vp9", "av1", "vp8"],
784
- mkv: ["vp9", "avc", "hevc", "av1", "vp8"]
785
- };
786
- var CODEC_ALIASES = {
787
- h264: "avc",
788
- "h.264": "avc",
789
- x264: "avc",
790
- avc1: "avc",
791
- h265: "hevc",
792
- "h.265": "hevc",
793
- x265: "hevc",
794
- hvc1: "hevc",
795
- hev1: "hevc",
796
- vp09: "vp9",
797
- "vp-9": "vp9",
798
- av01: "av1",
799
- vp80: "vp8",
800
- "vp-8": "vp8"
801
- };
802
- async function chooseVideoCodec(a) {
803
- const held = a.supported ?? VIDEO_CODEC_PREFERENCE[a.fmt];
804
- const order = VIDEO_CODEC_PREFERENCE[a.fmt].filter((c) => held.includes(c));
805
- const firstThatWorks = async (list) => {
806
- for (const c of list) if (await a.canEncode(c)) return c;
807
- return null;
808
- };
809
- if (a.requested) {
810
- const alias = CODEC_ALIASES[a.requested.toLowerCase()];
811
- if (alias) throw new Error(`pixi-effects: render({ video: { codec: "${a.requested}" } }): "${a.requested}" is not a codec name here; did you mean "${alias}"?`);
812
- if (!order.includes(a.requested)) throw new Error(`pixi-effects: render(): "${a.requested}" cannot go in ${a.fmt} (${a.fmt} holds ${order.join(", ")})`);
813
- if (await a.canEncode(a.requested)) return { codec: a.requested };
814
- const alt = await firstThatWorks(order.filter((c) => c !== a.requested));
815
- throw new Error(`pixi-effects: this browser cannot encode "${a.requested}" video for ${a.fmt}` + (alt ? `; "${alt}" would work: render({ format: '${a.fmt}', video: { codec: '${alt}' } }), or leave video.codec out and the best one is chosen.` : "; it can encode none of " + order.join(", ") + "."));
816
- }
817
- const chosen = await firstThatWorks(order);
818
- if (!chosen) throw new Error(`pixi-effects: this browser has no video encoder for ${a.fmt} (it tried ${order.join(", ")}): try another browser, or another format`);
819
- return chosen === order[0] ? { codec: chosen } : { codec: chosen, fellBackFrom: order[0] };
820
- }
821
-
822
- // src/core/Renderer.ts
823
- var AUDIO_PREFERENCE = {
824
- mp4: ["aac", "opus", "mp3", "flac"],
825
- mov: ["aac", "mp3"],
826
- webm: ["opus", "vorbis"],
827
- mkv: ["opus", "vorbis", "aac", "flac", "mp3"],
828
- ogg: ["opus", "vorbis", "flac"]
829
- };
830
- async function chooseAudioCodec(fmt, requested, container, audio, bitrate) {
831
- const supported = container.getSupportedAudioCodecs?.();
832
- const preferred = AUDIO_PREFERENCE[fmt].filter((c) => !supported || supported.includes(c));
833
- const canEncode = (list) => getFirstEncodableAudioCodec(list, { numberOfChannels: audio.numberOfChannels ?? 2, sampleRate: audio.sampleRate, bitrate });
834
- if (requested) {
835
- if (await canEncode([requested])) return requested;
836
- const alt = await canEncode(preferred.filter((c) => c !== requested));
837
- throw new Error(`pixi-effects: this browser cannot encode "${requested}" audio for ${fmt}` + (alt ? `; "${alt}" would work: render({ format: '${fmt}', audio: { codec: '${alt}' } }), or leave audio.codec out and the best one is chosen.` : "; it has no audio encoder for this format at all (try another browser, or a movie without sound)."));
838
- }
839
- const chosen = await canEncode(preferred);
840
- if (!chosen) throw new Error(`pixi-effects: this browser has no audio encoder for ${fmt} (it tried ${preferred.join(", ")}): try another browser, or render a movie without sound.`);
841
- if (chosen !== preferred[0]) {
842
- console.warn(`pixi-effects: this browser cannot encode ${preferred[0].toUpperCase()} audio (Chrome on Linux has no AAC encoder), so the ${fmt} carries ${chosen} instead; current players play it. A webm export needs no AAC.`);
843
- }
844
- return chosen;
845
- }
846
- var qualityMap = {
847
- "very-low": QUALITY_VERY_LOW,
848
- "low": QUALITY_LOW,
849
- "medium": QUALITY_MEDIUM,
850
- "high": QUALITY_HIGH,
851
- "very-high": QUALITY_VERY_HIGH
852
- };
853
- function bitrateOf(v, fallback) {
854
- const b = parseBitrate(v);
855
- return b.bps ?? (b.quality ? qualityMap[b.quality] : fallback);
856
- }
857
- function makeOutputFormat(name) {
858
- switch (name) {
859
- case "mp4":
860
- return new Mp4OutputFormat({ fastStart: "in-memory" });
861
- case "mov":
862
- return new MovOutputFormat({ fastStart: "in-memory" });
863
- case "webm":
864
- return new WebMOutputFormat();
865
- case "mkv":
866
- return new MkvOutputFormat();
867
- }
868
- }
869
- function sliceAudio(buffer, fromSec, toSec, fadeIn, fadeOut) {
870
- const channels = Array.from({ length: buffer.numberOfChannels }, (_, c) => buffer.getChannelData(c));
871
- const cut = sliceChannels(channels, buffer.sampleRate, fromSec, toSec, { fadeIn, fadeOut });
872
- const out = new AudioBuffer({ length: cut[0].length, numberOfChannels: cut.length, sampleRate: buffer.sampleRate });
873
- cut.forEach((data, c) => out.copyToChannel(data, c));
874
- return out;
875
- }
876
- async function exportAudioOnly(movie, options, fmt, range2) {
877
- if (!movie.audioBuffer) {
878
- throw new Error(`pixi-effects: render({ format: '${fmt}' }): this movie has no sound to export (add an audio layer, an sfx or a music score); for the picture use mp4, webm, mov or mkv`);
879
- }
880
- if (options.video) console.warn(`pixi-effects: render({ format: '${fmt}' }): video options are ignored, only the sound is written`);
881
- const audio = range2.partial ? sliceAudio(movie.audioBuffer, range2.fromSec, range2.toSec, range2.fromFrame > 0, range2.toFrame < movie.totalFrames) : movie.audioBuffer;
882
- const container = fmt === "wav" ? new WavOutputFormat() : new OggOutputFormat();
883
- let codec;
884
- if (fmt === "wav") {
885
- codec = options.audio?.codec ?? "pcm-s16";
886
- const held = container.getSupportedAudioCodecs();
887
- if (!held.includes(codec)) throw new Error(`pixi-effects: render({ format: 'wav', audio: { codec: "${codec}" } }): wav holds ${held.join(", ")} (the usual is pcm-s16)`);
888
- } else {
889
- codec = await chooseAudioCodec("ogg", options.audio?.codec, container, audio, bitrateOf(options.audio?.bitrate, QUALITY_HIGH));
890
- }
891
- const output = new Output({ format: container, target: new BufferTarget() });
892
- const source = new AudioBufferSource({ codec, ...fmt === "wav" ? {} : { bitrate: bitrateOf(options.audio?.bitrate, QUALITY_HIGH) } });
893
- output.addAudioTrack(source);
894
- movie.emit("progress", { progress: 0, frame: range2.fromFrame, totalFrames: movie.totalFrames });
895
- await output.start();
896
- await source.add(audio);
897
- await source.close();
898
- await output.finalize();
899
- movie.emit("progress", { progress: 100, frame: range2.toFrame, totalFrames: movie.totalFrames });
900
- return new Blob([output.target.buffer], { type: output.format.mimeType });
901
- }
902
- async function exportFrames(movie, options = {}) {
903
- const fmt = options.format ?? "mp4";
904
- const eff = resolveRenderOptions(options);
905
- const range2 = resolveRange(options.range, { frameRate: movie.frameRate, totalFrames: movie.totalFrames, duration: movie.duration, rows: typeof options.range === "string" ? movie.timelineData().rows : void 0 });
906
- if (isAudioFormat(fmt)) return exportAudioOnly(movie, options, fmt, range2);
907
- if (!["mp4", "mov", "webm", "mkv"].includes(fmt)) throw new Error(`pixi-effects: render({ format: "${String(fmt)}" }): use mp4, mov, webm, mkv (a picture) or wav, ogg (only the sound)`);
908
- warnUnknownOptions("movie.render({ video })", options.video, ["codec", "bitrate", "hardware", "keyFrameInterval"]);
909
- warnUnknownOptions("movie.render({ audio })", options.audio, ["codec", "bitrate"]);
910
- const videoBitrate = bitrateOf(eff.bitrate ?? "high", QUALITY_HIGH);
911
- const opts = {
912
- format: fmt,
913
- video: { codec: "", bitrate: videoBitrate },
914
- audio: {
915
- codec: options.audio?.codec ?? (fmt === "mp4" || fmt === "mov" ? "aac" : "opus"),
916
- bitrate: bitrateOf(options.audio?.bitrate, QUALITY_HIGH)
917
- }
918
- };
919
- const container = makeOutputFormat(opts.format);
920
- const audio = movie.audioBuffer && range2.partial ? sliceAudio(movie.audioBuffer, range2.fromSec, range2.toSec, range2.fromFrame > 0, range2.toFrame < movie.totalFrames) : movie.audioBuffer;
921
- if (audio) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, audio, opts.audio.bitrate);
922
- const output = new Output({
923
- format: container,
924
- target: new BufferTarget()
925
- });
926
- const mb = eff.motionBlur !== void 0 ? resolveMotionBlur(eff.motionBlur, "movie.render()") : movie.motionBlur ?? null;
927
- const stage = movie.app.canvas;
928
- const blurCanvas = mb ? Object.assign(document.createElement("canvas"), { width: stage.width, height: stage.height }) : null;
929
- const small = eff.scale < 1 ? Object.assign(document.createElement("canvas"), { width: Math.max(2, Math.round(stage.width * eff.scale / 2) * 2), height: Math.max(2, Math.round(stage.height * eff.scale / 2) * 2) }) : null;
930
- const smallCtx = small?.getContext("2d") ?? null;
931
- const picture = small ?? blurCanvas ?? stage;
932
- const chosen = await chooseVideoCodec({
933
- fmt,
934
- requested: options.video?.codec,
935
- supported: container.getSupportedVideoCodecs(),
936
- canEncode: (c) => canEncodeVideo(c, { width: picture.width, height: picture.height, bitrate: opts.video.bitrate })
937
- });
938
- if (chosen.fellBackFrom) console.warn(`pixi-effects: this browser cannot encode ${chosen.fellBackFrom.toUpperCase()} video, so the ${fmt} carries ${chosen.codec.toUpperCase()} instead (name a codec with render({ video: { codec } }) to choose yourself).`);
939
- opts.video.codec = chosen.codec;
940
- const hardware = options.video?.hardware;
941
- if (hardware !== void 0 && !["no-preference", "prefer-hardware", "prefer-software"].includes(hardware)) {
942
- throw new Error(`pixi-effects: render({ video: { hardware: "${String(hardware)}" } }): use 'prefer-hardware', 'prefer-software' or 'no-preference'`);
943
- }
944
- const canvasSource = new CanvasSource(picture, {
945
- codec: opts.video.codec,
946
- bitrate: opts.video.bitrate,
947
- ...hardware ? { hardwareAcceleration: hardware } : {}
948
- });
949
- output.addVideoTrack(canvasSource, { frameRate: movie.frameRate });
950
- if (audio) {
951
- const audioSource = new AudioBufferSource({
952
- codec: opts.audio.codec,
953
- bitrate: opts.audio.bitrate
954
- }, { startTimestamp: opts.audio.codec === "aac" ? -2112 / audio.sampleRate : 0 });
955
- output.addAudioTrack(audioSource);
956
- await output.start();
957
- await audioSource.add(audio);
958
- await audioSource.close();
959
- } else {
960
- await output.start();
961
- }
962
- movie.app.ticker.stop();
963
- const interval = options.video?.keyFrameInterval ?? 2;
964
- if (!(typeof interval === "number" && Number.isFinite(interval) && interval > 0)) throw new Error(`pixi-effects: render({ video: { keyFrameInterval: ${String(interval)} } }): seconds between keyframes, above 0 (default 2)`);
965
- const keyframeIntervalFrames = Math.max(1, Math.round(interval * movie.frameRate));
966
- try {
967
- const count = Math.max(1, range2.toFrame - range2.fromFrame);
968
- for (let frame = range2.fromFrame; frame <= range2.toFrame; frame++) {
969
- if (mb && blurCanvas) await movie._exposeFrame(frame, mb, blurCanvas, false);
970
- else await movie.gotoFrame(frame, true);
971
- if (small && smallCtx) smallCtx.drawImage(blurCanvas ?? stage, 0, 0, small.width, small.height);
972
- const rel = frame - range2.fromFrame;
973
- const isKey = rel === 0 || rel % keyframeIntervalFrames === 0;
974
- const addOpts = isKey ? { keyFrame: true } : void 0;
975
- await canvasSource.add(rel / movie.frameRate, 1 / movie.frameRate, addOpts);
976
- const progress = Math.floor(rel / count * 100);
977
- movie.emit("progress", { progress, frame, totalFrames: movie.totalFrames });
978
- }
979
- await canvasSource.close();
980
- await output.finalize();
981
- if (mb) await movie.gotoFrame(range2.toFrame, true);
982
- return new Blob([output.target.buffer], { type: output.format.mimeType });
983
- } finally {
984
- movie.app.ticker.start();
985
- }
986
- }
987
- var MODE_CODES = {
988
- "wipe-left": 0,
989
- "wipe-right": 1,
990
- "wipe-up": 2,
991
- "wipe-down": 3,
992
- "iris-in": 4,
993
- "iris-out": 5,
994
- "dissolve": 6
995
- };
996
- var GL_FRAGMENT = `
997
- in vec2 vTextureCoord;
998
- uniform sampler2D uTexture;
999
- uniform highp vec4 uInputSize; // PIXI-bound: xy = sprite render texture size (with padding)
1000
- uniform highp vec4 uInputClamp; // PIXI-bound: xy = min uv of unpadded content, zw = max uv
1001
- uniform highp vec4 uOutputFrame; // PIXI-bound: xy = sprite top-left on output, zw = bbox size in px
1002
- uniform highp vec4 uGlobalFrame; // PIXI-bound: xy = global frame offset, zw = canvas size in px
1003
- uniform float uProgress;
1004
- uniform float uSmoothing;
1005
- uniform float uMode;
1006
- uniform float uInvert;
1007
- uniform float uScale;
1008
- uniform float uSeed;
1009
-
1010
- out vec4 finalColor;
1011
-
1012
- // Classic 2D Perlin noise (Stefan Gustavson). Deterministic \u2014 same input
1013
- // always returns the same output, which is what makes the dissolve pattern
1014
- // reproducible across renders.
1015
- vec4 _permute(vec4 x) { return mod(((x * 34.0) + 1.0) * x, 289.0); }
1016
- vec2 _fade(vec2 t) { return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); }
1017
- float perlin2(vec2 P) {
1018
- vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
1019
- vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
1020
- Pi = mod(Pi, 289.0);
1021
- vec4 ix = Pi.xzxz;
1022
- vec4 iy = Pi.yyww;
1023
- vec4 fx = Pf.xzxz;
1024
- vec4 fy = Pf.yyww;
1025
- vec4 i = _permute(_permute(ix) + iy);
1026
- vec4 gx = 2.0 * fract(i / 41.0) - 1.0;
1027
- vec4 gy = abs(gx) - 0.5;
1028
- vec4 tx = floor(gx + 0.5);
1029
- gx = gx - tx;
1030
- vec2 g00 = vec2(gx.x, gy.x);
1031
- vec2 g10 = vec2(gx.y, gy.y);
1032
- vec2 g01 = vec2(gx.z, gy.z);
1033
- vec2 g11 = vec2(gx.w, gy.w);
1034
- vec4 norm = 1.79284291400159 - 0.85373472095314 * vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11));
1035
- g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;
1036
- float n00 = dot(g00, vec2(fx.x, fy.x));
1037
- float n10 = dot(g10, vec2(fx.y, fy.y));
1038
- float n01 = dot(g01, vec2(fx.z, fy.z));
1039
- float n11 = dot(g11, vec2(fx.w, fy.w));
1040
- vec2 fade_xy = _fade(Pf.xy);
1041
- vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
1042
- return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);
1043
- }
1044
-
1045
- // Map sprite-local vTextureCoord \u2192 canvas-pixel position. We do this in two
1046
- // steps: first un-pad vTextureCoord (it spans 0..bbox/inputSize because the
1047
- // filter target has padding) into a true 0..1 across the bbox, then offset
1048
- // and scale by uOutputFrame to get canvas pixels. Operating in canvas space
1049
- // (rather than sprite-uv space) is what makes the wipe / iris edge land at
1050
- // the same canvas position for sprites of any size: at uProgress=0.25 the
1051
- // wipe edge is at canvas x = 0.75 * canvasW for every sprite, regardless of
1052
- // each sprite's bbox.
1053
- vec2 toCanvasPx(vec2 uv) {
1054
- vec2 bboxUV = (uv - uInputClamp.xy) / (uInputClamp.zw - uInputClamp.xy);
1055
- return uOutputFrame.xy + bboxUV * uOutputFrame.zw;
1056
- }
1057
-
1058
- vec2 toCanvasUV(vec2 uv) {
1059
- return toCanvasPx(uv) / uGlobalFrame.zw;
1060
- }
1061
-
1062
- // Aspect-corrected, corner-normalized distance from canvas center.
1063
- // Returns 0 at canvas center, 1 at the canvas corner \u2014 so iris is a true
1064
- // circle in pixel space and p=1 reveals the entire canvas.
1065
- float irisDistCanvas(vec2 uv) {
1066
- vec2 px = toCanvasPx(uv);
1067
- vec2 canvasSize = uGlobalFrame.zw;
1068
- vec2 offsetPx = px - canvasSize * 0.5;
1069
- float halfDiag = 0.5 * length(canvasSize);
1070
- return length(offsetPx) / max(halfDiag, 1.0);
1071
- }
1072
-
1073
- float wipeReveal(vec2 uv, float p, float s, float mode) {
1074
- // Remap p so the smoothstep edge lies fully outside [0,1] at p=0 / p=1
1075
- // (otherwise smoothstep returns 0.5 at the very edge instead of 0 or 1).
1076
- float ep = p * (1.0 + 2.0 * s) - s;
1077
- vec2 cuv = toCanvasUV(uv);
1078
- if (mode < 0.5) {
1079
- // wipe-left: B reveals from the right edge moving left
1080
- return smoothstep(1.0 - ep - s, 1.0 - ep + s, cuv.x);
1081
- } else if (mode < 1.5) {
1082
- // wipe-right: B reveals from the left edge moving right
1083
- return smoothstep(1.0 - ep - s, 1.0 - ep + s, 1.0 - cuv.x);
1084
- } else if (mode < 2.5) {
1085
- // wipe-up
1086
- return smoothstep(1.0 - ep - s, 1.0 - ep + s, cuv.y);
1087
- } else if (mode < 3.5) {
1088
- // wipe-down: B reveals from the top edge moving down
1089
- return smoothstep(1.0 - ep - s, 1.0 - ep + s, 1.0 - cuv.y);
1090
- } else if (mode < 4.5) {
1091
- // iris-in: circle grows from center
1092
- float d = irisDistCanvas(uv);
1093
- return 1.0 - smoothstep(ep - s, ep + s, d);
1094
- } else if (mode < 5.5) {
1095
- // iris-out: circle shrinks toward center
1096
- float d = irisDistCanvas(uv);
1097
- return smoothstep(ep - s, ep + s, d);
1098
- } else {
1099
- // dissolve: pixel-grain Perlin noise threshold. Pixels with a low noise
1100
- // value reveal first; as p grows, more pixels reveal.
1101
- vec2 sampleUV = cuv * uScale + vec2(uSeed, uSeed * 0.7);
1102
- float n = perlin2(sampleUV) * 0.5 + 0.5; // remap to 0..1
1103
- return 1.0 - smoothstep(ep - s, ep + s, n);
1104
- }
1105
- }
1106
-
1107
- void main(void) {
1108
- vec4 raw = texture(uTexture, vTextureCoord);
1109
- float reveal = wipeReveal(vTextureCoord, uProgress, max(uSmoothing, 0.0001), uMode);
1110
- // Inverted mode uses a HARD binary cutoff: A stays at alpha=1 across
1111
- // B's smoothstep zone and snaps off only where B has fully revealed.
1112
- if (uInvert > 0.5) reveal = 1.0 - step(0.99, reveal);
1113
- finalColor = raw * reveal;
1114
- }
1115
- `;
1116
- var WGSL_SOURCE = `
1117
- struct GlobalFilterUniforms {
1118
- uInputSize: vec4<f32>,
1119
- uInputPixel: vec4<f32>,
1120
- uInputClamp: vec4<f32>,
1121
- uOutputFrame: vec4<f32>,
1122
- uGlobalFrame: vec4<f32>,
1123
- uOutputTexture: vec4<f32>,
1124
- };
1125
-
1126
- struct TransitionUniforms {
1127
- uProgress: f32,
1128
- uMode: f32,
1129
- uSmoothing: f32,
1130
- uInvert: f32,
1131
- uScale: f32,
1132
- uSeed: f32,
1133
- };
1134
-
1135
- @group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;
1136
- @group(0) @binding(1) var uTexture: texture_2d<f32>;
1137
- @group(0) @binding(2) var uSampler : sampler;
1138
- @group(1) @binding(0) var<uniform> transitionUniforms : TransitionUniforms;
1139
-
1140
- struct VSOutput {
1141
- @builtin(position) position: vec4<f32>,
1142
- @location(0) uv : vec2<f32>,
1143
- };
1144
-
1145
- fn filterVertexPosition(aPosition : vec2<f32>) -> vec4<f32> {
1146
- var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;
1147
- position.x = position.x * (2.0 / gfu.uOutputTexture.x) - 1.0;
1148
- position.y = position.y * (2.0 * gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z;
1149
- return vec4<f32>(position, 0.0, 1.0);
1150
- }
1151
-
1152
- fn filterTextureCoord(aPosition : vec2<f32>) -> vec2<f32> {
1153
- return aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw);
1154
- }
1155
-
1156
- // NOTE on whitespace: PIXI v8's WGSL attribute extractor is regex-based and
1157
- // expects the noise/blur reference format \u2014 multi-line param list + space
1158
- // before the colon. A compact one-liner with no space before the colon parses
1159
- // as having NO vertex attributes, which then causes a "Vertex attribute slot 0
1160
- // not present in the VertexState" pipeline validation error.
1161
- @vertex
1162
- fn mainVertex(
1163
- @location(0) aPosition : vec2<f32>,
1164
- ) -> VSOutput {
1165
- return VSOutput(filterVertexPosition(aPosition), filterTextureCoord(aPosition));
1166
- }
1167
-
1168
- // Aspect-corrected, corner-normalized distance from sprite center.
1169
- // Map sprite-local uv \u2192 canvas-pixel position (see GLSL note).
1170
- fn toCanvasPx(uv: vec2<f32>) -> vec2<f32> {
1171
- let bboxUV = (uv - gfu.uInputClamp.xy) / (gfu.uInputClamp.zw - gfu.uInputClamp.xy);
1172
- return gfu.uOutputFrame.xy + bboxUV * gfu.uOutputFrame.zw;
1173
- }
1174
-
1175
- fn toCanvasUV(uv: vec2<f32>) -> vec2<f32> {
1176
- return toCanvasPx(uv) / gfu.uGlobalFrame.zw;
1177
- }
1178
-
1179
- fn irisDistCanvas(uv: vec2<f32>) -> f32 {
1180
- let px = toCanvasPx(uv);
1181
- let canvasSize = gfu.uGlobalFrame.zw;
1182
- let offsetPx = px - canvasSize * 0.5;
1183
- let halfDiag = 0.5 * length(canvasSize);
1184
- return length(offsetPx) / max(halfDiag, 1.0);
1185
- }
1186
-
1187
- // Classic 2D Perlin noise (Stefan Gustavson). Deterministic \u2014 same input
1188
- // always returns the same output, so the dissolve pattern is reproducible.
1189
- fn _wgsl_permute(x: vec4<f32>) -> vec4<f32> { return ((x * 34.0) + 1.0) * x % vec4<f32>(289.0); }
1190
- fn _wgsl_fade(t: vec2<f32>) -> vec2<f32> { return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); }
1191
- fn perlin2(P: vec2<f32>) -> f32 {
1192
- var Pi: vec4<f32> = floor(vec4<f32>(P, P)) + vec4<f32>(0.0, 0.0, 1.0, 1.0);
1193
- var Pf: vec4<f32> = fract(vec4<f32>(P, P)) - vec4<f32>(0.0, 0.0, 1.0, 1.0);
1194
- Pi = Pi % vec4<f32>(289.0);
1195
- let ix = vec4<f32>(Pi.x, Pi.z, Pi.x, Pi.z);
1196
- let iy = vec4<f32>(Pi.y, Pi.y, Pi.w, Pi.w);
1197
- let fx = vec4<f32>(Pf.x, Pf.z, Pf.x, Pf.z);
1198
- let fy = vec4<f32>(Pf.y, Pf.y, Pf.w, Pf.w);
1199
- let i = _wgsl_permute(_wgsl_permute(ix) + iy);
1200
- var gx = 2.0 * fract(i / 41.0) - 1.0;
1201
- let gy = abs(gx) - 0.5;
1202
- let tx = floor(gx + 0.5);
1203
- gx = gx - tx;
1204
- var g00 = vec2<f32>(gx.x, gy.x);
1205
- var g10 = vec2<f32>(gx.y, gy.y);
1206
- var g01 = vec2<f32>(gx.z, gy.z);
1207
- var g11 = vec2<f32>(gx.w, gy.w);
1208
- let norm = 1.79284291400159 - 0.85373472095314 * vec4<f32>(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11));
1209
- g00 = g00 * norm.x; g01 = g01 * norm.y; g10 = g10 * norm.z; g11 = g11 * norm.w;
1210
- let n00 = dot(g00, vec2<f32>(fx.x, fy.x));
1211
- let n10 = dot(g10, vec2<f32>(fx.y, fy.y));
1212
- let n01 = dot(g01, vec2<f32>(fx.z, fy.z));
1213
- let n11 = dot(g11, vec2<f32>(fx.w, fy.w));
1214
- let fade_xy = _wgsl_fade(vec2<f32>(Pf.x, Pf.y));
1215
- let n_x = mix(vec2<f32>(n00, n01), vec2<f32>(n10, n11), vec2<f32>(fade_xy.x));
1216
- return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);
1217
- }
1218
-
1219
- fn wipeReveal(uv: vec2<f32>, p: f32, s: f32, mode: f32) -> f32 {
1220
- let ep = p * (1.0 + 2.0 * s) - s;
1221
- let cuv = toCanvasUV(uv);
1222
- if (mode < 0.5) {
1223
- return smoothstep(1.0 - ep - s, 1.0 - ep + s, cuv.x);
1224
- } else if (mode < 1.5) {
1225
- // wipe-right: B reveals from the left edge moving right
1226
- return smoothstep(1.0 - ep - s, 1.0 - ep + s, 1.0 - cuv.x);
1227
- } else if (mode < 2.5) {
1228
- return smoothstep(1.0 - ep - s, 1.0 - ep + s, cuv.y);
1229
- } else if (mode < 3.5) {
1230
- // wipe-down: B reveals from the top edge moving down
1231
- return smoothstep(1.0 - ep - s, 1.0 - ep + s, 1.0 - cuv.y);
1232
- } else if (mode < 4.5) {
1233
- let d = irisDistCanvas(uv);
1234
- return 1.0 - smoothstep(ep - s, ep + s, d);
1235
- } else if (mode < 5.5) {
1236
- let d = irisDistCanvas(uv);
1237
- return smoothstep(ep - s, ep + s, d);
1238
- } else {
1239
- let sampleUV = cuv * transitionUniforms.uScale + vec2<f32>(transitionUniforms.uSeed, transitionUniforms.uSeed * 0.7);
1240
- let n = perlin2(sampleUV) * 0.5 + 0.5;
1241
- return 1.0 - smoothstep(ep - s, ep + s, n);
1242
- }
1243
- }
1244
-
1245
- @fragment
1246
- fn mainFragment(@location(0) uv : vec2<f32>) -> @location(0) vec4<f32> {
1247
- let raw = textureSample(uTexture, uSampler, uv);
1248
- var reveal = wipeReveal(uv, transitionUniforms.uProgress, max(transitionUniforms.uSmoothing, 0.0001), transitionUniforms.uMode);
1249
- if (transitionUniforms.uInvert > 0.5) { reveal = 1.0 - step(0.99, reveal); }
1250
- return raw * reveal;
1251
- }
1252
- `;
1253
- var TransitionMaskFilter = class extends Filter {
1254
- constructor(options = {}) {
1255
- const mode = options.mode ?? "wipe-left";
1256
- const smoothing = options.smoothing ?? 0.02;
1257
- const progress = options.progress ?? 0;
1258
- const invert = options.invert ?? false;
1259
- const scale = options.scale ?? 30;
1260
- const seed = options.seed ?? 0;
1261
- super({
1262
- glProgram: GlProgram.from({
1263
- vertex: defaultFilterVert,
1264
- fragment: GL_FRAGMENT,
1265
- name: "transition-mask-filter"
1266
- }),
1267
- gpuProgram: GpuProgram.from({
1268
- vertex: { source: WGSL_SOURCE, entryPoint: "mainVertex" },
1269
- fragment: { source: WGSL_SOURCE, entryPoint: "mainFragment" }
1270
- }),
1271
- resources: {
1272
- transitionUniforms: new UniformGroup({
1273
- uProgress: { value: progress, type: "f32" },
1274
- uMode: { value: MODE_CODES[mode], type: "f32" },
1275
- uSmoothing: { value: smoothing, type: "f32" },
1276
- uInvert: { value: invert ? 1 : 0, type: "f32" },
1277
- uScale: { value: scale, type: "f32" },
1278
- uSeed: { value: seed, type: "f32" }
1279
- })
1280
- }
1281
- });
1282
- }
1283
- get uProgress() {
1284
- return this.resources.transitionUniforms.uniforms.uProgress;
1285
- }
1286
- set uProgress(v) {
1287
- this.resources.transitionUniforms.uniforms.uProgress = v;
1288
- }
1289
- get uMode() {
1290
- return this.resources.transitionUniforms.uniforms.uMode;
1291
- }
1292
- set uMode(v) {
1293
- this.resources.transitionUniforms.uniforms.uMode = v;
1294
- }
1295
- get uSmoothing() {
1296
- return this.resources.transitionUniforms.uniforms.uSmoothing;
1297
- }
1298
- set uSmoothing(v) {
1299
- this.resources.transitionUniforms.uniforms.uSmoothing = v;
1300
- }
1301
- get uInvert() {
1302
- return this.resources.transitionUniforms.uniforms.uInvert;
1303
- }
1304
- set uInvert(v) {
1305
- this.resources.transitionUniforms.uniforms.uInvert = v;
1306
- }
1307
- get uScale() {
1308
- return this.resources.transitionUniforms.uniforms.uScale;
1309
- }
1310
- set uScale(v) {
1311
- this.resources.transitionUniforms.uniforms.uScale = v;
1312
- }
1313
- get uSeed() {
1314
- return this.resources.transitionUniforms.uniforms.uSeed;
1315
- }
1316
- set uSeed(v) {
1317
- this.resources.transitionUniforms.uniforms.uSeed = v;
1318
- }
1319
- };
1320
- var LUMA_MAPS = ["linear", "diagonal", "radial"];
1321
- var GL = `in vec2 vTextureCoord;
1322
- uniform sampler2D uTexture;
1323
- uniform sampler2D uMap;
1324
- uniform highp vec4 uInputClamp;
1325
- uniform highp vec4 uOutputFrame;
1326
- uniform highp vec4 uGlobalFrame;
1327
- uniform float uProgress;
1328
- uniform float uSoftness;
1329
- uniform float uInvert;
1330
- uniform float uFlip;
1331
- uniform float uMode;
1332
- out vec4 finalColor;
1333
- float lumaOf(vec2 cuv, float aspect) {
1334
- if (uMode > 2.5) { vec2 p = (cuv - 0.5) * vec2(aspect, 1.0); return clamp(length(p) / (0.5 * length(vec2(aspect, 1.0))), 0.0, 1.0); }
1335
- if (uMode > 1.5) return clamp((cuv.x + cuv.y) * 0.5, 0.0, 1.0);
1336
- if (uMode > 0.5) return clamp(cuv.x, 0.0, 1.0);
1337
- return dot(texture(uMap, cuv).rgb, vec3(0.2126, 0.7152, 0.0722));
1338
- }
1339
- void main(void) {
1340
- vec4 raw = texture(uTexture, vTextureCoord);
1341
- vec2 bboxUV = (vTextureCoord - uInputClamp.xy) / (uInputClamp.zw - uInputClamp.xy);
1342
- vec2 cuv = (uOutputFrame.xy + bboxUV * uOutputFrame.zw) / uGlobalFrame.zw;
1343
- float l = lumaOf(cuv, uGlobalFrame.z / uGlobalFrame.w);
1344
- if (uFlip > 0.5) l = 1.0 - l;
1345
- float s = max(uSoftness, 0.0001);
1346
- float ep = uProgress * (1.0 + 2.0 * s) - s;
1347
- float reveal = 1.0 - smoothstep(ep - s, ep + s, l);
1348
- if (uInvert > 0.5) reveal = 1.0 - step(0.99, reveal);
1349
- finalColor = raw * reveal;
1350
- }`;
1351
- var WGSL = `
1352
- struct GlobalFilterUniforms {
1353
- uInputSize:vec4<f32>, uInputPixel:vec4<f32>, uInputClamp:vec4<f32>, uOutputFrame:vec4<f32>, uGlobalFrame:vec4<f32>, uOutputTexture:vec4<f32>,
1354
- };
1355
- struct LumaUniforms { uProgress:f32, uSoftness:f32, uInvert:f32, uFlip:f32, uMode:f32, };
1356
- @group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;
1357
- @group(0) @binding(1) var uTexture: texture_2d<f32>;
1358
- @group(0) @binding(2) var uSampler : sampler;
1359
- @group(1) @binding(0) var<uniform> lumaUniforms : LumaUniforms;
1360
- @group(1) @binding(1) var uMap: texture_2d<f32>;
1361
- @group(1) @binding(2) var uMapSampler : sampler;
1362
- struct VSOutput {
1363
- @builtin(position) position: vec4<f32>,
1364
- @location(0) uv : vec2<f32>,
1365
- };
1366
- @vertex
1367
- fn mainVertex(
1368
- @location(0) aPosition : vec2<f32>,
1369
- ) -> VSOutput {
1370
- var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;
1371
- position.x = position.x * (2.0 / gfu.uOutputTexture.x) - 1.0;
1372
- position.y = position.y * (2.0 * gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z;
1373
- return VSOutput(vec4<f32>(position, 0.0, 1.0), aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw));
1374
- }
1375
- fn lumaOf(cuv: vec2<f32>, aspect: f32) -> f32 {
1376
- let mode = lumaUniforms.uMode;
1377
- if (mode > 2.5) {
1378
- let p = (cuv - vec2<f32>(0.5)) * vec2<f32>(aspect, 1.0);
1379
- return clamp(length(p) / (0.5 * length(vec2<f32>(aspect, 1.0))), 0.0, 1.0);
1380
- }
1381
- if (mode > 1.5) { return clamp((cuv.x + cuv.y) * 0.5, 0.0, 1.0); }
1382
- if (mode > 0.5) { return clamp(cuv.x, 0.0, 1.0); }
1383
- let m = textureSampleLevel(uMap, uMapSampler, cuv, 0.0).rgb;
1384
- return dot(m, vec3<f32>(0.2126, 0.7152, 0.0722));
1385
- }
1386
- @fragment
1387
- fn mainFragment(@location(0) uv : vec2<f32>) -> @location(0) vec4<f32> {
1388
- let raw = textureSample(uTexture, uSampler, uv);
1389
- let bboxUV = (uv - gfu.uInputClamp.xy) / (gfu.uInputClamp.zw - gfu.uInputClamp.xy);
1390
- let cuv = (gfu.uOutputFrame.xy + bboxUV * gfu.uOutputFrame.zw) / gfu.uGlobalFrame.zw;
1391
- var l = lumaOf(cuv, gfu.uGlobalFrame.z / gfu.uGlobalFrame.w);
1392
- if (lumaUniforms.uFlip > 0.5) { l = 1.0 - l; }
1393
- let s = max(lumaUniforms.uSoftness, 0.0001);
1394
- let ep = lumaUniforms.uProgress * (1.0 + 2.0 * s) - s;
1395
- var reveal = 1.0 - smoothstep(ep - s, ep + s, l);
1396
- if (lumaUniforms.uInvert > 0.5) { reveal = 1.0 - step(0.99, reveal); }
1397
- return raw * reveal;
1398
- }`;
1399
- var LumaWipeFilter = class extends Filter {
1400
- _mapName;
1401
- _builtIn;
1402
- _resolved = false;
1403
- _warned = false;
1404
- constructor({ map, softness = 0.1, progress = 0, invert = false, flip = false }) {
1405
- const t = Texture.WHITE;
1406
- const mode = LUMA_MAPS.indexOf(map) + 1;
1407
- super({
1408
- glProgram: GlProgram.from({ vertex: defaultFilterVert, fragment: GL, name: "pe-luma-wipe" }),
1409
- gpuProgram: GpuProgram.from({ vertex: { source: WGSL, entryPoint: "mainVertex" }, fragment: { source: WGSL, entryPoint: "mainFragment" } }),
1410
- resources: {
1411
- lumaUniforms: new UniformGroup({
1412
- uProgress: { value: progress, type: "f32" },
1413
- uSoftness: { value: softness, type: "f32" },
1414
- uInvert: { value: invert ? 1 : 0, type: "f32" },
1415
- uFlip: { value: flip ? 1 : 0, type: "f32" },
1416
- uMode: { value: mode, type: "f32" }
1417
- }),
1418
- uMap: t.source,
1419
- uMapSampler: t.source.style
1420
- }
1421
- });
1422
- this._mapName = map;
1423
- this._builtIn = mode > 0;
1424
- }
1425
- get uProgress() {
1426
- return this.resources.lumaUniforms.uniforms.uProgress;
1427
- }
1428
- set uProgress(v) {
1429
- this.resources.lumaUniforms.uniforms.uProgress = v;
1430
- }
1431
- apply(fm, input, output, clear) {
1432
- if (!this._builtIn && !this._resolved) {
1433
- const t = Assets.get(this._mapName);
1434
- if (t) {
1435
- this.resources.uMap = t.source;
1436
- this.resources.uMapSampler = t.source.style;
1437
- this._resolved = true;
1438
- } else if (!this._warned) {
1439
- this._warned = true;
1440
- const guess = suggestName(this._mapName, LUMA_MAPS);
1441
- console.warn(`pixi-effects: luma transition map "${this._mapName}" is not a built-in map (${LUMA_MAPS.join(", ")}) and no image asset has that name${guess ? `; did you mean "${guess}"?` : ""}; the wipe follows a white map (everything changes at once)`);
1442
- }
1443
- }
1444
- fm.applyFilter(this, input, output, clear);
1445
- }
1446
- };
1447
-
1448
- // src/core/Transitions.ts
1449
- var EPS = 1e-6;
1450
- var windows = /* @__PURE__ */ new WeakMap();
1451
- function transitionWindowsOf(spec) {
1452
- return windows.get(spec) ?? [];
1453
- }
1454
- function carryTransitionWindows(from, to) {
1455
- const w = windows.get(from);
1456
- if (w) windows.set(to, w);
1457
- return to;
1458
- }
1459
- function expandTransitions(spec) {
1460
- const out = {
1461
- ...spec,
1462
- sequences: spec.sequences ? spec.sequences.map((s) => {
1463
- const cloned = {
1464
- ...s,
1465
- // Deep-clone the mutation surfaces only.
1466
- initial: "initial" in s && s.initial ? { ...s.initial } : void 0,
1467
- // Shallow: Keyframe / FilterSpec elements are shared references.
1468
- // Expansion only pushes new literals — never mutate existing ones.
1469
- keyframes: "keyframes" in s && s.keyframes ? s.keyframes.slice() : void 0,
1470
- filters: "filters" in s && s.filters ? s.filters.slice() : void 0
1471
- };
1472
- if (cloned.type === "composition") {
1473
- const c = cloned;
1474
- if (c.width === void 0) c.width = spec.width;
1475
- if (c.height === void 0) c.height = spec.height;
1476
- return expandTransitions(c);
1477
- }
1478
- return cloned;
1479
- }) : void 0
1480
- };
1481
- if (!out.transitions || out.transitions.length === 0) {
1482
- delete out.transitions;
1483
- return out;
1484
- }
1485
- const transitions = out.transitions;
1486
- const sequences = out.sequences ?? [];
1487
- const remap = out.duration !== void 0 ? timeRemapOf(out) : null;
1488
- const parentDuration = remap ? contentLength(remap, out.duration) : out.duration ?? Infinity;
1489
- const compW = out.width;
1490
- const compH = out.height;
1491
- const threeDByName = new Set(sequences.filter((s) => s.name && s.threeD).map((s) => s.name));
1492
- transitions.forEach((t, i) => {
1493
- for (const name of [t.from, t.to]) {
1494
- if (threeDByName.has(name)) {
1495
- console.warn(`pixi-effects: transitions[${i}]: "${name}" is threeD, but transitions are not supported on threeD layers yet \u2014 it will render without the camera`);
1496
- }
1497
- }
1498
- });
1499
- const masksParticipants = /* @__PURE__ */ new Set();
1500
- for (const t of transitions) {
1501
- if (t.kind === "wipe" || t.kind === "iris" || t.kind === "dissolve" || t.kind === "zoom" || t.kind === "luma") {
1502
- masksParticipants.add(t.from);
1503
- masksParticipants.add(t.to);
1504
- }
1505
- }
1506
- if (masksParticipants.size > 0 && compW !== void 0 && compH !== void 0) {
1507
- for (let i = 0; i < sequences.length; i++) {
1508
- const s = sequences[i];
1509
- if (!s.name || !masksParticipants.has(s.name)) continue;
1510
- const userFilters = s.filters;
1511
- if (userFilters) rejectReservedNameCollision(userFilters, s.name);
1512
- sequences[i] = wrapAsFullComposition(s, compW, compH);
1513
- }
1514
- }
1515
- const byName = /* @__PURE__ */ new Map();
1516
- for (let i = 0; i < sequences.length; i++) {
1517
- const s = sequences[i];
1518
- if (!s.name) continue;
1519
- if (byName.has(s.name)) {
1520
- throw new Error(`pixi-effects: composition has two sequences named "${s.name}" (transitions need unique names)`);
1521
- }
1522
- byName.set(s.name, { index: i, seq: s });
1523
- }
1524
- const fromCount = /* @__PURE__ */ new Map();
1525
- const toCount = /* @__PURE__ */ new Map();
1526
- for (let i = 0; i < transitions.length; i++) {
1527
- const t = transitions[i];
1528
- const tag = `transitions[${i}] (${t.kind})`;
1529
- if (t.from === t.to) {
1530
- throw new Error(`pixi-effects: ${tag} cannot transition a sequence to itself ("${t.from}")`);
1531
- }
1532
- if (!(t.duration > 0)) {
1533
- throw new Error(`pixi-effects: ${tag} duration must be > 0 (got ${t.duration})`);
1534
- }
1535
- const fromEntry = byName.get(t.from);
1536
- if (!fromEntry) {
1537
- throw new Error(`pixi-effects: ${tag} \`from\` "${t.from}" is not found among sibling sequences`);
1538
- }
1539
- const toEntry = byName.get(t.to);
1540
- if (!toEntry) {
1541
- throw new Error(`pixi-effects: ${tag} \`to\` "${t.to}" is not found among sibling sequences`);
1542
- }
1543
- if (toEntry.index <= fromEntry.index) {
1544
- throw new Error(`pixi-effects: ${tag} \`to\` "${t.to}" must be declared after \`from\` "${t.from}" in sibling order`);
1545
- }
1546
- const fromUses = (fromCount.get(t.from) ?? 0) + 1;
1547
- fromCount.set(t.from, fromUses);
1548
- if (fromUses > 1) {
1549
- throw new Error(`pixi-effects: ${tag} sequence "${t.from}" is used as \`from\` in more than one transition`);
1550
- }
1551
- const toUses = (toCount.get(t.to) ?? 0) + 1;
1552
- toCount.set(t.to, toUses);
1553
- if (toUses > 1) {
1554
- throw new Error(`pixi-effects: ${tag} sequence "${t.to}" is used as \`to\` in more than one transition`);
1555
- }
1556
- const tStart = resolveAt(t.at, parentDuration);
1557
- const tEnd = tStart + t.duration;
1558
- const fromSeq = fromEntry.seq;
1559
- const fromStart = resolveAt(fromSeq.at, parentDuration);
1560
- const fromEnd = fromStart + (fromSeq.duration ?? parentDuration);
1561
- if (tStart < fromStart - EPS || tEnd > fromEnd + EPS) {
1562
- throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`from\` "${t.from}" (lives [${fromStart}, ${fromEnd}], so it ends at ${fromEnd})`);
1563
- }
1564
- const toSeq = toEntry.seq;
1565
- const toStart = resolveAt(toSeq.at, parentDuration);
1566
- const toEnd = toStart + (toSeq.duration ?? parentDuration);
1567
- if (tStart < toStart - EPS || tEnd > toEnd + EPS) {
1568
- throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
1569
- }
1570
- const record = windows.get(out) ?? [];
1571
- record.push({ from: t.from, to: t.to, start: tStart, end: tEnd });
1572
- windows.set(out, record);
1573
- const abs = { ...t, at: tStart };
1574
- const nFrom = keyframeCount(fromEntry.seq);
1575
- const nTo = keyframeCount(toEntry.seq);
1576
- switch (abs.kind) {
1577
- case "crossfade":
1578
- expandCrossfade(out, abs, fromEntry.seq, toEntry.seq);
1579
- break;
1580
- case "wipe":
1581
- expandMask(out, abs, fromEntry.seq, toEntry.seq, i, wipeMode(abs.direction), abs.smoothing);
1582
- break;
1583
- case "iris":
1584
- expandMask(out, abs, fromEntry.seq, toEntry.seq, i, abs.mode === "out" ? "iris-out" : "iris-in", abs.smoothing);
1585
- break;
1586
- case "slide":
1587
- expandSlide(out, abs, fromEntry.seq, toEntry.seq);
1588
- break;
1589
- case "dip":
1590
- expandDip(out, abs, fromEntry.seq, toEntry.seq);
1591
- break;
1592
- case "zoom":
1593
- expandZoom(out, abs, fromEntry.seq, toEntry.seq);
1594
- break;
1595
- case "dissolve":
1596
- expandDissolve(out, abs, fromEntry.seq, toEntry.seq, i);
1597
- break;
1598
- case "luma":
1599
- expandLuma(out, abs, fromEntry.seq, toEntry.seq, i);
1600
- break;
1601
- }
1602
- rebaseNewKeyframes(fromEntry.seq, nFrom, fromStart);
1603
- rebaseNewKeyframes(toEntry.seq, nTo, toStart);
1604
- }
1605
- delete out.transitions;
1606
- return out;
1607
- }
1608
- function wrapAsFullComposition(seq, compW, compH) {
1609
- const centring = {
1610
- x: compW / 2,
1611
- y: compH / 2,
1612
- pivotX: compW / 2,
1613
- pivotY: compH / 2
1614
- };
1615
- if (seq.type === "composition" && seq.width === compW && seq.height === compH) {
1616
- const existing = seq.initial;
1617
- seq.initial = { ...centring, ...existing ?? {} };
1618
- return seq;
1619
- }
1620
- const inner = { ...seq };
1621
- delete inner.name;
1622
- delete inner.at;
1623
- delete inner.duration;
1624
- const m = inner.mask;
1625
- const carriesMatte = m !== void 0 && maskSourceOf(m).kind === "refs";
1626
- if (carriesMatte) delete inner.mask;
1627
- return {
1628
- ...carriesMatte ? { mask: m } : {},
1629
- type: "composition",
1630
- name: seq.name,
1631
- at: seq.at,
1632
- duration: seq.duration,
1633
- width: compW,
1634
- height: compH,
1635
- initial: centring,
1636
- sequences: [inner]
1637
- };
1638
- }
1639
- function keyframeCount(seq) {
1640
- return seq.keyframes?.length ?? 0;
1641
- }
1642
- function rebaseNewKeyframes(seq, from, seqStart) {
1643
- const kfs = seq.keyframes;
1644
- if (!kfs) return;
1645
- for (let i = from; i < kfs.length; i++) {
1646
- const kf = kfs[i];
1647
- if (typeof kf.at === "number") kfs[i] = { ...kf, at: kf.at - seqStart };
1648
- }
1649
- }
1650
- function ensureKeyframes(seq) {
1651
- const s = seq;
1652
- if (!s.keyframes) s.keyframes = [];
1653
- return s.keyframes;
1654
- }
1655
- function ensureInitial(seq) {
1656
- const s = seq;
1657
- if (!s.initial) s.initial = {};
1658
- return s.initial;
1659
- }
1660
- function ensureFilters(seq) {
1661
- const s = seq;
1662
- if (!s.filters) s.filters = [];
1663
- return s.filters;
1664
- }
1665
- function expandCrossfade(_comp, t, fromSeq, toSeq) {
1666
- const ease = t.ease ?? "none";
1667
- const fromKfs = ensureKeyframes(fromSeq);
1668
- fromKfs.push({ at: t.at, to: { alpha: 0 }, duration: t.duration, ease });
1669
- const toInitial = ensureInitial(toSeq);
1670
- if (toInitial.alpha !== void 0 && toInitial.alpha !== 0) {
1671
- throw new Error(
1672
- `pixi-effects: crossfade "${t.from}"\u2192"${t.to}" requires "${t.to}" to start invisible, but it already has initial.alpha=${String(toInitial.alpha)}. Remove the manual setting.`
1673
- );
1674
- }
1675
- toInitial.alpha = 0;
1676
- const toKfs = ensureKeyframes(toSeq);
1677
- toKfs.push({ at: t.at, to: { alpha: 1 }, duration: t.duration, ease });
1678
- }
1679
- function wipeMode(direction) {
1680
- switch (direction) {
1681
- case "left":
1682
- return "wipe-left";
1683
- case "right":
1684
- return "wipe-right";
1685
- case "up":
1686
- return "wipe-up";
1687
- case "down":
1688
- return "wipe-down";
1689
- }
1690
- }
1691
- function transitionFilterName(index) {
1692
- return `_pe-transition-${index}`;
1693
- }
1694
- function expandSlide(_comp, t, fromSeq, toSeq) {
1695
- const ease = t.ease ?? "none";
1696
- const isHorizontal = t.direction === "left" || t.direction === "right";
1697
- const axis = isHorizontal ? "x" : "y";
1698
- const dim = isHorizontal ? "W" : "H";
1699
- const sign = t.direction === "left" || t.direction === "up" ? -1 : 1;
1700
- const fromInitial = ensureInitial(fromSeq);
1701
- const fromNatural = fromInitial[axis];
1702
- const fromTarget = composeDimOffset(fromNatural, sign, dim);
1703
- const fromKfs = ensureKeyframes(fromSeq);
1704
- fromKfs.push({ at: t.at, to: { [axis]: fromTarget }, duration: t.duration, ease });
1705
- const toInitial = ensureInitial(toSeq);
1706
- const toNatural = toInitial[axis];
1707
- toInitial[axis] = composeDimOffset(toNatural, -sign, dim);
1708
- const toKfs = ensureKeyframes(toSeq);
1709
- toKfs.push({ at: t.at, to: { [axis]: toNatural ?? 0 }, duration: t.duration, ease });
1710
- }
1711
- function composeDimOffset(natural, sign, dim) {
1712
- const signed = sign === 1 ? dim : `-${dim}`;
1713
- if (natural === void 0 || natural === 0) return signed;
1714
- if (typeof natural === "number") {
1715
- return sign === 1 ? `${natural} + ${dim}` : `${natural} - ${dim}`;
1716
- }
1717
- return sign === 1 ? `(${String(natural)}) + ${dim}` : `(${String(natural)}) - ${dim}`;
1718
- }
1719
- function expandMask(comp, t, fromSeq, toSeq, transitionIndex, mode, smoothing) {
1720
- const ease = t.ease ?? "none";
1721
- const inName = transitionFilterName(transitionIndex);
1722
- const outName = `${transitionFilterName(transitionIndex)}-out`;
1723
- const sm = smoothing ?? 0.02;
1724
- const compW = comp.width ?? 0;
1725
- const compH = comp.height ?? 0;
1726
- const fullArea = { x: 0, y: 0, width: compW, height: compH };
1727
- const toFilters = ensureFilters(toSeq);
1728
- rejectReservedNameCollision(toFilters, toSeq.name);
1729
- toFilters.push({
1730
- type: "custom",
1731
- name: inName,
1732
- filter: new TransitionMaskFilter({ mode, smoothing: sm, progress: 0 })
1733
- });
1734
- ensureKeyframes(toSeq).push({
1735
- at: t.at,
1736
- to: { [`filters.${inName}.uProgress`]: 1 },
1737
- duration: t.duration,
1738
- ease
1739
- });
1740
- toSeq.filterArea = fullArea;
1741
- const fromFilters = ensureFilters(fromSeq);
1742
- rejectReservedNameCollision(fromFilters, fromSeq.name);
1743
- fromFilters.push({
1744
- type: "custom",
1745
- name: outName,
1746
- filter: new TransitionMaskFilter({ mode, smoothing: sm, progress: 0, invert: true })
1747
- });
1748
- ensureKeyframes(fromSeq).push({
1749
- at: t.at,
1750
- to: { [`filters.${outName}.uProgress`]: 1 },
1751
- duration: t.duration,
1752
- ease
1753
- });
1754
- fromSeq.filterArea = fullArea;
1755
- }
1756
- function expandDip(_comp, t, fromSeq, toSeq) {
1757
- const ease = t.ease ?? "none";
1758
- const half = t.duration / 2;
1759
- ensureKeyframes(fromSeq).push({
1760
- at: t.at,
1761
- to: { alpha: 0 },
1762
- duration: half,
1763
- ease
1764
- });
1765
- const toInitial = ensureInitial(toSeq);
1766
- if (toInitial.alpha !== void 0 && toInitial.alpha !== 0) {
1767
- throw new Error(
1768
- `pixi-effects: dip "${t.from}"\u2192"${t.to}" requires "${t.to}" to start invisible, but it already has initial.alpha=${String(toInitial.alpha)}. Remove the manual setting.`
1769
- );
1770
- }
1771
- toInitial.alpha = 0;
1772
- ensureKeyframes(toSeq).push({
1773
- at: t.at + half,
1774
- to: { alpha: 1 },
1775
- duration: half,
1776
- ease
1777
- });
1778
- }
1779
- function expandZoom(_comp, t, fromSeq, toSeq) {
1780
- const ease = t.ease ?? "none";
1781
- const startScale = t.fromScale ?? 4;
1782
- const mode = t.mode ?? "in";
1783
- if (mode === "in") {
1784
- ensureKeyframes(fromSeq).push({
1785
- at: t.at,
1786
- to: { alpha: 0 },
1787
- duration: t.duration,
1788
- ease
1789
- });
1790
- const toInitial = ensureInitial(toSeq);
1791
- toInitial.alpha = 0;
1792
- toInitial.scale = startScale;
1793
- ensureKeyframes(toSeq).push({
1794
- at: t.at,
1795
- to: { alpha: 1, scale: 1 },
1796
- duration: t.duration,
1797
- ease
1798
- });
1799
- } else {
1800
- ensureKeyframes(fromSeq).push({
1801
- at: t.at,
1802
- to: { alpha: 0, scale: startScale },
1803
- duration: t.duration,
1804
- ease
1805
- });
1806
- const toInitial = ensureInitial(toSeq);
1807
- toInitial.alpha = 0;
1808
- ensureKeyframes(toSeq).push({
1809
- at: t.at,
1810
- to: { alpha: 1 },
1811
- duration: t.duration,
1812
- ease
1813
- });
1814
- }
1815
- }
1816
- function expandDissolve(comp, t, fromSeq, toSeq, transitionIndex) {
1817
- const ease = t.ease ?? "none";
1818
- const inName = transitionFilterName(transitionIndex);
1819
- const outName = `${transitionFilterName(transitionIndex)}-out`;
1820
- const sm = t.smoothing ?? 0.05;
1821
- const scale = t.scale ?? 30;
1822
- const seed = t.seed ?? 0;
1823
- const compW = comp.width ?? 0;
1824
- const compH = comp.height ?? 0;
1825
- const fullArea = { x: 0, y: 0, width: compW, height: compH };
1826
- const toFilters = ensureFilters(toSeq);
1827
- rejectReservedNameCollision(toFilters, toSeq.name);
1828
- toFilters.push({
1829
- type: "custom",
1830
- name: inName,
1831
- filter: new TransitionMaskFilter({ mode: "dissolve", smoothing: sm, progress: 0, scale, seed })
1832
- });
1833
- ensureKeyframes(toSeq).push({
1834
- at: t.at,
1835
- to: { [`filters.${inName}.uProgress`]: 1 },
1836
- duration: t.duration,
1837
- ease
1838
- });
1839
- toSeq.filterArea = fullArea;
1840
- const fromFilters = ensureFilters(fromSeq);
1841
- rejectReservedNameCollision(fromFilters, fromSeq.name);
1842
- fromFilters.push({
1843
- type: "custom",
1844
- name: outName,
1845
- filter: new TransitionMaskFilter({ mode: "dissolve", smoothing: sm, progress: 0, scale, seed, invert: true })
1846
- });
1847
- ensureKeyframes(fromSeq).push({
1848
- at: t.at,
1849
- to: { [`filters.${outName}.uProgress`]: 1 },
1850
- duration: t.duration,
1851
- ease
1852
- });
1853
- fromSeq.filterArea = fullArea;
1854
- }
1855
- function expandLuma(comp, t, fromSeq, toSeq, transitionIndex) {
1856
- const ease = t.ease ?? "none";
1857
- const inName = transitionFilterName(transitionIndex);
1858
- const outName = `${transitionFilterName(transitionIndex)}-out`;
1859
- const fullArea = { x: 0, y: 0, width: comp.width ?? 0, height: comp.height ?? 0 };
1860
- const opts = { map: t.map ?? "linear", softness: t.softness ?? 0.1, flip: t.flip ?? false };
1861
- for (const [seq, name, invert] of [[toSeq, inName, false], [fromSeq, outName, true]]) {
1862
- const filters = ensureFilters(seq);
1863
- rejectReservedNameCollision(filters, seq.name);
1864
- filters.push({ type: "custom", name, filter: new LumaWipeFilter({ ...opts, invert }) });
1865
- ensureKeyframes(seq).push({ at: t.at, to: { [`filters.${name}.uProgress`]: 1 }, duration: t.duration, ease });
1866
- seq.filterArea = fullArea;
1867
- }
1868
- }
1869
- var OWN_NAME_PATTERN = /^_pe-transition-\d+(-out)?$/;
1870
- function rejectReservedNameCollision(filters, seqName) {
1871
- for (const f of filters) {
1872
- if (typeof f.name === "string" && f.name.startsWith("_pe-transition-") && !OWN_NAME_PATTERN.test(f.name)) {
1873
- throw new Error(
1874
- `pixi-effects: sequence "${seqName}" already has a filter named "${f.name}" \u2014 the reserved name prefix "_pe-transition-" is for internally generated transition filters`
1875
- );
1876
- }
1877
- }
1878
- }
1879
-
1880
- // src/core/inspect.ts
1881
- var FAINT = 0.3;
1882
- var intersection = (a, b) => {
1883
- const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
1884
- const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
1885
- return w > 0 && h > 0 ? w * h : 0;
1886
- };
1887
- var clip = (a, b) => {
1888
- const x = Math.max(a.x, b.x), y = Math.max(a.y, b.y);
1889
- const w = Math.min(a.x + a.width, b.x + b.width) - x, h = Math.min(a.y + a.height, b.y + b.height) - y;
1890
- return w > 0 && h > 0 ? { x, y, width: w, height: h } : null;
1891
- };
1892
- function inspectScene(root, frame, time, canvas, opts = {}) {
1893
- const layers = [];
1894
- const view = { x: 0, y: 0, width: canvas.width, height: canvas.height };
1895
- const seen = /* @__PURE__ */ new Map();
1896
- const scene = /* @__PURE__ */ new Map();
1897
- const scaledToNothing = /* @__PURE__ */ new Set();
1898
- const walk2 = (comp, prefix, insideThreeD, parentVisible, parentScene, parentMoving = false, parentZero = false) => {
1899
- const blending = /* @__PURE__ */ new Set();
1900
- const now = comp.childTime(time);
1901
- for (const w of transitionWindowsOf(comp.spec)) {
1902
- const start = comp.childBase + w.start, end = comp.childBase + w.end;
1903
- if (now >= start - 1e-9 && now <= end + 1e-9) {
1904
- blending.add(w.from);
1905
- blending.add(w.to);
1906
- }
1907
- }
1908
- comp.layers().forEach(({ seq, display, threeD, carriers, depthBlur, light }, i) => {
1909
- const t = seq.target;
1910
- const name = seq.spec.name;
1911
- const label = name ?? `${seq.spec.type}#${i}`;
1912
- const alpha = (t?.alpha ?? 1) * carriers.reduce((a, c) => a * (c.target?.alpha ?? 1), 1);
1913
- const alive = (threeD ? display.visible : t?.renderable ?? true) && carriers.every((c) => c.target?.renderable ?? true);
1914
- const visible = parentVisible && alive && alpha > 0;
1915
- let bounds = null;
1916
- if (!insideThreeD) {
1917
- const b = display.getBounds();
1918
- bounds = { x: b.x, y: b.y, width: b.width, height: b.height };
1919
- }
1920
- let onCanvas = null;
1921
- if (bounds) {
1922
- const inter = intersection(bounds, view);
1923
- const area = bounds.width * bounds.height;
1924
- onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
1925
- if (area === 0) onCanvas = "none";
1926
- }
1927
- const keys = collectPropKeys(seq.spec);
1928
- const carried = carriers.some((c) => {
1929
- const k = collectPropKeys(c.spec);
1930
- return (c.spec.keyframes ?? []).length > 0 && (k.has("x") || k.has("y") || k.has("rotation") || k.has("scale"));
1931
- });
1932
- const moving = parentMoving || carried || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
1933
- const zeroScale = (o) => {
1934
- const s = o?.scale;
1935
- return !!s && (s.x === 0 || s.y === 0);
1936
- };
1937
- const zero = parentZero || zeroScale(t) || carriers.some((c) => zeroScale(c.target));
1938
- const info = { path: prefix + label, name, type: seq.spec.type, threeD, visible, alpha, moving, bounds, onCanvas };
1939
- if (threeD) info.depthBlur = Number(depthBlur.toFixed(2));
1940
- if (threeD && light !== void 0) info.light = Number(light.toFixed(2));
1941
- layers.push(info);
1942
- let shown = bounds ? clip(bounds, view) : null;
1943
- const maskTarget = seq.maskSequence?.target;
1944
- if (shown && maskTarget && !seq.spec.maskInverted) {
1945
- const m = maskTarget.getBounds();
1946
- shown = clip(shown, { x: m.x, y: m.y, width: m.width, height: m.height });
1947
- }
1948
- seen.set(info, shown);
1949
- const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
1950
- if (group) scene.set(info, group);
1951
- if (zero || seq.showsNothingYet) scaledToNothing.add(info);
1952
- if (seq instanceof CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
1953
- });
1954
- };
1955
- walk2(root, "", false, true);
1956
- const issues = [];
1957
- const texts = layers.filter((l) => l.type === "text" && l.visible && l.alpha >= FAINT && l.bounds);
1958
- for (const l of texts) {
1959
- const b = l.bounds;
1960
- const who = `text layer "${l.path}"`;
1961
- if (b.width < 1 || b.height < 1) {
1962
- if (scaledToNothing.has(l)) continue;
1963
- issues.push(`${who} has no size (empty text, or not drawn yet)`);
1964
- } else if (l.moving || scene.has(l)) ; else if (l.onCanvas === "none") {
1965
- issues.push(`${who} is entirely outside the canvas (at ${Math.round(b.x)},${Math.round(b.y)}, ${Math.round(b.width)}\xD7${Math.round(b.height)})`);
1966
- } else if (l.onCanvas === "partial") {
1967
- const over = [];
1968
- if (b.x < 0) over.push(`${Math.round(-b.x)}px beyond the left edge`);
1969
- if (b.x + b.width > canvas.width) over.push(`${Math.round(b.x + b.width - canvas.width)}px beyond the right edge`);
1970
- if (b.y < 0) over.push(`${Math.round(-b.y)}px beyond the top edge`);
1971
- if (b.y + b.height > canvas.height) over.push(`${Math.round(b.y + b.height - canvas.height)}px beyond the bottom edge`);
1972
- issues.push(`${who} is cut off by the canvas edge: ${over.join(", ")}`);
1973
- }
1974
- }
1975
- for (let i = 0; i < texts.length; i++) {
1976
- for (let j = i + 1; j < texts.length; j++) {
1977
- const a = seen.get(texts[i]), b = seen.get(texts[j]);
1978
- if (!a || !b) continue;
1979
- const sa = scene.get(texts[i]), sb = scene.get(texts[j]);
1980
- if (sa && sb && sa !== sb) continue;
1981
- const smaller = Math.min(a.width * a.height, b.width * b.height);
1982
- if (smaller <= 0) continue;
1983
- const share = intersection(a, b) / smaller;
1984
- if (share > 0.25) issues.push(`text layers "${texts[i].path}" and "${texts[j].path}" overlap by ${Math.round(share * 100)}% of the smaller one`);
1985
- }
1986
- }
1987
- const mode = opts.layers ?? "all";
1988
- const listed = mode === "none" ? [] : mode === "visible" ? layers.filter((l) => l.visible) : layers;
1989
- return { frame, time, canvas, summary: { layers: layers.length, visible: layers.filter((l) => l.visible).length }, issues, layers: listed };
1990
- }
1991
-
1992
- // src/core/timelineChart.ts
1993
- var GROUP_MIN = 4;
1994
- var MAX_KEYS = 200;
1995
- var round = (n) => Math.round(n * 1e6) / 1e6;
1996
- var family = (name) => name.replace(/\d+/g, "#");
1997
- function detailOf(seq) {
1998
- const s = seq.spec;
1999
- if (typeof s.text === "string") return s.text.length > 60 ? s.text.slice(0, 57) + "\u2026" : s.text;
2000
- if (typeof s.asset === "string") return `asset \u201C${s.asset}\u201D`;
2001
- if (typeof s.sfx === "string") return `sfx \u201C${s.sfx}\u201D`;
2002
- if (s.sfx && typeof s.sfx === "object") return "sfx (custom)";
2003
- if (s.music && typeof s.music === "object") return "music";
2004
- return void 0;
2005
- }
2006
- function walk(comp, parentStart, parentEnd, depth, prefix, rows, transitions, local) {
2007
- const entries = [];
2008
- comp._children.forEach((seq, i) => {
2009
- const name = seq.spec.name ?? `${seq.spec.type}#${i}`;
2010
- const start = parentStart + (seq.at ?? 0);
2011
- const end = Math.min(parentEnd, seq.duration !== void 0 ? start + seq.duration : parentEnd);
2012
- const span = end - start;
2013
- const keys = (seq.spec.keyframes ?? []).map((kf) => round(start + resolveAt(kf.at, span))).sort((a, b) => a - b).slice(0, MAX_KEYS);
2014
- const row = { path: prefix + name, name, type: seq.spec.type, depth, start: round(start), end: round(end), keys, detail: detailOf(seq), ...local ? { local } : {} };
2015
- const inner = [];
2016
- const isComp = seq instanceof CompositionSequence;
2017
- if (isComp) {
2018
- const label = seq.remapLabel();
2019
- walk(seq, label ? 0 : start, label ? seq.contentSpan : end, depth + 1, prefix + name + "/", inner, transitions, label ? `${name} ${label}` : local);
2020
- }
2021
- entries.push({ row, inner, groupable: !isComp });
2022
- });
2023
- rows.push(...groupFamilies(entries));
2024
- for (const w of transitionWindowsOf(comp.spec)) {
2025
- transitions.push({ from: prefix + w.from, to: prefix + w.to, start: round(parentStart + w.start), end: round(parentStart + w.end), ...local ? { local } : {} });
2026
- }
2027
- }
2028
- function groupFamilies(entries) {
2029
- const buckets = /* @__PURE__ */ new Map();
2030
- for (const e of entries) {
2031
- if (!e.groupable) continue;
2032
- const key = e.row.type + "|" + family(e.row.name);
2033
- (buckets.get(key) ?? buckets.set(key, []).get(key)).push(e.row);
2034
- }
2035
- const out = [];
2036
- const done = /* @__PURE__ */ new Set();
2037
- for (const e of entries) {
2038
- const key = e.row.type + "|" + family(e.row.name);
2039
- const members = e.groupable ? buckets.get(key) : null;
2040
- if (members && members.length >= GROUP_MIN) {
2041
- if (done.has(key)) continue;
2042
- done.add(key);
2043
- const label = `${family(e.row.name)} \xD7${members.length}`;
2044
- out.push({
2045
- path: e.row.path.slice(0, e.row.path.length - e.row.name.length) + label,
2046
- name: label,
2047
- type: e.row.type,
2048
- depth: e.row.depth,
2049
- start: Math.min(...members.map((x) => x.start)),
2050
- end: Math.max(...members.map((x) => x.end)),
2051
- keys: members.flatMap((x) => x.keys).sort((a, b) => a - b).slice(0, MAX_KEYS),
2052
- parts: members.map((x) => ({ start: x.start, end: x.end })),
2053
- partNames: members.map((x) => x.name)
2054
- });
2055
- } else {
2056
- out.push(e.row, ...e.inner);
2057
- continue;
2058
- }
2059
- out.push(...e.inner);
2060
- }
2061
- return out;
2062
- }
2063
- function collectTimeline(root, duration) {
2064
- const rows = [];
2065
- const transitions = [];
2066
- const label = root.remapLabel();
2067
- walk(root, 0, label ? root.contentSpan : duration, 0, "", rows, transitions, label ? `movie ${label}` : void 0);
2068
- return { duration, rows, transitions };
2069
- }
2070
- var COLORS = {
2071
- text: "#4cc9f0",
2072
- shape: "#ffd166",
2073
- image: "#7bd88f",
2074
- video: "#c77dff",
2075
- audio: "#ff7aa8",
2076
- composition: "#8ea4c8",
2077
- three: "#ff9f68",
2078
- camera: "#b0b0b0",
2079
- light: "#b0b0b0",
2080
- null: "#b0b0b0"
2081
- };
2082
- var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2083
- var num = (n) => String(Math.round(n * 100) / 100);
2084
- function tickStep(duration, chartWidth) {
2085
- for (const s of [0.04, 0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 1200, 3600]) if (s / duration * chartWidth >= 70) return s;
2086
- return 7200;
2087
- }
2088
- function timelineSvg(data, opts = {}) {
2089
- const CHART = Math.max(100, opts.chartWidth ?? 950), withLabels = opts.labels !== false;
2090
- const LABEL = withLabels ? 250 : 0, ROW = 22, TOP = 34, PAD = 16;
2091
- const W = LABEL + CHART + PAD * 2, H = TOP + data.rows.length * ROW + PAD;
2092
- const x = (t) => PAD + LABEL + Math.max(0, Math.min(data.duration, t)) / data.duration * CHART;
2093
- const step = tickStep(data.duration, CHART);
2094
- const out = [];
2095
- for (let t = 0; t <= data.duration + 1e-9; t += step) {
2096
- out.push(`<line class="grid" x1="${num(x(t))}" y1="${TOP - 6}" x2="${num(x(t))}" y2="${H - PAD}"/>`);
2097
- out.push(`<text class="tick" x="${num(x(t))}" y="${TOP - 12}" text-anchor="middle">${num(t)}${t === 0 || Math.abs(t - data.duration) < 1e-9 ? " s" : ""}</text>`);
2098
- }
2099
- if (Math.abs(data.duration % step) > 1e-9) out.push(`<text class="tick" x="${num(x(data.duration))}" y="${TOP - 12}" text-anchor="middle">${num(data.duration)} s</text>`);
2100
- for (const t of data.transitions) {
2101
- out.push(`<g><title>transition ${esc(t.from)} \u2192 ${esc(t.to)} (${num(t.start)}\u2013${num(t.end)} s${t.local ? `, local time of ${esc(t.local)}` : ""})</title><rect class="transition" x="${num(x(t.start))}" y="${TOP - 4}" width="${num(Math.max(2, x(t.end) - x(t.start)))}" height="${H - TOP - PAD + 4}"/></g>`);
2102
- }
2103
- data.rows.forEach((r, i) => {
2104
- const y = TOP + i * ROW;
2105
- const color = COLORS[r.type] ?? "#9aa7b8";
2106
- const tip = `${r.path} \xB7 ${r.type} \xB7 ${num(r.start)}\u2013${num(r.end)} s (${num(r.end - r.start)} s)${r.keys.length ? ` \xB7 ${r.keys.length} keyframe${r.keys.length > 1 ? "s" : ""}` : ""}${r.detail ? ` \xB7 ${r.detail}` : ""}${r.local ? ` \xB7 local time of ${r.local}` : ""}`;
2107
- out.push(`<g class="row" data-start="${num(r.start)}"><title>${esc(tip)}</title>`);
2108
- out.push(`<rect class="band" x="${PAD}" y="${y}" width="${LABEL + CHART}" height="${ROW}" ${i % 2 ? 'fill-opacity=".05"' : 'fill-opacity="0"'}/>`);
2109
- if (withLabels) out.push(`<text class="label" x="${PAD + 6 + r.depth * 14}" y="${y + 15}">${esc(r.name)}</text>`);
2110
- for (const p of r.parts ?? [{ start: r.start, end: r.end }]) {
2111
- out.push(`<rect class="bar" x="${num(x(p.start))}" y="${y + 4}" width="${num(Math.max(2, x(p.end) - x(p.start)))}" height="${ROW - 8}" rx="3" fill="${color}"/>`);
2112
- }
2113
- for (const k of r.keys) out.push(`<path class="key" d="M ${num(x(k))} ${y + 7} l 3 4 l -3 4 l -3 -4 z"/>`);
2114
- out.push("</g>");
2115
- });
2116
- const LEGEND_STEP = 108, perLine = Math.max(1, Math.floor((W - PAD * 2) / LEGEND_STEP));
2117
- const kinds = Object.entries(COLORS), lines = Math.ceil(kinds.length / perLine), HH = H + 14 + lines * 18;
2118
- const legend = kinds.map(([type, c], i) => `<g transform="translate(${PAD + i % perLine * LEGEND_STEP},${H + 10 + Math.floor(i / perLine) * 18})"><rect width="12" height="12" rx="3" fill="${c}"/><text x="18" y="10" class="tick">${type}</text></g>`).join("");
2119
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${HH}" viewBox="0 0 ${W} ${HH}" role="img" aria-label="Timeline of ${data.rows.length} rows" data-duration="${num(data.duration)}" data-x0="${num(x(0))}" data-x1="${num(x(data.duration))}" data-top="${TOP - 6}" data-bottom="${H - PAD}" data-row="${ROW}">
2120
- ${out.join("\n")}
2121
- ${legend}
2122
- </svg>`;
2123
- }
2124
- function timelineHtml(data, opts = {}) {
2125
- const W = 250 + 950 + 32;
2126
- const title = esc(opts.title ?? "Timeline");
2127
- return `<!doctype html>
2128
- <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title} \u2014 timeline</title>
2129
- <style>
2130
- :root { --bg: #f7f5f1; --ink: #1d2433; --dim: #6b7587; --grid: #d9d5cc; --key: #1d2433; --trans: #4cc9f0; }
2131
- @media (prefers-color-scheme: dark) { :root { --bg: #0e1320; --ink: #e8eefc; --dim: #8fa0bf; --grid: #26304a; --key: #ffffff; --trans: #4cc9f0; } }
2132
- body { margin: 0; background: var(--bg); color: var(--ink); font: 14px system-ui, sans-serif; }
2133
- main { padding: 20px 16px 32px; max-width: ${W + 32}px; margin: 0 auto; overflow-x: auto; }
2134
- h1 { font-size: 18px; font-weight: 600; margin: 0 0 4px; }
2135
- p { margin: 0 0 14px; color: var(--dim); font-size: 13px; }
2136
- svg { display: block; }
2137
- .grid { stroke: var(--grid); stroke-width: 1; }
2138
- .tick { fill: var(--dim); font-size: 11px; }
2139
- .label { fill: var(--ink); font-size: 12px; font-family: ui-monospace, Menlo, monospace; }
2140
- .band { fill: var(--ink); }
2141
- .key { fill: var(--key); opacity: .85; }
2142
- .transition { fill: var(--trans); opacity: .16; }
2143
- .bar { opacity: .88; }
2144
- .row:hover .bar { opacity: 1; }
2145
- </style></head>
2146
- <body><main>
2147
- <h1>${title}</h1>
2148
- <p>${data.rows.length} row${data.rows.length === 1 ? "" : "s"} \xB7 ${num(data.duration)} s \xB7 \u25C6 keyframe start \xB7 shaded band: a transition \xB7 hover a bar for details</p>
2149
- ${timelineSvg(data)}
2150
- </main></body></html>
2151
- `;
2152
- }
2153
-
2154
- // src/core/frames.ts
2155
- function pickFrames(lastFrame, count) {
2156
- const last = Math.max(0, Math.floor(lastFrame));
2157
- const n = Math.max(1, Math.floor(count));
2158
- if (n === 1) return [0];
2159
- const out = [];
2160
- for (let i = 0; i < n; i++) {
2161
- const f = Math.round(last * i / (n - 1));
2162
- if (out[out.length - 1] !== f) out.push(f);
2163
- }
2164
- return out;
2165
- }
2166
- function sheetLayout(n, columns, cellW, srcW, srcH, labelH) {
2167
- const cols = Math.max(1, Math.min(Math.floor(columns), n));
2168
- const rows = Math.ceil(n / cols);
2169
- const cellH = Math.round(cellW * srcH / srcW);
2170
- const positions = Array.from({ length: n }, (_, i) => ({ x: i % cols * cellW, y: Math.floor(i / cols) * (cellH + labelH) }));
2171
- return { cols, rows, cellW, cellH, width: cols * cellW, height: rows * (cellH + labelH), positions };
2172
- }
2173
-
2174
- // src/core/startWhenRunning.ts
2175
- function startWhenRunning(ctx, start, gesture) {
2176
- if (ctx.state === "running") {
2177
- start();
2178
- return () => {
2179
- };
2180
- }
2181
- let done = false;
2182
- const resume = () => {
2183
- ctx.resume?.()?.catch(() => {
2184
- });
2185
- };
2186
- const stop = () => {
2187
- done = true;
2188
- ctx.removeEventListener("statechange", onState);
2189
- for (const e of GESTURES) gesture?.removeEventListener(e, resume, true);
2190
- };
2191
- const onState = () => {
2192
- if (done || ctx.state !== "running") return;
2193
- stop();
2194
- start();
2195
- };
2196
- ctx.addEventListener("statechange", onState);
2197
- for (const e of GESTURES) gesture?.addEventListener(e, resume, true);
2198
- resume();
2199
- return stop;
2200
- }
2201
- var GESTURES = ["pointerdown", "mousedown", "touchend", "keydown"];
2202
- function audioIsBlocked(ctx, hasAudio) {
2203
- return !!ctx && hasAudio && ctx.state !== "running";
2204
- }
2205
-
2206
- // src/core/poster.ts
2207
- function normalizePoster(poster, duration, frameRate) {
2208
- if (poster === void 0 || poster === null) return { seconds: null, frame: null };
2209
- if (typeof poster !== "number" || !Number.isFinite(poster)) {
2210
- console.warn(`pixi-effects: movie.init({ poster }): ${JSON.stringify(poster)} is not a time in seconds; ignored`);
2211
- return { seconds: null, frame: null };
2212
- }
2213
- let seconds = poster < 0 ? Math.max(0, duration + poster) : poster;
2214
- if (seconds > duration) {
2215
- console.warn(`pixi-effects: movie.init({ poster }): the poster time ${poster} s is past the end of the movie (${duration} s); using the last frame`);
2216
- seconds = duration;
2217
- }
2218
- return { seconds, frame: Math.round(seconds * frameRate) };
2219
- }
2220
-
2221
- // src/core/pdf.ts
2222
- var enc = (s) => {
2223
- const out = new Uint8Array(s.length);
2224
- for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 255;
2225
- return out;
2226
- };
2227
- var num2 = (n) => String(Math.round(n * 1e3) / 1e3);
2228
- var textString = (s) => {
2229
- if (/^[\x20-\x7e]*$/.test(s)) return `(${s.replace(/[\\()]/g, (m) => `\\${m}`)})`;
2230
- let hex = "FEFF";
2231
- for (let i = 0; i < s.length; i++) hex += s.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0");
2232
- return `<${hex}>`;
2233
- };
2234
- function buildPdf(pages, opts = {}) {
2235
- if (pages.length === 0) throw new Error("pixi-effects: a PDF needs at least one page");
2236
- const first = (i) => 3 + i * 3;
2237
- const objects = [];
2238
- objects[1] = [enc("<< /Type /Catalog /Pages 2 0 R >>")];
2239
- objects[2] = [enc(`<< /Type /Pages /Kids [${pages.map((_, i) => `${first(i)} 0 R`).join(" ")}] /Count ${pages.length} >>`)];
2240
- pages.forEach((p, i) => {
2241
- const w = p.width * 0.75, h = p.height * 0.75, n = first(i);
2242
- objects[n] = [enc(`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${num2(w)} ${num2(h)}] /Resources << /XObject << /Im0 ${n + 2} 0 R >> >> /Contents ${n + 1} 0 R >>`)];
2243
- const content = `q ${num2(w)} 0 0 ${num2(h)} 0 0 cm /Im0 Do Q`;
2244
- objects[n + 1] = [enc(`<< /Length ${content.length} >>
2245
- stream
2246
- ${content}
2247
- endstream`)];
2248
- objects[n + 2] = [enc(`<< /Type /XObject /Subtype /Image /Width ${p.width} /Height ${p.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${p.jpeg.length} >>
2249
- stream
2250
- `), p.jpeg, enc("\nendstream")];
2251
- });
2252
- const infoNo = opts.title !== void 0 ? first(pages.length) : 0;
2253
- if (infoNo) objects[infoNo] = [enc(`<< /Title ${textString(opts.title)} /Producer (pixi-effects) >>`)];
2254
- const count = infoNo || first(pages.length);
2255
- const chunks = [enc("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n")];
2256
- let offset = chunks[0].length;
2257
- const offsets = [];
2258
- for (let n = 1; n < count + (infoNo ? 1 : 0); n++) {
2259
- const body = objects[n];
2260
- if (!body) continue;
2261
- offsets[n] = offset;
2262
- const parts = [enc(`${n} 0 obj
2263
- `), ...body, enc("\nendobj\n")];
2264
- for (const part of parts) {
2265
- chunks.push(part);
2266
- offset += part.length;
2267
- }
2268
- }
2269
- const size = (infoNo || first(pages.length)) + (infoNo ? 1 : 0);
2270
- let xref = `xref
2271
- 0 ${size}
2272
- 0000000000 65535 f
2273
- `;
2274
- for (let n = 1; n < size; n++) xref += `${String(offsets[n] ?? 0).padStart(10, "0")} 00000 n
2275
- `;
2276
- xref += `trailer
2277
- << /Size ${size} /Root 1 0 R${infoNo ? ` /Info ${infoNo} 0 R` : ""} >>
2278
- startxref
2279
- ${offset}
2280
- %%EOF
2281
- `;
2282
- chunks.push(enc(xref));
2283
- const total = chunks.reduce((sum, c) => sum + c.length, 0);
2284
- const out = new Uint8Array(total);
2285
- let at = 0;
2286
- for (const c of chunks) {
2287
- out.set(c, at);
2288
- at += c.length;
2289
- }
2290
- return out;
2291
- }
2292
-
2293
- // src/core/stops.ts
2294
- var KEYS = ["at", "page", "notes", "advance", "pdf"];
2295
- function normalizeStops(raw, duration, frameRate) {
2296
- if (!raw || raw.length === 0) return [];
2297
- const total = Math.round(duration * frameRate);
2298
- const specs = [];
2299
- raw.forEach((r, i) => {
2300
- const spec = typeof r === "number" ? { at: r } : r && typeof r === "object" ? r : null;
2301
- if (!spec || typeof spec.at !== "number" || !Number.isFinite(spec.at)) {
2302
- console.warn(`pixi-effects: stops[${i}] ${JSON.stringify(r)} is not a time in seconds (or { at, page?, notes?, advance?, pdf? }); ignored`);
2303
- return;
2304
- }
2305
- if (typeof r === "object") warnUnknownOptions(`stops[${i}]`, r, KEYS);
2306
- const at = spec.at < 0 ? duration + spec.at : spec.at;
2307
- if (at < -1e-9 || at > duration + 1e-9) {
2308
- console.warn(`pixi-effects: stops[${i}] at ${spec.at} s is outside the movie (0 to ${duration} s); ignored`);
2309
- return;
2310
- }
2311
- specs.push({ ...spec, at });
2312
- });
2313
- specs.sort((a, b) => a.at - b.at);
2314
- const kept = [];
2315
- for (const s of specs) {
2316
- const frame = Math.min(total, Math.max(0, Math.round(s.at * frameRate)));
2317
- const last = kept[kept.length - 1];
2318
- if (last && Math.min(total, Math.max(0, Math.round(last.at * frameRate))) === frame) {
2319
- console.warn(`pixi-effects: two stops are on the same frame (${frame}, about ${s.at.toFixed(2)} s); one is kept`);
2320
- continue;
2321
- }
2322
- kept.push(s);
2323
- }
2324
- const anyPage = kept.some((s) => s.page !== void 0);
2325
- let pageIndex = -1, pageName = null;
2326
- return kept.map((s, index) => {
2327
- const pageStart = index === 0 || (anyPage ? s.page !== void 0 : true);
2328
- if (pageStart) {
2329
- pageIndex++;
2330
- pageName = typeof s.page === "string" ? s.page : null;
2331
- }
2332
- const stop = { index, at: s.at, frame: Math.min(total, Math.max(0, Math.round(s.at * frameRate))), page: pageName, pageIndex, pageStart };
2333
- if (typeof s.notes === "string") stop.notes = s.notes;
2334
- if (s.advance !== void 0) {
2335
- if (typeof s.advance === "number" && Number.isFinite(s.advance) && s.advance >= 0) stop.advance = s.advance;
2336
- else console.warn(`pixi-effects: stops[${index}] advance must be a number of seconds >= 0, got ${JSON.stringify(s.advance)}; ignored`);
2337
- }
2338
- if (s.pdf !== void 0) {
2339
- if (typeof s.pdf === "boolean") stop.pdf = s.pdf;
2340
- else console.warn(`pixi-effects: stops[${index}] pdf must be true or false, got ${JSON.stringify(s.pdf)}; ignored`);
2341
- }
2342
- return stop;
2343
- });
2344
- }
2345
- function nextStopAfter(stops, frame) {
2346
- return stops.find((s) => s.frame > frame) ?? null;
2347
- }
2348
- function previousStopBefore(stops, frame) {
2349
- for (let i = stops.length - 1; i >= 0; i--) if (stops[i].frame < frame) return stops[i];
2350
- return null;
2351
- }
2352
- function stopAtOrBefore(stops, frame) {
2353
- for (let i = stops.length - 1; i >= 0; i--) if (stops[i].frame <= frame) return stops[i];
2354
- return null;
2355
- }
2356
- function pageStarts(stops) {
2357
- return stops.filter((s) => s.pageStart);
2358
- }
2359
- function pictureStops(stops, opts = {}) {
2360
- if (opts.which === "stops") return opts.pdf ? stops.filter((s) => s.pdf !== false) : [...stops];
2361
- const byPage = /* @__PURE__ */ new Map();
2362
- for (const s of stops) byPage.set(s.pageIndex, [...byPage.get(s.pageIndex) ?? [], s]);
2363
- const take = (list) => opts.pick === "first" ? list[0] : list[list.length - 1];
2364
- const picks = [];
2365
- for (const list of byPage.values()) {
2366
- const flagged = list.filter((s) => s.pdf === true);
2367
- const usable = list.filter((s) => s.pdf !== false);
2368
- const one = take(flagged.length ? flagged : usable.length ? usable : opts.pdf ? [] : list);
2369
- if (one) picks.push(one);
2370
- }
2371
- return picks;
2372
- }
2373
- function changedFraction(a, b, threshold = 12) {
2374
- const n = Math.min(a.length, b.length) >> 2;
2375
- if (n === 0) return 0;
2376
- let changed = 0;
2377
- for (let i = 0; i < n; i++) {
2378
- const k = i << 2;
2379
- if (Math.abs(a[k] - b[k]) > threshold || Math.abs(a[k + 1] - b[k + 1]) > threshold || Math.abs(a[k + 2] - b[k + 2]) > threshold) changed++;
2380
- }
2381
- return changed / n;
2382
- }
2383
-
2384
- // src/core/loadProgress.ts
2385
- var STAGE_LABEL = {
2386
- assets: "LOADING ASSETS",
2387
- build: "BUILDING LAYERS",
2388
- sound: "MIXING SOUND",
2389
- frames: "PREPARING"
2390
- };
2391
- var ORDER = ["assets", "build", "sound", "frames"];
2392
- var WEIGHT = { assets: 0.15, build: 0.6, sound: 0.2, frames: 0.05 };
2393
- var YIELD_EVERY_MS = 50;
2394
- function defaultYielder() {
2395
- if (typeof MessageChannel === "undefined") return new Promise((resolve) => setTimeout(resolve, 0));
2396
- return new Promise((resolve) => {
2397
- const ch = new MessageChannel();
2398
- ch.port1.onmessage = () => {
2399
- ch.port1.close();
2400
- resolve();
2401
- };
2402
- ch.port2.postMessage(0);
2403
- });
2404
- }
2405
- var LoadProgress = class {
2406
- constructor(_onChange, opts = {}) {
2407
- this._onChange = _onChange;
2408
- this._now = opts.now ?? (() => typeof performance !== "undefined" ? performance.now() : Date.now());
2409
- this._yielder = opts.yielder ?? defaultYielder;
2410
- this._hidden = opts.hidden ?? (() => typeof document !== "undefined" && document.hidden === true);
2411
- this._every = opts.yieldEveryMs ?? YIELD_EVERY_MS;
2412
- this._lastYield = this._now();
2413
- }
2414
- _onChange;
2415
- _stage = null;
2416
- _loaded = 0;
2417
- _part = 0;
2418
- _total = 0;
2419
- _high = 0;
2420
- _started = 0;
2421
- _ms = { assets: 0, build: 0, sound: 0, frames: 0 };
2422
- _lastYield;
2423
- _now;
2424
- _yielder;
2425
- _every;
2426
- _hidden;
2427
- get progress() {
2428
- return this._high;
2429
- }
2430
- /** Start a stage with `total` items (0 = nothing to wait for). Every earlier stage is done. */
2431
- begin(stage, total) {
2432
- this._closeTiming();
2433
- this._stage = stage;
2434
- this._loaded = 0;
2435
- this._part = 0;
2436
- this._total = Math.max(0, total);
2437
- this._started = this._now();
2438
- this._emit();
2439
- }
2440
- tick(n = 1) {
2441
- if (!this._stage) return;
2442
- this._loaded = Math.min(this._total, this._loaded + n);
2443
- this._part = 0;
2444
- this._emit();
2445
- }
2446
- /** How much of the item being worked on is done (0..1), for an item that takes long: the number moves between two `tick`s. */
2447
- part(done) {
2448
- if (!this._stage) return;
2449
- this._part = Math.min(1, Math.max(0, done));
2450
- this._emit();
2451
- }
2452
- /** Everything is done: 1, whatever stage it was in. */
2453
- finish() {
2454
- this._closeTiming();
2455
- this._stage = this._stage ?? "frames";
2456
- this._loaded = this._total;
2457
- this._high = 1;
2458
- this._onChange({ stage: this._stage, loaded: this._loaded, total: this._total, progress: 1 });
2459
- }
2460
- /** Milliseconds each stage took (a stage never begun is 0). */
2461
- timings() {
2462
- return { ...this._ms };
2463
- }
2464
- /** Await one macrotask when 50 ms of work have gone by since the last time, so the page can paint the bar. Cheap when it is not due. */
2465
- async yieldIfDue() {
2466
- const t = this._now();
2467
- if (t - this._lastYield < this._every || this._hidden()) return;
2468
- await this._yielder();
2469
- this._lastYield = this._now();
2470
- }
2471
- _closeTiming() {
2472
- if (this._stage) this._ms[this._stage] += Math.max(0, this._now() - this._started);
2473
- }
2474
- _emit() {
2475
- const stage = this._stage;
2476
- let done = 0;
2477
- for (const s of ORDER) {
2478
- if (s === stage) break;
2479
- done += WEIGHT[s];
2480
- }
2481
- const within = this._total > 0 ? Math.min(1, (this._loaded + this._part) / this._total) : 1;
2482
- this._high = Math.min(1, Math.max(this._high, done + WEIGHT[stage] * within));
2483
- this._onChange({ stage, loaded: this._loaded, total: this._total, progress: this._high });
2484
- }
2485
- };
2486
-
2487
- // src/core/loader.ts
2488
- var FADE_FALLBACK_MS = 900;
2489
- function resolveLoader(option, canvas) {
2490
- if (option === false) return null;
2491
- if (option instanceof HTMLElement) return option;
2492
- if (typeof option === "string") return document.querySelector(option);
2493
- let box = canvas?.parentElement ?? null;
2494
- for (let level = 0; box && level < 4; level++, box = box.parentElement) {
2495
- if (level > 0 && (box === document.body || box === document.documentElement)) break;
2496
- const found = box.querySelector(".pe-loader");
2497
- if (found) return found;
2498
- }
2499
- return null;
2500
- }
2501
- function dismissLoader(loader) {
2502
- if (!loader || loader.classList.contains("pe-loader--done")) return;
2503
- loader.classList.add("pe-loader--done");
2504
- loader.setAttribute("aria-hidden", "true");
2505
- let removed = false;
2506
- const remove = () => {
2507
- if (!removed) {
2508
- removed = true;
2509
- loader.remove();
2510
- }
2511
- };
2512
- loader.addEventListener("transitionend", (e) => {
2513
- if (e.target === loader && e.propertyName === "opacity") remove();
2514
- });
2515
- setTimeout(remove, FADE_FALLBACK_MS);
2516
- }
2517
- function failLoader(loader, message) {
2518
- if (!loader) return;
2519
- loader.setAttribute("data-state", "error");
2520
- for (const a of ["data-stage", "data-percent"]) loader.removeAttribute(a);
2521
- loader.querySelector(".pe-loader__bar")?.remove();
2522
- if (loader.hasAttribute("data-label")) loader.setAttribute("data-label", message);
2523
- loader.setAttribute("aria-busy", "false");
2524
- }
2525
- function setLoaderProgress(loader, state) {
2526
- if (!loader || loader.getAttribute("data-state") === "error" || loader.classList.contains("pe-loader--done")) return;
2527
- if (!loader.querySelector(".pe-loader__bar")) {
2528
- const bar = loader.ownerDocument.createElement("i");
2529
- bar.className = "pe-loader__bar";
2530
- bar.setAttribute("aria-hidden", "true");
2531
- loader.appendChild(bar);
2532
- }
2533
- loader.style.setProperty("--pe-progress", String(Math.round(state.progress * 1e3) / 1e3));
2534
- loader.setAttribute("data-stage", STAGE_LABEL[state.stage]);
2535
- loader.setAttribute("data-percent", String(Math.round(state.progress * 100)));
2536
- }
2537
-
2538
- // src/core/Movie.ts
2539
- extensions.add(CullerPlugin);
2540
- var Movie = class {
2541
- _events = {};
2542
- _initState = "idle";
2543
- app = null;
2544
- timeline = null;
2545
- audioBuffer = null;
2546
- audioSource = null;
2547
- gainNode = null;
2548
- _cancelAudioStart = null;
2549
- /** The poster time in seconds (`movie.init({ poster })`), or null. */
2550
- poster = null;
2551
- /** The stops of the composition (`composition.stops`), checked and in order: where `next()` pauses. Empty for an ordinary movie. */
2552
- stops = [];
2553
- /** The frame `next()` is playing toward, or null. */
2554
- _stopAt = null;
2555
- _stopWaiters = [];
2556
- _arriving = false;
2557
- /** The motion blur `render()`, `snapshot()` and `contactSheet()` apply unless told otherwise (`movie.init({ motionBlur })`), or null. */
2558
- motionBlur = null;
2559
- _posterFrame = null;
2560
- /** The canvas shows the poster picture while the playhead is still at frame 0 (like `<video poster>`): the first seek or play replaces it. */
2561
- _atPoster = false;
2562
- /** > 0 while the movie moves its own playhead (playback, render, snapshot, contactSheet): those are not the seeks `seeking` / `seeked` report. */
2563
- _quiet = 0;
2564
- _volume = 1;
2565
- _muted = false;
2566
- isPlaying = false;
2567
- currentFrame = 0;
2568
- totalFrames = 0;
2569
- width = 0;
2570
- height = 0;
2571
- duration = 0;
2572
- frameRate = 30;
2573
- background = "#000000";
2574
- _audioContext = null;
2575
- _rootSequence = null;
2576
- /** The filters that take the time of the frame they draw (`grain`), collected once after the build. */
2577
- _timeFilters = [];
2578
- /** While a motion-blurred frame is exposed: the frame's own time, so all its sub-frames share one grain frame (and not the playhead's). */
2579
- _nominalTime = null;
2580
- _feedTimeFilters() {
2581
- if (!this._timeFilters.length) return;
2582
- const t = this._nominalTime ?? this._timeOf(this.currentFrame);
2583
- for (const f of this._timeFilters) f.setTime(t);
2584
- }
2585
- _rootContainer = null;
2586
- _raf = null;
2587
- /** What went into the mix, and its peak before limiting — kept for inspectAudio(). */
2588
- _audioSources = [];
2589
- _mixStats = null;
2590
- on(event, fn) {
2591
- (this._events[event] ??= []).push(fn);
2592
- if (event === "ready" && this._initState === "ready") {
2593
- try {
2594
- fn();
2595
- } catch (e) {
2596
- console.warn("pixi-effects: ready listener threw:", e);
2597
- }
2598
- }
2599
- return this;
2600
- }
2601
- off(event, fn) {
2602
- const list = this._events[event];
2603
- if (!list) return this;
2604
- const i = list.indexOf(fn);
2605
- if (i > -1) list.splice(i, 1);
2606
- return this;
2607
- }
2608
- emit(event, ...args) {
2609
- for (const fn of this._events[event] ?? []) fn(...args);
2610
- }
2611
- get isReady() {
2612
- return this._initState === "ready";
2613
- }
2614
- /** Milliseconds `init()` spent in each stage (assets, build, sound, frames), or null before it has finished: `check` says which stage is the slow one. */
2615
- get loadStages() {
2616
- return this._loadStages;
2617
- }
2618
- _loadStages = null;
2619
- /** True while the movie has sound but the browser keeps it silent until the viewer taps (iOS Safari, a page nobody has touched): a player can show "tap for sound"; the sound starts at a tap or key press that arrives while play() is pending, or at the next play() made from a tap. */
2620
- get audioBlocked() {
2621
- return audioIsBlocked(this._audioContext, !!this.audioBuffer);
2622
- }
2623
- /** The poster time as a frame number, or null. */
2624
- get posterFrame() {
2625
- return this._posterFrame;
2626
- }
2627
- /** Draw the poster frame on the canvas and leave the playhead where it is (frame 0): the picture before the movie plays. */
2628
- async _showPoster() {
2629
- if (this._posterFrame === null || !this.timeline) return;
2630
- const back = this.currentFrame;
2631
- this.currentFrame = this._posterFrame;
2632
- try {
2633
- this.timeline.time(this._timeOf(this._posterFrame));
2634
- await this._awaitVideoFrames();
2635
- this._updateSpace();
2636
- this._renderNow();
2637
- } finally {
2638
- this.currentFrame = back;
2639
- }
2640
- this._atPoster = true;
2641
- }
2642
- async posterImage(opts = {}) {
2643
- this._requireReady("posterImage");
2644
- const back = this.currentFrame, wasAtPoster = this._atPoster;
2645
- try {
2646
- return await this.snapshot(this._posterFrame ?? 0, opts);
2647
- } finally {
2648
- this.currentFrame = back;
2649
- if (wasAtPoster) await this._showPoster();
2650
- else await this._quietly(() => this.gotoFrame(back, true));
2651
- }
2652
- }
2653
- _ensureAudioContext() {
2654
- if (!this._audioContext) {
2655
- const Ctx = window.AudioContext ?? window.webkitAudioContext;
2656
- this._audioContext = new Ctx();
2657
- }
2658
- return this._audioContext;
2659
- }
2660
- async init(options = {}) {
2661
- warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition", "poster", "motionBlur", "loader"]);
2662
- this._initState = "pending";
2663
- try {
2664
- this.width = options.width ?? 1920;
2665
- this.height = options.height ?? 1080;
2666
- this.duration = options.duration ?? 10;
2667
- this.frameRate = options.frameRate ?? 30;
2668
- this.totalFrames = Math.round(this.duration * this.frameRate);
2669
- const poster = normalizePoster(options.poster, this.duration, this.frameRate);
2670
- this.poster = poster.seconds;
2671
- this.motionBlur = resolveMotionBlur(options.motionBlur, "movie.init()");
2672
- this.stops = normalizeStops(options.composition?.stops, this.duration, this.frameRate);
2673
- this._posterFrame = poster.frame;
2674
- this.background = options.background ?? "#000000";
2675
- this.timeline = createTimeline({ paused: true, defaults: { ease: "none" } });
2676
- lengthen(this.timeline, this.duration);
2677
- const baseAppOptions = {
2678
- width: this.width,
2679
- height: this.height,
2680
- background: this.background,
2681
- antialias: true,
2682
- resolution: 1,
2683
- autoDensity: false,
2684
- preserveDrawingBuffer: true,
2685
- canvas: options.canvas
2686
- };
2687
- this.app = new Application();
2688
- try {
2689
- await this.app.init({ ...baseAppOptions, preference: "webgpu" });
2690
- } catch (err) {
2691
- console.warn("pixi-effects: WebGPU init failed, falling back to WebGL \u2014", err);
2692
- this.app = new Application();
2693
- await this.app.init({ ...baseAppOptions, preference: "webgl" });
2694
- }
2695
- if (usesAdvancedBlend(options.composition)) await enableAdvancedBlend(this.app.renderer);
2696
- const root = new Container();
2697
- root.cullable = true;
2698
- root.cullableChildren = true;
2699
- root.cullArea = new Rectangle(0, 0, this.width, this.height);
2700
- this.app.stage.addChild(root);
2701
- this._rootContainer = root;
2702
- const audioContext = this._ensureAudioContext();
2703
- let loaderEl;
2704
- const lp = new LoadProgress((state) => {
2705
- if (loaderEl === void 0) loaderEl = resolveLoader(options.loader, options.canvas ?? this.app?.canvas ?? null);
2706
- setLoaderProgress(loaderEl, state);
2707
- this.emit("loadprogress", state);
2708
- }, { yieldEveryMs: globalThis.__peLoadYieldMs });
2709
- const assetCount = (options.assets ?? []).length;
2710
- lp.begin("assets", assetCount);
2711
- let assetsSeen = 0;
2712
- await loadAssetBundle(options.assets ?? [], audioContext, (p) => {
2713
- const n = Math.round(p * assetCount);
2714
- if (n > assetsSeen) {
2715
- lp.tick(n - assetsSeen);
2716
- assetsSeen = n;
2717
- }
2718
- });
2719
- const rootShape = {
2720
- width: this.width,
2721
- height: this.height,
2722
- duration: this.duration,
2723
- frameRate: this.frameRate,
2724
- onLayerBuilt: async () => {
2725
- lp.tick();
2726
- await lp.yieldIfDue();
2727
- }
2728
- };
2729
- const seededComposition = options.composition ? {
2730
- ...options.composition,
2731
- width: options.composition.width ?? this.width,
2732
- height: options.composition.height ?? this.height,
2733
- duration: options.composition.duration ?? this.duration
2734
- } : void 0;
2735
- await ensureFilterLibrary(seededComposition);
2736
- const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
2737
- const rootSeqSpec = {
2738
- type: "composition",
2739
- width: this.width,
2740
- height: this.height,
2741
- duration: this.duration,
2742
- ...userComposition
2743
- };
2744
- if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
2745
- const composition = new CompositionSequence(rootSeqSpec, null, rootShape);
2746
- lp.begin("build", countLayers(rootSeqSpec));
2747
- await composition.build();
2748
- this._rootSequence = composition;
2749
- if (composition.target) root.addChild(composition.target);
2750
- composition.bindTimeline(this.timeline);
2751
- this._timeFilters = collectTimeFilters(composition);
2752
- const audios = [];
2753
- composition.collectAudio(audios, 0);
2754
- lp.begin("sound", 1);
2755
- if (audios.length > 0) {
2756
- this.audioBuffer = await mixdown(audios, this.duration, audioContext.sampleRate, async (done) => {
2757
- lp.part(done);
2758
- await lp.yieldIfDue();
2759
- });
2760
- lp.tick();
2761
- if (this.audioBuffer) this._mixStats = limitMix(this.audioBuffer, audios);
2762
- }
2763
- this._audioSources = audios;
2764
- lp.begin("frames", 0);
2765
- this.timeline.time(this._timeOf(0));
2766
- await this._awaitVideoFrames();
2767
- this._updateSpace();
2768
- this._renderNow();
2769
- lp.finish();
2770
- this._loadStages = lp.timings();
2771
- this._initState = "ready";
2772
- if (this._posterFrame !== null) await this._showPoster();
2773
- this.emit("ready");
2774
- dismissLoader(resolveLoader(options.loader, options.canvas ?? this.app?.canvas ?? null));
2775
- } catch (err) {
2776
- failLoader(resolveLoader(options.loader, options.canvas ?? null), "COULD NOT LOAD");
2777
- try {
2778
- await this.destroy();
2779
- } catch (cleanupErr) {
2780
- console.warn("pixi-effects: cleanup after init failure also threw:", cleanupErr);
2781
- }
2782
- this._emitError("init", err);
2783
- throw err;
2784
- }
2785
- }
2786
- async gotoFrame(frame, force = false) {
2787
- if (this._initState !== "ready") return;
2788
- if (!this._arriving) this._cancelStop();
2789
- if (!force && !this._atPoster && this.currentFrame === frame) return;
2790
- if (this._quiet > 0) return this._goto(frame);
2791
- const target = Math.max(0, Math.min(frame, this.totalFrames));
2792
- this.emit("seeking", { frame: target, totalFrames: this.totalFrames });
2793
- await this._goto(frame);
2794
- this.emit("seeked", { frame: this.currentFrame, totalFrames: this.totalFrames });
2795
- }
2796
- /** Run `fn` with `seeking` / `seeked` silenced: the movie is moving its own playhead, not answering a seek. */
2797
- async _quietly(fn) {
2798
- this._quiet++;
2799
- try {
2800
- return await fn();
2801
- } finally {
2802
- this._quiet--;
2803
- }
2804
- }
2805
- _emitError(where, error) {
2806
- this.emit("error", { where, message: error instanceof Error ? error.message : String(error), error });
2807
- }
2808
- async _goto(frame) {
2809
- this._atPoster = false;
2810
- this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
2811
- this.timeline.time(this._timeOf(this.currentFrame));
2812
- await this._awaitVideoFrames();
2813
- this._updateSpace();
2814
- this._renderNow();
2815
- this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
2816
- }
2817
- /**
2818
- * The moment frame `frame` shows. The very last frame (`totalFrames`) is at the end of the movie, where every layer's lifespan
2819
- * has just closed: it is drawn a hair before, so a layer that lasts to the end is still on screen in the final frame
2820
- * (it used to be an empty one, the last frame of every export).
2821
- */
2822
- _timeOf(frame) {
2823
- return Math.min(frame / this.frameRate, Math.max(0, this.duration - END_MARGIN));
2824
- }
2825
- async _awaitVideoFrames(t = this._timeOf(this.currentFrame)) {
2826
- if (!this._rootSequence) return;
2827
- const collected = [];
2828
- const clocks = /* @__PURE__ */ new Map();
2829
- collectVideoSequences(this._rootSequence, collected, clocks);
2830
- await Promise.all(collected.map((v) => {
2831
- if (v.selfTimed) return v.awaitFrameAt(t);
2832
- const local = (clocks.get(v)?.() ?? t) - (v.absoluteStart ?? v.at);
2833
- if (local < 0 || local > (v.duration ?? 0)) return Promise.resolve();
2834
- return v.awaitFrameAt(local);
2835
- }));
2836
- }
2837
- async snapshot(frame = this.currentFrame, opts = {}) {
2838
- warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as", "motionBlur"]);
2839
- this._requireReady("snapshot");
2840
- return this._captureFrame(clampFrame(frame, this.totalFrames), { scale: opts.scale ?? 1, type: opts.type ?? "image/png", as: opts.as ?? "blob", motionBlur: opts.motionBlur });
2841
- }
2842
- /** Seek to `frame` (quietly, and stay there) and draw the canvas on a new canvas of `scale` times its size. */
2843
- async _frameCanvas(frame, scale, motionBlur) {
2844
- const mb = this._blurFor(motionBlur, "a picture");
2845
- let source = this.app.canvas;
2846
- if (mb) {
2847
- source = this._blurCanvas();
2848
- await this._quietly(() => this._exposeFrame(frame, mb, source, true));
2849
- } else await this._quietly(() => this.gotoFrame(frame, true));
2850
- const out = document.createElement("canvas");
2851
- out.width = Math.max(1, Math.round(this.width * scale));
2852
- out.height = Math.max(1, Math.round(this.height * scale));
2853
- out.getContext("2d").drawImage(source, 0, 0, out.width, out.height);
2854
- return out;
2855
- }
2856
- /** The pixels of `frame`, small (about 320 px wide) and without motion blur, to compare pictures. */
2857
- async _framePixels(frame) {
2858
- const c = await this._frameCanvas(frame, Math.min(1, 320 / Math.max(1, this.width)), false);
2859
- return c.getContext("2d").getImageData(0, 0, c.width, c.height).data;
2860
- }
2861
- /** Seek to `frame` (quietly, and stay there) and encode the canvas as a picture. */
2862
- async _captureFrame(frame, o) {
2863
- const out = await this._frameCanvas(frame, o.scale, o.motionBlur);
2864
- return encodeCanvas(out, o.type, o.as, o.quality);
2865
- }
2866
- async stopImages(opts = {}) {
2867
- warnUnknownOptions("movie.stopImages()", opts, ["which", "pick", "scale", "type", "quality", "as", "motionBlur", "pdf", "onImage"]);
2868
- this._requireReady("stopImages");
2869
- if (this.stops.length === 0) return [];
2870
- const picks = pictureStops(this.stops, { which: opts.which, pick: opts.pick, pdf: opts.pdf });
2871
- this.pause();
2872
- const back = this.currentFrame, wasAtPoster = this._atPoster;
2873
- const out = [];
2874
- try {
2875
- for (const stop of picks) {
2876
- const image = await this._captureFrame(stop.frame, {
2877
- scale: opts.scale ?? 1,
2878
- type: opts.type ?? "image/png",
2879
- quality: opts.quality,
2880
- as: opts.as ?? "blob",
2881
- motionBlur: opts.motionBlur === void 0 ? false : opts.motionBlur
2882
- });
2883
- const item = { stop, page: stop.pageIndex, image };
2884
- out.push(item);
2885
- opts.onImage?.(item);
2886
- }
2887
- } finally {
2888
- if (wasAtPoster) await this._showPoster();
2889
- else await this._quietly(() => this.gotoFrame(back, true));
2890
- }
2891
- return out;
2892
- }
2893
- /**
2894
- * Look at every stop: is the picture there still changing? A stop is compared with the picture `lookback` frames before it (default 3):
2895
- * when more than `tolerance` (default 0.4 %) of the picture differs, the audience, a page overview and a PDF all see a half-finished
2896
- * animation. `issues` says which stop, which page and what to do; leaves the playhead where it was.
2897
- */
2898
- async inspectStops(opts = {}) {
2899
- warnUnknownOptions("movie.inspectStops()", opts, ["lookback", "tolerance"]);
2900
- this._requireReady("inspectStops");
2901
- const out = { stops: [], issues: [] };
2902
- if (this.stops.length === 0) return out;
2903
- const lookback = Math.max(1, Math.round(opts.lookback ?? 3));
2904
- const tolerance = opts.tolerance ?? 4e-3;
2905
- this.pause();
2906
- const back = this.currentFrame, wasAtPoster = this._atPoster;
2907
- try {
2908
- for (const stop of this.stops) {
2909
- const before = Math.max(0, stop.frame - lookback);
2910
- let moving = 0;
2911
- if (before < stop.frame) {
2912
- const now = await this._framePixels(stop.frame);
2913
- moving = changedFraction(now, await this._framePixels(before));
2914
- }
2915
- const settled = moving <= tolerance;
2916
- out.stops.push({ index: stop.index, page: stop.page, at: stop.at, frame: stop.frame, moving, settled });
2917
- if (!settled) {
2918
- out.issues.push(`stop ${stop.index + 1}${stop.page ? ` (page "${stop.page}")` : ""} at ${stop.at.toFixed(1)} s: the picture is still changing there (${moving < 0.1 ? (moving * 100).toFixed(1) : Math.round(moving * 100)}% of it changed in the last ${stop.frame - before} frames), so the audience, the page overview and a PDF see it mid-animation. Move the stop to after the animation has finished, or flag a settled stop of the page with \`pdf: true\`.`);
2919
- }
2920
- }
2921
- } finally {
2922
- if (wasAtPoster) await this._showPoster();
2923
- else await this._quietly(() => this.gotoFrame(back, true));
2924
- }
2925
- return out;
2926
- }
2927
- /**
2928
- * The deck as a PDF: one page per page of the talk (the page's last stop, fully built, unless a stop says `pdf: true` or `pdf: false`;
2929
- * `which: 'stops'` makes a page of every stop that is not `pdf: false`), each a
2930
- * JPEG at the canvas size. Needs `composition.stops`. A transparent background comes out black: give the movie a `background`.
2931
- */
2932
- async exportPDF(opts = {}) {
2933
- warnUnknownOptions("movie.exportPDF()", opts, ["which", "pick", "scale", "quality", "title", "motionBlur", "onImage"]);
2934
- this._requireReady("exportPDF");
2935
- if (this.stops.length === 0) throw new Error("pixi-effects: movie.exportPDF(): this movie has no stops, so there are no pages; add `stops` to the composition (or build it with deck())");
2936
- const scale = opts.scale ?? 1;
2937
- const images = await this.stopImages({
2938
- which: opts.which,
2939
- pick: opts.pick,
2940
- scale,
2941
- type: "image/jpeg",
2942
- quality: opts.quality ?? 0.92,
2943
- as: "blob",
2944
- motionBlur: opts.motionBlur,
2945
- pdf: true,
2946
- onImage: opts.onImage
2947
- });
2948
- const pages = await Promise.all(images.map(async (i) => ({
2949
- jpeg: new Uint8Array(await i.image.arrayBuffer()),
2950
- width: Math.max(1, Math.round(this.width * scale)),
2951
- height: Math.max(1, Math.round(this.height * scale))
2952
- })));
2953
- return new Blob([buildPdf(pages, { title: opts.title })], { type: "application/pdf" });
2954
- }
2955
- async onionSkin(opts = {}) {
2956
- warnUnknownOptions("movie.onionSkin()", opts, ["from", "to", "count", "scale", "as", "motionBlur"]);
2957
- this._requireReady("onionSkin");
2958
- const from = opts.from ?? 0, to = opts.to ?? this.duration, count = opts.count ?? 8, scale = opts.scale ?? 1;
2959
- if (!(Number.isInteger(count) && count >= 1 && count <= 64)) throw new Error(`pixi-effects: movie.onionSkin(): count must be a whole number from 1 to 64, got ${count}`);
2960
- if (!(from >= 0 && to <= this.duration + 1e-9 && (count === 1 || from < to))) throw new Error(`pixi-effects: movie.onionSkin(): from must be before to, inside the movie (0 to ${this.duration} s), got ${from} to ${to}`);
2961
- if (!(scale > 0 && scale <= 4)) throw new Error(`pixi-effects: movie.onionSkin(): scale must be above 0 and at most 4, got ${scale}`);
2962
- const mb = this._blurFor(opts.motionBlur, "movie.onionSkin()");
2963
- const blurred = mb ? this._blurCanvas() : null;
2964
- const out = document.createElement("canvas");
2965
- out.width = Math.max(1, Math.round(this.width * scale));
2966
- out.height = Math.max(1, Math.round(this.height * scale));
2967
- const g = out.getContext("2d");
2968
- const alphas = onionAlphas(count);
2969
- const frames = onionTimes(from, to, count).map((t) => clampFrame(t * this.frameRate, this.totalFrames));
2970
- const back = this.currentFrame, wasAtPoster = this._atPoster;
2971
- try {
2972
- for (let i = 0; i < frames.length; i++) {
2973
- const f = frames[i];
2974
- if (mb && blurred) await this._quietly(() => this._exposeFrame(f, mb, blurred, true));
2975
- else await this._quietly(() => this.gotoFrame(f, true));
2976
- g.globalAlpha = alphas[i];
2977
- g.drawImage(blurred ?? this.app.canvas, 0, 0, out.width, out.height);
2978
- }
2979
- } finally {
2980
- g.globalAlpha = 1;
2981
- if (wasAtPoster) await this._showPoster();
2982
- else await this._quietly(() => this.gotoFrame(back, true));
2983
- }
2984
- return encodeCanvas(out, "image/png", opts.as ?? "blob");
2985
- }
2986
- async contactSheet(opts = {}) {
2987
- warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as", "motionBlur"]);
2988
- this._requireReady("contactSheet");
2989
- const mb = this._blurFor(opts.motionBlur, "movie.contactSheet()");
2990
- const blurred = mb ? this._blurCanvas() : null;
2991
- const frames = (opts.frames ?? opts.times?.map((t) => Math.round(t * this.frameRate)) ?? pickFrames(this.totalFrames, opts.count ?? 6)).map((f) => clampFrame(f, this.totalFrames));
2992
- const LABEL_H = 26;
2993
- const L = sheetLayout(frames.length, opts.columns ?? 3, opts.cellWidth ?? 480, this.width, this.height, LABEL_H);
2994
- const sheet = document.createElement("canvas");
2995
- sheet.width = L.width;
2996
- sheet.height = L.height;
2997
- const g = sheet.getContext("2d");
2998
- g.fillStyle = "#10131c";
2999
- g.fillRect(0, 0, L.width, L.height);
3000
- const back = this.currentFrame, wasAtPoster = this._atPoster;
3001
- try {
3002
- for (let i = 0; i < frames.length; i++) {
3003
- const f = frames[i];
3004
- if (mb && blurred) await this._quietly(() => this._exposeFrame(f, mb, blurred, true));
3005
- else await this._quietly(() => this.gotoFrame(f, true));
3006
- const { x, y } = L.positions[i];
3007
- g.drawImage(blurred ?? this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
3008
- g.fillStyle = "#e8ecf8";
3009
- g.font = "600 15px system-ui, sans-serif";
3010
- g.textBaseline = "middle";
3011
- g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
3012
- }
3013
- } finally {
3014
- if (wasAtPoster) await this._showPoster();
3015
- else await this._quietly(() => this.gotoFrame(back, true));
3016
- }
3017
- return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
3018
- }
3019
- /**
3020
- * Where every layer is drawn at `frame` (canvas pixels, visibility) plus `issues` — text that is off
3021
- * the canvas, cut by an edge, empty, or overlapping other text. Seeks there and stays there.
3022
- */
3023
- async inspect(frame = this.currentFrame, opts = { layers: "visible" }) {
3024
- warnUnknownOptions("movie.inspect()", opts, ["layers"]);
3025
- this._requireReady("inspect");
3026
- const f = clampFrame(frame, this.totalFrames);
3027
- await this.gotoFrame(f, true);
3028
- return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
3029
- }
3030
- /**
3031
- * Every layer as a row with absolute start / end / keyframe times (seconds), plus the transition windows. Runs of
3032
- * similar layers (`pop-1` … `pop-12`) are one row with a `parts` span each. Read it, or draw it with `timelineChart()`.
3033
- */
3034
- timelineData() {
3035
- this._requireReady("timelineData");
3036
- return collectTimeline(this._rootSequence, this.duration);
3037
- }
3038
- /** The timeline as one self-contained HTML page (an inline SVG, no scripts): layers as bars on a time axis, ◆ keyframes, transition bands. Open it in a browser. */
3039
- timelineChart(opts = {}) {
3040
- warnUnknownOptions("movie.timelineChart()", opts, ["title"]);
3041
- return timelineHtml(this.timelineData(), opts);
3042
- }
3043
- /** Just the chart: one `<svg>` whose geometry is in `data-*` attributes (a viewer puts a playhead on it: `pixi-effects-view`). */
3044
- timelineSvg(opts = {}) {
3045
- warnUnknownOptions("movie.timelineSvg()", opts, ["chartWidth", "labels"]);
3046
- return timelineSvg(this.timelineData(), opts);
3047
- }
3048
- /**
3049
- * The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
3050
- * (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
3051
- */
3052
- /** The pictures to look at for moments such as `3.5`, `50%`, `f120`, `title@end` (comma-separated). A mistake says what could not be read. */
3053
- resolveAt(list) {
3054
- this._requireReady("resolveAt");
3055
- return resolveAtList(list, { frameRate: this.frameRate, totalFrames: this.totalFrames, duration: this.duration, rows: this.timelineData().rows });
3056
- }
3057
- /**
3058
- * One call that looks at everything `pixi-effects-check` looks at inside the page: layout issues over the whole timeline (every
3059
- * scene's first and last frame and a frame every 0.25 s), fonts and the sound. See `ReviewReport`. Page warnings are in `window.__logs`. The movie is left as it was.
3060
- */
3061
- async review(opts = {}) {
3062
- warnUnknownOptions("movie.review()", opts, ["at", "strict"]);
3063
- this._requireReady("review");
3064
- const back = this.currentFrame, wasAtPoster = this._atPoster;
3065
- try {
3066
- return await reviewMovie(this, opts);
3067
- } finally {
3068
- this.currentFrame = back;
3069
- if (wasAtPoster) await this._showPoster();
3070
- else await this._quietly(() => this.gotoFrame(back, true));
3071
- }
3072
- }
3073
- /**
3074
- * The fonts of the text layers: `missing` lists layers none of whose `fontFamily` entries is available in this browser (they are
3075
- * drawn in a fallback font), `failed` lists web fonts (`@font-face`) whose file could not be loaded.
3076
- */
3077
- inspectFonts() {
3078
- this._requireReady("inspectFonts");
3079
- return inspectFonts(this._rootSequence);
3080
- }
3081
- inspectAudio(opts = {}) {
3082
- warnUnknownOptions("movie.inspectAudio()", opts, ["window", "scenes"]);
3083
- this._requireReady("inspectAudio");
3084
- const scenes = opts.scenes ?? namedScenes(this.timelineData().rows);
3085
- return analyzeAudio(this.audioBuffer, this._audioSources, this._mixStats, this.duration, { ...opts, scenes });
3086
- }
3087
- /**
3088
- * Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
3089
- * by hand — while seeking and especially during `render()`, which stops the ticker — so without this the
3090
- * `culled` flags are whatever the last tick left: a layer that is now on screen can stay culled and be
3091
- * missing from the frame or from the exported video. Cull again (updating transforms: the timeline just
3092
- * moved things) right before drawing.
3093
- */
3094
- _renderNow() {
3095
- const app = this.app;
3096
- if (!app?.renderer) return;
3097
- if (!this._synced) this._rootSequence?.syncFrame();
3098
- this._synced = false;
3099
- this._feedTimeFilters();
3100
- Culler.shared.cull(app.stage, app.renderer.screen, false);
3101
- app.renderer.render({ container: app.stage });
3102
- }
3103
- _requireReady(what) {
3104
- if (this._initState !== "ready" || !this._rootSequence || !this.app) {
3105
- throw new Error(`pixi-effects: movie.${what}() needs a ready movie \u2014 await movie.init(...) first`);
3106
- }
3107
- }
3108
- /** True when the layers were already synced to the playhead for the frame about to be drawn (so `_renderNow` need not do it again). */
3109
- _synced = false;
3110
- /**
3111
- * Projects every `threeD` layer for the current frame (see src/space). A threeD card is drawn into its own texture HERE, before the stage is
3112
- * drawn, so the shapes inside it must be brought up to the playhead first: syncing only in `_renderNow` left every card showing the state of
3113
- * the frame before (after a jump seek, a card whose children were animated showed a stale picture).
3114
- */
3115
- _updateSpace(t = this._timeOf(this.currentFrame)) {
3116
- this._feedTimeFilters();
3117
- if (!this._rootSequence || !this.app) return;
3118
- this._rootSequence.syncFrame();
3119
- this._synced = true;
3120
- this._rootSequence.updateSpace(t, this.app.renderer);
3121
- }
3122
- /** The motion blur an operation uses: its own option when given (`false` is off), otherwise the movie's. */
3123
- _blurFor(option, where) {
3124
- return option === void 0 ? this.motionBlur : resolveMotionBlur(option, where);
3125
- }
3126
- /**
3127
- * @internal Draw `frame` with motion blur into `target` (a canvas the size of the stage): the average of `mb.samples` renders at moments
3128
- * spread over the shutter interval around the frame time. With `settle` the playhead and the stage are left exactly on the frame.
3129
- */
3130
- async _exposeFrame(frame, mb, target, settle) {
3131
- const g = target.getContext("2d");
3132
- const stage = this.app.canvas;
3133
- this._atPoster = false;
3134
- const times = blurTimes(frame, this.frameRate, mb, Math.max(0, this.duration - END_MARGIN));
3135
- this._nominalTime = this._timeOf(Math.max(0, Math.min(frame, this.totalFrames)));
3136
- try {
3137
- for (let k = 0; k < times.length; k++) {
3138
- const t = times[k];
3139
- this.timeline.time(t);
3140
- await this._awaitVideoFrames(t);
3141
- this._updateSpace(t);
3142
- this._renderNow();
3143
- if (k === 0) g.clearRect(0, 0, target.width, target.height);
3144
- g.globalAlpha = 1 / (k + 1);
3145
- g.drawImage(stage, 0, 0, target.width, target.height);
3146
- }
3147
- } finally {
3148
- this._nominalTime = null;
3149
- }
3150
- g.globalAlpha = 1;
3151
- if (settle) await this._goto(frame);
3152
- else {
3153
- this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
3154
- this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
3155
- }
3156
- }
3157
- _blurCanvas() {
3158
- const stage = this.app.canvas;
3159
- const c = document.createElement("canvas");
3160
- c.width = stage.width;
3161
- c.height = stage.height;
3162
- return c;
3163
- }
3164
- /** Which stop the playhead is on or has passed (-1 before the first). */
3165
- get stopIndex() {
3166
- return stopAtOrBefore(this.stops, this.currentFrame)?.index ?? -1;
3167
- }
3168
- /** The stop the playhead is on or has passed, or null. */
3169
- get currentStop() {
3170
- return stopAtOrBefore(this.stops, this.currentFrame);
3171
- }
3172
- /** Which page the playhead is in (0-based), -1 before the first stop. */
3173
- get pageIndex() {
3174
- return this.currentStop?.pageIndex ?? -1;
3175
- }
3176
- /** How many pages the stops make. */
3177
- get pageCount() {
3178
- return pageStarts(this.stops).length;
3179
- }
3180
- _cancelStop() {
3181
- this._stopAt = null;
3182
- this._settleStopWaiters();
3183
- }
3184
- _settleStopWaiters() {
3185
- const waiters = this._stopWaiters;
3186
- this._stopWaiters = [];
3187
- waiters.forEach((w) => w());
3188
- }
3189
- _emitStop(stop) {
3190
- this.emit("stop", { index: stop.index, stop, pageIndex: stop.pageIndex });
3191
- }
3192
- /** Land exactly on a stop's frame, pause, and say so. */
3193
- async _arriveAtStop(frame) {
3194
- this._arriving = true;
3195
- this._stopAt = null;
3196
- try {
3197
- this.pause();
3198
- await this._goto(frame);
3199
- } catch (err) {
3200
- console.warn("pixi-effects: could not land on a stop:", err);
3201
- this._emitError("playback", err);
3202
- } finally {
3203
- this._arriving = false;
3204
- }
3205
- const stop = this.stops.find((s) => s.frame === frame);
3206
- if (stop) this._emitStop(stop);
3207
- this._settleStopWaiters();
3208
- }
3209
- /**
3210
- * Play to the next stop and pause exactly there (a presentation's "next"). Pressed again while it is playing toward a stop, it skips the
3211
- * rest of the animation and lands on that stop at once. After the last stop it plays to the end. Resolves when it has landed (or
3212
- * stopped for another reason). An ordinary `play()` ignores the stops.
3213
- */
3214
- async next() {
3215
- if (this._initState !== "ready") return;
3216
- if (this._stopAt !== null) {
3217
- await this._arriveAtStop(this._stopAt);
3218
- return;
3219
- }
3220
- if (this.currentFrame >= this.totalFrames) return;
3221
- const target = nextStopAfter(this.stops, this.currentFrame);
3222
- this._stopAt = target ? target.frame : null;
3223
- const landed = new Promise((resolve) => this._stopWaiters.push(resolve));
3224
- this.play();
3225
- await landed;
3226
- }
3227
- /** Jump back to the previous stop at once (no reverse playback); from the first stop, to the start. */
3228
- async prev() {
3229
- if (this._initState !== "ready") return;
3230
- this.pause();
3231
- const target = previousStopBefore(this.stops, this.currentFrame);
3232
- await this.gotoFrame(target ? target.frame : 0, true);
3233
- if (target) this._emitStop(target);
3234
- }
3235
- /** Jump to stop `index` (0-based). */
3236
- async goToStop(index) {
3237
- if (this._initState !== "ready") return;
3238
- const stop = this.stops[index];
3239
- if (!stop) {
3240
- console.warn(`pixi-effects: movie.goToStop(${index}): there are ${this.stops.length} stops (0 to ${this.stops.length - 1})`);
3241
- return;
3242
- }
3243
- this.pause();
3244
- await this.gotoFrame(stop.frame, true);
3245
- this._emitStop(stop);
3246
- }
3247
- /** Jump to the first stop of page `page` (0-based). */
3248
- async goToPage(page) {
3249
- const starts = pageStarts(this.stops);
3250
- const stop = starts[page];
3251
- if (!stop) {
3252
- console.warn(`pixi-effects: movie.goToPage(${page}): there are ${starts.length} pages (0 to ${starts.length - 1})`);
3253
- return;
3254
- }
3255
- await this.goToStop(stop.index);
3256
- }
3257
- play() {
3258
- if (this._initState !== "ready") return;
3259
- if (this.currentFrame >= this.totalFrames) this.currentFrame = 0;
3260
- const wasPlaying = this.isPlaying;
3261
- this.isPlaying = true;
3262
- const startTime = performance.now() - this.currentFrame / this.frameRate * 1e3;
3263
- let inFlight = false;
3264
- const tick = (time) => {
3265
- if (!this.isPlaying) return;
3266
- if (inFlight) {
3267
- this._raf = requestAnimationFrame(tick);
3268
- return;
3269
- }
3270
- const elapsed = (time - startTime) / 1e3;
3271
- const frame = Math.floor(elapsed * this.frameRate);
3272
- if (this._stopAt !== null && frame >= this._stopAt) {
3273
- void this._arriveAtStop(this._stopAt);
3274
- return;
3275
- }
3276
- if (frame <= this.totalFrames) {
3277
- inFlight = true;
3278
- this._goto(frame).catch((err) => {
3279
- console.warn("pixi-effects: gotoFrame failed during playback:", err);
3280
- this._emitError("playback", err);
3281
- }).finally(() => {
3282
- inFlight = false;
3283
- });
3284
- this._raf = requestAnimationFrame(tick);
3285
- } else {
3286
- this.pause();
3287
- this._goto(this.totalFrames).catch((err) => {
3288
- console.warn("pixi-effects: final gotoFrame failed:", err);
3289
- this._emitError("playback", err);
3290
- });
3291
- this.emit("ended");
3292
- this._settleStopWaiters();
3293
- }
3294
- };
3295
- this._raf = requestAnimationFrame(tick);
3296
- if (this.audioBuffer) {
3297
- const ctx = this._ensureAudioContext();
3298
- const buffer = this.audioBuffer;
3299
- this._cancelAudioStart?.();
3300
- this._cancelAudioStart = startWhenRunning(ctx, () => {
3301
- if (!this.isPlaying) return;
3302
- this.audioSource = ctx.createBufferSource();
3303
- this.audioSource.buffer = buffer;
3304
- this.gainNode = ctx.createGain();
3305
- this.gainNode.gain.value = this._muted ? 0 : this._volume;
3306
- this.audioSource.connect(this.gainNode).connect(ctx.destination);
3307
- this.audioSource.start(0, Math.min(this.currentFrame / this.frameRate, buffer.duration));
3308
- }, typeof window !== "undefined" ? window : void 0);
3309
- }
3310
- if (!wasPlaying) this.emit("play");
3311
- }
3312
- pause() {
3313
- const wasPlaying = this.isPlaying;
3314
- this.isPlaying = false;
3315
- this._cancelAudioStart?.();
3316
- this._cancelAudioStart = null;
3317
- if (this._raf) {
3318
- cancelAnimationFrame(this._raf);
3319
- this._raf = null;
3320
- }
3321
- if (this.audioSource) {
3322
- try {
3323
- this.audioSource.stop();
3324
- } catch {
3325
- }
3326
- this.audioSource.disconnect();
3327
- this.audioSource = null;
3328
- }
3329
- if (this.gainNode) {
3330
- this.gainNode.disconnect();
3331
- this.gainNode = null;
3332
- }
3333
- if (wasPlaying) this.emit("pause");
3334
- if (!this._arriving) this._cancelStop();
3335
- }
3336
- set volume(v) {
3337
- const next = Math.max(0, Math.min(1, v));
3338
- const changed = next !== this._volume;
3339
- this._volume = next;
3340
- if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
3341
- if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
3342
- }
3343
- get volume() {
3344
- return this._volume;
3345
- }
3346
- set muted(v) {
3347
- const changed = !!v !== this._muted;
3348
- this._muted = !!v;
3349
- if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
3350
- if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
3351
- }
3352
- get muted() {
3353
- return this._muted;
3354
- }
3355
- toggleMute() {
3356
- this.muted = !this.muted;
3357
- return this.muted;
3358
- }
3359
- async render(options) {
3360
- warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur", "range", "scale", "draft"]);
3361
- try {
3362
- return await this._quietly(() => exportFrames(this, options));
3363
- } catch (err) {
3364
- this._emitError("render", err);
3365
- throw err;
3366
- }
3367
- }
3368
- async destroy() {
3369
- safeRun(() => this.pause());
3370
- this._timeFilters = [];
3371
- safeRun(() => this._rootSequence?.destroy());
3372
- this._rootSequence = null;
3373
- safeRun(() => this.timeline?.kill());
3374
- this.timeline = null;
3375
- safeRun(() => this.app?.destroy(true, { children: true, texture: true }));
3376
- this.app = null;
3377
- this.audioBuffer = null;
3378
- this._audioSources = [];
3379
- this._mixStats = null;
3380
- safeRun(() => this._audioContext?.close().catch(() => {
3381
- }));
3382
- this._audioContext = null;
3383
- this._initState = "destroyed";
3384
- }
3385
- };
3386
- var END_MARGIN = 1e-4;
3387
- var clampFrame = (f, last) => Math.max(0, Math.min(Math.round(f), last));
3388
- function encodeCanvas(canvas, type, as, quality) {
3389
- if (as === "dataURL") return Promise.resolve(canvas.toDataURL(type, quality));
3390
- return new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("pixi-effects: could not encode the image")), type, quality));
3391
- }
3392
- function safeRun(fn) {
3393
- try {
3394
- fn();
3395
- } catch (e) {
3396
- console.warn("pixi-effects: cleanup step threw, continuing:", e);
3397
- }
3398
- }
3399
- function collectVideoSequences(seq, out, clocks, clock = null) {
3400
- if (typeof seq.awaitFrameAt === "function") {
3401
- out.push(seq);
3402
- if (clock && clocks) clocks.set(seq, clock);
3403
- }
3404
- if (seq.maskSequence) collectVideoSequences(seq.maskSequence, out, clocks, clock);
3405
- const own = seq.localClock?.() ?? clock;
3406
- for (const child of seq._children ?? []) {
3407
- collectVideoSequences(child, out, clocks, own);
3408
- }
3409
- }
3410
- function countLayers(spec) {
3411
- let n = 0;
3412
- for (const s of spec?.sequences ?? []) {
3413
- const mask = s.mask;
3414
- n += 1 + countLayers(s);
3415
- for (const m of Array.isArray(mask) ? mask : [mask]) if (m && typeof m === "object" && "type" in m) n += 1 + countLayers(m);
3416
- }
3417
- return n;
3418
- }
3419
- var BY = ["chars", "words", "lines"];
3420
- var ALIGN = ["left", "center", "right"];
3421
- var widthOf = (text, style) => CanvasTextMetrics.measureText(text, style).width;
3422
- function measureText(text, style = {}) {
3423
- const m = CanvasTextMetrics.measureText(text, new TextStyle(style));
3424
- return { width: m.width, height: m.height, lines: text.split("\n").length };
3425
- }
3426
- function splitText(text, style = {}, opts = {}) {
3427
- warnUnknownOptions("splitText()", opts, ["by", "x", "y", "align"]);
3428
- let by = opts.by ?? "chars";
3429
- if (!BY.includes(by)) {
3430
- const g = suggestName(String(by), BY);
3431
- console.warn(`pixi-effects: splitText(): by "${by}" is not one of ${BY.map((b) => `"${b}"`).join(", ")}${g ? ` \u2014 did you mean "${g}"?` : ""}; using "chars"`);
3432
- by = "chars";
3433
- }
3434
- let align = opts.align ?? "left";
3435
- if (!ALIGN.includes(align)) {
3436
- const g = suggestName(String(align), ALIGN);
3437
- console.warn(`pixi-effects: splitText(): align "${align}" is not one of ${ALIGN.join(", ")}${g ? ` \u2014 did you mean "${g}"?` : ""}; using "left"`);
3438
- align = "left";
3439
- }
3440
- const x0 = opts.x ?? 0, y0 = opts.y ?? 0;
3441
- for (const [n, v] of [["x", x0], ["y", y0]]) {
3442
- if (typeof v !== "number" || !Number.isFinite(v)) throw new Error(`splitText(): ${n} must be a number of pixels (e.g. ${n}: 640), got ${JSON.stringify(v)}: the pieces are placed by measuring, so an expression such as 'GW/2' cannot be used here. \`y\` is the top of the line.`);
3443
- }
3444
- if (text === "") return [];
3445
- const ts = new TextStyle(style);
3446
- const lineHeight = CanvasTextMetrics.measureText("M", ts).lineHeight ?? CanvasTextMetrics.measureText("M", ts).height;
3447
- const sentinel = widthOf("|", ts);
3448
- const out = [];
3449
- text.split("\n").forEach((line, lineNo) => {
3450
- const lineWidth = widthOf(line, ts);
3451
- const left = align === "center" ? x0 - lineWidth / 2 : align === "right" ? x0 - lineWidth : x0;
3452
- const y = y0 + lineNo * lineHeight;
3453
- const advance = (upTo) => upTo === 0 ? 0 : widthOf(line.slice(0, upTo) + "|", ts) - sentinel;
3454
- const spans = [];
3455
- if (by === "lines") {
3456
- if (line.trim() !== "") spans.push([0, line.length]);
3457
- } else if (by === "words") {
3458
- for (const m of line.matchAll(/\S+/g)) spans.push([m.index, m.index + m[0].length]);
3459
- } else {
3460
- for (let i = 0; i < line.length; i++) if (!/\s/.test(line[i])) spans.push([i, i + 1]);
3461
- }
3462
- for (const [a, b] of spans) {
3463
- const piece = line.slice(a, b);
3464
- out.push({ text: piece, x: left + advance(a), y, width: widthOf(piece, ts), index: out.length, line: lineNo });
3465
- }
3466
- });
3467
- return out;
3468
- }
3469
-
3470
- // src/presets/_ease.ts
3471
- function parseEase2(name) {
3472
- checkEase(name, "a preset");
3473
- return parseEase(name);
3474
- }
3475
-
3476
- // src/presets/kenBurns.ts
3477
- var KEN_BURNS_KEYS = ["asset", "name", "at", "duration", "fit", "ease", "motion", "origin", "zoom", "direction", "angle", "from", "to"];
3478
- function kenBurns(opts) {
3479
- warnUnknownOptions("kenBurns()", opts, KEN_BURNS_KEYS);
3480
- const {
3481
- asset,
3482
- name,
3483
- at,
3484
- duration,
3485
- fit = "cover",
3486
- ease = "sine.inOut"
3487
- } = opts;
3488
- const baseScale = fit;
3489
- let initial;
3490
- let keyframes;
3491
- if (opts.motion === "still") {
3492
- initial = {
3493
- x: "W/2",
3494
- y: "H/2",
3495
- anchorX: 0.5,
3496
- anchorY: 0.5,
3497
- scale: baseScale
3498
- };
3499
- keyframes = [];
3500
- } else if (opts.motion === "scale") {
3501
- const origin = opts.origin ?? [0.5, 0.5];
3502
- const zoom = opts.zoom ?? 1.15;
3503
- const direction = opts.direction ?? "in";
3504
- const fromZ = direction === "in" ? 1 : zoom;
3505
- const toZ = direction === "in" ? zoom : 1;
3506
- const pivotXExpr = `w * ${origin[0]}`;
3507
- const pivotYExpr = `h * ${origin[1]}`;
3508
- const initialScale = `${baseScale} * ${fromZ}`;
3509
- const xExpr = `W * ${origin[0]}`;
3510
- const yExpr = `H * ${origin[1]}`;
3511
- initial = {
3512
- x: xExpr,
3513
- y: yExpr,
3514
- pivotX: pivotXExpr,
3515
- pivotY: pivotYExpr,
3516
- scale: initialScale
3517
- };
3518
- keyframes = [
3519
- { at: 0, to: { scale: `${baseScale} * ${toZ}` }, duration, ease }
3520
- ];
3521
- } else if (opts.motion === "rotation") {
3522
- const angle = clamp(opts.angle ?? 8, 0, 30);
3523
- const direction = opts.direction ?? "cw";
3524
- const startAngle = direction === "cw" ? 0 : direction === "ccw" ? 0 : -angle / 2;
3525
- const endAngle = direction === "cw" ? +angle : direction === "ccw" ? -angle : +angle / 2;
3526
- const sampleStep = 1;
3527
- const span = Math.abs(endAngle - startAngle);
3528
- const sampleCount = Math.max(2, Math.ceil(span / sampleStep) + 1);
3529
- const scaleAt = (deg) => {
3530
- const \u03B8 = Math.abs(deg) * Math.PI / 180;
3531
- const c = Math.cos(\u03B8);
3532
- const s = Math.sin(\u03B8);
3533
- return `max(${baseScale}, max((W*${c} + H*${s}) / w, (W*${s} + H*${c}) / h))`;
3534
- };
3535
- initial = {
3536
- x: "W/2",
3537
- y: "H/2",
3538
- pivotX: "w * 0.5",
3539
- pivotY: "h * 0.5",
3540
- scale: scaleAt(startAngle),
3541
- rotation: startAngle
3542
- };
3543
- keyframes = [];
3544
- for (let i = 1; i < sampleCount; i++) {
3545
- const tFrac = i / (sampleCount - 1);
3546
- const angleHere = startAngle + (endAngle - startAngle) * tFrac;
3547
- const tHere = duration * tFrac;
3548
- const tPrev = duration * ((i - 1) / (sampleCount - 1));
3549
- keyframes.push({
3550
- at: tPrev,
3551
- to: { rotation: angleHere, scale: scaleAt(angleHere) },
3552
- duration: tHere - tPrev,
3553
- ease: i === 1 ? ease : "none"
3554
- });
3555
- }
3556
- } else {
3557
- const from = opts.from ?? [0.25, 0.25];
3558
- const to = opts.to ?? [0.75, 0.75];
3559
- const zoom = opts.zoom ?? 1.15;
3560
- const scaleExpr = `(${baseScale}) * ${zoom}`;
3561
- const xExpr = (panX) => `W/2 + (${0.5 - panX}) * (w * (${baseScale}) * ${zoom} - W)`;
3562
- const yExpr = (panY) => `H/2 + (${0.5 - panY}) * (h * (${baseScale}) * ${zoom} - H)`;
3563
- initial = {
3564
- x: xExpr(from[0]),
3565
- y: yExpr(from[1]),
3566
- pivotX: "w * 0.5",
3567
- pivotY: "h * 0.5",
3568
- scale: scaleExpr
3569
- };
3570
- keyframes = [
3571
- { at: 0, to: { x: xExpr(to[0]), y: yExpr(to[1]) }, duration, ease }
3572
- ];
3573
- }
3574
- const spec = {
3575
- type: "image",
3576
- asset,
3577
- duration,
3578
- initial,
3579
- keyframes
3580
- };
3581
- if (name !== void 0) spec.name = name;
3582
- if (at !== void 0) spec.at = at;
3583
- return spec;
3584
- }
3585
- function clamp(v, lo, hi) {
3586
- return Math.max(lo, Math.min(hi, v));
3587
- }
3588
-
3589
- // src/presets/orbit.ts
3590
- var ORBIT_KEYS = ["duration", "degrees", "start", "radius", "center", "width", "height", "fov", "ease", "dollyZoom", "at", "name", "stepsPerSecond"];
3591
- function orbit(opts) {
3592
- warnUnknownOptions("orbit()", opts, ORBIT_KEYS);
3593
- const {
3594
- duration,
3595
- degrees,
3596
- width = 1280,
3597
- height = 720,
3598
- fov,
3599
- ease = "sine.inOut",
3600
- stepsPerSecond = 10
3601
- } = opts;
3602
- if (!(duration > 0)) throw new Error(`pixi-effects: orbit duration must be > 0 (got ${duration})`);
3603
- const dolly = opts.dollyZoom;
3604
- if (dolly && opts.radius !== void 0) {
3605
- throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
3606
- }
3607
- const [cx, cy] = opts.center ?? [width / 2, height / 2];
3608
- const fixedR = opts.radius ?? homeDistance(height, fov ?? DEFAULT_FOV);
3609
- const start = opts.start ?? -degrees / 2;
3610
- const steps = Math.max(1, Math.round(duration * stepsPerSecond));
3611
- const easeFn = parseEase2(ease);
3612
- const dollyEase = dolly ? parseEase2(dolly.ease ?? ease) : null;
3613
- const pos = (i) => {
3614
- const p = i / steps;
3615
- const a = (start + degrees * easeFn(p)) * DEG;
3616
- if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
3617
- const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
3618
- const R = homeDistance(height, f);
3619
- return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
3620
- };
3621
- const keyframes = [];
3622
- for (let i = 0; i < steps; i++) {
3623
- keyframes.push({ at: duration * i / steps, to: pos(i + 1), duration: duration / steps, ease: "none" });
3624
- }
3625
- const p0 = pos(0);
3626
- const spec = {
3627
- type: "camera",
3628
- duration,
3629
- initial: {
3630
- x: p0.x,
3631
- y: cy,
3632
- z: p0.z,
3633
- lookAtX: cx,
3634
- lookAtY: cy,
3635
- lookAtZ: 0,
3636
- ...p0.fov !== void 0 ? { fov: p0.fov } : fov !== void 0 ? { fov } : {}
3637
- },
3638
- keyframes
3639
- };
3640
- if (opts.at !== void 0) spec.at = opts.at;
3641
- if (opts.name !== void 0) spec.name = opts.name;
3642
- return spec;
3643
- }
3644
-
3645
- // src/presets/particles.ts
3646
- var KEYS2 = ["count", "at", "emit", "life", "area", "angle", "speed", "gravity", "wind", "drag", "sway", "size", "scale", "fade", "colors", "shape", "spin", "seed", "name", "blendMode", "template", "sampleRate"];
3647
- var SHAPES = ["circle", "rect", "star"];
3648
- var MAX_KEYFRAMES = 3e4;
3649
- function particles(opts) {
3650
- warnUnknownOptions("particles()", opts, KEYS2);
3651
- const { count, at = 0, emit = 0, gravity = 0, wind = 0, drag = 0, sway, fade, colors = ["#ffffff"], shape = "circle", seed = 0, name = "particle", template, sampleRate = 20 } = opts;
3652
- if (!(Number.isInteger(count) && count > 0)) throw new Error(`particles(): count must be a whole number of particles (at least 1), got ${count}`);
3653
- const life = range("life", opts.life, 1e-6);
3654
- if (!opts.area || !Number.isFinite(opts.area.x) || !Number.isFinite(opts.area.y)) throw new Error("particles(): area is required: { x, y } (the centre) and optionally width / height to spread over");
3655
- if (!(Number.isFinite(emit) && emit >= 0)) throw new Error(`particles(): emit must be a number of seconds >= 0, got ${emit}`);
3656
- if (!(Number.isFinite(sampleRate) && sampleRate > 0)) throw new Error(`particles(): sampleRate must be a positive number (samples per second), got ${sampleRate}`);
3657
- if (!SHAPES.includes(shape)) {
3658
- const g = suggestName(String(shape), SHAPES);
3659
- throw new Error(`particles(): shape "${shape}" is not one of ${SHAPES.map((s) => `"${s}"`).join(", ")}${g ? `; did you mean "${g}"?` : ""}`);
3660
- }
3661
- const angle = range("angle", opts.angle ?? [0, 360], -Infinity);
3662
- const speed = range("speed", opts.speed ?? [100, 300], 0);
3663
- const size = range("size", opts.size ?? [3, 6], 0);
3664
- const spin = opts.spin === void 0 ? null : range("spin", opts.spin, -Infinity);
3665
- const curved = gravity !== 0 || wind !== 0 || drag !== 0 || !!sway;
3666
- const worst = curved ? Math.max(1, Math.ceil(life[1] * sampleRate)) : 1;
3667
- if (count * worst > MAX_KEYFRAMES) {
3668
- throw new Error(`particles(): too many keyframes (${count} particles \xD7 up to ${worst} samples each): lower count or life, or lower sampleRate (now ${sampleRate})`);
3669
- }
3670
- const round3 = (v) => Math.round(v * 1e6) / 1e6;
3671
- const out = [];
3672
- for (let i = 0; i < count; i++) {
3673
- const r = random(seed * 1000003 + i * 7919 + 17);
3674
- const pick = ([a2, b]) => a2 + r() * (b - a2);
3675
- const born = at + r() * emit;
3676
- const lifetime = pick(life);
3677
- const ax = (r() - 0.5) * (opts.area.width ?? 0), ay = (r() - 0.5) * (opts.area.height ?? 0);
3678
- const x0 = opts.area.x + ax, y0 = opts.area.y + ay;
3679
- const a = pick(angle) * Math.PI / 180, v = pick(speed);
3680
- const vx = v * Math.cos(a), vy = v * Math.sin(a);
3681
- const s = pick(size);
3682
- const color = colors[Math.min(colors.length - 1, Math.floor(r() * colors.length))];
3683
- const turn = spin ? pick(spin) : 0;
3684
- const startAngle = spin ? r() * 360 : 0;
3685
- const swayAmp = sway?.amp ?? 0, swayFreq = sway?.freq ?? 1;
3686
- const n0 = sway ? noise(0, seed * 1000003 + i) : 0;
3687
- const at_ = (t) => {
3688
- const axis = (p0, v0, acc) => {
3689
- if (drag > 0) {
3690
- const vInf = acc / drag;
3691
- return p0 + vInf * t + (v0 - vInf) * (1 - Math.exp(-drag * t)) / drag;
3692
- }
3693
- return p0 + v0 * t + 0.5 * acc * t * t;
3694
- };
3695
- const sw = sway ? swayAmp * (noise(t * swayFreq, seed * 1000003 + i) - n0) : 0;
3696
- return { x: axis(x0, vx, wind) + sw, y: axis(y0, vy, gravity) };
3697
- };
3698
- const kfs = [];
3699
- const steps = curved ? Math.max(1, Math.ceil(lifetime * sampleRate)) : 1;
3700
- const step = lifetime / steps;
3701
- for (let k = 1; k <= steps; k++) {
3702
- const p = at_(k * step);
3703
- kfs.push({ at: round3((k - 1) * step), to: { x: round3(p.x), y: round3(p.y) }, duration: round3(step), ease: "none" });
3704
- }
3705
- if (opts.scale) kfs.push({ at: 0, to: { scale: opts.scale[1] }, duration: round3(lifetime), ease: "none" });
3706
- if (turn !== 0) kfs.push({ at: 0, to: { rotation: round3(startAngle + turn * lifetime) }, duration: round3(lifetime), ease: "none" });
3707
- if (fade?.in) kfs.push({ at: 0, to: { alpha: 1 }, duration: fade.in, ease: "none" });
3708
- if (fade?.out) kfs.push({ at: -fade.out, from: { alpha: 1 }, to: { alpha: 0 }, duration: fade.out, ease: "none" });
3709
- const initial = { x: round3(x0), y: round3(y0) };
3710
- if (opts.scale) initial.scale = opts.scale[0];
3711
- if (fade?.in) initial.alpha = 0;
3712
- if (spin) initial.rotation = round3(startAngle);
3713
- const common = { name: `${name}-${i}`, at: round3(born), duration: round3(lifetime), ...opts.blendMode ? { blendMode: opts.blendMode } : {} };
3714
- if (template) {
3715
- const t = template;
3716
- out.push({ ...t, ...common, initial: { ...t.initial, ...initial }, keyframes: [...t.keyframes ?? [], ...kfs] });
3717
- continue;
3718
- }
3719
- const style = { ...initial, fillColor: color };
3720
- if (shape === "circle") out.push({ type: "shape", shape: "circle", radius: round3(s), ...common, initial: style, keyframes: kfs });
3721
- else if (shape === "rect") out.push({ type: "shape", shape: "rect", width: round3(s * 2), height: round3(s * 2), ...common, initial: style, keyframes: kfs });
3722
- else {
3723
- const points = [];
3724
- for (let p = 0; p < 10; p++) {
3725
- const rad = p % 2 === 0 ? s : s * 0.45, ang = -Math.PI / 2 + p * Math.PI / 5;
3726
- points.push([round3(rad * Math.cos(ang)), round3(rad * Math.sin(ang))]);
3727
- }
3728
- out.push({ type: "shape", shape: "polygon", points, ...common, initial: style, keyframes: kfs });
3729
- }
3730
- }
3731
- return out;
3732
- }
3733
- function range(name, v, min) {
3734
- const [a, b] = typeof v === "number" ? [v, v] : v;
3735
- if (!(Number.isFinite(a) && Number.isFinite(b)) || a > b || a < min || min > 0 && a <= 0) {
3736
- throw new Error(`particles(): ${name} must be a number or [min, max] with min <= max${min > -Infinity ? ` and min >= ${min}` : ""}, got ${JSON.stringify(v)}`);
3737
- }
3738
- return [a, b];
3739
- }
3740
-
3741
- // src/presets/followPath.ts
3742
- var KEYS3 = ["d", "at", "duration", "ease", "from", "to", "orient", "rotate", "frameRate"];
3743
- var MAX_STEPS = 2e3;
3744
- function followPath(opts) {
3745
- warnUnknownOptions("followPath()", opts, KEYS3);
3746
- const { d, at = 0, duration, ease = "none", from = 0, to = 1, orient = false, rotate = 0, frameRate = 30 } = opts;
3747
- if (typeof d !== "string" || d.trim() === "") throw new Error('followPath(): d must be SVG path data, e.g. "M 100 600 C 300 100 900 100 1180 600"');
3748
- if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`followPath(): duration must be a positive number of seconds, got ${duration}`);
3749
- if (!(Number.isFinite(frameRate) && frameRate > 0)) throw new Error(`followPath(): frameRate must be a positive number (samples per second), got ${frameRate}`);
3750
- for (const [n, v] of [["from", from], ["to", to]]) {
3751
- if (!(Number.isFinite(v) && v >= 0 && v <= 1)) throw new Error(`followPath(): ${n} must be a fraction of the path between 0 and 1, got ${v}`);
3752
- }
3753
- const steps = Math.max(1, Math.round(duration * frameRate));
3754
- if (steps > MAX_STEPS) throw new Error(`followPath(): ${steps} samples is too many (max ${MAX_STEPS}); lower frameRate or duration`);
3755
- const segments = [];
3756
- let total = 0;
3757
- for (const line of flattenSvgPath(d)) {
3758
- const p = line.closed ? [...line.pts, line.pts[0], line.pts[1]] : line.pts;
3759
- for (let i = 2; i < p.length; i += 2) {
3760
- const len = Math.hypot(p[i] - p[i - 2], p[i + 1] - p[i - 1]);
3761
- if (len === 0) continue;
3762
- segments.push({ x0: p[i - 2], y0: p[i - 1], x1: p[i], y1: p[i + 1], start: total, len });
3763
- total += len;
3764
- }
3765
- }
3766
- if (!(total > 0)) throw new Error(`followPath(): the path "${d}" has no length (it needs at least two different points)`);
3767
- const find = (s) => {
3768
- let lo = 0, hi = segments.length - 1;
3769
- while (lo < hi) {
3770
- const mid = lo + hi >> 1;
3771
- if (segments[mid].start + segments[mid].len < s) lo = mid + 1;
3772
- else hi = mid;
3773
- }
3774
- return segments[lo];
3775
- };
3776
- const at_ = (fraction) => {
3777
- const s = Math.min(Math.max(fraction, 0), 1) * total;
3778
- const g = find(s);
3779
- const t = Math.min(Math.max((s - g.start) / g.len, 0), 1);
3780
- return { x: g.x0 + (g.x1 - g.x0) * t, y: g.y0 + (g.y1 - g.y0) * t, heading: Math.atan2(g.y1 - g.y0, g.x1 - g.x0) * 180 / Math.PI };
3781
- };
3782
- const shape = parseEase2(ease);
3783
- const round3 = (v) => Math.round(v * 1e6) / 1e6;
3784
- let turn = 0;
3785
- const sample = (k) => {
3786
- const p = from + (to - from) * shape(k / steps);
3787
- const pt = at_(p);
3788
- const out2 = { x: round3(pt.x), y: round3(pt.y) };
3789
- if (orient) {
3790
- let h = pt.heading + rotate;
3791
- if (k === 0) turn = h;
3792
- else {
3793
- let delta = h - turn;
3794
- delta -= 360 * Math.round(delta / 360);
3795
- turn += delta;
3796
- }
3797
- out2.rotation = round3(turn);
3798
- }
3799
- return out2;
3800
- };
3801
- const out = [{ at, set: sample(0) }];
3802
- const step = duration / steps;
3803
- for (let k = 1; k <= steps; k++) out.push({ at: round3(at + (k - 1) * step), to: sample(k), duration: round3(step), ease: "none" });
3804
- return out;
3805
- }
3806
-
3807
- // src/presets/cameraPath.ts
3808
- var KEYS4 = ["points", "at", "duration", "ease", "look", "lookAhead", "smooth", "from", "to", "frameRate"];
3809
- var MAX_STEPS2 = 4e3;
3810
- var PER_SPAN = 48;
3811
- function cameraPath(opts) {
3812
- warnUnknownOptions("cameraPath()", opts, KEYS4);
3813
- const { points, at = 0, duration, ease = "none", look = "ahead", lookAhead = 0.08, smooth = true, from = 0, to = 1, frameRate = 30 } = opts;
3814
- if (!Array.isArray(points) || points.length < 2) throw new Error("cameraPath(): points needs at least two points: [[x, y, z], [x, y, z], \u2026]");
3815
- points.forEach((p, i) => {
3816
- if (!Array.isArray(p) || p.length !== 3 || !p.every(Number.isFinite)) throw new Error(`cameraPath(): points[${i}] must be [x, y, z] numbers, got ${JSON.stringify(p)}`);
3817
- });
3818
- if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`cameraPath(): duration must be a positive number of seconds, got ${duration}`);
3819
- if (!(Number.isFinite(frameRate) && frameRate > 0)) throw new Error(`cameraPath(): frameRate must be a positive number (samples per second), got ${frameRate}`);
3820
- if (look !== "ahead" && !(Array.isArray(look) && look.length === 3 && look.every(Number.isFinite))) throw new Error(`cameraPath(): look must be 'ahead' or [x, y, z] numbers, got ${JSON.stringify(look)}`);
3821
- if (!(Number.isFinite(lookAhead) && lookAhead >= 0 && lookAhead <= 1)) throw new Error(`cameraPath(): lookAhead must be a fraction of the route between 0 and 1, got ${lookAhead}`);
3822
- for (const [n, v] of [["from", from], ["to", to]]) {
3823
- if (!(Number.isFinite(v) && v >= 0 && v <= 1)) throw new Error(`cameraPath(): ${n} must be a fraction of the route between 0 and 1, got ${v}`);
3824
- }
3825
- const steps = Math.max(1, Math.round(duration * frameRate));
3826
- if (steps > MAX_STEPS2) throw new Error(`cameraPath(): ${steps} samples is too many (max ${MAX_STEPS2}); lower frameRate or duration`);
3827
- const curve = [];
3828
- if (smooth && points.length > 2) {
3829
- const pts = points;
3830
- for (let i = 0; i < pts.length - 1; i++) {
3831
- const p0 = pts[Math.max(i - 1, 0)], p1 = pts[i], p2 = pts[i + 1], p3 = pts[Math.min(i + 2, pts.length - 1)];
3832
- for (let s = 0; s < PER_SPAN; s++) curve.push(catmullRom(p0, p1, p2, p3, s / PER_SPAN));
3833
- }
3834
- curve.push(pts[pts.length - 1]);
3835
- } else curve.push(...points);
3836
- const cum = [0];
3837
- for (let i = 1; i < curve.length; i++) cum.push(cum[i - 1] + dist(curve[i - 1], curve[i]));
3838
- const total = cum[cum.length - 1];
3839
- if (!(total > 0)) throw new Error("cameraPath(): the route has no length (the points are all the same)");
3840
- const find = (s) => {
3841
- let lo = 0, hi = cum.length - 1;
3842
- while (lo < hi - 1) {
3843
- const mid = lo + hi >> 1;
3844
- if (cum[mid] <= s) lo = mid;
3845
- else hi = mid;
3846
- }
3847
- return lo;
3848
- };
3849
- const along = (fraction) => {
3850
- const s = fraction * total;
3851
- if (s >= total) {
3852
- const a2 = curve[curve.length - 2], b2 = curve[curve.length - 1], len = dist(a2, b2) || 1, extra = s - total;
3853
- return [b2[0] + (b2[0] - a2[0]) / len * extra, b2[1] + (b2[1] - a2[1]) / len * extra, b2[2] + (b2[2] - a2[2]) / len * extra];
3854
- }
3855
- const i = find(Math.max(s, 0)), span = cum[i + 1] - cum[i], t = span > 0 ? (s - cum[i]) / span : 0;
3856
- const a = curve[i], b = curve[i + 1];
3857
- return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
3858
- };
3859
- const shape = parseEase2(ease);
3860
- const round3 = (v) => Math.round(v * 1e6) / 1e6;
3861
- const sample = (k) => {
3862
- const p = from + (to - from) * shape(k / steps);
3863
- const pos = along(Math.min(Math.max(p, 0), 1));
3864
- const target = look === "ahead" ? along(Math.max(p + (to >= from ? lookAhead : -lookAhead), 0)) : look;
3865
- return { x: round3(pos[0]), y: round3(pos[1]), z: round3(pos[2]), lookAtX: round3(target[0]), lookAtY: round3(target[1]), lookAtZ: round3(target[2]) };
3866
- };
3867
- const out = [{ at, set: sample(0) }];
3868
- const step = duration / steps;
3869
- for (let k = 1; k <= steps; k++) out.push({ at: round3(at + (k - 1) * step), to: sample(k), duration: round3(step), ease: "none" });
3870
- return out;
3871
- }
3872
- var dist = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
3873
- function catmullRom(p0, p1, p2, p3, t) {
3874
- const t2 = t * t, t3 = t2 * t;
3875
- const c = (a, b, d, e) => 0.5 * (2 * b + (d - a) * t + (2 * a - 5 * b + 4 * d - e) * t2 + (3 * b - a - 3 * d + e) * t3);
3876
- return [c(p0[0], p1[0], p2[0], p3[0]), c(p0[1], p1[1], p2[1], p3[1]), c(p0[2], p1[2], p2[2], p3[2])];
3877
- }
3878
-
3879
- // src/presets/stagger.ts
3880
- var STAGGER_KEYS = ["each", "amount", "from", "grid", "ease", "seed"];
3881
- var FROM_NAMES = ["start", "end", "center", "edges", "random"];
3882
- function stagger(items, options = {}) {
3883
- const isCount = typeof items === "number";
3884
- const n = isCount ? items : items.length;
3885
- if (!Number.isInteger(n) || n < 0) throw new Error(`stagger(): count must be a whole number of items, got ${n}`);
3886
- warnUnknownOptions("stagger()", options, STAGGER_KEYS);
3887
- const { each, amount, from = "start", grid, ease = "none", seed = 0 } = options;
3888
- if (each !== void 0 && amount !== void 0) throw new Error("stagger(): give `each` (seconds between neighbours) or `amount` (seconds in total), not both");
3889
- if (each !== void 0 && !(Number.isFinite(each) && each >= 0)) throw new Error(`stagger(): each must be a number of seconds >= 0, got ${each}`);
3890
- if (amount !== void 0 && !(Number.isFinite(amount) && amount >= 0)) throw new Error(`stagger(): amount must be a number of seconds >= 0, got ${amount}`);
3891
- if (typeof from === "string" && !FROM_NAMES.includes(from)) {
3892
- throw new Error(`stagger(): from "${from}" is not one of ${FROM_NAMES.map((f) => `"${f}"`).join(", ")} or an item index`);
3893
- }
3894
- if (typeof from === "number" && !(Number.isInteger(from) && from >= 0 && from < Math.max(n, 1))) throw new Error(`stagger(): from ${from} is not an index of the ${n} items`);
3895
- if (grid && !(Array.isArray(grid) && grid.length === 2 && grid.every((g) => Number.isInteger(g) && g > 0) && grid[0] * grid[1] >= n)) {
3896
- throw new Error(`stagger(): grid must be [columns, rows] and hold all ${n} items, got ${JSON.stringify(grid)}`);
3897
- }
3898
- const dist2 = distances(n, from, grid, seed);
3899
- const max = dist2.reduce((m, d) => Math.max(m, d), 0);
3900
- const total = amount !== void 0 ? amount : (each ?? 0.1) * max;
3901
- const shape = parseEase2(ease);
3902
- const clamp01 = (v) => Math.min(1, Math.max(0, v));
3903
- const delays = dist2.map((d) => max === 0 ? 0 : Math.round(total * clamp01(shape(d / max)) * 1e6) / 1e6);
3904
- if (isCount) return delays;
3905
- return items.map((layer, i) => ({ ...layer, at: Math.round(((layer.at ?? 0) + delays[i]) * 1e6) / 1e6 }));
3906
- }
3907
- function distances(n, from, grid, seed) {
3908
- if (n === 0) return [];
3909
- if (from === "random") {
3910
- const order = Array.from({ length: n }, (_, i) => i);
3911
- const r = random(seed);
3912
- for (let i = n - 1; i > 0; i--) {
3913
- const j = Math.floor(r() * (i + 1));
3914
- [order[i], order[j]] = [order[j], order[i]];
3915
- }
3916
- return order;
3917
- }
3918
- if (!grid) {
3919
- const mid = (n - 1) / 2;
3920
- return Array.from({ length: n }, (_, i) => {
3921
- switch (from) {
3922
- case "start":
3923
- return i;
3924
- case "end":
3925
- return n - 1 - i;
3926
- case "center":
3927
- return Math.abs(i - mid);
3928
- case "edges":
3929
- return mid - Math.abs(i - mid);
3930
- default:
3931
- return Math.abs(i - from);
3932
- }
3933
- });
3934
- }
3935
- const [cols, rows] = grid;
3936
- const at = (i) => ({ x: i % cols, y: Math.floor(i / cols) });
3937
- const origin = from === "start" ? { x: 0, y: 0 } : from === "end" ? { x: cols - 1, y: rows - 1 } : from === "center" || from === "edges" ? { x: (cols - 1) / 2, y: (rows - 1) / 2 } : at(from);
3938
- const d = Array.from({ length: n }, (_, i) => Math.hypot(at(i).x - origin.x, at(i).y - origin.y));
3939
- if (from !== "edges") return d;
3940
- const far = Math.max(...d);
3941
- return d.map((v) => far - v);
3942
- }
3943
-
3944
- // src/presets/wiggle.ts
3945
- var WIGGLE_KEYS = ["at", "duration", "freq", "seed", "ease", "props"];
3946
- var MAX_STEPS3 = 2e3;
3947
- function wiggle(opts) {
3948
- warnUnknownOptions("wiggle()", opts, WIGGLE_KEYS);
3949
- const { at = 0, duration, freq = 3, seed = 0, ease = "sine.inOut", props } = opts;
3950
- if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`wiggle(): duration must be a positive number of seconds, got ${duration}`);
3951
- if (!(Number.isFinite(freq) && freq > 0)) throw new Error(`wiggle(): freq must be a positive number (steps per second), got ${freq}`);
3952
- const names = Object.keys(props ?? {});
3953
- if (names.length === 0) throw new Error("wiggle(): props is empty; name at least one property, e.g. props: { x: { around: 100, amp: 8 } }");
3954
- for (const n of names) {
3955
- const p = props[n];
3956
- if (!Number.isFinite(p.amp)) throw new Error(`wiggle(): props.${n}.amp must be a number, got ${p.amp}`);
3957
- if (p.around === void 0 || p.around === null || p.around === "") throw new Error(`wiggle(): props.${n}.around is missing`);
3958
- }
3959
- const steps = Math.ceil(duration * freq - 1e-9);
3960
- if (steps > MAX_STEPS3) throw new Error(`wiggle(): ${steps} steps is too many (max ${MAX_STEPS3}); lower freq or duration`);
3961
- const set = {};
3962
- for (const n of names) set[n] = props[n].around;
3963
- const out = [{ at, set }];
3964
- const stepLen = duration / steps;
3965
- for (let s = 0; s < steps; s++) {
3966
- const to = {};
3967
- names.forEach((n, pi) => {
3968
- const p = props[n];
3969
- if (s === steps - 1) {
3970
- to[n] = p.around;
3971
- return;
3972
- }
3973
- const off = (rand(seed * 7919 + pi * 104729 + s * 31 + 1) * 2 - 1) * p.amp;
3974
- to[n] = typeof p.around === "number" ? p.around + off : `(${p.around}) ${off < 0 ? "-" : "+"} ${Math.abs(off)}`;
3975
- });
3976
- out.push({ at: at + s * stepLen, to, duration: stepLen, ease });
3977
- }
3978
- return out;
3979
- }
3980
-
3981
- // src/presets/animateText.ts
3982
- var TWEEN_KEYS = ["preset", "from", "to", "duration", "ease"];
3983
- var TIME_KEYS = ["at", "delay", "start", "begin", "time"];
3984
- var KEYS5 = ["by", "x", "y", "align", "name", "at", "duration", "in", "out", "stagger", "idle", "styleFor"];
3985
- var PRESETS = {
3986
- rise: { from: { y: 40, alpha: 0 }, duration: 0.5, ease: "back.out(2)" },
3987
- drop: { from: { y: -50, alpha: 0 }, duration: 0.7, ease: "bounce.out" },
3988
- fade: { from: { alpha: 0 }, duration: 0.5, ease: "power1.out" },
3989
- pop: { from: { scale: 0, alpha: 0 }, duration: 0.45, ease: "back.out(2.4)" },
3990
- zoom: { from: { scale: 2.5, alpha: 0 }, duration: 0.6, ease: "power3.out" },
3991
- slide: { from: { x: -60, alpha: 0 }, duration: 0.5, ease: "power3.out" },
3992
- spin: { from: { rotation: -90, scale: 0, alpha: 0 }, duration: 0.6, ease: "back.out(1.7)" }
3993
- };
3994
- var PRESET_NAMES = Object.keys(PRESETS);
3995
- function animateText(text, style, options) {
3996
- warnUnknownOptions("animateText()", options, KEYS5);
3997
- const { by = "chars", x = 0, y = 0, align = "left", name = "text", at = 0, duration, idle, styleFor } = options;
3998
- for (const [n, v] of [["x", x], ["y", y]]) {
3999
- if (typeof v !== "number" || !Number.isFinite(v)) {
4000
- throw new Error(`animateText(): ${n} must be a number of pixels (e.g. ${n}: 640), got ${JSON.stringify(v)}. The letters are measured once, when the layers are made, so an expression such as 'GW/2' cannot be used here: compute the number (W / 2) yourself. \`y\` is the TOP of the line.`);
4001
- }
4002
- }
4003
- if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`animateText(): duration must be a positive number of seconds (how long until the last piece is gone), got ${duration}`);
4004
- const pieces = splitText(text, style, { by, x, y, align });
4005
- if (pieces.length === 0) return [];
4006
- const lineHeight = measureText("M", style).height;
4007
- const inTween = options.in === false ? null : resolveTween(options.in ?? "rise", "in");
4008
- const outTween = options.out === void 0 || options.out === false ? null : resolveTween(options.out, "out");
4009
- const delays = stagger(pieces.length, options.stagger ?? { each: 0.04 });
4010
- const wave = Math.max(...delays);
4011
- const need = (inTween?.duration ?? 0) + (outTween?.duration ?? 0) + wave;
4012
- if (duration < need - 1e-9) {
4013
- throw new Error(`animateText(): duration ${duration} is too short for this wave: the entrance${outTween ? ", the exit" : ""} and the stagger need at least ${+need.toFixed(3)} s`);
4014
- }
4015
- const end = at + duration;
4016
- return pieces.map((p, i) => {
4017
- const cx = p.x + p.width / 2, cy = p.y + lineHeight / 2;
4018
- const rest = { x: cx, y: cy, alpha: 1, scale: 1, scaleX: 1, scaleY: 1, rotation: 0 };
4019
- const start = at + delays[i];
4020
- const len = end - start;
4021
- const keyframes = [];
4022
- const place = (key, v) => key === "x" || key === "y" ? rest[key] + v : v;
4023
- if (inTween) {
4024
- keyframes.push({
4025
- at: 0,
4026
- duration: inTween.duration,
4027
- ease: inTween.ease,
4028
- from: Object.fromEntries(Object.entries(inTween.from).map(([k, v]) => [k, place(k, v)])),
4029
- to: Object.fromEntries(Object.keys(inTween.from).map((k) => [k, inTween.to[k] ?? rest[k]]))
4030
- });
4031
- }
4032
- let idleEnd = len;
4033
- if (outTween) {
4034
- const begin = outTween.duration + (wave - delays[i]);
4035
- idleEnd = len - begin;
4036
- keyframes.push({
4037
- at: -begin,
4038
- duration: outTween.duration,
4039
- ease: outTween.ease,
4040
- from: Object.fromEntries(Object.keys(outTween.from).map((k) => [k, outTween.to[k] ?? rest[k]])),
4041
- to: Object.fromEntries(Object.entries(outTween.from).map(([k, v]) => [k, place(k, v)]))
4042
- });
4043
- }
4044
- if (idle) {
4045
- const props = {};
4046
- for (const k of ["x", "y", "rotation", "scale"]) if (idle[k] !== void 0) props[k] = { around: rest[k], amp: idle[k] };
4047
- const freq = idle.freq ?? 1;
4048
- const from = inTween?.duration ?? 0;
4049
- if (Object.keys(props).length > 0 && idleEnd - from >= 1 / freq) {
4050
- keyframes.push(...wiggle({ at: from, duration: idleEnd - from, freq, seed: (idle.seed ?? 0) * 1e3 + i, props }));
4051
- }
4052
- }
4053
- return {
4054
- type: "text",
4055
- name: `${name}-${i}`,
4056
- text: p.text,
4057
- style: styleFor ? { ...style, ...styleFor(p) } : style,
4058
- at: start,
4059
- duration: len,
4060
- initial: { x: cx, y: cy, anchorX: 0.5, anchorY: 0.5 },
4061
- keyframes
4062
- };
4063
- });
4064
- }
4065
- function resolveTween(spec, which) {
4066
- const t = typeof spec === "string" ? { preset: spec } : spec;
4067
- if (typeof spec === "object" && spec) {
4068
- for (const key of Object.keys(spec)) {
4069
- if (TWEEN_KEYS.includes(key)) continue;
4070
- if (TIME_KEYS.includes(key)) {
4071
- console.warn(`pixi-effects: animateText(): ${which}.${key} ("${key}") is ignored: ${which === "out" ? "an out leaves from the END of the layer (at + duration), so to make the text gone at a time T set duration to T \u2212 at" : "the entrance starts when the piece starts (the options' at, plus the stagger)"}`);
4072
- } else warnUnknownOptions(`animateText() ${which}`, { [key]: 1 }, TWEEN_KEYS);
4073
- }
4074
- }
4075
- if (t.preset !== void 0 && !(t.preset in PRESETS)) {
4076
- const guess = suggestName(String(t.preset), PRESET_NAMES);
4077
- throw new Error(`animateText(): ${which} "${t.preset}" is not a preset${guess ? `; did you mean "${guess}"?` : ""} (presets: ${PRESET_NAMES.map((n) => `"${n}"`).join(", ")})`);
4078
- }
4079
- const base = t.preset ? PRESETS[t.preset] : void 0;
4080
- const from = { ...base?.from ?? {}, ...t.from ?? {} };
4081
- const to = t.to ?? {};
4082
- const known = /* @__PURE__ */ new Set(["x", "y", "alpha", "scale", "scaleX", "scaleY", "rotation"]);
4083
- for (const k of Object.keys(from)) {
4084
- if (!known.has(k) && to[k] === void 0) throw new Error(`animateText(): ${which}.from has "${k}", which has no resting value; give ${which}.to: { ${k}: \u2026 }`);
4085
- }
4086
- if (Object.keys(from).length === 0) throw new Error(`animateText(): ${which} needs a preset or a from`);
4087
- let ease = t.ease ?? base?.ease ?? "power2.out";
4088
- if (which === "out" && t.ease === void 0) ease = ease.replace(".out", ".in");
4089
- return { from, to: { ...to }, duration: t.duration ?? base?.duration ?? 0.5, ease };
4090
- }
4091
-
4092
- // src/presets/react.ts
4093
- var KEYS6 = ["at", "duration", "props", "audioOffset", "loop", "frameRate"];
4094
- var PROP_KEYS = ["base", "amount", "band", "beats", "decay", "attack", "release", "invert", "curve"];
4095
- var MAX_STEPS4 = 4e3;
4096
- function react(env, options) {
4097
- warnUnknownOptions("react()", options, KEYS6);
4098
- const { at = 0, duration, props, audioOffset = 0, loop = false } = options;
4099
- const frameRate = options.frameRate ?? env.frameRate;
4100
- if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`react(): duration must be a positive number of seconds, got ${duration}`);
4101
- if (!(Number.isFinite(frameRate) && frameRate > 0)) throw new Error(`react(): frameRate must be a positive number, got ${frameRate}`);
4102
- const names = Object.keys(props ?? {});
4103
- if (names.length === 0) throw new Error("react(): props is empty; name at least one property, e.g. props: { scale: { base: 1, amount: 0.3 } }");
4104
- for (const n of names) {
4105
- const p = props[n];
4106
- warnUnknownOptions(`react() props.${n}`, p, PROP_KEYS);
4107
- if (!Number.isFinite(p.amount)) throw new Error(`react(): props.${n}.amount must be a number, got ${p.amount}`);
4108
- if (!p.beats && p.band !== void 0 && !env.series[p.band]) throw new Error(`react(): props.${n}: band "${p.band}" is not in this envelope (it has: ${Object.keys(env.series).join(", ")})`);
4109
- if (p.beats && !(p.decay === void 0 || Number.isFinite(p.decay) && p.decay > 0)) throw new Error(`react(): props.${n}.decay must be a positive number of seconds, got ${p.decay}`);
4110
- }
4111
- const steps = Math.max(1, Math.round(duration * frameRate));
4112
- if (steps > MAX_STEPS4) throw new Error(`react(): too many keyframes (${steps} steps, max ${MAX_STEPS4}); lower frameRate (now ${frameRate}) or duration`);
4113
- const wrap = (a) => loop && env.duration > 0 ? (a % env.duration + env.duration) % env.duration : a;
4114
- const beatPulse = (a, decay) => {
4115
- const beats = env.beats;
4116
- let lo = 0, hi = beats.length - 1, found = -1;
4117
- while (lo <= hi) {
4118
- const mid = lo + hi >> 1;
4119
- if (beats[mid] <= a + 1e-9) {
4120
- found = mid;
4121
- lo = mid + 1;
4122
- } else hi = mid - 1;
4123
- }
4124
- if (found < 0) return 0;
4125
- return Math.exp(-(a - beats[found]) / decay);
4126
- };
4127
- const round3 = (v) => Math.round(v * 1e4) / 1e4;
4128
- const state = {};
4129
- const values = (k) => {
4130
- const t = k / frameRate, a = wrap(audioOffset + t);
4131
- const out2 = {};
4132
- for (const n of names) {
4133
- const p = props[n];
4134
- let x = p.beats ? beatPulse(a, p.decay ?? 0.2) : env.at(a, p.band ?? "level");
4135
- if (p.invert) x = 1 - x;
4136
- const prev = state[n];
4137
- if (prev !== void 0 && (p.attack || p.release)) {
4138
- const tau = x > prev ? p.attack ?? 0 : p.release ?? 0;
4139
- if (tau > 0) x = prev + (x - prev) * (1 - Math.exp(-(1 / frameRate) / tau));
4140
- }
4141
- state[n] = x;
4142
- const shaped = p.curve && p.curve !== 1 ? Math.pow(Math.max(0, x), p.curve) : x;
4143
- out2[n] = round3((p.base ?? 0) + p.amount * shaped);
4144
- }
4145
- return out2;
4146
- };
4147
- const out = [{ at, set: values(0) }];
4148
- const step = duration / steps;
4149
- for (let k = 1; k <= steps; k++) out.push({ at: round6(at + (k - 1) * step), to: values(k), duration: round6(step), ease: "none" });
4150
- return out;
4151
- }
4152
- var round6 = (v) => Math.round(v * 1e6) / 1e6;
4153
-
4154
- // src/audio/envelope.ts
4155
- var DEFAULT_BANDS = { bass: [20, 150], mid: [150, 2e3], treble: [2e3, 1e4] };
4156
- var WINDOW = 2048;
4157
- var RANGE_DB = 36;
4158
- var MIN_BEAT_GAP = 0.2;
4159
- var OPTION_KEYS = ["frameRate", "bands", "beatBand", "beatSensitivity"];
4160
- function makeAt(frameRate, series) {
4161
- return (time, name = "level") => {
4162
- const s = series[name];
4163
- if (!s) throw new Error(`pixi-effects: this envelope has no series "${name}" (it has: ${Object.keys(series).join(", ")})`);
4164
- const x = Math.min(Math.max(time * frameRate, 0), s.length - 1);
4165
- const i = Math.floor(x), f = x - i;
4166
- return i + 1 < s.length ? s[i] + (s[i + 1] - s[i]) * f : s[i];
4167
- };
4168
- }
4169
- function percentile(values, p) {
4170
- if (values.length === 0) return 0;
4171
- const sorted = Float64Array.from(values).sort();
4172
- return sorted[Math.min(sorted.length - 1, Math.floor(p * (sorted.length - 1)))];
4173
- }
4174
- function pickBeats(strength, frameRate, sensitivity) {
4175
- const n = strength.length;
4176
- const flux = new Float64Array(n);
4177
- for (let i = 1; i < n; i++) flux[i] = Math.max(0, strength[i] - strength[i - 1]);
4178
- const half = Math.max(2, Math.round(frameRate * 0.5));
4179
- const gap = Math.max(1, Math.round(MIN_BEAT_GAP * frameRate));
4180
- const beats = [];
4181
- let last = -gap;
4182
- for (let i = 1; i < n; i++) {
4183
- const v = flux[i];
4184
- if (v < 0.12 / sensitivity || v < flux[i - 1] || v <= (flux[i + 1] ?? 0)) continue;
4185
- let mean = 0, sq = 0, count = 0;
4186
- for (let j = Math.max(0, i - half); j <= Math.min(n - 1, i + half); j++) {
4187
- mean += flux[j];
4188
- sq += flux[j] * flux[j];
4189
- count++;
4190
- }
4191
- mean /= count;
4192
- const std = Math.sqrt(Math.max(0, sq / count - mean * mean));
4193
- if (v < mean + 1 * std / sensitivity) continue;
4194
- if (i - last < gap) {
4195
- if (beats.length && v > flux[last]) {
4196
- beats[beats.length - 1] = i / frameRate;
4197
- last = i;
4198
- }
4199
- continue;
4200
- }
4201
- beats.push(i / frameRate);
4202
- last = i;
4203
- }
4204
- return beats;
4205
- }
4206
- function tempo(beats) {
4207
- if (beats.length < 4) return null;
4208
- const gaps = beats.slice(1).map((b, i) => b - beats[i]).sort((a, b) => a - b);
4209
- const median = gaps[Math.floor(gaps.length / 2)];
4210
- return Math.round(60 / median * 10) / 10;
4211
- }
4212
- function computeEnvelope(audio, options = {}) {
4213
- warnUnknownOptions("audioEnvelope()", options, OPTION_KEYS);
4214
- const frameRate = options.frameRate ?? 30;
4215
- if (!(Number.isFinite(frameRate) && frameRate > 0)) throw new Error(`pixi-effects: audioEnvelope(): frameRate must be a positive number, got ${frameRate}`);
4216
- const bands = options.bands ?? DEFAULT_BANDS;
4217
- for (const [name, range2] of Object.entries(bands)) {
4218
- if (!(Array.isArray(range2) && range2.length === 2 && range2[0] >= 0 && range2[0] < range2[1])) throw new Error(`pixi-effects: audioEnvelope(): band "${name}" must be [from, to] in Hz with from < to, got ${JSON.stringify(range2)}`);
4219
- }
4220
- const sr = audio.sampleRate, length = audio.length;
4221
- const mono2 = new Float32Array(length);
4222
- for (let c = 0; c < audio.numberOfChannels; c++) {
4223
- const d = audio.getChannelData(c);
4224
- for (let i = 0; i < length; i++) mono2[i] += d[i] / audio.numberOfChannels;
4225
- }
4226
- const duration = length / sr;
4227
- const frames = Math.floor(duration * frameRate + 1e-9) + 1;
4228
- const hop = sr / frameRate;
4229
- const names = Object.keys(bands);
4230
- const raw = { level: new Float64Array(frames) };
4231
- for (const n of names) raw[n] = new Float64Array(frames);
4232
- const re = new Float64Array(WINDOW), im = new Float64Array(WINDOW);
4233
- const win = new Float64Array(WINDOW);
4234
- let winPower = 0;
4235
- for (let i = 0; i < WINDOW; i++) {
4236
- win[i] = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / (WINDOW - 1));
4237
- winPower += win[i] * win[i];
4238
- }
4239
- winPower /= WINDOW;
4240
- const binHz = sr / WINDOW;
4241
- for (let f = 0; f < frames; f++) {
4242
- const center = Math.round(f / frameRate * sr);
4243
- let sum = 0, cnt = 0;
4244
- for (let i = Math.max(0, Math.round(center - hop / 2)); i < Math.min(length, Math.round(center + hop / 2)); i++) {
4245
- sum += mono2[i] * mono2[i];
4246
- cnt++;
4247
- }
4248
- raw.level[f] = cnt ? Math.sqrt(sum / cnt) : 0;
4249
- for (let i = 0; i < WINDOW; i++) {
4250
- const k = center - WINDOW / 2 + i;
4251
- re[i] = k >= 0 && k < length ? mono2[k] * win[i] : 0;
4252
- im[i] = 0;
4253
- }
4254
- fft(re, im);
4255
- for (const n of names) {
4256
- const [lo, hi] = bands[n];
4257
- let p = 0;
4258
- for (let b = Math.max(1, Math.ceil(lo / binHz)); b <= Math.min(WINDOW / 2 - 1, Math.floor(hi / binHz)); b++) p += re[b] * re[b] + im[b] * im[b];
4259
- raw[n][f] = Math.sqrt(2 * p / (WINDOW * WINDOW * winPower));
4260
- }
4261
- }
4262
- let ref = 0;
4263
- for (const s of Object.values(raw)) ref = Math.max(ref, percentile(s, 0.98));
4264
- const series = {};
4265
- for (const [name, s] of Object.entries(raw)) {
4266
- const out = new Float32Array(frames);
4267
- if (ref > 1e-7) for (let f = 0; f < frames; f++) {
4268
- const v = s[f];
4269
- out[f] = v > 0 ? Math.min(1, Math.max(0, 1 + 20 * Math.log10(v / ref) / RANGE_DB)) : 0;
4270
- }
4271
- series[name] = out;
4272
- }
4273
- const beatName = options.beatBand ?? (series.bass ? "bass" : "level");
4274
- if (!series[beatName]) throw new Error(`pixi-effects: audioEnvelope(): beatBand "${beatName}" is not one of ${Object.keys(series).join(", ")}`);
4275
- const beats = ref > 1e-7 ? pickBeats(series[beatName], frameRate, options.beatSensitivity ?? 1) : [];
4276
- return { frameRate, duration, frames, series, beats, bpm: tempo(beats), at: makeAt(frameRate, series) };
4277
- }
4278
- function bpmEnvelope(bpm, options) {
4279
- const { duration, frameRate = 30, swing = 0 } = options;
4280
- if (!(Number.isFinite(bpm) && bpm > 0)) throw new Error(`pixi-effects: bpmEnvelope(): bpm must be a positive number, got ${bpm}`);
4281
- if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`pixi-effects: bpmEnvelope(): duration must be a positive number of seconds, got ${duration}`);
4282
- const beat = 60 / bpm;
4283
- const frames = Math.round(duration * frameRate) + 1;
4284
- const pulse = (hits, decay, t) => {
4285
- let v = 0;
4286
- for (let i = hits.length - 1; i >= 0; i--) {
4287
- if (hits[i] <= t + 1e-9) {
4288
- v = Math.exp(-(t - hits[i]) / decay);
4289
- break;
4290
- }
4291
- }
4292
- return v;
4293
- };
4294
- const beats = [], snares = [], hats = [];
4295
- for (let k = 0; k * beat < duration - 1e-9; k++) {
4296
- beats.push(round62(k * beat));
4297
- if (k % 2 === 1) snares.push(round62(k * beat));
4298
- hats.push(round62(k * beat), round62(k * beat + beat / 2 + swing * beat));
4299
- }
4300
- hats.sort((a, b) => a - b);
4301
- const series = { level: new Float32Array(frames), bass: new Float32Array(frames), mid: new Float32Array(frames), treble: new Float32Array(frames) };
4302
- for (let f = 0; f < frames; f++) {
4303
- const t = f / frameRate;
4304
- const b = pulse(beats, 0.14, t), m = pulse(snares, 0.1, t), h = pulse(hats, 0.045, t);
4305
- series.bass[f] = b;
4306
- series.mid[f] = m;
4307
- series.treble[f] = h;
4308
- series.level[f] = Math.max(b, m * 0.9, h * 0.55);
4309
- }
4310
- return { frameRate, duration, frames, series, beats, bpm, at: makeAt(frameRate, series) };
4311
- }
4312
- var round62 = (v) => Math.round(v * 1e6) / 1e6;
4313
- async function audioEnvelope(source, options = {}) {
4314
- let audio;
4315
- if (typeof source.getChannelData === "function") audio = source;
4316
- else {
4317
- let data;
4318
- if (typeof source === "string" || source instanceof URL) {
4319
- const res = await fetch(String(source));
4320
- if (!res.ok) throw new Error(`pixi-effects: audioEnvelope(): could not fetch ${String(source)} (${res.status})`);
4321
- data = await res.arrayBuffer();
4322
- } else if (source instanceof Blob) data = await source.arrayBuffer();
4323
- else data = source;
4324
- const Ctx = globalThis.OfflineAudioContext ?? globalThis.webkitOfflineAudioContext;
4325
- if (!Ctx) throw new Error("pixi-effects: audioEnvelope() needs a browser with Web Audio (OfflineAudioContext)");
4326
- try {
4327
- audio = await new Ctx(1, 1, 44100).decodeAudioData(data.slice(0));
4328
- } catch (err) {
4329
- throw new Error(`pixi-effects: audioEnvelope(): could not decode the audio (${err instanceof Error ? err.message : String(err)}); is it an mp3, wav, ogg, m4a or webm file?`);
4330
- }
4331
- }
4332
- return computeEnvelope(audio, options);
4333
- }
4334
-
4335
- // src/audio/musicEnvelope.ts
4336
- async function musicEnvelope(music, options = {}) {
4337
- const { resolveMusic, renderMusic, musicLength, musicBeatTimes, TempoMap } = await import('./music-JKOM5YK6.js');
4338
- const messages = [];
4339
- const m = resolveMusic(music, "musicEnvelope()", (msg) => messages.push(msg));
4340
- if (!m) throw new Error(`pixi-effects: musicEnvelope(): ${messages.join(" ") || "the music has nothing to play"}`);
4341
- for (const msg of messages) console.warn(msg);
4342
- const sampleRate = 22050;
4343
- const [left, right] = renderMusic(m, sampleRate, musicLength(m), false);
4344
- const env = computeEnvelope({ sampleRate, numberOfChannels: 2, length: left.length, getChannelData: (c) => c === 0 ? left : right }, options);
4345
- return { ...env, beats: musicBeatTimes(m, env.duration), bpm: Math.round(new TempoMap(m.tempo).bpmAt(0) * 100) / 100 };
4346
- }
4347
-
4348
- // src/audio/musicBuffer.ts
4349
- async function musicBuffer(music, options = {}) {
4350
- const { resolveMusic, renderMusic, musicLength } = await import('./music-JKOM5YK6.js');
4351
- const messages = [];
4352
- const m = resolveMusic(music, "musicBuffer()", (msg) => messages.push(msg));
4353
- if (!m) throw new Error(`pixi-effects: musicBuffer(): ${messages.join(" ") || "the music has nothing to play"}`);
4354
- for (const msg of messages) console.warn(msg);
4355
- const sampleRate = options.sampleRate ?? 44100;
4356
- const length = options.duration ?? musicLength(m);
4357
- if (!(Number.isFinite(length) && length > 0)) throw new Error(`pixi-effects: musicBuffer(): duration must be a positive number of seconds, got ${String(options.duration)}`);
4358
- const [left, right] = renderMusic(m, sampleRate, length, !!options.loop);
4359
- const buffer = typeof AudioBuffer === "function" ? new AudioBuffer({ length: left.length, sampleRate, numberOfChannels: 2 }) : new OfflineAudioContext(2, left.length, sampleRate).createBuffer(2, left.length, sampleRate);
4360
- buffer.copyToChannel(left, 0);
4361
- buffer.copyToChannel(right, 1);
4362
- return buffer;
4363
- }
4364
-
4365
- // src/audio/musicEvents.ts
4366
- async function musicEvents(music) {
4367
- const { resolveMusic } = await import('./music-JKOM5YK6.js');
4368
- const { describeEvents } = await import('./events-KXBWR4IV.js');
4369
- const messages = [];
4370
- const m = resolveMusic(music, "musicEvents()", (msg) => messages.push(msg));
4371
- if (!m) throw new Error(`pixi-effects: musicEvents(): ${messages.join(" ") || "the music has nothing to play"}`);
4372
- for (const msg of messages) console.warn(msg);
4373
- return describeEvents(m);
4374
- }
4375
-
4376
- // src/presets/deck.ts
4377
- var PAGE_KEYS = ["name", "duration", "sequences", "stops", "notes", "advance", "initial", "keyframes", "filters", "filterArea", "blendMode", "mask", "maskInverted"];
4378
- var DECK_KEYS = ["pages", "transition", "width", "height", "frameRate", "background", "canvas", "assets", "poster", "motionBlur", "loader"];
4379
- function deck(options) {
4380
- if (!options || !Array.isArray(options.pages) || options.pages.length === 0) throw new Error("deck(): pages must be a list with at least one page: { pages: [{ name, duration, stops, sequences }] }");
4381
- warnUnknownOptions("deck()", options, DECK_KEYS);
4382
- const { pages, transition, ...movieOptions } = options;
4383
- pages.forEach((p, i) => {
4384
- if (!p || typeof p !== "object") throw new Error(`deck(): pages[${i}] must be an object`);
4385
- warnUnknownOptions(`deck() pages[${i}]`, p, PAGE_KEYS);
4386
- if (!(typeof p.duration === "number" && Number.isFinite(p.duration) && p.duration > 0)) throw new Error(`deck(): pages[${i}].duration must be a positive number of seconds, got ${String(p.duration)}`);
4387
- if (!Array.isArray(p.sequences)) throw new Error(`deck(): pages[${i}].sequences must be a list of layers (it may be empty), got ${typeof p.sequences}`);
4388
- });
4389
- let overlap = 0;
4390
- if (transition) {
4391
- if (!(typeof transition.duration === "number" && transition.duration > 0)) throw new Error("deck(): transition needs a duration in seconds: { kind: 'crossfade', duration: 0.6 }");
4392
- const shortest = Math.min(...pages.map((p) => p.duration));
4393
- if (pages.length > 1 && transition.duration >= shortest) throw new Error(`deck(): the transition (${transition.duration} s) must be shorter than every page (the shortest is ${shortest} s)`);
4394
- overlap = pages.length > 1 ? transition.duration : 0;
4395
- }
4396
- const sequences = [];
4397
- const transitions = [];
4398
- const stops = [];
4399
- let start = 0;
4400
- pages.forEach((page, i) => {
4401
- const id = `page-${i + 1}`;
4402
- const label = page.name ? `"${page.name}"` : `pages[${i}]`;
4403
- const isLast = i === pages.length - 1;
4404
- const entries = (page.stops ?? [isLast ? page.duration : page.duration - overlap]).map((e, k) => {
4405
- if (e && typeof e === "object") warnUnknownOptions(`deck() page ${label} stops[${k}]`, e, ["at", "pdf"]);
4406
- return typeof e === "object" && e !== null ? e : { at: e };
4407
- });
4408
- const local = entries.map((e) => e.at);
4409
- local.forEach((s, k) => {
4410
- if (!(typeof s === "number" && Number.isFinite(s) && s > 0 && s <= page.duration + 1e-9)) throw new Error(`deck(): page ${label} has stop ${String(s)}, outside the page (0 to ${page.duration} s)`);
4411
- if (k > 0 && s <= local[k - 1]) throw new Error(`deck(): page ${label} stops must be in order, earliest first (got ${local.join(", ")})`);
4412
- });
4413
- const { name: _n, duration: _d, sequences: inner, stops: _s, notes: _no, advance: _a, ...layerProps } = page;
4414
- sequences.push({ type: "composition", name: id, at: round2(start), duration: page.duration, ...layerProps, sequences: inner });
4415
- local.forEach((s, k) => {
4416
- const stop = { at: round2(start + s) };
4417
- if (k === 0) {
4418
- stop.page = page.name ?? true;
4419
- if (page.notes !== void 0) stop.notes = page.notes;
4420
- }
4421
- if (k === local.length - 1 && page.advance !== void 0) stop.advance = page.advance;
4422
- if (entries[k].pdf !== void 0) stop.pdf = entries[k].pdf;
4423
- stops.push(stop);
4424
- });
4425
- if (transition && !isLast) {
4426
- const next = start + page.duration - overlap;
4427
- transitions.push({ ...transition, from: id, to: `page-${i + 2}`, at: round2(next), duration: transition.duration });
4428
- }
4429
- start = start + page.duration - (isLast ? 0 : overlap);
4430
- });
4431
- const composition = { sequences, stops };
4432
- if (transitions.length > 0) composition.transitions = transitions;
4433
- return { ...movieOptions, duration: round2(start), composition };
4434
- }
4435
- var round2 = (v) => Math.round(v * 1e6) / 1e6;
4436
-
4437
- // src/transforms/withFade.ts
4438
- function withFade(spec, opts) {
4439
- warnUnknownOptions("withFade()", opts, ["in", "out"]);
4440
- if (opts.in != null && opts.in > 0) {
4441
- spec.initial = { ...spec.initial ?? {}, alpha: 0 };
4442
- const kf = { at: 0, to: { alpha: 1 }, duration: opts.in };
4443
- spec.keyframes = [...spec.keyframes ?? [], kf];
4444
- }
4445
- if (opts.out != null && opts.out > 0) {
4446
- if (spec.duration == null) {
4447
- throw new Error(
4448
- "withFade(spec, { out }) requires spec.duration to be set so the fade-out can be anchored to the sequence end. Set spec.duration before passing the spec in."
4449
- );
4450
- }
4451
- const kf = { at: spec.duration - opts.out, to: { alpha: 0 }, duration: opts.out };
4452
- spec.keyframes = [...spec.keyframes ?? [], kf];
4453
- }
4454
- return spec;
4455
- }
4456
-
4457
- export { Movie, animateText, audioEnvelope, bpmEnvelope, cameraPath, computeEnvelope, deck, dismissLoader, parseEase2 as ease, failLoader, followPath, kenBurns, measureText, musicBuffer, musicEnvelope, musicEvents, normalizeStops, orbit, particles, pictureStops, react, resolveLoader, splitText, stagger, wiggle, withFade };
1
+ export { Movie, animateText, audioEnvelope, bpmEnvelope, cameraPath, computeEnvelope, dismissLoader, parseEase as ease, failLoader, followPath, kenBurns, measureText, musicBuffer, musicEnvelope, musicEvents, orbit, particles, react, resolveLoader, splitText, stagger, wiggle, withFade } from './chunk-CRQFDY7E.js';
2
+ import './chunk-A3Q3SDSH.js';
3
+ export { Sequence, noise, rand, random, registerSequenceType } from './chunk-ZAXYPXKW.js';
4
+ import './chunk-4KB3GUEK.js';
5
+ import './chunk-RCVZX2JP.js';
6
+ export { warnUnknownOptions } from './chunk-BXOCEZ4U.js';
4458
7
  //# sourceMappingURL=index.js.map
4459
8
  //# sourceMappingURL=index.js.map