pixi-effects 0.26.1 → 0.26.3

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 (49) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +6 -6
  3. package/ai/chat-template.html +3 -3
  4. package/ai/tools/render.mjs +16 -11
  5. package/dist/Composition-3O7L2VL5.js +7 -0
  6. package/dist/{Composition-Z2VY3KWZ.js.map → Composition-3O7L2VL5.js.map} +1 -1
  7. package/dist/Composition-EE3KEE2L.cjs +16 -0
  8. package/dist/{Composition-THQPG4E5.cjs.map → Composition-EE3KEE2L.cjs.map} +1 -1
  9. package/dist/Controller.d.cts +1 -1
  10. package/dist/Controller.d.ts +1 -1
  11. package/dist/{Movie-i05Oh9mJ.d.cts → Movie-BWLLWRRe.d.cts} +10 -3
  12. package/dist/{Movie-DVeDTdnc.d.ts → Movie-BkP82KXS.d.ts} +10 -3
  13. package/dist/Presenter.d.cts +1 -1
  14. package/dist/Presenter.d.ts +1 -1
  15. package/dist/{chunk-EABL3UKJ.cjs → chunk-A225HYSY.cjs} +351 -20
  16. package/dist/chunk-A225HYSY.cjs.map +1 -0
  17. package/dist/{chunk-JHJNEIDI.js → chunk-FXAHNPBP.js} +351 -20
  18. package/dist/chunk-FXAHNPBP.js.map +1 -0
  19. package/dist/{chunk-H3RHEH5W.cjs → chunk-IIDBEYQH.cjs} +59 -59
  20. package/dist/{chunk-H3RHEH5W.cjs.map → chunk-IIDBEYQH.cjs.map} +1 -1
  21. package/dist/{chunk-YEVAUQ6Y.js → chunk-WMMZDPWK.js} +3 -3
  22. package/dist/{chunk-YEVAUQ6Y.js.map → chunk-WMMZDPWK.js.map} +1 -1
  23. package/dist/index.cjs +144 -50
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +121 -27
  28. package/dist/index.js.map +1 -1
  29. package/dist/loader.css +1 -1
  30. package/dist/{music-AF4VTFAF.js → music-3O5MDQXY.js} +52 -6
  31. package/dist/music-3O5MDQXY.js.map +1 -0
  32. package/dist/{music-KB7YZLHP.cjs → music-4O367SPR.cjs} +52 -6
  33. package/dist/music-4O367SPR.cjs.map +1 -0
  34. package/dist/three.cjs +5 -5
  35. package/dist/three.js +1 -1
  36. package/docs/api.md +11 -5
  37. package/docs/dsl.md +4 -2
  38. package/llms-full.txt +21 -12
  39. package/package.json +1 -1
  40. package/skills/pixi-effects/SKILL.md +1 -1
  41. package/skills/pixi-effects/reference/cheatsheet.md +4 -4
  42. package/skills/pixi-effects/reference/pitfalls.md +1 -0
  43. package/skills/pixi-effects/template.html +6 -6
  44. package/dist/Composition-THQPG4E5.cjs +0 -16
  45. package/dist/Composition-Z2VY3KWZ.js +0 -7
  46. package/dist/chunk-EABL3UKJ.cjs.map +0 -1
  47. package/dist/chunk-JHJNEIDI.js.map +0 -1
  48. package/dist/music-AF4VTFAF.js.map +0 -1
  49. package/dist/music-KB7YZLHP.cjs.map +0 -1
@@ -373,19 +373,29 @@ function evaluateExpr(source, scope) {
373
373
  // src/expr/normalizeProps.ts
374
374
  function normalizeProps(input, scope, options = {}) {
375
375
  const skip = new Set(options.skipKeys ?? []);
376
- return walk(input, scope, skip, null);
376
+ return walk(input, scope, skip, null, options);
377
377
  }
378
- function walk(value, scope, skip, currentKey) {
378
+ var RELATIVE = /^\s*([+-])=([\s\S]*)$/;
379
+ function walk(value, scope, skip, currentKey, options) {
379
380
  if (Array.isArray(value)) {
380
- return value.map((v) => walk(v, scope, skip, currentKey));
381
+ return value.map((v) => walk(v, scope, skip, currentKey, options));
381
382
  }
382
383
  if (value !== null && typeof value === "object") {
383
384
  const out = {};
384
385
  for (const k of Object.keys(value)) {
385
- out[k] = walk(value[k], scope, skip, k);
386
+ out[k] = walk(value[k], scope, skip, k, options);
386
387
  }
387
388
  return out;
388
389
  }
390
+ if (typeof value === "string" && !(currentKey !== null && skip.has(currentKey))) {
391
+ const rel = RELATIVE.exec(value);
392
+ if (rel) {
393
+ const n = evaluateExpr(rel[2].trim(), scope);
394
+ if (options.relative === "allow") return `${rel[1]}=${n}`;
395
+ console.warn(`pixi-effects: "${value.trim()}"${currentKey ? ` (${currentKey})` : ""} is a relative value (measured from where the layer is): it works in a keyframe's to / from / set, not in ${options.where ?? "an initial or a style"}; it is read as the number ${rel[1] === "-" ? -n : n}`);
396
+ return rel[1] === "-" ? -n : n;
397
+ }
398
+ }
389
399
  if (isExpr(value) && !(currentKey !== null && skip.has(currentKey))) {
390
400
  if (looksLikeColorString(value)) return value;
391
401
  return evaluateExpr(value, scope);
@@ -616,7 +626,7 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
616
626
  const kf = normalizeKeyframe(raw, parentDuration);
617
627
  const at = offset + kf.at;
618
628
  if (kf.kind === "set") {
619
- const resolved = normalizeProps(kf.set, scope, { skipKeys });
629
+ const resolved = normalizeProps(kf.set, scope, { skipKeys, relative: "allow" });
620
630
  const { rest, routed } = splitRouted(resolved, routers);
621
631
  for (const r of routed) timeline.set(r.target, { [r.prop]: r.value }, at);
622
632
  const { ownProps, filterProps } = partitionProps(rest);
@@ -627,7 +637,7 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
627
637
  timeline.set(f, props, at);
628
638
  }
629
639
  } else if (kf.kind === "to") {
630
- const resolved = normalizeProps(kf.to, scope, { skipKeys });
640
+ const resolved = normalizeProps(kf.to, scope, { skipKeys, relative: "allow" });
631
641
  const { rest, routed } = splitRouted(resolved, routers);
632
642
  for (const r of routed)
633
643
  timeline.to(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
@@ -640,7 +650,7 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
640
650
  timeline.to(f, { ...props, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
641
651
  }
642
652
  } else if (kf.kind === "from") {
643
- const resolved = normalizeProps(kf.from, scope, { skipKeys });
653
+ const resolved = normalizeProps(kf.from, scope, { skipKeys, relative: "allow" });
644
654
  const { rest, routed } = splitRouted(resolved, routers);
645
655
  for (const r of routed)
646
656
  timeline.from(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
@@ -653,8 +663,8 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
653
663
  timeline.from(f, { ...props, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
654
664
  }
655
665
  } else {
656
- const fromResolved = normalizeProps(kf.from, scope, { skipKeys });
657
- const toResolved = normalizeProps(kf.to, scope, { skipKeys });
666
+ const fromResolved = normalizeProps(kf.from, scope, { skipKeys, where: "the from side of a from + to keyframe" });
667
+ const toResolved = normalizeProps(kf.to, scope, { skipKeys, relative: "allow" });
658
668
  const fromRouted = splitRouted(fromResolved, routers);
659
669
  const toRouted = splitRouted(toResolved, routers);
660
670
  const fromByKey = new Map(fromRouted.routed.map((r) => [r.key, r]));
@@ -704,7 +714,7 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
704
714
  }
705
715
  function applyInitial(target, initial, scope, skipKeys = [], routers) {
706
716
  if (!initial) return;
707
- const resolved = normalizeProps(initial, scope, { skipKeys });
717
+ const resolved = normalizeProps(initial, scope, { skipKeys, where: "initial" });
708
718
  const { rest, routed } = splitRouted(resolved, routers);
709
719
  for (const r of routed) gsap.set(r.target, { [r.prop]: r.value });
710
720
  const { ownProps, filterProps } = partitionProps(rest);
@@ -2965,7 +2975,7 @@ var AudioSequence = class extends Sequence {
2965
2975
  const raw = spec;
2966
2976
  if (raw.asset !== void 0) console.warn(`pixi-effects: ${who}: has both asset and music \u2014 the music plays; remove one`);
2967
2977
  if (raw.sfx !== void 0) console.warn(`pixi-effects: ${who}: has both sfx and music \u2014 the music plays; remove one`);
2968
- const { resolveMusic, renderMusic, musicLength, musicKey } = await import('./music-AF4VTFAF.js');
2978
+ const { resolveMusic, renderMusic, musicLength, musicKey } = await import('./music-3O5MDQXY.js');
2969
2979
  const music = resolveMusic(spec.music, who);
2970
2980
  if (!music) return;
2971
2981
  const natural = musicLength(music);
@@ -3042,19 +3052,57 @@ var AudioSequence = class extends Sequence {
3042
3052
  };
3043
3053
 
3044
3054
  // src/core/FrameCache.ts
3055
+ var BUDGET_BYTES = 96 * 1024 * 1024;
3056
+ var MIN_HELD = 3;
3057
+ var JUMP_SECONDS = 1;
3058
+ var EPS = 1e-6;
3059
+ var LOW_BEHIND = 14;
3060
+ var MIN_HELD_TO_READ_BACK = 20;
3061
+ var MIN_HELD_TO_READ_START = 9;
3062
+ function reportDecodeError(err, time) {
3063
+ const isTransient = err instanceof DOMException && (err.name === "EncodingError" || err.name === "NotSupportedError");
3064
+ if (isTransient) console.debug("pixi-effects: transient video decode failure at", time, "s \u2014", err.name);
3065
+ else console.warn("pixi-effects: video frame decode failed at", time, "s \u2014", err);
3066
+ }
3045
3067
  var FrameCache = class {
3046
3068
  sink;
3047
3069
  capacity;
3048
3070
  cache = /* @__PURE__ */ new Map();
3049
3071
  _pending = /* @__PURE__ */ new Map();
3072
+ // Sequential mode (the sink has `samples()`): one decoder pass that runs forward as the movie asks for frame after frame.
3073
+ _frames = [];
3074
+ _cur = null;
3075
+ _iter = null;
3076
+ _next = null;
3077
+ _held = 0;
3078
+ _lock = Promise.resolve();
3079
+ _lastTime = null;
3080
+ _dir = 0;
3081
+ _gen = 0;
3082
+ _backBusy = false;
3083
+ _backGen = 0;
3084
+ _standbyBusy = false;
3085
+ /** A second pass at the start of the file, ready for the moment a looping video comes round. */
3086
+ _standby = null;
3087
+ _duration;
3088
+ _loop;
3089
+ /** How often it had to wait for a decoder (`restarts`, `singles`) and how many reads ahead it made: for tests and for `check`. */
3090
+ stats = { restarts: 0, singles: 0, prefetches: 0 };
3050
3091
  constructor(sink, options = {}) {
3051
3092
  this.sink = sink;
3052
3093
  this.capacity = options.capacity ?? 30;
3094
+ this._duration = options.duration ?? null;
3095
+ this._loop = !!options.loop;
3096
+ }
3097
+ /** How many decoded frames are held right now (the sequential reader's own list, plus the exact-time cache of the old path). */
3098
+ get heldFrames() {
3099
+ return this._frames.length + this.cache.size;
3053
3100
  }
3054
3101
  _key(time) {
3055
3102
  return Math.round(time * 1e6);
3056
3103
  }
3057
3104
  async getFrameAt(time) {
3105
+ if (this.sink.samples) return this._enqueue(() => this._sequentialAt(time));
3058
3106
  const key = this._key(time);
3059
3107
  if (this.cache.has(key)) {
3060
3108
  const v = this.cache.get(key);
@@ -3092,14 +3140,287 @@ var FrameCache = class {
3092
3140
  sample.close?.();
3093
3141
  return frame;
3094
3142
  } catch (err) {
3095
- const isTransient = err instanceof DOMException && (err.name === "EncodingError" || err.name === "NotSupportedError");
3096
- if (isTransient) {
3097
- console.debug("pixi-effects: transient video decode failure at", time, "s \u2014", err.name);
3098
- } else {
3099
- console.warn("pixi-effects: video frame decode failed at", time, "s \u2014", err);
3143
+ reportDecodeError(err, time);
3144
+ return null;
3145
+ }
3146
+ }
3147
+ // ── sequential mode ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────
3148
+ /** Requests are answered one at a time, in the order they arrive: one pass cannot serve two places at once. */
3149
+ _enqueue(fn) {
3150
+ const run = this._lock.then(fn, fn);
3151
+ this._lock = run.then(() => void 0, () => void 0);
3152
+ return run;
3153
+ }
3154
+ _covering(time) {
3155
+ for (const e of this._frames) {
3156
+ if (e.end === null ? e.req !== void 0 && Math.abs(e.req - time) < EPS : e.ts <= time + EPS && time < e.end - EPS) {
3157
+ this._frames.splice(this._frames.indexOf(e), 1);
3158
+ this._frames.push(e);
3159
+ return e;
3160
+ }
3161
+ }
3162
+ return null;
3163
+ }
3164
+ async _sequentialAt(time) {
3165
+ const last = this._lastTime;
3166
+ this._lastTime = time;
3167
+ if (last !== null && Math.abs(time - last) <= JUMP_SECONDS) this._dir = time > last + EPS ? 1 : time < last - EPS ? -1 : this._dir;
3168
+ let frame;
3169
+ try {
3170
+ frame = await this._serve(time, last);
3171
+ } catch (err) {
3172
+ reportDecodeError(err, time);
3173
+ await this._closeIter();
3174
+ return null;
3175
+ }
3176
+ this._maybeReadAhead(time);
3177
+ return frame;
3178
+ }
3179
+ async _serve(time, last) {
3180
+ const hit = this._covering(time);
3181
+ if (hit) return hit.frame;
3182
+ const sb = this._standby;
3183
+ if (sb && time >= sb.cur.ts - EPS && time - sb.cur.ts <= JUMP_SECONDS && (!this._iter || !this._cur || this._cur.ts > time + JUMP_SECONDS)) return await this._adoptStandby(time);
3184
+ if (last === null || Math.abs(time - last) > JUMP_SECONDS) return await this._single(time);
3185
+ const cur = this._cur;
3186
+ if (this._iter && cur && time >= cur.ts && time - cur.ts <= JUMP_SECONDS) return await this._advance(time);
3187
+ const backwards = !!cur && time < cur.ts;
3188
+ return await this._restart(time, backwards ? Math.max(0, time - Math.max(1, this._held - 1) / 30) : time);
3189
+ }
3190
+ // ── reading ahead ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
3191
+ _maybeReadAhead(time) {
3192
+ if (this._held === 0) return;
3193
+ if (this._dir === -1) {
3194
+ this._dropStandby();
3195
+ let behind = 0, passed = 0, earliest = Infinity;
3196
+ for (const e of this._frames) {
3197
+ if (e.ts < time - EPS) behind++;
3198
+ else if (e.ts > time + EPS) passed++;
3199
+ if (e.ts < earliest) earliest = e.ts;
3200
+ }
3201
+ if (this._held >= MIN_HELD_TO_READ_BACK && behind < LOW_BEHIND && passed >= 3 && earliest > EPS && !this._backBusy) void this._readBack(earliest, passed);
3202
+ } else if (this._held >= MIN_HELD_TO_READ_START && this._dir === 1 && this._loop && this._duration !== null && time >= this._duration - JUMP_SECONDS && !this._standby && !this._standbyBusy) {
3203
+ void this._readStart();
3204
+ }
3205
+ }
3206
+ /** In the background: the window of frames before the earliest one held (reverse play comes to it next). */
3207
+ async _readBack(earliest, room) {
3208
+ this._backBusy = true;
3209
+ this.stats.prefetches++;
3210
+ const gen = this._gen, bg = this._backGen;
3211
+ const want = Math.max(3, Math.min(Math.floor(this._held * 0.6), room));
3212
+ let iter = null;
3213
+ const got = [];
3214
+ try {
3215
+ iter = this.sink.samples(Math.max(0, earliest - want / 30));
3216
+ for (; ; ) {
3217
+ const r = await iter.next();
3218
+ if (r.done) break;
3219
+ if (gen !== this._gen || r.value.timestamp >= earliest - EPS) {
3220
+ r.value.close?.();
3221
+ break;
3222
+ }
3223
+ got.push(r.value);
3224
+ if (bg !== this._backGen) break;
3225
+ }
3226
+ if (gen === this._gen && bg === this._backGen) {
3227
+ for (let k = 0; k < got.length; k++) {
3228
+ const s = got[k];
3229
+ const frame = s.toVideoFrame();
3230
+ this._keep({ ts: s.timestamp, end: got[k + 1]?.timestamp ?? earliest, frame }, frame);
3231
+ }
3232
+ }
3233
+ } catch (err) {
3234
+ reportDecodeError(err, earliest);
3235
+ } finally {
3236
+ for (const s of got) s.close?.();
3237
+ this._backBusy = false;
3238
+ try {
3239
+ await iter?.return?.(void 0);
3240
+ } catch {
3241
+ }
3242
+ }
3243
+ }
3244
+ /** In the background: the first frames of the file and a pass that stands behind them; held (pinned) until the loop comes round. */
3245
+ async _readStart() {
3246
+ this._standbyBusy = true;
3247
+ this.stats.prefetches++;
3248
+ const gen = this._gen;
3249
+ const want = Math.max(3, Math.min(10, Math.floor(this._held / 3)));
3250
+ let iter = null;
3251
+ const got = [];
3252
+ let keep = false;
3253
+ try {
3254
+ iter = this.sink.samples(0);
3255
+ for (; got.length < want + 1; ) {
3256
+ const r = await iter.next();
3257
+ if (r.done) break;
3258
+ if (gen !== this._gen) {
3259
+ r.value.close?.();
3260
+ break;
3261
+ }
3262
+ got.push(r.value);
3263
+ }
3264
+ if (gen === this._gen && got.length >= 2) {
3265
+ const next = got.pop();
3266
+ let cur = null;
3267
+ got.forEach((s, k) => {
3268
+ const frame = s.toVideoFrame();
3269
+ s.close?.();
3270
+ const entry = { ts: s.timestamp, end: (got[k + 1] ?? next).timestamp, frame, pinned: true };
3271
+ this._keep(entry, frame);
3272
+ cur = entry;
3273
+ });
3274
+ got.length = 0;
3275
+ if (cur) {
3276
+ this._standby = { iter, cur, next };
3277
+ keep = true;
3278
+ }
3279
+ }
3280
+ } catch (err) {
3281
+ reportDecodeError(err, 0);
3282
+ } finally {
3283
+ for (const s of got) s.close?.();
3284
+ this._standbyBusy = false;
3285
+ if (!keep) {
3286
+ try {
3287
+ await iter?.return?.(void 0);
3288
+ } catch {
3289
+ }
3100
3290
  }
3291
+ }
3292
+ }
3293
+ async _adoptStandby(time) {
3294
+ const sb = this._standby;
3295
+ this._standby = null;
3296
+ await this._closeIter();
3297
+ for (const e of this._frames) e.pinned = false;
3298
+ this._iter = sb.iter;
3299
+ this._next = sb.next;
3300
+ this._cur = sb.cur;
3301
+ return this._advance(time);
3302
+ }
3303
+ _dropStandby() {
3304
+ const sb = this._standby;
3305
+ if (!sb) return;
3306
+ this._standby = null;
3307
+ sb.next?.close?.();
3308
+ for (const e of this._frames.filter((x) => x.pinned)) {
3309
+ e.pinned = false;
3310
+ }
3311
+ void sb.iter.return?.(void 0);
3312
+ }
3313
+ /** One frame by itself (a seek): getSample, kept so that asking again for the same time costs nothing. The running pass, if any, is closed: it no longer follows the playhead. */
3314
+ async _single(time) {
3315
+ this.stats.singles++;
3316
+ await this._closeIter();
3317
+ this._cur = null;
3318
+ const sample = await this.sink.getSample(time);
3319
+ if (!sample) return null;
3320
+ const frame = sample.toVideoFrame();
3321
+ sample.close?.();
3322
+ this._keep({ ts: sample.timestamp, end: null, frame, req: time }, frame);
3323
+ return frame;
3324
+ }
3325
+ /** Start a pass at `time` (a seek and the frames of one group of pictures), keeping what is still held for a step back. */
3326
+ async _restart(time, from = time) {
3327
+ this.stats.restarts++;
3328
+ this._backGen++;
3329
+ await this._closeIter();
3330
+ this._cur = null;
3331
+ const iter = this.sink.samples(from);
3332
+ this._iter = iter;
3333
+ const first = await iter.next();
3334
+ if (first.done) {
3335
+ await this._closeIter();
3101
3336
  return null;
3102
3337
  }
3338
+ if (first.value.timestamp > time + EPS) {
3339
+ first.value.close?.();
3340
+ await this._closeIter();
3341
+ return null;
3342
+ }
3343
+ this._take(first.value);
3344
+ return this._advance(time);
3345
+ }
3346
+ /** Run the pass forward until the frame that covers `time` is the current one; the next frame is read ahead (its start is where this one ends). */
3347
+ async _advance(time) {
3348
+ const cur = () => this._cur;
3349
+ for (; ; ) {
3350
+ if (!this._next) {
3351
+ let r;
3352
+ try {
3353
+ r = await this._iter.next();
3354
+ } catch (err) {
3355
+ reportDecodeError(err, time);
3356
+ await this._closeIter();
3357
+ const prev = this._frames.length > 1 ? this._frames[this._frames.length - 2] : null;
3358
+ const spacing = prev ? cur().ts - prev.ts : 1 / 24;
3359
+ return time < cur().ts + spacing - EPS ? cur().frame : null;
3360
+ }
3361
+ if (r.done) {
3362
+ cur().end = Infinity;
3363
+ this._iter = null;
3364
+ break;
3365
+ }
3366
+ this._next = r.value;
3367
+ }
3368
+ cur().end = this._next.timestamp;
3369
+ if (this._next.timestamp <= time + EPS) {
3370
+ const s = this._next;
3371
+ this._next = null;
3372
+ this._take(s);
3373
+ } else break;
3374
+ }
3375
+ return cur().frame;
3376
+ }
3377
+ _take(sample) {
3378
+ const frame = sample.toVideoFrame();
3379
+ sample.close?.();
3380
+ const entry = { ts: sample.timestamp, end: null, frame };
3381
+ this._keep(entry, frame);
3382
+ this._cur = entry;
3383
+ }
3384
+ /** Hold a frame (the first one sets how many fit: a byte budget, not a count) and close the oldest beyond that; the frame the pass stands on is never closed. */
3385
+ _keep(entry, frame) {
3386
+ if (this._held === 0) {
3387
+ const bytes = frame.allocationSize?.() ?? frame.displayWidth * frame.displayHeight * 1.5;
3388
+ this._held = Math.max(MIN_HELD, Math.min(this.capacity, Math.floor(BUDGET_BYTES / Math.max(1, bytes))));
3389
+ }
3390
+ this._frames.push(entry);
3391
+ while (this._frames.length > this._held) {
3392
+ const victim = this._victim(entry);
3393
+ if (!victim) break;
3394
+ this._frames.splice(this._frames.indexOf(victim), 1);
3395
+ victim.frame.close?.();
3396
+ }
3397
+ }
3398
+ /**
3399
+ * The frame to let go: one the playhead has already passed (going forward those before it, going back those after it), the furthest
3400
+ * behind first; when there is none, the one furthest ahead of the playhead (the last to be needed). Least recently used when the
3401
+ * direction is not known. Frames of the standby (pinned) and the frame the pass stands on are never chosen.
3402
+ */
3403
+ _victim(except) {
3404
+ const cand = this._frames.filter((e) => e !== this._cur && e !== except && !e.pinned);
3405
+ if (cand.length === 0) return null;
3406
+ if (this._dir === 0) return cand[0];
3407
+ const at = this._lastTime ?? 0;
3408
+ const passed = cand.filter((e) => this._dir === 1 ? e.ts < at - EPS : e.ts > at + EPS);
3409
+ const farthest = (list, sign) => list.reduce((a, b) => sign * (b.ts - a.ts) > 0 ? b : a);
3410
+ if (passed.length) return farthest(passed, this._dir === 1 ? -1 : 1);
3411
+ return farthest(cand, this._dir === 1 ? 1 : -1);
3412
+ }
3413
+ async _closeIter() {
3414
+ const iter = this._iter;
3415
+ this._iter = null;
3416
+ this._next?.close?.();
3417
+ this._next = null;
3418
+ if (iter) {
3419
+ try {
3420
+ await iter.return?.(void 0);
3421
+ } catch {
3422
+ }
3423
+ }
3103
3424
  }
3104
3425
  _evictIfNeeded() {
3105
3426
  while (this.cache.size > this.capacity) {
@@ -3111,9 +3432,19 @@ var FrameCache = class {
3111
3432
  }
3112
3433
  }
3113
3434
  dispose() {
3435
+ this._gen++;
3436
+ this._dropStandby();
3114
3437
  for (const f of this.cache.values()) f?.close?.();
3115
3438
  this.cache.clear();
3116
3439
  this._pending.clear();
3440
+ for (const e of this._frames) e.frame.close?.();
3441
+ this._frames = [];
3442
+ this._cur = null;
3443
+ this._next?.close?.();
3444
+ this._next = null;
3445
+ const iter = this._iter;
3446
+ this._iter = null;
3447
+ void iter?.return?.(void 0);
3117
3448
  }
3118
3449
  };
3119
3450
 
@@ -3144,7 +3475,7 @@ var VideoSequence = class extends Sequence {
3144
3475
  this._sourceDuration = data.duration;
3145
3476
  this._remap = remapOf(this.spec, "currentTime", this._sourceDuration);
3146
3477
  this._audioBuffer = this.spec.audio !== false ? data.audioBuffer : null;
3147
- this._cache = new FrameCache(data.sink, { capacity: 30 });
3478
+ this._cache = new FrameCache(data.sink, { capacity: 30, duration: data.duration, loop: !!this.spec.loop });
3148
3479
  const probeFrame = await this._cache.getFrameAt(0);
3149
3480
  const w = probeFrame?.displayWidth ?? 1920;
3150
3481
  const h = probeFrame?.displayHeight ?? 1080;
@@ -4344,7 +4675,7 @@ function resolveDim(v, scope) {
4344
4675
  var _CompositionSequence = null;
4345
4676
  async function getCompositionSequence() {
4346
4677
  if (!_CompositionSequence) {
4347
- _CompositionSequence = (await import('./Composition-Z2VY3KWZ.js')).CompositionSequence;
4678
+ _CompositionSequence = (await import('./Composition-3O7L2VL5.js')).CompositionSequence;
4348
4679
  }
4349
4680
  return _CompositionSequence;
4350
4681
  }
@@ -4387,5 +4718,5 @@ async function buildSequenceTree(specs, parent, root) {
4387
4718
  }
4388
4719
 
4389
4720
  export { CameraSequence, DEFAULT_FOV, DEG2 as DEG, FALLOFFS, LIGHT_KINDS, LIGHT_PROPS, LightSequence, MANY_BLURRED, MAX_BLUR, MAX_CASTERS, MAX_LIGHTS, MIN_BLUR, NEAR, NullSequence, SHADOW_TAPS, Sequence, bindClock, blurRadius, buildScope, buildSequenceTree, cameraBasis, checkEase, clockTable, contentLength, ensureFilterLibrary, evaluateExpr, flattenSvgPath, focusProblems, homeCamera, homeDistance, layerInitialZ, layerToWorld, lightLevel, noise, packLights, projectLayer, projectPoint, rand, random, registerSequenceType, remapOf, resolveAt, timeRemapOf };
4390
- //# sourceMappingURL=chunk-JHJNEIDI.js.map
4391
- //# sourceMappingURL=chunk-JHJNEIDI.js.map
4721
+ //# sourceMappingURL=chunk-FXAHNPBP.js.map
4722
+ //# sourceMappingURL=chunk-FXAHNPBP.js.map