pixi-effects 0.26.2 → 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 (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -5
  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-H4QA24MV.js.map → Composition-3O7L2VL5.js.map} +1 -1
  7. package/dist/Composition-EE3KEE2L.cjs +16 -0
  8. package/dist/{Composition-HM4YT5L7.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-WPXDRSGN.cjs → chunk-A225HYSY.cjs} +205 -27
  16. package/dist/chunk-A225HYSY.cjs.map +1 -0
  17. package/dist/{chunk-X3HB4DSR.js → chunk-FXAHNPBP.js} +205 -27
  18. package/dist/chunk-FXAHNPBP.js.map +1 -0
  19. package/dist/{chunk-555URQHQ.cjs → chunk-IIDBEYQH.cjs} +59 -59
  20. package/dist/{chunk-555URQHQ.cjs.map → chunk-IIDBEYQH.cjs.map} +1 -1
  21. package/dist/{chunk-GBUM7X2S.js → chunk-WMMZDPWK.js} +3 -3
  22. package/dist/{chunk-GBUM7X2S.js.map → chunk-WMMZDPWK.js.map} +1 -1
  23. package/dist/index.cjs +142 -48
  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 +119 -25
  28. package/dist/index.js.map +1 -1
  29. package/dist/loader.css +1 -1
  30. package/dist/three.cjs +5 -5
  31. package/dist/three.js +1 -1
  32. package/docs/api.md +11 -5
  33. package/docs/dsl.md +2 -0
  34. package/llms-full.txt +17 -9
  35. package/package.json +1 -1
  36. package/skills/pixi-effects/SKILL.md +1 -1
  37. package/skills/pixi-effects/reference/cheatsheet.md +3 -3
  38. package/skills/pixi-effects/template.html +6 -6
  39. package/dist/Composition-H4QA24MV.js +0 -7
  40. package/dist/Composition-HM4YT5L7.cjs +0 -16
  41. package/dist/chunk-WPXDRSGN.cjs.map +0 -1
  42. package/dist/chunk-X3HB4DSR.js.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);
@@ -3067,6 +3077,9 @@ var BUDGET_BYTES = 96 * 1024 * 1024;
3067
3077
  var MIN_HELD = 3;
3068
3078
  var JUMP_SECONDS = 1;
3069
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;
3070
3083
  function reportDecodeError(err, time) {
3071
3084
  const isTransient = err instanceof DOMException && (err.name === "EncodingError" || err.name === "NotSupportedError");
3072
3085
  if (isTransient) console.debug("pixi-effects: transient video decode failure at", time, "s \u2014", err.name);
@@ -3085,9 +3098,22 @@ var FrameCache = class {
3085
3098
  _held = 0;
3086
3099
  _lock = Promise.resolve();
3087
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 };
3088
3112
  constructor(sink, options = {}) {
3089
3113
  this.sink = sink;
3090
3114
  this.capacity = options.capacity ?? 30;
3115
+ this._duration = options.duration ?? null;
3116
+ this._loop = !!options.loop;
3091
3117
  }
3092
3118
  /** How many decoded frames are held right now (the sequential reader's own list, plus the exact-time cache of the old path). */
3093
3119
  get heldFrames() {
@@ -3157,24 +3183,157 @@ var FrameCache = class {
3157
3183
  return null;
3158
3184
  }
3159
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;
3160
3190
  try {
3161
- const last = this._lastTime;
3162
- this._lastTime = time;
3163
- const hit = this._covering(time);
3164
- if (hit) return hit.frame;
3165
- if (last === null || Math.abs(time - last) > JUMP_SECONDS) return await this._single(time);
3166
- const cur = this._cur;
3167
- if (this._iter && cur && time >= cur.ts && time - cur.ts <= JUMP_SECONDS) return await this._advance(time);
3168
- const backwards = !!cur && time < cur.ts;
3169
- return await this._restart(time, backwards ? Math.max(0, time - this._held / 30) : time);
3191
+ frame = await this._serve(time, last);
3170
3192
  } catch (err) {
3171
3193
  reportDecodeError(err, time);
3172
3194
  await this._closeIter();
3173
3195
  return null;
3174
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
+ }
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);
3175
3333
  }
3176
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. */
3177
3335
  async _single(time) {
3336
+ this.stats.singles++;
3178
3337
  await this._closeIter();
3179
3338
  this._cur = null;
3180
3339
  const sample = await this.sink.getSample(time);
@@ -3186,6 +3345,8 @@ var FrameCache = class {
3186
3345
  }
3187
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. */
3188
3347
  async _restart(time, from = time) {
3348
+ this.stats.restarts++;
3349
+ this._backGen++;
3189
3350
  await this._closeIter();
3190
3351
  this._cur = null;
3191
3352
  const iter = this.sink.samples(from);
@@ -3249,12 +3410,27 @@ var FrameCache = class {
3249
3410
  }
3250
3411
  this._frames.push(entry);
3251
3412
  while (this._frames.length > this._held) {
3252
- const oldest = this._frames.find((e) => e !== this._cur && e !== entry);
3253
- if (!oldest) break;
3254
- this._frames.splice(this._frames.indexOf(oldest), 1);
3255
- oldest.frame.close?.();
3413
+ const victim = this._victim(entry);
3414
+ if (!victim) break;
3415
+ this._frames.splice(this._frames.indexOf(victim), 1);
3416
+ victim.frame.close?.();
3256
3417
  }
3257
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
+ }
3258
3434
  async _closeIter() {
3259
3435
  const iter = this._iter;
3260
3436
  this._iter = null;
@@ -3277,6 +3453,8 @@ var FrameCache = class {
3277
3453
  }
3278
3454
  }
3279
3455
  dispose() {
3456
+ this._gen++;
3457
+ this._dropStandby();
3280
3458
  for (const f of this.cache.values()) f?.close?.();
3281
3459
  this.cache.clear();
3282
3460
  this._pending.clear();
@@ -3318,7 +3496,7 @@ var VideoSequence = class extends Sequence {
3318
3496
  this._sourceDuration = data.duration;
3319
3497
  this._remap = remapOf(this.spec, "currentTime", this._sourceDuration);
3320
3498
  this._audioBuffer = this.spec.audio !== false ? data.audioBuffer : null;
3321
- this._cache = new FrameCache(data.sink, { capacity: 30 });
3499
+ this._cache = new FrameCache(data.sink, { capacity: 30, duration: data.duration, loop: !!this.spec.loop });
3322
3500
  const probeFrame = await this._cache.getFrameAt(0);
3323
3501
  const w = probeFrame?.displayWidth ?? 1920;
3324
3502
  const h = probeFrame?.displayHeight ?? 1080;
@@ -4518,7 +4696,7 @@ function resolveDim(v, scope) {
4518
4696
  var _CompositionSequence = null;
4519
4697
  async function getCompositionSequence() {
4520
4698
  if (!_CompositionSequence) {
4521
- _CompositionSequence = (await import('./Composition-HM4YT5L7.cjs')).CompositionSequence;
4699
+ _CompositionSequence = (await import('./Composition-EE3KEE2L.cjs')).CompositionSequence;
4522
4700
  }
4523
4701
  return _CompositionSequence;
4524
4702
  }
@@ -4603,5 +4781,5 @@ exports.registerSequenceType = registerSequenceType;
4603
4781
  exports.remapOf = remapOf;
4604
4782
  exports.resolveAt = resolveAt;
4605
4783
  exports.timeRemapOf = timeRemapOf;
4606
- //# sourceMappingURL=chunk-WPXDRSGN.cjs.map
4607
- //# sourceMappingURL=chunk-WPXDRSGN.cjs.map
4784
+ //# sourceMappingURL=chunk-A225HYSY.cjs.map
4785
+ //# sourceMappingURL=chunk-A225HYSY.cjs.map