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
@@ -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);
@@ -3046,6 +3056,9 @@ var BUDGET_BYTES = 96 * 1024 * 1024;
3046
3056
  var MIN_HELD = 3;
3047
3057
  var JUMP_SECONDS = 1;
3048
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;
3049
3062
  function reportDecodeError(err, time) {
3050
3063
  const isTransient = err instanceof DOMException && (err.name === "EncodingError" || err.name === "NotSupportedError");
3051
3064
  if (isTransient) console.debug("pixi-effects: transient video decode failure at", time, "s \u2014", err.name);
@@ -3064,9 +3077,22 @@ var FrameCache = class {
3064
3077
  _held = 0;
3065
3078
  _lock = Promise.resolve();
3066
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 };
3067
3091
  constructor(sink, options = {}) {
3068
3092
  this.sink = sink;
3069
3093
  this.capacity = options.capacity ?? 30;
3094
+ this._duration = options.duration ?? null;
3095
+ this._loop = !!options.loop;
3070
3096
  }
3071
3097
  /** How many decoded frames are held right now (the sequential reader's own list, plus the exact-time cache of the old path). */
3072
3098
  get heldFrames() {
@@ -3136,24 +3162,157 @@ var FrameCache = class {
3136
3162
  return null;
3137
3163
  }
3138
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;
3139
3169
  try {
3140
- const last = this._lastTime;
3141
- this._lastTime = time;
3142
- const hit = this._covering(time);
3143
- if (hit) return hit.frame;
3144
- if (last === null || Math.abs(time - last) > JUMP_SECONDS) return await this._single(time);
3145
- const cur = this._cur;
3146
- if (this._iter && cur && time >= cur.ts && time - cur.ts <= JUMP_SECONDS) return await this._advance(time);
3147
- const backwards = !!cur && time < cur.ts;
3148
- return await this._restart(time, backwards ? Math.max(0, time - this._held / 30) : time);
3170
+ frame = await this._serve(time, last);
3149
3171
  } catch (err) {
3150
3172
  reportDecodeError(err, time);
3151
3173
  await this._closeIter();
3152
3174
  return null;
3153
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
+ }
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);
3154
3312
  }
3155
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. */
3156
3314
  async _single(time) {
3315
+ this.stats.singles++;
3157
3316
  await this._closeIter();
3158
3317
  this._cur = null;
3159
3318
  const sample = await this.sink.getSample(time);
@@ -3165,6 +3324,8 @@ var FrameCache = class {
3165
3324
  }
3166
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. */
3167
3326
  async _restart(time, from = time) {
3327
+ this.stats.restarts++;
3328
+ this._backGen++;
3168
3329
  await this._closeIter();
3169
3330
  this._cur = null;
3170
3331
  const iter = this.sink.samples(from);
@@ -3228,12 +3389,27 @@ var FrameCache = class {
3228
3389
  }
3229
3390
  this._frames.push(entry);
3230
3391
  while (this._frames.length > this._held) {
3231
- const oldest = this._frames.find((e) => e !== this._cur && e !== entry);
3232
- if (!oldest) break;
3233
- this._frames.splice(this._frames.indexOf(oldest), 1);
3234
- oldest.frame.close?.();
3392
+ const victim = this._victim(entry);
3393
+ if (!victim) break;
3394
+ this._frames.splice(this._frames.indexOf(victim), 1);
3395
+ victim.frame.close?.();
3235
3396
  }
3236
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
+ }
3237
3413
  async _closeIter() {
3238
3414
  const iter = this._iter;
3239
3415
  this._iter = null;
@@ -3256,6 +3432,8 @@ var FrameCache = class {
3256
3432
  }
3257
3433
  }
3258
3434
  dispose() {
3435
+ this._gen++;
3436
+ this._dropStandby();
3259
3437
  for (const f of this.cache.values()) f?.close?.();
3260
3438
  this.cache.clear();
3261
3439
  this._pending.clear();
@@ -3297,7 +3475,7 @@ var VideoSequence = class extends Sequence {
3297
3475
  this._sourceDuration = data.duration;
3298
3476
  this._remap = remapOf(this.spec, "currentTime", this._sourceDuration);
3299
3477
  this._audioBuffer = this.spec.audio !== false ? data.audioBuffer : null;
3300
- this._cache = new FrameCache(data.sink, { capacity: 30 });
3478
+ this._cache = new FrameCache(data.sink, { capacity: 30, duration: data.duration, loop: !!this.spec.loop });
3301
3479
  const probeFrame = await this._cache.getFrameAt(0);
3302
3480
  const w = probeFrame?.displayWidth ?? 1920;
3303
3481
  const h = probeFrame?.displayHeight ?? 1080;
@@ -4497,7 +4675,7 @@ function resolveDim(v, scope) {
4497
4675
  var _CompositionSequence = null;
4498
4676
  async function getCompositionSequence() {
4499
4677
  if (!_CompositionSequence) {
4500
- _CompositionSequence = (await import('./Composition-H4QA24MV.js')).CompositionSequence;
4678
+ _CompositionSequence = (await import('./Composition-3O7L2VL5.js')).CompositionSequence;
4501
4679
  }
4502
4680
  return _CompositionSequence;
4503
4681
  }
@@ -4540,5 +4718,5 @@ async function buildSequenceTree(specs, parent, root) {
4540
4718
  }
4541
4719
 
4542
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 };
4543
- //# sourceMappingURL=chunk-X3HB4DSR.js.map
4544
- //# sourceMappingURL=chunk-X3HB4DSR.js.map
4721
+ //# sourceMappingURL=chunk-FXAHNPBP.js.map
4722
+ //# sourceMappingURL=chunk-FXAHNPBP.js.map