pixi-effects 0.2.0 → 0.4.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 (51) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +16 -6
  3. package/ai/SKILL.md +46 -0
  4. package/ai/reference/cheatsheet.md +121 -0
  5. package/ai/reference/pitfalls.md +75 -0
  6. package/ai/reference/recipes.md +375 -0
  7. package/ai/template.html +82 -0
  8. package/ai/tools/save-image.py +24 -0
  9. package/dist/{Base-Bh_VSusi.d.ts → Base-5R7C5pHR.d.cts} +7 -1
  10. package/dist/{Base-B1Tdn0Cv.d.cts → Base-DhY9oMxr.d.ts} +7 -1
  11. package/dist/Composition-D7RCAGEA.cjs +13 -0
  12. package/dist/{Composition-BI5HZJFL.cjs.map → Composition-D7RCAGEA.cjs.map} +1 -1
  13. package/dist/Composition-RGTRZ2JX.js +4 -0
  14. package/dist/{Composition-IO7ZN32J.js.map → Composition-RGTRZ2JX.js.map} +1 -1
  15. package/dist/Controller.d.cts +2 -2
  16. package/dist/Controller.d.ts +2 -2
  17. package/dist/Movie-CghFXprP.d.cts +179 -0
  18. package/dist/Movie-DRZtIGU7.d.ts +179 -0
  19. package/dist/{chunk-7OIWYXGV.cjs → chunk-6BQ6IEW2.cjs} +363 -91
  20. package/dist/chunk-6BQ6IEW2.cjs.map +1 -0
  21. package/dist/{chunk-H55V3U56.js → chunk-A574IA4F.js} +68 -13
  22. package/dist/chunk-A574IA4F.js.map +1 -0
  23. package/dist/{chunk-64IHCYYN.cjs → chunk-QGBOC5YL.cjs} +76 -21
  24. package/dist/chunk-QGBOC5YL.cjs.map +1 -0
  25. package/dist/{chunk-VJCDG6YG.js → chunk-ZQX7WIYL.js} +359 -93
  26. package/dist/chunk-ZQX7WIYL.js.map +1 -0
  27. package/dist/index.cjs +366 -24
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +61 -31
  30. package/dist/index.d.ts +61 -31
  31. package/dist/index.js +363 -22
  32. package/dist/index.js.map +1 -1
  33. package/dist/three.cjs +5 -5
  34. package/dist/three.d.cts +2 -2
  35. package/dist/three.d.ts +2 -2
  36. package/dist/three.js +1 -1
  37. package/dist/{types-CNBilhpz.d.cts → types-CRE9WKd4.d.cts} +59 -6
  38. package/dist/{types-CNBilhpz.d.ts → types-CRE9WKd4.d.ts} +59 -6
  39. package/docs/api.md +333 -0
  40. package/docs/dsl.md +1028 -0
  41. package/llms-full.txt +2001 -0
  42. package/llms.txt +30 -0
  43. package/package.json +5 -3
  44. package/dist/Composition-BI5HZJFL.cjs +0 -13
  45. package/dist/Composition-IO7ZN32J.js +0 -4
  46. package/dist/Movie-CcR6h2jO.d.cts +0 -82
  47. package/dist/Movie-D-n8glA6.d.ts +0 -82
  48. package/dist/chunk-64IHCYYN.cjs.map +0 -1
  49. package/dist/chunk-7OIWYXGV.cjs.map +0 -1
  50. package/dist/chunk-H55V3U56.js.map +0 -1
  51. package/dist/chunk-VJCDG6YG.js.map +0 -1
@@ -246,6 +246,23 @@ function looksLikeColorString(s) {
246
246
  }
247
247
 
248
248
  // src/core/Timeline.ts
249
+ function loopVars(kf) {
250
+ const out = {};
251
+ let repeat = 0;
252
+ if (kf.repeat !== void 0) {
253
+ if (Number.isFinite(kf.repeat) && kf.repeat >= 0) {
254
+ repeat = Math.floor(kf.repeat);
255
+ out.repeat = repeat;
256
+ } else {
257
+ console.warn(`pixi-effects: keyframe repeat ${String(kf.repeat)} ignored \u2014 repeat must be a finite count >= 0 (the timeline needs a fixed length; for endless motion repeat many times).`);
258
+ }
259
+ }
260
+ if (repeat > 0) {
261
+ if (kf.yoyo !== void 0) out.yoyo = kf.yoyo;
262
+ if (kf.repeatDelay !== void 0 && kf.repeatDelay > 0) out.repeatDelay = kf.repeatDelay;
263
+ }
264
+ return out;
265
+ }
249
266
  function resolveAt(at, duration) {
250
267
  if (at === void 0 || at === null) return 0;
251
268
  return at < 0 ? duration + at : at;
@@ -263,7 +280,7 @@ function normalizeKeyframe(kf, parentDuration) {
263
280
  else if (hasFrom) kind = "from";
264
281
  else if (hasTo) kind = "to";
265
282
  else kind = "to";
266
- return { at, duration, ease, kind, set: kf.set, from: kf.from, to: kf.to };
283
+ return { at, duration, ease, loop: loopVars(kf), kind, set: kf.set, from: kf.from, to: kf.to };
267
284
  }
268
285
  function partitionProps(props) {
269
286
  const ownProps = {};
@@ -373,27 +390,27 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
373
390
  const resolved = normalizeProps(kf.to, scope, { skipKeys });
374
391
  const { rest, routed } = splitRouted(resolved, routers);
375
392
  for (const r of routed)
376
- timeline.to(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease }, at);
393
+ timeline.to(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
377
394
  const { ownProps, filterProps } = partitionProps(rest);
378
395
  if (Object.keys(ownProps).length > 0)
379
- timeline.to(target, { ...pixiwrap(ownProps), duration: kf.duration, ease: kf.ease }, at);
396
+ timeline.to(target, { ...pixiwrap(ownProps), duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
380
397
  for (const [name, props] of Object.entries(filterProps)) {
381
398
  const f = findFilter(target, name);
382
399
  if (!f) continue;
383
- timeline.to(f, { ...props, duration: kf.duration, ease: kf.ease }, at);
400
+ timeline.to(f, { ...props, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
384
401
  }
385
402
  } else if (kf.kind === "from") {
386
403
  const resolved = normalizeProps(kf.from, scope, { skipKeys });
387
404
  const { rest, routed } = splitRouted(resolved, routers);
388
405
  for (const r of routed)
389
- timeline.from(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease }, at);
406
+ timeline.from(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
390
407
  const { ownProps, filterProps } = partitionProps(rest);
391
408
  if (Object.keys(ownProps).length > 0)
392
- timeline.from(target, { ...pixiwrap(ownProps), duration: kf.duration, ease: kf.ease }, at);
409
+ timeline.from(target, { ...pixiwrap(ownProps), duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
393
410
  for (const [name, props] of Object.entries(filterProps)) {
394
411
  const f = findFilter(target, name);
395
412
  if (!f) continue;
396
- timeline.from(f, { ...props, duration: kf.duration, ease: kf.ease }, at);
413
+ timeline.from(f, { ...props, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
397
414
  }
398
415
  } else {
399
416
  const fromResolved = normalizeProps(kf.from, scope, { skipKeys });
@@ -408,15 +425,15 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
408
425
  timeline.fromTo(
409
426
  r.target,
410
427
  { [r.prop]: f.value },
411
- { [r.prop]: r.value, duration: kf.duration, ease: kf.ease },
428
+ { [r.prop]: r.value, duration: kf.duration, ease: kf.ease, ...kf.loop },
412
429
  at
413
430
  );
414
431
  } else {
415
- timeline.to(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease }, at);
432
+ timeline.to(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
416
433
  }
417
434
  }
418
435
  for (const f of fromByKey.values())
419
- timeline.from(f.target, { [f.prop]: f.value, duration: kf.duration, ease: kf.ease }, at);
436
+ timeline.from(f.target, { [f.prop]: f.value, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
420
437
  const fromSplit = partitionProps(fromRouted.rest);
421
438
  const toSplit = partitionProps(toRouted.rest);
422
439
  const ownKeys = /* @__PURE__ */ new Set([...Object.keys(fromSplit.ownProps), ...Object.keys(toSplit.ownProps)]);
@@ -424,7 +441,7 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
424
441
  timeline.fromTo(
425
442
  target,
426
443
  { ...pixiwrap(fromSplit.ownProps) },
427
- { ...pixiwrap(toSplit.ownProps), duration: kf.duration, ease: kf.ease },
444
+ { ...pixiwrap(toSplit.ownProps), duration: kf.duration, ease: kf.ease, ...kf.loop },
428
445
  at
429
446
  );
430
447
  }
@@ -438,7 +455,7 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
438
455
  timeline.fromTo(
439
456
  f,
440
457
  { ...fromSplit.filterProps[name] ?? {} },
441
- { ...toSplit.filterProps[name] ?? {}, duration: kf.duration, ease: kf.ease },
458
+ { ...toSplit.filterProps[name] ?? {}, duration: kf.duration, ease: kf.ease, ...kf.loop },
442
459
  at
443
460
  );
444
461
  }
@@ -763,9 +780,9 @@ var Sequence = class {
763
780
  if (!this.target) return;
764
781
  const scope = this.scope();
765
782
  const routers = this.pathRouters();
766
- applyInitial(this.target, this.spec.initial, scope, [], routers);
767
- applyKeyframes(timeline, this.target, this.spec.keyframes, this.duration, scope, [], offset, routers);
768
783
  const startTime = offset + this.at;
784
+ applyInitial(this.target, this.spec.initial, scope, [], routers);
785
+ applyKeyframes(timeline, this.target, this.spec.keyframes, this.duration, scope, [], startTime, routers);
769
786
  const endTime = startTime + this.duration;
770
787
  this.absoluteStart = startTime;
771
788
  this.target.renderable = startTime <= 0;
@@ -781,6 +798,14 @@ var Sequence = class {
781
798
  */
782
799
  updateSpace(_t, _host) {
783
800
  }
801
+ /**
802
+ * Runs once per rendered frame, after the timeline seek and before the cull. Layers whose drawn
803
+ * geometry follows tweened state (shapes) redraw here, so the culler measures where the layer is NOW
804
+ * and not where it was drawn at the previous render.
805
+ */
806
+ syncFrame() {
807
+ this.maskSequence?.syncFrame();
808
+ }
784
809
  destroy() {
785
810
  this.maskSequence?.destroy();
786
811
  this.maskSequence = null;
@@ -789,6 +814,51 @@ var Sequence = class {
789
814
  }
790
815
  };
791
816
 
817
+ // src/space/specKeys.ts
818
+ function collectPropKeys(spec) {
819
+ const out = /* @__PURE__ */ new Set();
820
+ for (const k of Object.keys(spec.initial ?? {})) out.add(k);
821
+ for (const kf of spec.keyframes ?? []) {
822
+ for (const bag of [kf.set, kf.to, kf.from]) {
823
+ for (const k of Object.keys(bag ?? {})) out.add(k);
824
+ }
825
+ }
826
+ return out;
827
+ }
828
+
829
+ // src/core/lint.ts
830
+ var defaultWarn = (m) => console.warn(m);
831
+ function describeLayer(spec) {
832
+ return spec.name ? `layer "${spec.name}"` : `unnamed ${spec.type} layer`;
833
+ }
834
+ function lintTiming(spec, parentDuration, warn = defaultWarn) {
835
+ const who = describeLayer(spec);
836
+ const at = spec.at ?? 0;
837
+ if (at >= parentDuration) {
838
+ warn(`pixi-effects: ${who} starts at ${at}s, after its composition ends (${parentDuration}s), so it is never visible`);
839
+ }
840
+ const duration = spec.duration ?? parentDuration;
841
+ (spec.keyframes ?? []).forEach((kf, i) => {
842
+ const kat = kf.at ?? 0;
843
+ if (kat >= duration) {
844
+ warn(
845
+ `pixi-effects: ${who}: keyframes[${i}] starts at ${kat}s, after the layer ends (duration ${duration}s), so it never plays. Keyframe \`at\` is measured from the start of this layer, not from the start of the composition.`,
846
+ "late-keyframe"
847
+ );
848
+ }
849
+ });
850
+ }
851
+ function summarizeWarnings(messages, shown = 3) {
852
+ if (messages.length <= shown) return messages;
853
+ return [...messages.slice(0, shown), `pixi-effects: ${messages.length - shown} more layers with the same problem (the same loop or helper is probably building them): fix the ones above and these go away.`];
854
+ }
855
+ function lintText(spec, warn = defaultWarn) {
856
+ if (spec.type !== "text") return;
857
+ if (collectPropKeys(spec).has("value") && !(spec.text ?? "").includes("{value}")) {
858
+ warn(`pixi-effects: ${describeLayer(spec)}: \`value\` is set or animated but the text has no {value} placeholder, so nothing shows it. Use text: '{value}' (or e.g. '{value} users').`);
859
+ }
860
+ }
861
+
792
862
  // src/space/math.ts
793
863
  var DEG = Math.PI / 180;
794
864
  var DEFAULT_FOV = 40;
@@ -900,20 +970,6 @@ function projectLayer(t, frame, b) {
900
970
  const origin = projectPoint(b, { x: t.x, y: t.y, z: t.z });
901
971
  return { corners, depth: origin.depth, visible };
902
972
  }
903
-
904
- // src/space/specKeys.ts
905
- function collectPropKeys(spec) {
906
- const out = /* @__PURE__ */ new Set();
907
- for (const k of Object.keys(spec.initial ?? {})) out.add(k);
908
- for (const kf of spec.keyframes ?? []) {
909
- for (const bag of [kf.set, kf.to, kf.from]) {
910
- for (const k of Object.keys(bag ?? {})) out.add(k);
911
- }
912
- }
913
- return out;
914
- }
915
-
916
- // src/space/CameraSequence.ts
917
973
  var CameraSequence = class extends Sequence {
918
974
  /** True when `z` is never specified anywhere: z then follows `fov`. */
919
975
  autoZ = true;
@@ -970,6 +1026,27 @@ var CameraSequence = class extends Sequence {
970
1026
  }
971
1027
  };
972
1028
 
1029
+ // src/core/revertibleSet.ts
1030
+ function revertibleSet(timeline, at, read, write, value) {
1031
+ let p = 0;
1032
+ let before;
1033
+ const proxy = {
1034
+ get p() {
1035
+ return p;
1036
+ },
1037
+ set p(next) {
1038
+ if (next >= 1 && p < 1) {
1039
+ before = read();
1040
+ write(value);
1041
+ } else if (next < 1 && p >= 1) {
1042
+ write(before);
1043
+ }
1044
+ p = next;
1045
+ }
1046
+ };
1047
+ timeline.set(proxy, { p: 1 }, at);
1048
+ }
1049
+
973
1050
  // src/expr/colorInterp.ts
974
1051
  function buildColorInterp(from, to, space) {
975
1052
  const fromRGBA = parseToRGBA(from);
@@ -1096,7 +1173,7 @@ function shortestHueDelta(from, to) {
1096
1173
  }
1097
1174
 
1098
1175
  // src/expr/colorTween.ts
1099
- function tweenColor(timeline, target, key, fromValue, toValue, duration, ease, at, colorSpace, onUpdate) {
1176
+ function tweenColor(timeline, target, key, fromValue, toValue, duration, ease, at, colorSpace, onUpdate, loop = {}) {
1100
1177
  let interp = null;
1101
1178
  const proxy = { p: 0 };
1102
1179
  timeline.fromTo(
@@ -1106,6 +1183,7 @@ function tweenColor(timeline, target, key, fromValue, toValue, duration, ease, a
1106
1183
  p: 1,
1107
1184
  duration,
1108
1185
  ease,
1186
+ ...loop,
1109
1187
  onStart: () => {
1110
1188
  const start = fromValue !== void 0 ? fromValue : target[key];
1111
1189
  if (start === void 0) return;
@@ -1144,23 +1222,25 @@ var ImageSequence = class extends Sequence {
1144
1222
  return;
1145
1223
  }
1146
1224
  const scope = this.scope();
1225
+ const startTime = offset + this.at;
1147
1226
  const initial = stripKeys(this.spec.initial, ["tint"]);
1148
1227
  const keyframes = this.spec.keyframes ? this.spec.keyframes.map((kf) => stripKeysInKeyframe(kf, ["tint"])) : void 0;
1149
1228
  applyInitial(this.target, initial, scope);
1150
- applyKeyframes(timeline, this.target, keyframes, this.duration, scope, [], offset);
1229
+ applyKeyframes(timeline, this.target, keyframes, this.duration, scope, [], startTime);
1151
1230
  const initTint = this.spec.initial?.tint;
1152
1231
  if (initTint !== void 0) this.target.tint = initTint;
1153
1232
  for (const kf of this.spec.keyframes ?? []) {
1154
- const at = offset + resolveAt(kf.at, this.duration);
1233
+ const at = startTime + resolveAt(kf.at, this.duration);
1155
1234
  const duration = kf.duration ?? 0;
1156
1235
  const ease = kf.ease ?? "none";
1157
1236
  const fromTint = kf.from?.tint;
1158
1237
  const toTint = kf.to?.tint;
1159
1238
  const setTint = kf.set?.tint;
1160
1239
  if (setTint !== void 0) {
1161
- timeline.call(() => {
1162
- this.target.tint = setTint;
1163
- }, [], at);
1240
+ const sprite = this.target;
1241
+ revertibleSet(timeline, at, () => sprite.tint, (c) => {
1242
+ sprite.tint = c;
1243
+ }, setTint);
1164
1244
  }
1165
1245
  if (toTint !== void 0) {
1166
1246
  tweenColor(
@@ -1172,11 +1252,12 @@ var ImageSequence = class extends Sequence {
1172
1252
  duration,
1173
1253
  ease,
1174
1254
  at,
1175
- colorSpace
1255
+ colorSpace,
1256
+ void 0,
1257
+ loopVars(kf)
1176
1258
  );
1177
1259
  }
1178
1260
  }
1179
- const startTime = offset + this.at;
1180
1261
  this.absoluteStart = startTime;
1181
1262
  const endTime = startTime + this.duration;
1182
1263
  this.target.renderable = startTime <= 0;
@@ -1206,8 +1287,40 @@ function stripKeysInKeyframe(kf, keys) {
1206
1287
  from: stripKeys(kf.from, keys)
1207
1288
  };
1208
1289
  }
1209
- var STYLE_OPAQUE_KEYS = ["fontFamily", "fill", "align", "fontStyle", "fontWeight"];
1290
+ var STYLE_OPAQUE_KEYS = [
1291
+ "fontFamily",
1292
+ "fill",
1293
+ "align",
1294
+ "fontStyle",
1295
+ "fontWeight",
1296
+ "fontVariant",
1297
+ "textBaseline",
1298
+ "whiteSpace",
1299
+ "lineJoin",
1300
+ "join",
1301
+ "cap",
1302
+ "color",
1303
+ "stroke"
1304
+ ];
1305
+ function formatValue(v, f) {
1306
+ const fixed = v.toFixed(Math.max(0, Math.floor(f?.decimals ?? 0)));
1307
+ if (!f?.grouping) return fixed;
1308
+ const [int, frac] = fixed.split(".");
1309
+ return int.replace(/\B(?=(\d{3})+(?!\d))/g, ",") + (frac !== void 0 ? "." + frac : "");
1310
+ }
1210
1311
  var TextSequence = class extends Sequence {
1312
+ /** Current number for the `{value}` placeholder (tweened by `value` keyframes). */
1313
+ _value = { value: 0 };
1314
+ _lastText = "";
1315
+ /** Rewrite the displayed string from the template and the current `value`. */
1316
+ _refreshText() {
1317
+ const template = this.spec.text ?? "";
1318
+ if (!template.includes("{value}")) return;
1319
+ const next = template.split("{value}").join(formatValue(this._value.value, this.spec.format));
1320
+ if (next === this._lastText) return;
1321
+ this._lastText = next;
1322
+ this.target.text = next;
1323
+ }
1211
1324
  async build() {
1212
1325
  const baseStyle = {
1213
1326
  fontFamily: "Arial",
@@ -1237,15 +1350,25 @@ var TextSequence = class extends Sequence {
1237
1350
  for (const k of Object.keys(resolved)) {
1238
1351
  text.style[k] = resolved[k];
1239
1352
  }
1353
+ this.intrinsicWidth = text.width;
1354
+ this.intrinsicHeight = text.height;
1355
+ }
1356
+ const initialValue = this.spec.initial?.value;
1357
+ if (initialValue !== void 0) this._value.value = resolveNumber(initialValue, this.scope());
1358
+ this._refreshText();
1359
+ if (this._lastText !== "") {
1360
+ this.intrinsicWidth = text.width;
1361
+ this.intrinsicHeight = text.height;
1240
1362
  }
1241
1363
  this.buildFilters();
1242
1364
  }
1243
1365
  bindTimeline(timeline, offset = 0) {
1244
1366
  if (!this.target) return;
1245
1367
  const colorSpace = this.spec.colorSpace ?? "rgb";
1246
- const stripped = stripFill(this.spec);
1368
+ const stripped = stripKeys2(this.spec, ["fill", "value"]);
1247
1369
  runSuperWithStrippedSpec(this, stripped, timeline, offset);
1248
- bindFillKeyframes(timeline, this.target, this.spec.keyframes ?? [], this.duration, offset, colorSpace);
1370
+ bindFillKeyframes(timeline, this.target, this.spec.keyframes ?? [], this.duration, offset + this.at, colorSpace);
1371
+ bindValueKeyframes(timeline, this._value, () => this._refreshText(), this.spec.keyframes ?? [], this.duration, offset + this.at, this.scope());
1249
1372
  }
1250
1373
  };
1251
1374
  function runSuperWithStrippedSpec(inst, strippedSpec, timeline, offset) {
@@ -1257,25 +1380,61 @@ function runSuperWithStrippedSpec(inst, strippedSpec, timeline, offset) {
1257
1380
  inst.spec = originalSpec;
1258
1381
  }
1259
1382
  }
1260
- function stripFill(spec) {
1383
+ function stripKeys2(spec, keys) {
1261
1384
  return {
1262
1385
  ...spec,
1263
- initial: stripKey(spec.initial, "fill"),
1386
+ initial: stripProps(spec.initial, keys),
1264
1387
  keyframes: spec.keyframes ? spec.keyframes.map((kf) => ({
1265
1388
  ...kf,
1266
- set: stripKey(kf.set, "fill"),
1267
- to: stripKey(kf.to, "fill"),
1268
- from: stripKey(kf.from, "fill")
1389
+ set: stripProps(kf.set, keys),
1390
+ to: stripProps(kf.to, keys),
1391
+ from: stripProps(kf.from, keys)
1269
1392
  })) : void 0
1270
1393
  };
1271
1394
  }
1272
- function stripKey(props, key) {
1395
+ function stripProps(props, keys) {
1273
1396
  if (!props) return props;
1274
- if (!(key in props)) return props;
1397
+ if (!keys.some((k) => k in props)) return props;
1275
1398
  const out = {};
1276
- for (const k of Object.keys(props)) if (k !== key) out[k] = props[k];
1399
+ for (const k of Object.keys(props)) if (!keys.includes(k)) out[k] = props[k];
1277
1400
  return out;
1278
1401
  }
1402
+ function resolveNumber(v, scope) {
1403
+ if (typeof v === "number") return v;
1404
+ const out = normalizeProps({ v }, scope).v;
1405
+ return typeof out === "number" ? out : Number(out) || 0;
1406
+ }
1407
+ function bindValueKeyframes(timeline, state, refresh, keyframes, parentDuration, origin, scope) {
1408
+ for (const kf of keyframes) {
1409
+ const setV = kf.set?.value;
1410
+ const fromV = kf.from?.value;
1411
+ const toV = kf.to?.value;
1412
+ if (setV === void 0 && fromV === void 0 && toV === void 0) continue;
1413
+ const at = origin + resolveAt(kf.at, parentDuration);
1414
+ const duration = kf.duration ?? 0;
1415
+ const ease = kf.ease ?? "none";
1416
+ const loop = loopVars(kf);
1417
+ if (setV !== void 0) {
1418
+ const v = resolveNumber(setV, scope);
1419
+ revertibleSet(timeline, at, () => state.value, (n) => {
1420
+ state.value = n;
1421
+ refresh();
1422
+ }, v);
1423
+ }
1424
+ if (toV !== void 0 && fromV !== void 0) {
1425
+ timeline.fromTo(
1426
+ state,
1427
+ { value: resolveNumber(fromV, scope) },
1428
+ { value: resolveNumber(toV, scope), duration, ease, ...loop, onUpdate: refresh },
1429
+ at
1430
+ );
1431
+ } else if (toV !== void 0) {
1432
+ timeline.to(state, { value: resolveNumber(toV, scope), duration, ease, ...loop, onUpdate: refresh }, at);
1433
+ } else if (fromV !== void 0) {
1434
+ timeline.from(state, { value: resolveNumber(fromV, scope), duration, ease, ...loop, onUpdate: refresh }, at);
1435
+ }
1436
+ }
1437
+ }
1279
1438
  function bindFillKeyframes(timeline, text, keyframes, parentDuration, offset, colorSpace) {
1280
1439
  const state = { fill: text.style.fill };
1281
1440
  const writeFill = () => {
@@ -1289,10 +1448,10 @@ function bindFillKeyframes(timeline, text, keyframes, parentDuration, offset, co
1289
1448
  const fromFill = pickFill(kf.from);
1290
1449
  const toFill = pickFill(kf.to);
1291
1450
  if (setFill !== void 0) {
1292
- timeline.call(() => {
1293
- state.fill = setFill;
1451
+ revertibleSet(timeline, at, () => state.fill, (c) => {
1452
+ state.fill = c;
1294
1453
  writeFill();
1295
- }, [], at);
1454
+ }, setFill);
1296
1455
  }
1297
1456
  if (toFill !== void 0) {
1298
1457
  tweenColor(
@@ -1305,7 +1464,8 @@ function bindFillKeyframes(timeline, text, keyframes, parentDuration, offset, co
1305
1464
  ease,
1306
1465
  at,
1307
1466
  colorSpace,
1308
- writeFill
1467
+ writeFill,
1468
+ loopVars(kf)
1309
1469
  );
1310
1470
  }
1311
1471
  }
@@ -1322,7 +1482,13 @@ var AudioSequence = class extends Sequence {
1322
1482
  const data = await pixi_js.Assets.get(this.spec.asset);
1323
1483
  this._audioBuffer = data.audioBuffer;
1324
1484
  if (this.duration === void 0) {
1325
- this.duration = this.spec.duration ?? data.duration ?? this.root.duration;
1485
+ const rest = Math.max(0, (this.parent?.duration ?? this.root.duration) - this.at);
1486
+ this.duration = this.spec.duration ?? (this.spec.loop ? rest : data.duration) ?? this.root.duration;
1487
+ }
1488
+ if (!this.spec.loop && data.duration !== void 0 && this.duration > data.duration + 0.05) {
1489
+ console.warn(
1490
+ `pixi-effects: ${describeLayer(this.spec)}: audio asset "${this.spec.asset}" is ${data.duration.toFixed(1)}s but the layer lasts ${this.duration.toFixed(1)}s, so it goes silent after ${data.duration.toFixed(1)}s. Add loop: true, or shorten the layer's duration.`
1491
+ );
1326
1492
  }
1327
1493
  this.target = null;
1328
1494
  }
@@ -1333,21 +1499,32 @@ var AudioSequence = class extends Sequence {
1333
1499
  const initialVolume = this.spec.volume ?? 1;
1334
1500
  const volumeKeyframes = [];
1335
1501
  const dur = this.duration;
1336
- for (const raw of this.spec.keyframes ?? []) {
1337
- const kf = normalizeKeyframe(raw, dur);
1338
- const set = kf.set;
1339
- const to = kf.to;
1340
- const from = kf.from;
1341
- if (kf.kind === "set" && set && "volume" in set) {
1342
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: set.volume });
1343
- } else if (kf.kind === "to" && to && "volume" in to) {
1344
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: to.volume });
1345
- } else if (kf.kind === "fromTo" && to && "volume" in to) {
1346
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: from?.volume ?? initialVolume });
1347
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: to.volume });
1348
- } else if (kf.kind === "from" && from && "volume" in from) {
1349
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: from.volume });
1350
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: initialVolume });
1502
+ const t0 = baseTime + this.at;
1503
+ const keyframes = (this.spec.keyframes ?? []).map((raw) => normalizeKeyframe(raw, dur)).filter((kf) => [kf.set, kf.to, kf.from].some((p) => p && "volume" in p)).sort((x, y) => x.at - y.at);
1504
+ let current = initialVolume;
1505
+ const point = (time, value) => {
1506
+ volumeKeyframes.push({ time, value });
1507
+ };
1508
+ for (const kf of keyframes) {
1509
+ const start = t0 + kf.at;
1510
+ const end = start + kf.duration;
1511
+ const set = kf.set?.volume;
1512
+ const to = kf.to?.volume;
1513
+ const from = kf.from?.volume;
1514
+ point(start, current);
1515
+ if (kf.kind === "set" && set !== void 0) {
1516
+ point(start, set);
1517
+ current = set;
1518
+ } else if (kf.kind === "to" && to !== void 0) {
1519
+ point(end, to);
1520
+ current = to;
1521
+ } else if (kf.kind === "fromTo" && to !== void 0) {
1522
+ point(start, from ?? current);
1523
+ point(end, to);
1524
+ current = to;
1525
+ } else if (kf.kind === "from" && from !== void 0) {
1526
+ point(start, from);
1527
+ point(end, current);
1351
1528
  }
1352
1529
  }
1353
1530
  out.push({
@@ -1493,14 +1670,15 @@ var VideoSequence = class extends Sequence {
1493
1670
  });
1494
1671
  this.buildFilters();
1495
1672
  }
1496
- bindTimeline(timeline) {
1497
- super.bindTimeline(timeline);
1673
+ bindTimeline(timeline, offset = 0) {
1674
+ super.bindTimeline(timeline, offset);
1498
1675
  const playDuration = this.spec.loop ? this.duration : Math.min(this.duration, this._sourceDuration);
1499
1676
  timeline.fromTo(
1500
1677
  this.target,
1501
1678
  { currentTime: 0 },
1502
1679
  { currentTime: playDuration, duration: playDuration, ease: "none" },
1503
- this.at
1680
+ offset + this.at
1681
+ // offset = start of the enclosing composition(s); dropping it misplaced videos inside nested compositions
1504
1682
  );
1505
1683
  }
1506
1684
  collectAudio(out, baseTime) {
@@ -1541,6 +1719,55 @@ var VideoSequence = class extends Sequence {
1541
1719
  super.destroy();
1542
1720
  }
1543
1721
  };
1722
+
1723
+ // src/sequences/gradient.ts
1724
+ function cssColor(c) {
1725
+ return typeof c === "number" ? "#" + c.toString(16).padStart(6, "0") : c;
1726
+ }
1727
+ function normalizedStops(g) {
1728
+ const stops = g.stops ?? [];
1729
+ if (stops.length < 2) throw new Error('pixi-effects: fillGradient needs at least two colour stops, e.g. stops: [[0, "#000"], [1, "#fff"]]');
1730
+ return stops.map((s) => Array.isArray(s) ? { offset: s[0], color: s[1] } : { offset: s.offset, color: s.color });
1731
+ }
1732
+ function gradientOptions(g) {
1733
+ const colorStops = normalizedStops(g);
1734
+ const a = (g.angle ?? 90) * Math.PI / 180;
1735
+ const dx = Math.cos(a), dy = Math.sin(a);
1736
+ const h = (Math.abs(dx) + Math.abs(dy)) / 2;
1737
+ return {
1738
+ type: "linear",
1739
+ start: { x: 0.5 - dx * h, y: 0.5 - dy * h },
1740
+ end: { x: 0.5 + dx * h, y: 0.5 + dy * h },
1741
+ colorStops,
1742
+ textureSpace: "local"
1743
+ };
1744
+ }
1745
+ function paintRadial(ctx, size, g) {
1746
+ const stops = normalizedStops(g);
1747
+ const [cx, cy] = g.center ?? [0.5, 0.5];
1748
+ const grad = ctx.createRadialGradient(cx * size, cy * size, (g.innerRadius ?? 0) * size, cx * size, cy * size, (g.radius ?? 0.5) * size);
1749
+ for (const s of stops) grad.addColorStop(s.offset, cssColor(s.color));
1750
+ ctx.fillStyle = grad;
1751
+ ctx.fillRect(0, 0, size, size);
1752
+ }
1753
+
1754
+ // src/sequences/gradientFill.ts
1755
+ var RADIAL_SIZE = 256;
1756
+ function makeGradientFill(g) {
1757
+ if (g.type === "radial") {
1758
+ const canvas = document.createElement("canvas");
1759
+ canvas.width = RADIAL_SIZE;
1760
+ canvas.height = RADIAL_SIZE;
1761
+ const ctx = canvas.getContext("2d");
1762
+ if (ctx) {
1763
+ paintRadial(ctx, RADIAL_SIZE, g);
1764
+ return { texture: pixi_js.Texture.from(canvas), textureSpace: "local" };
1765
+ }
1766
+ }
1767
+ return new pixi_js.FillGradient(gradientOptions(g));
1768
+ }
1769
+
1770
+ // src/sequences/Shape.ts
1544
1771
  var STYLE_KEYS = ["fillColor", "fillAlpha", "strokeColor", "strokeAlpha", "strokeWidth"];
1545
1772
  var COLOR_KEYS = /* @__PURE__ */ new Set(["fillColor", "strokeColor"]);
1546
1773
  var GEOMETRY_KEYS_BY_SHAPE = {
@@ -1578,6 +1805,8 @@ var ShapeSequence = class extends Sequence {
1578
1805
  for (const k of geomKeys) this._liveKeys.add(k);
1579
1806
  seedGeometry(this._state, this.spec, scope);
1580
1807
  seedStyle(this._state, this.spec.initial ?? {}, scope);
1808
+ const gradientSpec = this.spec.fillGradient ?? this.spec.initial?.fillGradient;
1809
+ if (gradientSpec) this._state.fillStyle = makeGradientFill(gradientSpec);
1581
1810
  this._drawGeometry = makeDrawGeometry(this.spec, scope, this._state);
1582
1811
  this._drawGeometry(graphics);
1583
1812
  applyState(graphics, this._state);
@@ -1585,25 +1814,50 @@ var ShapeSequence = class extends Sequence {
1585
1814
  this.intrinsicWidth = bounds.width;
1586
1815
  this.intrinsicHeight = bounds.height;
1587
1816
  if (!ANCHORED_SHAPES.has(this.spec.shape)) {
1588
- graphics.pivot.set(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
1817
+ const cx = bounds.x + bounds.width / 2;
1818
+ const cy = bounds.y + bounds.height / 2;
1819
+ graphics.pivot.set(cx, cy);
1820
+ const init = this.spec.initial ?? {};
1821
+ if (init.x === void 0) graphics.x = cx;
1822
+ if (init.y === void 0) graphics.y = cy;
1589
1823
  }
1590
1824
  graphics.onRender = () => {
1591
- graphics.clear();
1592
- this._drawGeometry(graphics);
1593
- applyState(graphics, this._state);
1825
+ if (this._fresh) {
1826
+ this._fresh = false;
1827
+ return;
1828
+ }
1829
+ this._redraw();
1594
1830
  };
1595
1831
  this.buildFilters();
1596
1832
  }
1833
+ _fresh = false;
1834
+ _redraw() {
1835
+ const graphics = this.target;
1836
+ graphics.clear();
1837
+ this._drawGeometry(graphics);
1838
+ applyState(graphics, this._state);
1839
+ }
1840
+ /**
1841
+ * Draw from the tweened `_state` before the culler runs. The culler measures the geometry as last
1842
+ * drawn; a layer drawn only inside `onRender` is culled on stale bounds (a rect growing from
1843
+ * `width: 0` at the left edge was skipped after a jump seek, and one frame late in playback).
1844
+ */
1845
+ syncFrame() {
1846
+ super.syncFrame();
1847
+ if (!this.target?.renderable) return;
1848
+ this._redraw();
1849
+ this._fresh = true;
1850
+ }
1597
1851
  bindTimeline(timeline, offset = 0) {
1598
1852
  if (!this.target) return;
1599
1853
  const scope = this.scope();
1600
- const initial = stripLive(this.spec.initial, this._liveKeys);
1854
+ const startTime = offset + this.at;
1855
+ const initial = stripLive(stripGradient(this.spec.initial), this._liveKeys);
1601
1856
  const keyframes = this.spec.keyframes ? this.spec.keyframes.map((kf) => stripLiveKeyframe(kf, this._liveKeys)) : void 0;
1602
1857
  applyInitial(this.target, initial, scope);
1603
- applyKeyframes(timeline, this.target, keyframes, this.duration, scope, [], offset);
1858
+ applyKeyframes(timeline, this.target, keyframes, this.duration, scope, [], startTime);
1604
1859
  const colorSpace = this.spec.colorSpace ?? "rgb";
1605
- bindLiveKeyframes(timeline, this._state, this._liveKeys, this.spec.keyframes ?? [], this.duration, scope, offset, colorSpace);
1606
- const startTime = offset + this.at;
1860
+ bindLiveKeyframes(timeline, this._state, this._liveKeys, this.spec.keyframes ?? [], this.duration, scope, startTime, colorSpace);
1607
1861
  this.absoluteStart = startTime;
1608
1862
  const endTime = startTime + this.duration;
1609
1863
  this.target.renderable = startTime <= 0;
@@ -1613,8 +1867,9 @@ var ShapeSequence = class extends Sequence {
1613
1867
  };
1614
1868
  function seedGeometry(state, spec, scope) {
1615
1869
  const keys = GEOMETRY_KEYS_BY_SHAPE[spec.shape];
1870
+ const initial = spec.initial ?? {};
1616
1871
  for (const k of keys) {
1617
- const v = spec[k];
1872
+ const v = spec[k] ?? initial[k];
1618
1873
  if (v !== void 0) state[k] = num(v, scope);
1619
1874
  }
1620
1875
  }
@@ -1625,6 +1880,11 @@ function seedStyle(state, initial, scope) {
1625
1880
  state[k] = COLOR_KEYS.has(k) ? v : numOrZero(v, scope);
1626
1881
  }
1627
1882
  }
1883
+ function stripGradient(props) {
1884
+ if (!props || !("fillGradient" in props)) return props;
1885
+ const { fillGradient: _drop, ...rest } = props;
1886
+ return rest;
1887
+ }
1628
1888
  function stripLive(props, liveKeys) {
1629
1889
  if (!props) return props;
1630
1890
  const out = {};
@@ -1663,7 +1923,9 @@ function resolveLiveValue(key, value, scope) {
1663
1923
  return numOrZero(value, scope);
1664
1924
  }
1665
1925
  function applyState(g, s) {
1666
- if (s.fillColor !== void 0) {
1926
+ if (s.fillStyle) {
1927
+ g.fill(s.fillStyle);
1928
+ } else if (s.fillColor !== void 0) {
1667
1929
  g.fill({ color: s.fillColor, alpha: s.fillAlpha });
1668
1930
  }
1669
1931
  if (s.strokeColor !== void 0 && s.strokeWidth > 0) {
@@ -1743,14 +2005,16 @@ function bindLiveKeyframes(timeline, state, liveKeys, keyframes, parentDuration,
1743
2005
  const at = offset + resolveAt(kf.at, parentDuration);
1744
2006
  const duration = kf.duration ?? 0;
1745
2007
  const ease = kf.ease ?? "none";
2008
+ const loop = loopVars(kf);
1746
2009
  if (kf.set) {
1747
2010
  const live = pickLive(kf.set, liveKeys);
1748
2011
  if (live) {
1749
2012
  for (const [k, v] of Object.entries(live)) {
1750
2013
  const resolved = resolveLiveValue(k, v, scope);
1751
- timeline.call(() => {
1752
- state[k] = resolved;
1753
- }, [], at);
2014
+ const live2 = state;
2015
+ revertibleSet(timeline, at, () => live2[k], (v2) => {
2016
+ live2[k] = v2;
2017
+ }, resolved);
1754
2018
  }
1755
2019
  }
1756
2020
  }
@@ -1766,11 +2030,11 @@ function bindLiveKeyframes(timeline, state, liveKeys, keyframes, parentDuration,
1766
2030
  const toRaw = toLive?.[key];
1767
2031
  const fromValue = fromRaw !== void 0 ? resolveLiveValue(key, fromRaw, scope) : void 0;
1768
2032
  const toValue = toRaw !== void 0 ? resolveLiveValue(key, toRaw, scope) : void 0;
1769
- tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease, at, colorSpace);
2033
+ tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease, at, colorSpace, loop);
1770
2034
  }
1771
2035
  }
1772
2036
  }
1773
- function tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease, at, colorSpace) {
2037
+ function tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease, at, colorSpace, loop) {
1774
2038
  if (COLOR_KEYS.has(key) && toValue !== void 0) {
1775
2039
  tweenColor(
1776
2040
  timeline,
@@ -1781,13 +2045,15 @@ function tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease,
1781
2045
  duration,
1782
2046
  ease,
1783
2047
  at,
1784
- colorSpace
2048
+ colorSpace,
2049
+ void 0,
2050
+ loop
1785
2051
  );
1786
2052
  } else if (toValue !== void 0) {
1787
2053
  if (fromValue !== void 0) {
1788
- timeline.fromTo(state, { [key]: fromValue }, { [key]: toValue, duration, ease }, at);
2054
+ timeline.fromTo(state, { [key]: fromValue }, { [key]: toValue, duration, ease, ...loop }, at);
1789
2055
  } else {
1790
- timeline.to(state, { [key]: toValue, duration, ease }, at);
2056
+ timeline.to(state, { [key]: toValue, duration, ease, ...loop }, at);
1791
2057
  }
1792
2058
  }
1793
2059
  }
@@ -1807,7 +2073,7 @@ function numOrZero(v, scope) {
1807
2073
  var _CompositionSequence = null;
1808
2074
  async function getCompositionSequence() {
1809
2075
  if (!_CompositionSequence) {
1810
- _CompositionSequence = (await import('./Composition-BI5HZJFL.cjs')).CompositionSequence;
2076
+ _CompositionSequence = (await import('./Composition-D7RCAGEA.cjs')).CompositionSequence;
1811
2077
  }
1812
2078
  return _CompositionSequence;
1813
2079
  }
@@ -1845,16 +2111,22 @@ async function buildSequenceTree(specs, parent, root) {
1845
2111
  }
1846
2112
 
1847
2113
  exports.CameraSequence = CameraSequence;
2114
+ exports.DEFAULT_FOV = DEFAULT_FOV;
1848
2115
  exports.DEG = DEG;
1849
2116
  exports.Sequence = Sequence;
1850
2117
  exports.buildScope = buildScope;
1851
2118
  exports.buildSequenceTree = buildSequenceTree;
1852
2119
  exports.cameraBasis = cameraBasis;
1853
2120
  exports.collectPropKeys = collectPropKeys;
2121
+ exports.describeLayer = describeLayer;
1854
2122
  exports.evaluateExpr = evaluateExpr;
1855
2123
  exports.homeCamera = homeCamera;
2124
+ exports.homeDistance = homeDistance;
2125
+ exports.lintText = lintText;
2126
+ exports.lintTiming = lintTiming;
1856
2127
  exports.projectLayer = projectLayer;
1857
2128
  exports.registerSequenceType = registerSequenceType;
1858
2129
  exports.resolveAt = resolveAt;
1859
- //# sourceMappingURL=chunk-7OIWYXGV.cjs.map
1860
- //# sourceMappingURL=chunk-7OIWYXGV.cjs.map
2130
+ exports.summarizeWarnings = summarizeWarnings;
2131
+ //# sourceMappingURL=chunk-6BQ6IEW2.cjs.map
2132
+ //# sourceMappingURL=chunk-6BQ6IEW2.cjs.map