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
@@ -1,4 +1,4 @@
1
- import { Rectangle, Container, Graphics, Assets, Texture, Sprite, Text, Filter, GlProgram, defaultFilterVert, GpuProgram, UniformGroup, Color, GraphicsPath } from 'pixi.js';
1
+ import { Rectangle, Container, Graphics, Assets, Texture, Sprite, Text, Filter, GlProgram, defaultFilterVert, GpuProgram, UniformGroup, FillGradient, Color, GraphicsPath } from 'pixi.js';
2
2
  import { gsap } from 'gsap';
3
3
 
4
4
  // src/sequences/Base.ts
@@ -244,6 +244,23 @@ function looksLikeColorString(s) {
244
244
  }
245
245
 
246
246
  // src/core/Timeline.ts
247
+ function loopVars(kf) {
248
+ const out = {};
249
+ let repeat = 0;
250
+ if (kf.repeat !== void 0) {
251
+ if (Number.isFinite(kf.repeat) && kf.repeat >= 0) {
252
+ repeat = Math.floor(kf.repeat);
253
+ out.repeat = repeat;
254
+ } else {
255
+ 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).`);
256
+ }
257
+ }
258
+ if (repeat > 0) {
259
+ if (kf.yoyo !== void 0) out.yoyo = kf.yoyo;
260
+ if (kf.repeatDelay !== void 0 && kf.repeatDelay > 0) out.repeatDelay = kf.repeatDelay;
261
+ }
262
+ return out;
263
+ }
247
264
  function resolveAt(at, duration) {
248
265
  if (at === void 0 || at === null) return 0;
249
266
  return at < 0 ? duration + at : at;
@@ -261,7 +278,7 @@ function normalizeKeyframe(kf, parentDuration) {
261
278
  else if (hasFrom) kind = "from";
262
279
  else if (hasTo) kind = "to";
263
280
  else kind = "to";
264
- return { at, duration, ease, kind, set: kf.set, from: kf.from, to: kf.to };
281
+ return { at, duration, ease, loop: loopVars(kf), kind, set: kf.set, from: kf.from, to: kf.to };
265
282
  }
266
283
  function partitionProps(props) {
267
284
  const ownProps = {};
@@ -371,27 +388,27 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
371
388
  const resolved = normalizeProps(kf.to, scope, { skipKeys });
372
389
  const { rest, routed } = splitRouted(resolved, routers);
373
390
  for (const r of routed)
374
- timeline.to(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease }, at);
391
+ timeline.to(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
375
392
  const { ownProps, filterProps } = partitionProps(rest);
376
393
  if (Object.keys(ownProps).length > 0)
377
- timeline.to(target, { ...pixiwrap(ownProps), duration: kf.duration, ease: kf.ease }, at);
394
+ timeline.to(target, { ...pixiwrap(ownProps), duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
378
395
  for (const [name, props] of Object.entries(filterProps)) {
379
396
  const f = findFilter(target, name);
380
397
  if (!f) continue;
381
- timeline.to(f, { ...props, duration: kf.duration, ease: kf.ease }, at);
398
+ timeline.to(f, { ...props, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
382
399
  }
383
400
  } else if (kf.kind === "from") {
384
401
  const resolved = normalizeProps(kf.from, scope, { skipKeys });
385
402
  const { rest, routed } = splitRouted(resolved, routers);
386
403
  for (const r of routed)
387
- timeline.from(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease }, at);
404
+ timeline.from(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
388
405
  const { ownProps, filterProps } = partitionProps(rest);
389
406
  if (Object.keys(ownProps).length > 0)
390
- timeline.from(target, { ...pixiwrap(ownProps), duration: kf.duration, ease: kf.ease }, at);
407
+ timeline.from(target, { ...pixiwrap(ownProps), duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
391
408
  for (const [name, props] of Object.entries(filterProps)) {
392
409
  const f = findFilter(target, name);
393
410
  if (!f) continue;
394
- timeline.from(f, { ...props, duration: kf.duration, ease: kf.ease }, at);
411
+ timeline.from(f, { ...props, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
395
412
  }
396
413
  } else {
397
414
  const fromResolved = normalizeProps(kf.from, scope, { skipKeys });
@@ -406,15 +423,15 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
406
423
  timeline.fromTo(
407
424
  r.target,
408
425
  { [r.prop]: f.value },
409
- { [r.prop]: r.value, duration: kf.duration, ease: kf.ease },
426
+ { [r.prop]: r.value, duration: kf.duration, ease: kf.ease, ...kf.loop },
410
427
  at
411
428
  );
412
429
  } else {
413
- timeline.to(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease }, at);
430
+ timeline.to(r.target, { [r.prop]: r.value, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
414
431
  }
415
432
  }
416
433
  for (const f of fromByKey.values())
417
- timeline.from(f.target, { [f.prop]: f.value, duration: kf.duration, ease: kf.ease }, at);
434
+ timeline.from(f.target, { [f.prop]: f.value, duration: kf.duration, ease: kf.ease, ...kf.loop }, at);
418
435
  const fromSplit = partitionProps(fromRouted.rest);
419
436
  const toSplit = partitionProps(toRouted.rest);
420
437
  const ownKeys = /* @__PURE__ */ new Set([...Object.keys(fromSplit.ownProps), ...Object.keys(toSplit.ownProps)]);
@@ -422,7 +439,7 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
422
439
  timeline.fromTo(
423
440
  target,
424
441
  { ...pixiwrap(fromSplit.ownProps) },
425
- { ...pixiwrap(toSplit.ownProps), duration: kf.duration, ease: kf.ease },
442
+ { ...pixiwrap(toSplit.ownProps), duration: kf.duration, ease: kf.ease, ...kf.loop },
426
443
  at
427
444
  );
428
445
  }
@@ -436,7 +453,7 @@ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skip
436
453
  timeline.fromTo(
437
454
  f,
438
455
  { ...fromSplit.filterProps[name] ?? {} },
439
- { ...toSplit.filterProps[name] ?? {}, duration: kf.duration, ease: kf.ease },
456
+ { ...toSplit.filterProps[name] ?? {}, duration: kf.duration, ease: kf.ease, ...kf.loop },
440
457
  at
441
458
  );
442
459
  }
@@ -761,9 +778,9 @@ var Sequence = class {
761
778
  if (!this.target) return;
762
779
  const scope = this.scope();
763
780
  const routers = this.pathRouters();
764
- applyInitial(this.target, this.spec.initial, scope, [], routers);
765
- applyKeyframes(timeline, this.target, this.spec.keyframes, this.duration, scope, [], offset, routers);
766
781
  const startTime = offset + this.at;
782
+ applyInitial(this.target, this.spec.initial, scope, [], routers);
783
+ applyKeyframes(timeline, this.target, this.spec.keyframes, this.duration, scope, [], startTime, routers);
767
784
  const endTime = startTime + this.duration;
768
785
  this.absoluteStart = startTime;
769
786
  this.target.renderable = startTime <= 0;
@@ -779,6 +796,14 @@ var Sequence = class {
779
796
  */
780
797
  updateSpace(_t, _host) {
781
798
  }
799
+ /**
800
+ * Runs once per rendered frame, after the timeline seek and before the cull. Layers whose drawn
801
+ * geometry follows tweened state (shapes) redraw here, so the culler measures where the layer is NOW
802
+ * and not where it was drawn at the previous render.
803
+ */
804
+ syncFrame() {
805
+ this.maskSequence?.syncFrame();
806
+ }
782
807
  destroy() {
783
808
  this.maskSequence?.destroy();
784
809
  this.maskSequence = null;
@@ -787,6 +812,51 @@ var Sequence = class {
787
812
  }
788
813
  };
789
814
 
815
+ // src/space/specKeys.ts
816
+ function collectPropKeys(spec) {
817
+ const out = /* @__PURE__ */ new Set();
818
+ for (const k of Object.keys(spec.initial ?? {})) out.add(k);
819
+ for (const kf of spec.keyframes ?? []) {
820
+ for (const bag of [kf.set, kf.to, kf.from]) {
821
+ for (const k of Object.keys(bag ?? {})) out.add(k);
822
+ }
823
+ }
824
+ return out;
825
+ }
826
+
827
+ // src/core/lint.ts
828
+ var defaultWarn = (m) => console.warn(m);
829
+ function describeLayer(spec) {
830
+ return spec.name ? `layer "${spec.name}"` : `unnamed ${spec.type} layer`;
831
+ }
832
+ function lintTiming(spec, parentDuration, warn = defaultWarn) {
833
+ const who = describeLayer(spec);
834
+ const at = spec.at ?? 0;
835
+ if (at >= parentDuration) {
836
+ warn(`pixi-effects: ${who} starts at ${at}s, after its composition ends (${parentDuration}s), so it is never visible`);
837
+ }
838
+ const duration = spec.duration ?? parentDuration;
839
+ (spec.keyframes ?? []).forEach((kf, i) => {
840
+ const kat = kf.at ?? 0;
841
+ if (kat >= duration) {
842
+ warn(
843
+ `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.`,
844
+ "late-keyframe"
845
+ );
846
+ }
847
+ });
848
+ }
849
+ function summarizeWarnings(messages, shown = 3) {
850
+ if (messages.length <= shown) return messages;
851
+ 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.`];
852
+ }
853
+ function lintText(spec, warn = defaultWarn) {
854
+ if (spec.type !== "text") return;
855
+ if (collectPropKeys(spec).has("value") && !(spec.text ?? "").includes("{value}")) {
856
+ 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').`);
857
+ }
858
+ }
859
+
790
860
  // src/space/math.ts
791
861
  var DEG = Math.PI / 180;
792
862
  var DEFAULT_FOV = 40;
@@ -898,20 +968,6 @@ function projectLayer(t, frame, b) {
898
968
  const origin = projectPoint(b, { x: t.x, y: t.y, z: t.z });
899
969
  return { corners, depth: origin.depth, visible };
900
970
  }
901
-
902
- // src/space/specKeys.ts
903
- function collectPropKeys(spec) {
904
- const out = /* @__PURE__ */ new Set();
905
- for (const k of Object.keys(spec.initial ?? {})) out.add(k);
906
- for (const kf of spec.keyframes ?? []) {
907
- for (const bag of [kf.set, kf.to, kf.from]) {
908
- for (const k of Object.keys(bag ?? {})) out.add(k);
909
- }
910
- }
911
- return out;
912
- }
913
-
914
- // src/space/CameraSequence.ts
915
971
  var CameraSequence = class extends Sequence {
916
972
  /** True when `z` is never specified anywhere: z then follows `fov`. */
917
973
  autoZ = true;
@@ -968,6 +1024,27 @@ var CameraSequence = class extends Sequence {
968
1024
  }
969
1025
  };
970
1026
 
1027
+ // src/core/revertibleSet.ts
1028
+ function revertibleSet(timeline, at, read, write, value) {
1029
+ let p = 0;
1030
+ let before;
1031
+ const proxy = {
1032
+ get p() {
1033
+ return p;
1034
+ },
1035
+ set p(next) {
1036
+ if (next >= 1 && p < 1) {
1037
+ before = read();
1038
+ write(value);
1039
+ } else if (next < 1 && p >= 1) {
1040
+ write(before);
1041
+ }
1042
+ p = next;
1043
+ }
1044
+ };
1045
+ timeline.set(proxy, { p: 1 }, at);
1046
+ }
1047
+
971
1048
  // src/expr/colorInterp.ts
972
1049
  function buildColorInterp(from, to, space) {
973
1050
  const fromRGBA = parseToRGBA(from);
@@ -1094,7 +1171,7 @@ function shortestHueDelta(from, to) {
1094
1171
  }
1095
1172
 
1096
1173
  // src/expr/colorTween.ts
1097
- function tweenColor(timeline, target, key, fromValue, toValue, duration, ease, at, colorSpace, onUpdate) {
1174
+ function tweenColor(timeline, target, key, fromValue, toValue, duration, ease, at, colorSpace, onUpdate, loop = {}) {
1098
1175
  let interp = null;
1099
1176
  const proxy = { p: 0 };
1100
1177
  timeline.fromTo(
@@ -1104,6 +1181,7 @@ function tweenColor(timeline, target, key, fromValue, toValue, duration, ease, a
1104
1181
  p: 1,
1105
1182
  duration,
1106
1183
  ease,
1184
+ ...loop,
1107
1185
  onStart: () => {
1108
1186
  const start = fromValue !== void 0 ? fromValue : target[key];
1109
1187
  if (start === void 0) return;
@@ -1142,23 +1220,25 @@ var ImageSequence = class extends Sequence {
1142
1220
  return;
1143
1221
  }
1144
1222
  const scope = this.scope();
1223
+ const startTime = offset + this.at;
1145
1224
  const initial = stripKeys(this.spec.initial, ["tint"]);
1146
1225
  const keyframes = this.spec.keyframes ? this.spec.keyframes.map((kf) => stripKeysInKeyframe(kf, ["tint"])) : void 0;
1147
1226
  applyInitial(this.target, initial, scope);
1148
- applyKeyframes(timeline, this.target, keyframes, this.duration, scope, [], offset);
1227
+ applyKeyframes(timeline, this.target, keyframes, this.duration, scope, [], startTime);
1149
1228
  const initTint = this.spec.initial?.tint;
1150
1229
  if (initTint !== void 0) this.target.tint = initTint;
1151
1230
  for (const kf of this.spec.keyframes ?? []) {
1152
- const at = offset + resolveAt(kf.at, this.duration);
1231
+ const at = startTime + resolveAt(kf.at, this.duration);
1153
1232
  const duration = kf.duration ?? 0;
1154
1233
  const ease = kf.ease ?? "none";
1155
1234
  const fromTint = kf.from?.tint;
1156
1235
  const toTint = kf.to?.tint;
1157
1236
  const setTint = kf.set?.tint;
1158
1237
  if (setTint !== void 0) {
1159
- timeline.call(() => {
1160
- this.target.tint = setTint;
1161
- }, [], at);
1238
+ const sprite = this.target;
1239
+ revertibleSet(timeline, at, () => sprite.tint, (c) => {
1240
+ sprite.tint = c;
1241
+ }, setTint);
1162
1242
  }
1163
1243
  if (toTint !== void 0) {
1164
1244
  tweenColor(
@@ -1170,11 +1250,12 @@ var ImageSequence = class extends Sequence {
1170
1250
  duration,
1171
1251
  ease,
1172
1252
  at,
1173
- colorSpace
1253
+ colorSpace,
1254
+ void 0,
1255
+ loopVars(kf)
1174
1256
  );
1175
1257
  }
1176
1258
  }
1177
- const startTime = offset + this.at;
1178
1259
  this.absoluteStart = startTime;
1179
1260
  const endTime = startTime + this.duration;
1180
1261
  this.target.renderable = startTime <= 0;
@@ -1204,8 +1285,40 @@ function stripKeysInKeyframe(kf, keys) {
1204
1285
  from: stripKeys(kf.from, keys)
1205
1286
  };
1206
1287
  }
1207
- var STYLE_OPAQUE_KEYS = ["fontFamily", "fill", "align", "fontStyle", "fontWeight"];
1288
+ var STYLE_OPAQUE_KEYS = [
1289
+ "fontFamily",
1290
+ "fill",
1291
+ "align",
1292
+ "fontStyle",
1293
+ "fontWeight",
1294
+ "fontVariant",
1295
+ "textBaseline",
1296
+ "whiteSpace",
1297
+ "lineJoin",
1298
+ "join",
1299
+ "cap",
1300
+ "color",
1301
+ "stroke"
1302
+ ];
1303
+ function formatValue(v, f) {
1304
+ const fixed = v.toFixed(Math.max(0, Math.floor(f?.decimals ?? 0)));
1305
+ if (!f?.grouping) return fixed;
1306
+ const [int, frac] = fixed.split(".");
1307
+ return int.replace(/\B(?=(\d{3})+(?!\d))/g, ",") + (frac !== void 0 ? "." + frac : "");
1308
+ }
1208
1309
  var TextSequence = class extends Sequence {
1310
+ /** Current number for the `{value}` placeholder (tweened by `value` keyframes). */
1311
+ _value = { value: 0 };
1312
+ _lastText = "";
1313
+ /** Rewrite the displayed string from the template and the current `value`. */
1314
+ _refreshText() {
1315
+ const template = this.spec.text ?? "";
1316
+ if (!template.includes("{value}")) return;
1317
+ const next = template.split("{value}").join(formatValue(this._value.value, this.spec.format));
1318
+ if (next === this._lastText) return;
1319
+ this._lastText = next;
1320
+ this.target.text = next;
1321
+ }
1209
1322
  async build() {
1210
1323
  const baseStyle = {
1211
1324
  fontFamily: "Arial",
@@ -1235,15 +1348,25 @@ var TextSequence = class extends Sequence {
1235
1348
  for (const k of Object.keys(resolved)) {
1236
1349
  text.style[k] = resolved[k];
1237
1350
  }
1351
+ this.intrinsicWidth = text.width;
1352
+ this.intrinsicHeight = text.height;
1353
+ }
1354
+ const initialValue = this.spec.initial?.value;
1355
+ if (initialValue !== void 0) this._value.value = resolveNumber(initialValue, this.scope());
1356
+ this._refreshText();
1357
+ if (this._lastText !== "") {
1358
+ this.intrinsicWidth = text.width;
1359
+ this.intrinsicHeight = text.height;
1238
1360
  }
1239
1361
  this.buildFilters();
1240
1362
  }
1241
1363
  bindTimeline(timeline, offset = 0) {
1242
1364
  if (!this.target) return;
1243
1365
  const colorSpace = this.spec.colorSpace ?? "rgb";
1244
- const stripped = stripFill(this.spec);
1366
+ const stripped = stripKeys2(this.spec, ["fill", "value"]);
1245
1367
  runSuperWithStrippedSpec(this, stripped, timeline, offset);
1246
- bindFillKeyframes(timeline, this.target, this.spec.keyframes ?? [], this.duration, offset, colorSpace);
1368
+ bindFillKeyframes(timeline, this.target, this.spec.keyframes ?? [], this.duration, offset + this.at, colorSpace);
1369
+ bindValueKeyframes(timeline, this._value, () => this._refreshText(), this.spec.keyframes ?? [], this.duration, offset + this.at, this.scope());
1247
1370
  }
1248
1371
  };
1249
1372
  function runSuperWithStrippedSpec(inst, strippedSpec, timeline, offset) {
@@ -1255,25 +1378,61 @@ function runSuperWithStrippedSpec(inst, strippedSpec, timeline, offset) {
1255
1378
  inst.spec = originalSpec;
1256
1379
  }
1257
1380
  }
1258
- function stripFill(spec) {
1381
+ function stripKeys2(spec, keys) {
1259
1382
  return {
1260
1383
  ...spec,
1261
- initial: stripKey(spec.initial, "fill"),
1384
+ initial: stripProps(spec.initial, keys),
1262
1385
  keyframes: spec.keyframes ? spec.keyframes.map((kf) => ({
1263
1386
  ...kf,
1264
- set: stripKey(kf.set, "fill"),
1265
- to: stripKey(kf.to, "fill"),
1266
- from: stripKey(kf.from, "fill")
1387
+ set: stripProps(kf.set, keys),
1388
+ to: stripProps(kf.to, keys),
1389
+ from: stripProps(kf.from, keys)
1267
1390
  })) : void 0
1268
1391
  };
1269
1392
  }
1270
- function stripKey(props, key) {
1393
+ function stripProps(props, keys) {
1271
1394
  if (!props) return props;
1272
- if (!(key in props)) return props;
1395
+ if (!keys.some((k) => k in props)) return props;
1273
1396
  const out = {};
1274
- for (const k of Object.keys(props)) if (k !== key) out[k] = props[k];
1397
+ for (const k of Object.keys(props)) if (!keys.includes(k)) out[k] = props[k];
1275
1398
  return out;
1276
1399
  }
1400
+ function resolveNumber(v, scope) {
1401
+ if (typeof v === "number") return v;
1402
+ const out = normalizeProps({ v }, scope).v;
1403
+ return typeof out === "number" ? out : Number(out) || 0;
1404
+ }
1405
+ function bindValueKeyframes(timeline, state, refresh, keyframes, parentDuration, origin, scope) {
1406
+ for (const kf of keyframes) {
1407
+ const setV = kf.set?.value;
1408
+ const fromV = kf.from?.value;
1409
+ const toV = kf.to?.value;
1410
+ if (setV === void 0 && fromV === void 0 && toV === void 0) continue;
1411
+ const at = origin + resolveAt(kf.at, parentDuration);
1412
+ const duration = kf.duration ?? 0;
1413
+ const ease = kf.ease ?? "none";
1414
+ const loop = loopVars(kf);
1415
+ if (setV !== void 0) {
1416
+ const v = resolveNumber(setV, scope);
1417
+ revertibleSet(timeline, at, () => state.value, (n) => {
1418
+ state.value = n;
1419
+ refresh();
1420
+ }, v);
1421
+ }
1422
+ if (toV !== void 0 && fromV !== void 0) {
1423
+ timeline.fromTo(
1424
+ state,
1425
+ { value: resolveNumber(fromV, scope) },
1426
+ { value: resolveNumber(toV, scope), duration, ease, ...loop, onUpdate: refresh },
1427
+ at
1428
+ );
1429
+ } else if (toV !== void 0) {
1430
+ timeline.to(state, { value: resolveNumber(toV, scope), duration, ease, ...loop, onUpdate: refresh }, at);
1431
+ } else if (fromV !== void 0) {
1432
+ timeline.from(state, { value: resolveNumber(fromV, scope), duration, ease, ...loop, onUpdate: refresh }, at);
1433
+ }
1434
+ }
1435
+ }
1277
1436
  function bindFillKeyframes(timeline, text, keyframes, parentDuration, offset, colorSpace) {
1278
1437
  const state = { fill: text.style.fill };
1279
1438
  const writeFill = () => {
@@ -1287,10 +1446,10 @@ function bindFillKeyframes(timeline, text, keyframes, parentDuration, offset, co
1287
1446
  const fromFill = pickFill(kf.from);
1288
1447
  const toFill = pickFill(kf.to);
1289
1448
  if (setFill !== void 0) {
1290
- timeline.call(() => {
1291
- state.fill = setFill;
1449
+ revertibleSet(timeline, at, () => state.fill, (c) => {
1450
+ state.fill = c;
1292
1451
  writeFill();
1293
- }, [], at);
1452
+ }, setFill);
1294
1453
  }
1295
1454
  if (toFill !== void 0) {
1296
1455
  tweenColor(
@@ -1303,7 +1462,8 @@ function bindFillKeyframes(timeline, text, keyframes, parentDuration, offset, co
1303
1462
  ease,
1304
1463
  at,
1305
1464
  colorSpace,
1306
- writeFill
1465
+ writeFill,
1466
+ loopVars(kf)
1307
1467
  );
1308
1468
  }
1309
1469
  }
@@ -1320,7 +1480,13 @@ var AudioSequence = class extends Sequence {
1320
1480
  const data = await Assets.get(this.spec.asset);
1321
1481
  this._audioBuffer = data.audioBuffer;
1322
1482
  if (this.duration === void 0) {
1323
- this.duration = this.spec.duration ?? data.duration ?? this.root.duration;
1483
+ const rest = Math.max(0, (this.parent?.duration ?? this.root.duration) - this.at);
1484
+ this.duration = this.spec.duration ?? (this.spec.loop ? rest : data.duration) ?? this.root.duration;
1485
+ }
1486
+ if (!this.spec.loop && data.duration !== void 0 && this.duration > data.duration + 0.05) {
1487
+ console.warn(
1488
+ `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.`
1489
+ );
1324
1490
  }
1325
1491
  this.target = null;
1326
1492
  }
@@ -1331,21 +1497,32 @@ var AudioSequence = class extends Sequence {
1331
1497
  const initialVolume = this.spec.volume ?? 1;
1332
1498
  const volumeKeyframes = [];
1333
1499
  const dur = this.duration;
1334
- for (const raw of this.spec.keyframes ?? []) {
1335
- const kf = normalizeKeyframe(raw, dur);
1336
- const set = kf.set;
1337
- const to = kf.to;
1338
- const from = kf.from;
1339
- if (kf.kind === "set" && set && "volume" in set) {
1340
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: set.volume });
1341
- } else if (kf.kind === "to" && to && "volume" in to) {
1342
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: to.volume });
1343
- } else if (kf.kind === "fromTo" && to && "volume" in to) {
1344
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: from?.volume ?? initialVolume });
1345
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: to.volume });
1346
- } else if (kf.kind === "from" && from && "volume" in from) {
1347
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: from.volume });
1348
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: initialVolume });
1500
+ const t0 = baseTime + this.at;
1501
+ 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);
1502
+ let current = initialVolume;
1503
+ const point = (time, value) => {
1504
+ volumeKeyframes.push({ time, value });
1505
+ };
1506
+ for (const kf of keyframes) {
1507
+ const start = t0 + kf.at;
1508
+ const end = start + kf.duration;
1509
+ const set = kf.set?.volume;
1510
+ const to = kf.to?.volume;
1511
+ const from = kf.from?.volume;
1512
+ point(start, current);
1513
+ if (kf.kind === "set" && set !== void 0) {
1514
+ point(start, set);
1515
+ current = set;
1516
+ } else if (kf.kind === "to" && to !== void 0) {
1517
+ point(end, to);
1518
+ current = to;
1519
+ } else if (kf.kind === "fromTo" && to !== void 0) {
1520
+ point(start, from ?? current);
1521
+ point(end, to);
1522
+ current = to;
1523
+ } else if (kf.kind === "from" && from !== void 0) {
1524
+ point(start, from);
1525
+ point(end, current);
1349
1526
  }
1350
1527
  }
1351
1528
  out.push({
@@ -1491,14 +1668,15 @@ var VideoSequence = class extends Sequence {
1491
1668
  });
1492
1669
  this.buildFilters();
1493
1670
  }
1494
- bindTimeline(timeline) {
1495
- super.bindTimeline(timeline);
1671
+ bindTimeline(timeline, offset = 0) {
1672
+ super.bindTimeline(timeline, offset);
1496
1673
  const playDuration = this.spec.loop ? this.duration : Math.min(this.duration, this._sourceDuration);
1497
1674
  timeline.fromTo(
1498
1675
  this.target,
1499
1676
  { currentTime: 0 },
1500
1677
  { currentTime: playDuration, duration: playDuration, ease: "none" },
1501
- this.at
1678
+ offset + this.at
1679
+ // offset = start of the enclosing composition(s); dropping it misplaced videos inside nested compositions
1502
1680
  );
1503
1681
  }
1504
1682
  collectAudio(out, baseTime) {
@@ -1539,6 +1717,55 @@ var VideoSequence = class extends Sequence {
1539
1717
  super.destroy();
1540
1718
  }
1541
1719
  };
1720
+
1721
+ // src/sequences/gradient.ts
1722
+ function cssColor(c) {
1723
+ return typeof c === "number" ? "#" + c.toString(16).padStart(6, "0") : c;
1724
+ }
1725
+ function normalizedStops(g) {
1726
+ const stops = g.stops ?? [];
1727
+ if (stops.length < 2) throw new Error('pixi-effects: fillGradient needs at least two colour stops, e.g. stops: [[0, "#000"], [1, "#fff"]]');
1728
+ return stops.map((s) => Array.isArray(s) ? { offset: s[0], color: s[1] } : { offset: s.offset, color: s.color });
1729
+ }
1730
+ function gradientOptions(g) {
1731
+ const colorStops = normalizedStops(g);
1732
+ const a = (g.angle ?? 90) * Math.PI / 180;
1733
+ const dx = Math.cos(a), dy = Math.sin(a);
1734
+ const h = (Math.abs(dx) + Math.abs(dy)) / 2;
1735
+ return {
1736
+ type: "linear",
1737
+ start: { x: 0.5 - dx * h, y: 0.5 - dy * h },
1738
+ end: { x: 0.5 + dx * h, y: 0.5 + dy * h },
1739
+ colorStops,
1740
+ textureSpace: "local"
1741
+ };
1742
+ }
1743
+ function paintRadial(ctx, size, g) {
1744
+ const stops = normalizedStops(g);
1745
+ const [cx, cy] = g.center ?? [0.5, 0.5];
1746
+ const grad = ctx.createRadialGradient(cx * size, cy * size, (g.innerRadius ?? 0) * size, cx * size, cy * size, (g.radius ?? 0.5) * size);
1747
+ for (const s of stops) grad.addColorStop(s.offset, cssColor(s.color));
1748
+ ctx.fillStyle = grad;
1749
+ ctx.fillRect(0, 0, size, size);
1750
+ }
1751
+
1752
+ // src/sequences/gradientFill.ts
1753
+ var RADIAL_SIZE = 256;
1754
+ function makeGradientFill(g) {
1755
+ if (g.type === "radial") {
1756
+ const canvas = document.createElement("canvas");
1757
+ canvas.width = RADIAL_SIZE;
1758
+ canvas.height = RADIAL_SIZE;
1759
+ const ctx = canvas.getContext("2d");
1760
+ if (ctx) {
1761
+ paintRadial(ctx, RADIAL_SIZE, g);
1762
+ return { texture: Texture.from(canvas), textureSpace: "local" };
1763
+ }
1764
+ }
1765
+ return new FillGradient(gradientOptions(g));
1766
+ }
1767
+
1768
+ // src/sequences/Shape.ts
1542
1769
  var STYLE_KEYS = ["fillColor", "fillAlpha", "strokeColor", "strokeAlpha", "strokeWidth"];
1543
1770
  var COLOR_KEYS = /* @__PURE__ */ new Set(["fillColor", "strokeColor"]);
1544
1771
  var GEOMETRY_KEYS_BY_SHAPE = {
@@ -1576,6 +1803,8 @@ var ShapeSequence = class extends Sequence {
1576
1803
  for (const k of geomKeys) this._liveKeys.add(k);
1577
1804
  seedGeometry(this._state, this.spec, scope);
1578
1805
  seedStyle(this._state, this.spec.initial ?? {}, scope);
1806
+ const gradientSpec = this.spec.fillGradient ?? this.spec.initial?.fillGradient;
1807
+ if (gradientSpec) this._state.fillStyle = makeGradientFill(gradientSpec);
1579
1808
  this._drawGeometry = makeDrawGeometry(this.spec, scope, this._state);
1580
1809
  this._drawGeometry(graphics);
1581
1810
  applyState(graphics, this._state);
@@ -1583,25 +1812,50 @@ var ShapeSequence = class extends Sequence {
1583
1812
  this.intrinsicWidth = bounds.width;
1584
1813
  this.intrinsicHeight = bounds.height;
1585
1814
  if (!ANCHORED_SHAPES.has(this.spec.shape)) {
1586
- graphics.pivot.set(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
1815
+ const cx = bounds.x + bounds.width / 2;
1816
+ const cy = bounds.y + bounds.height / 2;
1817
+ graphics.pivot.set(cx, cy);
1818
+ const init = this.spec.initial ?? {};
1819
+ if (init.x === void 0) graphics.x = cx;
1820
+ if (init.y === void 0) graphics.y = cy;
1587
1821
  }
1588
1822
  graphics.onRender = () => {
1589
- graphics.clear();
1590
- this._drawGeometry(graphics);
1591
- applyState(graphics, this._state);
1823
+ if (this._fresh) {
1824
+ this._fresh = false;
1825
+ return;
1826
+ }
1827
+ this._redraw();
1592
1828
  };
1593
1829
  this.buildFilters();
1594
1830
  }
1831
+ _fresh = false;
1832
+ _redraw() {
1833
+ const graphics = this.target;
1834
+ graphics.clear();
1835
+ this._drawGeometry(graphics);
1836
+ applyState(graphics, this._state);
1837
+ }
1838
+ /**
1839
+ * Draw from the tweened `_state` before the culler runs. The culler measures the geometry as last
1840
+ * drawn; a layer drawn only inside `onRender` is culled on stale bounds (a rect growing from
1841
+ * `width: 0` at the left edge was skipped after a jump seek, and one frame late in playback).
1842
+ */
1843
+ syncFrame() {
1844
+ super.syncFrame();
1845
+ if (!this.target?.renderable) return;
1846
+ this._redraw();
1847
+ this._fresh = true;
1848
+ }
1595
1849
  bindTimeline(timeline, offset = 0) {
1596
1850
  if (!this.target) return;
1597
1851
  const scope = this.scope();
1598
- const initial = stripLive(this.spec.initial, this._liveKeys);
1852
+ const startTime = offset + this.at;
1853
+ const initial = stripLive(stripGradient(this.spec.initial), this._liveKeys);
1599
1854
  const keyframes = this.spec.keyframes ? this.spec.keyframes.map((kf) => stripLiveKeyframe(kf, this._liveKeys)) : void 0;
1600
1855
  applyInitial(this.target, initial, scope);
1601
- applyKeyframes(timeline, this.target, keyframes, this.duration, scope, [], offset);
1856
+ applyKeyframes(timeline, this.target, keyframes, this.duration, scope, [], startTime);
1602
1857
  const colorSpace = this.spec.colorSpace ?? "rgb";
1603
- bindLiveKeyframes(timeline, this._state, this._liveKeys, this.spec.keyframes ?? [], this.duration, scope, offset, colorSpace);
1604
- const startTime = offset + this.at;
1858
+ bindLiveKeyframes(timeline, this._state, this._liveKeys, this.spec.keyframes ?? [], this.duration, scope, startTime, colorSpace);
1605
1859
  this.absoluteStart = startTime;
1606
1860
  const endTime = startTime + this.duration;
1607
1861
  this.target.renderable = startTime <= 0;
@@ -1611,8 +1865,9 @@ var ShapeSequence = class extends Sequence {
1611
1865
  };
1612
1866
  function seedGeometry(state, spec, scope) {
1613
1867
  const keys = GEOMETRY_KEYS_BY_SHAPE[spec.shape];
1868
+ const initial = spec.initial ?? {};
1614
1869
  for (const k of keys) {
1615
- const v = spec[k];
1870
+ const v = spec[k] ?? initial[k];
1616
1871
  if (v !== void 0) state[k] = num(v, scope);
1617
1872
  }
1618
1873
  }
@@ -1623,6 +1878,11 @@ function seedStyle(state, initial, scope) {
1623
1878
  state[k] = COLOR_KEYS.has(k) ? v : numOrZero(v, scope);
1624
1879
  }
1625
1880
  }
1881
+ function stripGradient(props) {
1882
+ if (!props || !("fillGradient" in props)) return props;
1883
+ const { fillGradient: _drop, ...rest } = props;
1884
+ return rest;
1885
+ }
1626
1886
  function stripLive(props, liveKeys) {
1627
1887
  if (!props) return props;
1628
1888
  const out = {};
@@ -1661,7 +1921,9 @@ function resolveLiveValue(key, value, scope) {
1661
1921
  return numOrZero(value, scope);
1662
1922
  }
1663
1923
  function applyState(g, s) {
1664
- if (s.fillColor !== void 0) {
1924
+ if (s.fillStyle) {
1925
+ g.fill(s.fillStyle);
1926
+ } else if (s.fillColor !== void 0) {
1665
1927
  g.fill({ color: s.fillColor, alpha: s.fillAlpha });
1666
1928
  }
1667
1929
  if (s.strokeColor !== void 0 && s.strokeWidth > 0) {
@@ -1741,14 +2003,16 @@ function bindLiveKeyframes(timeline, state, liveKeys, keyframes, parentDuration,
1741
2003
  const at = offset + resolveAt(kf.at, parentDuration);
1742
2004
  const duration = kf.duration ?? 0;
1743
2005
  const ease = kf.ease ?? "none";
2006
+ const loop = loopVars(kf);
1744
2007
  if (kf.set) {
1745
2008
  const live = pickLive(kf.set, liveKeys);
1746
2009
  if (live) {
1747
2010
  for (const [k, v] of Object.entries(live)) {
1748
2011
  const resolved = resolveLiveValue(k, v, scope);
1749
- timeline.call(() => {
1750
- state[k] = resolved;
1751
- }, [], at);
2012
+ const live2 = state;
2013
+ revertibleSet(timeline, at, () => live2[k], (v2) => {
2014
+ live2[k] = v2;
2015
+ }, resolved);
1752
2016
  }
1753
2017
  }
1754
2018
  }
@@ -1764,11 +2028,11 @@ function bindLiveKeyframes(timeline, state, liveKeys, keyframes, parentDuration,
1764
2028
  const toRaw = toLive?.[key];
1765
2029
  const fromValue = fromRaw !== void 0 ? resolveLiveValue(key, fromRaw, scope) : void 0;
1766
2030
  const toValue = toRaw !== void 0 ? resolveLiveValue(key, toRaw, scope) : void 0;
1767
- tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease, at, colorSpace);
2031
+ tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease, at, colorSpace, loop);
1768
2032
  }
1769
2033
  }
1770
2034
  }
1771
- function tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease, at, colorSpace) {
2035
+ function tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease, at, colorSpace, loop) {
1772
2036
  if (COLOR_KEYS.has(key) && toValue !== void 0) {
1773
2037
  tweenColor(
1774
2038
  timeline,
@@ -1779,13 +2043,15 @@ function tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease,
1779
2043
  duration,
1780
2044
  ease,
1781
2045
  at,
1782
- colorSpace
2046
+ colorSpace,
2047
+ void 0,
2048
+ loop
1783
2049
  );
1784
2050
  } else if (toValue !== void 0) {
1785
2051
  if (fromValue !== void 0) {
1786
- timeline.fromTo(state, { [key]: fromValue }, { [key]: toValue, duration, ease }, at);
2052
+ timeline.fromTo(state, { [key]: fromValue }, { [key]: toValue, duration, ease, ...loop }, at);
1787
2053
  } else {
1788
- timeline.to(state, { [key]: toValue, duration, ease }, at);
2054
+ timeline.to(state, { [key]: toValue, duration, ease, ...loop }, at);
1789
2055
  }
1790
2056
  }
1791
2057
  }
@@ -1805,7 +2071,7 @@ function numOrZero(v, scope) {
1805
2071
  var _CompositionSequence = null;
1806
2072
  async function getCompositionSequence() {
1807
2073
  if (!_CompositionSequence) {
1808
- _CompositionSequence = (await import('./Composition-IO7ZN32J.js')).CompositionSequence;
2074
+ _CompositionSequence = (await import('./Composition-RGTRZ2JX.js')).CompositionSequence;
1809
2075
  }
1810
2076
  return _CompositionSequence;
1811
2077
  }
@@ -1842,6 +2108,6 @@ async function buildSequenceTree(specs, parent, root) {
1842
2108
  return out;
1843
2109
  }
1844
2110
 
1845
- export { CameraSequence, DEG, Sequence, buildScope, buildSequenceTree, cameraBasis, collectPropKeys, evaluateExpr, homeCamera, projectLayer, registerSequenceType, resolveAt };
1846
- //# sourceMappingURL=chunk-VJCDG6YG.js.map
1847
- //# sourceMappingURL=chunk-VJCDG6YG.js.map
2111
+ export { CameraSequence, DEFAULT_FOV, DEG, Sequence, buildScope, buildSequenceTree, cameraBasis, collectPropKeys, describeLayer, evaluateExpr, homeCamera, homeDistance, lintText, lintTiming, projectLayer, registerSequenceType, resolveAt, summarizeWarnings };
2112
+ //# sourceMappingURL=chunk-ZQX7WIYL.js.map
2113
+ //# sourceMappingURL=chunk-ZQX7WIYL.js.map