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
@@ -394,19 +394,29 @@ function evaluateExpr(source, scope) {
394
394
  // src/expr/normalizeProps.ts
395
395
  function normalizeProps(input, scope, options = {}) {
396
396
  const skip = new Set(options.skipKeys ?? []);
397
- return walk(input, scope, skip, null);
397
+ return walk(input, scope, skip, null, options);
398
398
  }
399
- function walk(value, scope, skip, currentKey) {
399
+ var RELATIVE = /^\s*([+-])=([\s\S]*)$/;
400
+ function walk(value, scope, skip, currentKey, options) {
400
401
  if (Array.isArray(value)) {
401
- return value.map((v) => walk(v, scope, skip, currentKey));
402
+ return value.map((v) => walk(v, scope, skip, currentKey, options));
402
403
  }
403
404
  if (value !== null && typeof value === "object") {
404
405
  const out = {};
405
406
  for (const k of Object.keys(value)) {
406
- out[k] = walk(value[k], scope, skip, k);
407
+ out[k] = walk(value[k], scope, skip, k, options);
407
408
  }
408
409
  return out;
409
410
  }
411
+ if (typeof value === "string" && !(currentKey !== null && skip.has(currentKey))) {
412
+ const rel = RELATIVE.exec(value);
413
+ if (rel) {
414
+ const n = evaluateExpr(rel[2].trim(), scope);
415
+ if (options.relative === "allow") return `${rel[1]}=${n}`;
416
+ 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}`);
417
+ return rel[1] === "-" ? -n : n;
418
+ }
419
+ }
410
420
  if (isExpr(value) && !(currentKey !== null && skip.has(currentKey))) {
411
421
  if (looksLikeColorString(value)) return value;
412
422
  return evaluateExpr(value, scope);
@@ -637,7 +647,7 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
637
647
  const kf = normalizeKeyframe(raw, parentDuration);
638
648
  const at = offset + kf.at;
639
649
  if (kf.kind === "set") {
640
- const resolved = normalizeProps(kf.set, scope, { skipKeys });
650
+ const resolved = normalizeProps(kf.set, scope, { skipKeys, relative: "allow" });
641
651
  const { rest, routed } = splitRouted(resolved, routers);
642
652
  for (const r of routed) timeline.set(r.target, { [r.prop]: r.value }, at);
643
653
  const { ownProps, filterProps } = partitionProps(rest);
@@ -648,7 +658,7 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
648
658
  timeline.set(f, props, at);
649
659
  }
650
660
  } else if (kf.kind === "to") {
651
- const resolved = normalizeProps(kf.to, scope, { skipKeys });
661
+ const resolved = normalizeProps(kf.to, scope, { skipKeys, relative: "allow" });
652
662
  const { rest, routed } = splitRouted(resolved, routers);
653
663
  for (const r of routed)
654
664
  timeline.to(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
@@ -661,7 +671,7 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
661
671
  timeline.to(f, { ...props, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
662
672
  }
663
673
  } else if (kf.kind === "from") {
664
- const resolved = normalizeProps(kf.from, scope, { skipKeys });
674
+ const resolved = normalizeProps(kf.from, scope, { skipKeys, relative: "allow" });
665
675
  const { rest, routed } = splitRouted(resolved, routers);
666
676
  for (const r of routed)
667
677
  timeline.from(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
@@ -674,8 +684,8 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
674
684
  timeline.from(f, { ...props, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
675
685
  }
676
686
  } else {
677
- const fromResolved = normalizeProps(kf.from, scope, { skipKeys });
678
- const toResolved = normalizeProps(kf.to, scope, { skipKeys });
687
+ const fromResolved = normalizeProps(kf.from, scope, { skipKeys, where: "the from side of a from + to keyframe" });
688
+ const toResolved = normalizeProps(kf.to, scope, { skipKeys, relative: "allow" });
679
689
  const fromRouted = splitRouted(fromResolved, routers);
680
690
  const toRouted = splitRouted(toResolved, routers);
681
691
  const fromByKey = new Map(fromRouted.routed.map((r) => [r.key, r]));
@@ -725,7 +735,7 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
725
735
  }
726
736
  function applyInitial(target, initial, scope, skipKeys = [], routers) {
727
737
  if (!initial) return;
728
- const resolved = normalizeProps(initial, scope, { skipKeys });
738
+ const resolved = normalizeProps(initial, scope, { skipKeys, where: "initial" });
729
739
  const { rest, routed } = splitRouted(resolved, routers);
730
740
  for (const r of routed) gsap.gsap.set(r.target, { [r.prop]: r.value });
731
741
  const { ownProps, filterProps } = partitionProps(rest);
@@ -2986,7 +2996,7 @@ var AudioSequence = class extends Sequence {
2986
2996
  const raw = spec;
2987
2997
  if (raw.asset !== void 0) console.warn(`pixi-effects: ${who}: has both asset and music \u2014 the music plays; remove one`);
2988
2998
  if (raw.sfx !== void 0) console.warn(`pixi-effects: ${who}: has both sfx and music \u2014 the music plays; remove one`);
2989
- const { resolveMusic, renderMusic, musicLength, musicKey } = await import('./music-KB7YZLHP.cjs');
2999
+ const { resolveMusic, renderMusic, musicLength, musicKey } = await import('./music-4O367SPR.cjs');
2990
3000
  const music = resolveMusic(spec.music, who);
2991
3001
  if (!music) return;
2992
3002
  const natural = musicLength(music);
@@ -3063,19 +3073,57 @@ var AudioSequence = class extends Sequence {
3063
3073
  };
3064
3074
 
3065
3075
  // src/core/FrameCache.ts
3076
+ var BUDGET_BYTES = 96 * 1024 * 1024;
3077
+ var MIN_HELD = 3;
3078
+ var JUMP_SECONDS = 1;
3079
+ var EPS = 1e-6;
3080
+ var LOW_BEHIND = 14;
3081
+ var MIN_HELD_TO_READ_BACK = 20;
3082
+ var MIN_HELD_TO_READ_START = 9;
3083
+ function reportDecodeError(err, time) {
3084
+ const isTransient = err instanceof DOMException && (err.name === "EncodingError" || err.name === "NotSupportedError");
3085
+ if (isTransient) console.debug("pixi-effects: transient video decode failure at", time, "s \u2014", err.name);
3086
+ else console.warn("pixi-effects: video frame decode failed at", time, "s \u2014", err);
3087
+ }
3066
3088
  var FrameCache = class {
3067
3089
  sink;
3068
3090
  capacity;
3069
3091
  cache = /* @__PURE__ */ new Map();
3070
3092
  _pending = /* @__PURE__ */ new Map();
3093
+ // Sequential mode (the sink has `samples()`): one decoder pass that runs forward as the movie asks for frame after frame.
3094
+ _frames = [];
3095
+ _cur = null;
3096
+ _iter = null;
3097
+ _next = null;
3098
+ _held = 0;
3099
+ _lock = Promise.resolve();
3100
+ _lastTime = null;
3101
+ _dir = 0;
3102
+ _gen = 0;
3103
+ _backBusy = false;
3104
+ _backGen = 0;
3105
+ _standbyBusy = false;
3106
+ /** A second pass at the start of the file, ready for the moment a looping video comes round. */
3107
+ _standby = null;
3108
+ _duration;
3109
+ _loop;
3110
+ /** How often it had to wait for a decoder (`restarts`, `singles`) and how many reads ahead it made: for tests and for `check`. */
3111
+ stats = { restarts: 0, singles: 0, prefetches: 0 };
3071
3112
  constructor(sink, options = {}) {
3072
3113
  this.sink = sink;
3073
3114
  this.capacity = options.capacity ?? 30;
3115
+ this._duration = options.duration ?? null;
3116
+ this._loop = !!options.loop;
3117
+ }
3118
+ /** How many decoded frames are held right now (the sequential reader's own list, plus the exact-time cache of the old path). */
3119
+ get heldFrames() {
3120
+ return this._frames.length + this.cache.size;
3074
3121
  }
3075
3122
  _key(time) {
3076
3123
  return Math.round(time * 1e6);
3077
3124
  }
3078
3125
  async getFrameAt(time) {
3126
+ if (this.sink.samples) return this._enqueue(() => this._sequentialAt(time));
3079
3127
  const key = this._key(time);
3080
3128
  if (this.cache.has(key)) {
3081
3129
  const v = this.cache.get(key);
@@ -3113,14 +3161,287 @@ var FrameCache = class {
3113
3161
  sample.close?.();
3114
3162
  return frame;
3115
3163
  } catch (err) {
3116
- const isTransient = err instanceof DOMException && (err.name === "EncodingError" || err.name === "NotSupportedError");
3117
- if (isTransient) {
3118
- console.debug("pixi-effects: transient video decode failure at", time, "s \u2014", err.name);
3119
- } else {
3120
- console.warn("pixi-effects: video frame decode failed at", time, "s \u2014", err);
3164
+ reportDecodeError(err, time);
3165
+ return null;
3166
+ }
3167
+ }
3168
+ // ── sequential mode ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────
3169
+ /** Requests are answered one at a time, in the order they arrive: one pass cannot serve two places at once. */
3170
+ _enqueue(fn) {
3171
+ const run = this._lock.then(fn, fn);
3172
+ this._lock = run.then(() => void 0, () => void 0);
3173
+ return run;
3174
+ }
3175
+ _covering(time) {
3176
+ for (const e of this._frames) {
3177
+ if (e.end === null ? e.req !== void 0 && Math.abs(e.req - time) < EPS : e.ts <= time + EPS && time < e.end - EPS) {
3178
+ this._frames.splice(this._frames.indexOf(e), 1);
3179
+ this._frames.push(e);
3180
+ return e;
3181
+ }
3182
+ }
3183
+ return null;
3184
+ }
3185
+ async _sequentialAt(time) {
3186
+ const last = this._lastTime;
3187
+ this._lastTime = time;
3188
+ if (last !== null && Math.abs(time - last) <= JUMP_SECONDS) this._dir = time > last + EPS ? 1 : time < last - EPS ? -1 : this._dir;
3189
+ let frame;
3190
+ try {
3191
+ frame = await this._serve(time, last);
3192
+ } catch (err) {
3193
+ reportDecodeError(err, time);
3194
+ await this._closeIter();
3195
+ return null;
3196
+ }
3197
+ this._maybeReadAhead(time);
3198
+ return frame;
3199
+ }
3200
+ async _serve(time, last) {
3201
+ const hit = this._covering(time);
3202
+ if (hit) return hit.frame;
3203
+ const sb = this._standby;
3204
+ 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);
3205
+ if (last === null || Math.abs(time - last) > JUMP_SECONDS) return await this._single(time);
3206
+ const cur = this._cur;
3207
+ if (this._iter && cur && time >= cur.ts && time - cur.ts <= JUMP_SECONDS) return await this._advance(time);
3208
+ const backwards = !!cur && time < cur.ts;
3209
+ return await this._restart(time, backwards ? Math.max(0, time - Math.max(1, this._held - 1) / 30) : time);
3210
+ }
3211
+ // ── reading ahead ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
3212
+ _maybeReadAhead(time) {
3213
+ if (this._held === 0) return;
3214
+ if (this._dir === -1) {
3215
+ this._dropStandby();
3216
+ let behind = 0, passed = 0, earliest = Infinity;
3217
+ for (const e of this._frames) {
3218
+ if (e.ts < time - EPS) behind++;
3219
+ else if (e.ts > time + EPS) passed++;
3220
+ if (e.ts < earliest) earliest = e.ts;
3221
+ }
3222
+ if (this._held >= MIN_HELD_TO_READ_BACK && behind < LOW_BEHIND && passed >= 3 && earliest > EPS && !this._backBusy) void this._readBack(earliest, passed);
3223
+ } 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) {
3224
+ void this._readStart();
3225
+ }
3226
+ }
3227
+ /** In the background: the window of frames before the earliest one held (reverse play comes to it next). */
3228
+ async _readBack(earliest, room) {
3229
+ this._backBusy = true;
3230
+ this.stats.prefetches++;
3231
+ const gen = this._gen, bg = this._backGen;
3232
+ const want = Math.max(3, Math.min(Math.floor(this._held * 0.6), room));
3233
+ let iter = null;
3234
+ const got = [];
3235
+ try {
3236
+ iter = this.sink.samples(Math.max(0, earliest - want / 30));
3237
+ for (; ; ) {
3238
+ const r = await iter.next();
3239
+ if (r.done) break;
3240
+ if (gen !== this._gen || r.value.timestamp >= earliest - EPS) {
3241
+ r.value.close?.();
3242
+ break;
3243
+ }
3244
+ got.push(r.value);
3245
+ if (bg !== this._backGen) break;
3246
+ }
3247
+ if (gen === this._gen && bg === this._backGen) {
3248
+ for (let k = 0; k < got.length; k++) {
3249
+ const s = got[k];
3250
+ const frame = s.toVideoFrame();
3251
+ this._keep({ ts: s.timestamp, end: got[k + 1]?.timestamp ?? earliest, frame }, frame);
3252
+ }
3253
+ }
3254
+ } catch (err) {
3255
+ reportDecodeError(err, earliest);
3256
+ } finally {
3257
+ for (const s of got) s.close?.();
3258
+ this._backBusy = false;
3259
+ try {
3260
+ await iter?.return?.(void 0);
3261
+ } catch {
3262
+ }
3263
+ }
3264
+ }
3265
+ /** In the background: the first frames of the file and a pass that stands behind them; held (pinned) until the loop comes round. */
3266
+ async _readStart() {
3267
+ this._standbyBusy = true;
3268
+ this.stats.prefetches++;
3269
+ const gen = this._gen;
3270
+ const want = Math.max(3, Math.min(10, Math.floor(this._held / 3)));
3271
+ let iter = null;
3272
+ const got = [];
3273
+ let keep = false;
3274
+ try {
3275
+ iter = this.sink.samples(0);
3276
+ for (; got.length < want + 1; ) {
3277
+ const r = await iter.next();
3278
+ if (r.done) break;
3279
+ if (gen !== this._gen) {
3280
+ r.value.close?.();
3281
+ break;
3282
+ }
3283
+ got.push(r.value);
3284
+ }
3285
+ if (gen === this._gen && got.length >= 2) {
3286
+ const next = got.pop();
3287
+ let cur = null;
3288
+ got.forEach((s, k) => {
3289
+ const frame = s.toVideoFrame();
3290
+ s.close?.();
3291
+ const entry = { ts: s.timestamp, end: (got[k + 1] ?? next).timestamp, frame, pinned: true };
3292
+ this._keep(entry, frame);
3293
+ cur = entry;
3294
+ });
3295
+ got.length = 0;
3296
+ if (cur) {
3297
+ this._standby = { iter, cur, next };
3298
+ keep = true;
3299
+ }
3300
+ }
3301
+ } catch (err) {
3302
+ reportDecodeError(err, 0);
3303
+ } finally {
3304
+ for (const s of got) s.close?.();
3305
+ this._standbyBusy = false;
3306
+ if (!keep) {
3307
+ try {
3308
+ await iter?.return?.(void 0);
3309
+ } catch {
3310
+ }
3121
3311
  }
3312
+ }
3313
+ }
3314
+ async _adoptStandby(time) {
3315
+ const sb = this._standby;
3316
+ this._standby = null;
3317
+ await this._closeIter();
3318
+ for (const e of this._frames) e.pinned = false;
3319
+ this._iter = sb.iter;
3320
+ this._next = sb.next;
3321
+ this._cur = sb.cur;
3322
+ return this._advance(time);
3323
+ }
3324
+ _dropStandby() {
3325
+ const sb = this._standby;
3326
+ if (!sb) return;
3327
+ this._standby = null;
3328
+ sb.next?.close?.();
3329
+ for (const e of this._frames.filter((x) => x.pinned)) {
3330
+ e.pinned = false;
3331
+ }
3332
+ void sb.iter.return?.(void 0);
3333
+ }
3334
+ /** 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. */
3335
+ async _single(time) {
3336
+ this.stats.singles++;
3337
+ await this._closeIter();
3338
+ this._cur = null;
3339
+ const sample = await this.sink.getSample(time);
3340
+ if (!sample) return null;
3341
+ const frame = sample.toVideoFrame();
3342
+ sample.close?.();
3343
+ this._keep({ ts: sample.timestamp, end: null, frame, req: time }, frame);
3344
+ return frame;
3345
+ }
3346
+ /** Start a pass at `time` (a seek and the frames of one group of pictures), keeping what is still held for a step back. */
3347
+ async _restart(time, from = time) {
3348
+ this.stats.restarts++;
3349
+ this._backGen++;
3350
+ await this._closeIter();
3351
+ this._cur = null;
3352
+ const iter = this.sink.samples(from);
3353
+ this._iter = iter;
3354
+ const first = await iter.next();
3355
+ if (first.done) {
3356
+ await this._closeIter();
3122
3357
  return null;
3123
3358
  }
3359
+ if (first.value.timestamp > time + EPS) {
3360
+ first.value.close?.();
3361
+ await this._closeIter();
3362
+ return null;
3363
+ }
3364
+ this._take(first.value);
3365
+ return this._advance(time);
3366
+ }
3367
+ /** 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). */
3368
+ async _advance(time) {
3369
+ const cur = () => this._cur;
3370
+ for (; ; ) {
3371
+ if (!this._next) {
3372
+ let r;
3373
+ try {
3374
+ r = await this._iter.next();
3375
+ } catch (err) {
3376
+ reportDecodeError(err, time);
3377
+ await this._closeIter();
3378
+ const prev = this._frames.length > 1 ? this._frames[this._frames.length - 2] : null;
3379
+ const spacing = prev ? cur().ts - prev.ts : 1 / 24;
3380
+ return time < cur().ts + spacing - EPS ? cur().frame : null;
3381
+ }
3382
+ if (r.done) {
3383
+ cur().end = Infinity;
3384
+ this._iter = null;
3385
+ break;
3386
+ }
3387
+ this._next = r.value;
3388
+ }
3389
+ cur().end = this._next.timestamp;
3390
+ if (this._next.timestamp <= time + EPS) {
3391
+ const s = this._next;
3392
+ this._next = null;
3393
+ this._take(s);
3394
+ } else break;
3395
+ }
3396
+ return cur().frame;
3397
+ }
3398
+ _take(sample) {
3399
+ const frame = sample.toVideoFrame();
3400
+ sample.close?.();
3401
+ const entry = { ts: sample.timestamp, end: null, frame };
3402
+ this._keep(entry, frame);
3403
+ this._cur = entry;
3404
+ }
3405
+ /** 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. */
3406
+ _keep(entry, frame) {
3407
+ if (this._held === 0) {
3408
+ const bytes = frame.allocationSize?.() ?? frame.displayWidth * frame.displayHeight * 1.5;
3409
+ this._held = Math.max(MIN_HELD, Math.min(this.capacity, Math.floor(BUDGET_BYTES / Math.max(1, bytes))));
3410
+ }
3411
+ this._frames.push(entry);
3412
+ while (this._frames.length > this._held) {
3413
+ const victim = this._victim(entry);
3414
+ if (!victim) break;
3415
+ this._frames.splice(this._frames.indexOf(victim), 1);
3416
+ victim.frame.close?.();
3417
+ }
3418
+ }
3419
+ /**
3420
+ * The frame to let go: one the playhead has already passed (going forward those before it, going back those after it), the furthest
3421
+ * behind first; when there is none, the one furthest ahead of the playhead (the last to be needed). Least recently used when the
3422
+ * direction is not known. Frames of the standby (pinned) and the frame the pass stands on are never chosen.
3423
+ */
3424
+ _victim(except) {
3425
+ const cand = this._frames.filter((e) => e !== this._cur && e !== except && !e.pinned);
3426
+ if (cand.length === 0) return null;
3427
+ if (this._dir === 0) return cand[0];
3428
+ const at = this._lastTime ?? 0;
3429
+ const passed = cand.filter((e) => this._dir === 1 ? e.ts < at - EPS : e.ts > at + EPS);
3430
+ const farthest = (list, sign) => list.reduce((a, b) => sign * (b.ts - a.ts) > 0 ? b : a);
3431
+ if (passed.length) return farthest(passed, this._dir === 1 ? -1 : 1);
3432
+ return farthest(cand, this._dir === 1 ? 1 : -1);
3433
+ }
3434
+ async _closeIter() {
3435
+ const iter = this._iter;
3436
+ this._iter = null;
3437
+ this._next?.close?.();
3438
+ this._next = null;
3439
+ if (iter) {
3440
+ try {
3441
+ await iter.return?.(void 0);
3442
+ } catch {
3443
+ }
3444
+ }
3124
3445
  }
3125
3446
  _evictIfNeeded() {
3126
3447
  while (this.cache.size > this.capacity) {
@@ -3132,9 +3453,19 @@ var FrameCache = class {
3132
3453
  }
3133
3454
  }
3134
3455
  dispose() {
3456
+ this._gen++;
3457
+ this._dropStandby();
3135
3458
  for (const f of this.cache.values()) f?.close?.();
3136
3459
  this.cache.clear();
3137
3460
  this._pending.clear();
3461
+ for (const e of this._frames) e.frame.close?.();
3462
+ this._frames = [];
3463
+ this._cur = null;
3464
+ this._next?.close?.();
3465
+ this._next = null;
3466
+ const iter = this._iter;
3467
+ this._iter = null;
3468
+ void iter?.return?.(void 0);
3138
3469
  }
3139
3470
  };
3140
3471
 
@@ -3165,7 +3496,7 @@ var VideoSequence = class extends Sequence {
3165
3496
  this._sourceDuration = data.duration;
3166
3497
  this._remap = remapOf(this.spec, "currentTime", this._sourceDuration);
3167
3498
  this._audioBuffer = this.spec.audio !== false ? data.audioBuffer : null;
3168
- this._cache = new FrameCache(data.sink, { capacity: 30 });
3499
+ this._cache = new FrameCache(data.sink, { capacity: 30, duration: data.duration, loop: !!this.spec.loop });
3169
3500
  const probeFrame = await this._cache.getFrameAt(0);
3170
3501
  const w = probeFrame?.displayWidth ?? 1920;
3171
3502
  const h = probeFrame?.displayHeight ?? 1080;
@@ -4365,7 +4696,7 @@ function resolveDim(v, scope) {
4365
4696
  var _CompositionSequence = null;
4366
4697
  async function getCompositionSequence() {
4367
4698
  if (!_CompositionSequence) {
4368
- _CompositionSequence = (await import('./Composition-THQPG4E5.cjs')).CompositionSequence;
4699
+ _CompositionSequence = (await import('./Composition-EE3KEE2L.cjs')).CompositionSequence;
4369
4700
  }
4370
4701
  return _CompositionSequence;
4371
4702
  }
@@ -4450,5 +4781,5 @@ exports.registerSequenceType = registerSequenceType;
4450
4781
  exports.remapOf = remapOf;
4451
4782
  exports.resolveAt = resolveAt;
4452
4783
  exports.timeRemapOf = timeRemapOf;
4453
- //# sourceMappingURL=chunk-EABL3UKJ.cjs.map
4454
- //# sourceMappingURL=chunk-EABL3UKJ.cjs.map
4784
+ //# sourceMappingURL=chunk-A225HYSY.cjs.map
4785
+ //# sourceMappingURL=chunk-A225HYSY.cjs.map