pixi-effects 0.3.0 → 0.5.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 (61) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +10 -6
  3. package/ai/SKILL.md +9 -4
  4. package/ai/reference/cheatsheet.md +19 -7
  5. package/ai/reference/pitfalls.md +32 -7
  6. package/ai/reference/recipes.md +117 -0
  7. package/ai/template.html +6 -3
  8. package/ai/tools/check.mjs +385 -0
  9. package/dist/{Base-BDw6dKRB.d.cts → Base-CPSFBkPm.d.ts} +12 -1
  10. package/dist/{Base-Ba4Ta4ap.d.ts → Base-DnRAxroQ.d.cts} +12 -1
  11. package/dist/Composition-63EXHNJJ.cjs +13 -0
  12. package/dist/{Composition-6FDH5OPM.cjs.map → Composition-63EXHNJJ.cjs.map} +1 -1
  13. package/dist/Composition-TUCRSMJU.js +4 -0
  14. package/dist/{Composition-FEYAUMFI.js.map → Composition-TUCRSMJU.js.map} +1 -1
  15. package/dist/Controller.cjs +37 -13
  16. package/dist/Controller.cjs.map +1 -1
  17. package/dist/Controller.d.cts +4 -2
  18. package/dist/Controller.d.ts +4 -2
  19. package/dist/Controller.js +37 -13
  20. package/dist/Controller.js.map +1 -1
  21. package/dist/{Movie-W8ISiEBB.d.cts → Movie-CuU_aaCm.d.cts} +56 -2
  22. package/dist/{Movie-DMpaT67V.d.ts → Movie-VyDiUYOj.d.ts} +56 -2
  23. package/dist/{chunk-ZL262ZRM.cjs → chunk-23M5QTWW.cjs} +70 -20
  24. package/dist/chunk-23M5QTWW.cjs.map +1 -0
  25. package/dist/{chunk-SIRVULFF.cjs → chunk-2RR43OIN.cjs} +256 -60
  26. package/dist/chunk-2RR43OIN.cjs.map +1 -0
  27. package/dist/{chunk-DIJG2RSF.js → chunk-5Q5XZG7Z.js} +59 -9
  28. package/dist/chunk-5Q5XZG7Z.js.map +1 -0
  29. package/dist/chunk-DKGG7WD4.js +61 -0
  30. package/dist/chunk-DKGG7WD4.js.map +1 -0
  31. package/dist/{chunk-PN5A6QA7.js → chunk-UBPZLGLU.js} +255 -61
  32. package/dist/chunk-UBPZLGLU.js.map +1 -0
  33. package/dist/chunk-UQBGW5DO.cjs +64 -0
  34. package/dist/chunk-UQBGW5DO.cjs.map +1 -0
  35. package/dist/index.cjs +336 -89
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +54 -5
  38. package/dist/index.d.ts +54 -5
  39. package/dist/index.js +320 -75
  40. package/dist/index.js.map +1 -1
  41. package/dist/sfx-MZJZXYY3.cjs +446 -0
  42. package/dist/sfx-MZJZXYY3.cjs.map +1 -0
  43. package/dist/sfx-S37DDC66.js +434 -0
  44. package/dist/sfx-S37DDC66.js.map +1 -0
  45. package/dist/three.cjs +5 -5
  46. package/dist/three.d.cts +2 -2
  47. package/dist/three.d.ts +2 -2
  48. package/dist/three.js +1 -1
  49. package/dist/{types-3c8Vymgw.d.ts → types-7VtTEKUW.d.cts} +102 -6
  50. package/dist/{types-3c8Vymgw.d.cts → types-7VtTEKUW.d.ts} +102 -6
  51. package/docs/api.md +36 -0
  52. package/docs/dsl.md +56 -4
  53. package/llms-full.txt +268 -21
  54. package/llms.txt +1 -1
  55. package/package.json +3 -1
  56. package/dist/Composition-6FDH5OPM.cjs +0 -13
  57. package/dist/Composition-FEYAUMFI.js +0 -4
  58. package/dist/chunk-DIJG2RSF.js.map +0 -1
  59. package/dist/chunk-PN5A6QA7.js.map +0 -1
  60. package/dist/chunk-SIRVULFF.cjs.map +0 -1
  61. package/dist/chunk-ZL262ZRM.cjs.map +0 -1
@@ -741,6 +741,11 @@ var Sequence = class {
741
741
  * (not a normal child) by PIXI when `target.mask = maskSequence.target`.
742
742
  */
743
743
  maskSequence = null;
744
+ /**
745
+ * Set by the parent composition on a sprite-based (image / video) mask. PIXI hides such a mask with
746
+ * `renderable = false`; the lifespan toggles must not switch it back on, or the mask is drawn as a picture.
747
+ */
748
+ keepHidden = false;
744
749
  /**
745
750
  * Absolute start time on the global timeline (offset + at), recorded by
746
751
  * bindTimeline. Movie._awaitVideoFrames uses it to determine the correct
@@ -785,6 +790,7 @@ var Sequence = class {
785
790
  applyKeyframes(timeline, this.target, this.spec.keyframes, this.duration, scope, [], startTime, routers);
786
791
  const endTime = startTime + this.duration;
787
792
  this.absoluteStart = startTime;
793
+ if (this.keepHidden) return;
788
794
  this.target.renderable = startTime <= 0;
789
795
  timeline.set(this.target, { renderable: true }, startTime);
790
796
  timeline.set(this.target, { renderable: false }, endTime);
@@ -798,6 +804,14 @@ var Sequence = class {
798
804
  */
799
805
  updateSpace(_t, _host) {
800
806
  }
807
+ /**
808
+ * Runs once per rendered frame, after the timeline seek and before the cull. Layers whose drawn
809
+ * geometry follows tweened state (shapes) redraw here, so the culler measures where the layer is NOW
810
+ * and not where it was drawn at the previous render.
811
+ */
812
+ syncFrame() {
813
+ this.maskSequence?.syncFrame();
814
+ }
801
815
  destroy() {
802
816
  this.maskSequence?.destroy();
803
817
  this.maskSequence = null;
@@ -829,18 +843,27 @@ function lintTiming(spec, parentDuration, warn = defaultWarn) {
829
843
  if (at >= parentDuration) {
830
844
  warn(`pixi-effects: ${who} starts at ${at}s, after its composition ends (${parentDuration}s), so it is never visible`);
831
845
  }
846
+ if (spec.type === "audio" && spec.sfx !== void 0 && spec.duration === void 0) return;
832
847
  const duration = spec.duration ?? parentDuration;
833
848
  (spec.keyframes ?? []).forEach((kf, i) => {
834
849
  const kat = kf.at ?? 0;
835
850
  if (kat >= duration) {
836
851
  warn(
837
- `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.`
852
+ `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.`,
853
+ "late-keyframe"
838
854
  );
839
855
  }
840
856
  });
841
857
  }
858
+ function summarizeWarnings(messages, shown = 3) {
859
+ if (messages.length <= shown) return messages;
860
+ 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.`];
861
+ }
842
862
  function lintText(spec, warn = defaultWarn) {
843
863
  if (spec.type !== "text") return;
864
+ if ((spec.text ?? "").includes("{value}") && !collectPropKeys(spec).has("value")) {
865
+ warn(`pixi-effects: ${describeLayer(spec)}: the text has a {value} placeholder but nothing sets or animates \`value\`, so it prints 0. Set initial: { value: \u2026 } or animate it; {value} is reserved for counters (there is no way to print a literal "{value}").`);
866
+ }
844
867
  if (collectPropKeys(spec).has("value") && !(spec.text ?? "").includes("{value}")) {
845
868
  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').`);
846
869
  }
@@ -1013,7 +1036,30 @@ var CameraSequence = class extends Sequence {
1013
1036
  }
1014
1037
  };
1015
1038
 
1039
+ // src/core/revertibleSet.ts
1040
+ function revertibleSet(timeline, at, read, write, value) {
1041
+ let p = 0;
1042
+ let before;
1043
+ const proxy = {
1044
+ get p() {
1045
+ return p;
1046
+ },
1047
+ set p(next) {
1048
+ if (next >= 1 && p < 1) {
1049
+ before = read();
1050
+ write(value);
1051
+ } else if (next < 1 && p >= 1) {
1052
+ write(before);
1053
+ }
1054
+ p = next;
1055
+ }
1056
+ };
1057
+ timeline.set(proxy, { p: 1 }, at);
1058
+ }
1059
+
1016
1060
  // src/expr/colorInterp.ts
1061
+ var CHROMA_FLOOR = 0.03;
1062
+ var CHROMA_FULL = 0.08;
1017
1063
  function buildColorInterp(from, to, space) {
1018
1064
  const fromRGBA = parseToRGBA(from);
1019
1065
  const toRGBA = parseToRGBA(to);
@@ -1030,8 +1076,9 @@ function buildColorInterp(from, to, space) {
1030
1076
  }
1031
1077
  const fromLCH = oklabToOklch(fromOk);
1032
1078
  const toLCH = oklabToOklch(toOk);
1033
- if (fromLCH.C < 1e-4) fromLCH.h = toLCH.h;
1034
- if (toLCH.C < 1e-4) toLCH.h = fromLCH.h;
1079
+ const fromHue = fromLCH.h, toHue = toLCH.h;
1080
+ fromLCH.h = borrowHue(fromHue, fromLCH.C, toHue);
1081
+ toLCH.h = borrowHue(toHue, toLCH.C, fromHue);
1035
1082
  const hueDelta = shortestHueDelta(fromLCH.h, toLCH.h);
1036
1083
  return (t) => {
1037
1084
  const L = lerp(fromLCH.L, toLCH.L, t);
@@ -1042,6 +1089,11 @@ function buildColorInterp(from, to, space) {
1042
1089
  return rgbaString(oklabToRgb(ab), alpha);
1043
1090
  };
1044
1091
  }
1092
+ function borrowHue(own, chroma, other) {
1093
+ const u = Math.min(1, Math.max(0, (chroma - CHROMA_FLOOR) / (CHROMA_FULL - CHROMA_FLOOR)));
1094
+ const keep = u * u * (3 - 2 * u);
1095
+ return own + shortestHueDelta(own, other) * (1 - keep);
1096
+ }
1045
1097
  function parseToRGBA(input) {
1046
1098
  if (typeof input === "number") {
1047
1099
  return { r: (input >> 16 & 255) / 255, g: (input >> 8 & 255) / 255, b: (input & 255) / 255, a: 1 };
@@ -1203,9 +1255,10 @@ var ImageSequence = class extends Sequence {
1203
1255
  const toTint = kf.to?.tint;
1204
1256
  const setTint = kf.set?.tint;
1205
1257
  if (setTint !== void 0) {
1206
- timeline.call(() => {
1207
- this.target.tint = setTint;
1208
- }, [], at);
1258
+ const sprite = this.target;
1259
+ revertibleSet(timeline, at, () => sprite.tint, (c) => {
1260
+ sprite.tint = c;
1261
+ }, setTint);
1209
1262
  }
1210
1263
  if (toTint !== void 0) {
1211
1264
  tweenColor(
@@ -1225,6 +1278,7 @@ var ImageSequence = class extends Sequence {
1225
1278
  }
1226
1279
  this.absoluteStart = startTime;
1227
1280
  const endTime = startTime + this.duration;
1281
+ if (this.keepHidden) return;
1228
1282
  this.target.renderable = startTime <= 0;
1229
1283
  timeline.set(this.target, { renderable: true }, startTime);
1230
1284
  timeline.set(this.target, { renderable: false }, endTime);
@@ -1252,12 +1306,31 @@ function stripKeysInKeyframe(kf, keys) {
1252
1306
  from: stripKeys(kf.from, keys)
1253
1307
  };
1254
1308
  }
1255
- var STYLE_OPAQUE_KEYS = ["fontFamily", "fill", "align", "fontStyle", "fontWeight"];
1309
+ var STYLE_OPAQUE_KEYS = [
1310
+ "fontFamily",
1311
+ "fill",
1312
+ "align",
1313
+ "fontStyle",
1314
+ "fontWeight",
1315
+ "fontVariant",
1316
+ "textBaseline",
1317
+ "whiteSpace",
1318
+ "lineJoin",
1319
+ "join",
1320
+ "cap",
1321
+ "color",
1322
+ "stroke"
1323
+ ];
1256
1324
  function formatValue(v, f) {
1257
1325
  const fixed = v.toFixed(Math.max(0, Math.floor(f?.decimals ?? 0)));
1258
- if (!f?.grouping) return fixed;
1259
- const [int, frac] = fixed.split(".");
1260
- return int.replace(/\B(?=(\d{3})+(?!\d))/g, ",") + (frac !== void 0 ? "." + frac : "");
1326
+ const pad = Math.max(0, Math.floor(f?.pad ?? 0));
1327
+ if (!f?.grouping && !pad) return fixed;
1328
+ const [signed, frac] = fixed.split(".");
1329
+ const sign = signed.startsWith("-") ? "-" : "";
1330
+ let int = sign ? signed.slice(1) : signed;
1331
+ if (pad) int = int.padStart(pad, "0");
1332
+ if (f?.grouping) int = int.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1333
+ return sign + int + (frac !== void 0 ? "." + frac : "");
1261
1334
  }
1262
1335
  var TextSequence = class extends Sequence {
1263
1336
  /** Current number for the `{value}` placeholder (tweened by `value` keyframes). */
@@ -1367,10 +1440,10 @@ function bindValueKeyframes(timeline, state, refresh, keyframes, parentDuration,
1367
1440
  const loop = loopVars(kf);
1368
1441
  if (setV !== void 0) {
1369
1442
  const v = resolveNumber(setV, scope);
1370
- timeline.call(() => {
1371
- state.value = v;
1443
+ revertibleSet(timeline, at, () => state.value, (n) => {
1444
+ state.value = n;
1372
1445
  refresh();
1373
- }, [], at);
1446
+ }, v);
1374
1447
  }
1375
1448
  if (toV !== void 0 && fromV !== void 0) {
1376
1449
  timeline.fromTo(
@@ -1399,10 +1472,10 @@ function bindFillKeyframes(timeline, text, keyframes, parentDuration, offset, co
1399
1472
  const fromFill = pickFill(kf.from);
1400
1473
  const toFill = pickFill(kf.to);
1401
1474
  if (setFill !== void 0) {
1402
- timeline.call(() => {
1403
- state.fill = setFill;
1475
+ revertibleSet(timeline, at, () => state.fill, (c) => {
1476
+ state.fill = c;
1404
1477
  writeFill();
1405
- }, [], at);
1478
+ }, setFill);
1406
1479
  }
1407
1480
  if (toFill !== void 0) {
1408
1481
  tweenColor(
@@ -1429,54 +1502,96 @@ function pickFill(props) {
1429
1502
  }
1430
1503
  var AudioSequence = class extends Sequence {
1431
1504
  _audioBuffer = null;
1505
+ _synth = null;
1506
+ _source = "";
1432
1507
  async build() {
1433
- const data = await pixi_js.Assets.get(this.spec.asset);
1508
+ this.target = null;
1509
+ if (this.spec.sfx !== void 0) return this._buildSfx(this.spec);
1510
+ const spec = this.spec;
1511
+ this._source = `asset "${spec.asset}"`;
1512
+ const data = await pixi_js.Assets.get(spec.asset);
1434
1513
  this._audioBuffer = data.audioBuffer;
1435
1514
  if (this.duration === void 0) {
1436
- this.duration = this.spec.duration ?? data.duration ?? this.root.duration;
1515
+ const rest = Math.max(0, (this.parent?.duration ?? this.root.duration) - this.at);
1516
+ this.duration = spec.duration ?? (spec.loop ? rest : data.duration) ?? this.root.duration;
1437
1517
  }
1438
- if (!this.spec.loop && data.duration !== void 0 && this.duration > data.duration + 0.05) {
1518
+ if (!spec.loop && data.duration !== void 0 && this.duration > data.duration + 0.05) {
1439
1519
  console.warn(
1440
- `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.`
1520
+ `pixi-effects: ${describeLayer(spec)}: audio asset "${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.`
1441
1521
  );
1442
1522
  }
1443
- this.target = null;
1523
+ }
1524
+ async _buildSfx(spec) {
1525
+ const who = describeLayer(spec);
1526
+ const raw = spec;
1527
+ if (raw.asset !== void 0) console.warn(`pixi-effects: ${who}: has both asset and sfx \u2014 the sfx plays; remove one`);
1528
+ if (raw.loop) console.warn(`pixi-effects: ${who}: loop has no effect on an sfx \u2014 to repeat it, add one audio layer per hit (a JS loop)`);
1529
+ const { resolveSfx, renderSfx, sfxKey } = await import('./sfx-MZJZXYY3.cjs');
1530
+ const sfx = resolveSfx(spec.sfx, spec.duration, who);
1531
+ if (!sfx) return;
1532
+ this.duration = sfx.length;
1533
+ this._source = `sfx "${sfx.preset ?? "custom"}"`;
1534
+ this._synth = { key: sfxKey(sfx), render: (sr) => renderSfx(sfx, sr) };
1535
+ (spec.keyframes ?? []).forEach((kf, i) => {
1536
+ if ((kf.at ?? 0) >= sfx.length) {
1537
+ console.warn(`pixi-effects: ${who}: keyframes[${i}] starts at ${kf.at}s, after the sound ends (${sfx.length}s), so it never plays`);
1538
+ } else if ((kf.at ?? 0) < 0 && sfx.length + (kf.at ?? 0) < 0) {
1539
+ console.warn(`pixi-effects: ${who}: keyframes[${i}] starts ${-kf.at}s before the END of a ${sfx.length}s sound, which is before the sound begins (a negative \`at\` counts back from the end of the layer). Use a shorter \`at\`, or stretch the sound with \`duration\`.`);
1540
+ }
1541
+ });
1444
1542
  }
1445
1543
  bindTimeline(_timeline) {
1446
1544
  }
1447
1545
  collectAudio(out, baseTime) {
1448
- if (!this._audioBuffer) return;
1449
- const initialVolume = this.spec.volume ?? 1;
1450
- const volumeKeyframes = [];
1546
+ if (!this._audioBuffer && !this._synth) return;
1547
+ const initialVolume = this.spec.volume ?? this.spec.initial?.volume ?? 1;
1548
+ const t0 = baseTime + this.at;
1451
1549
  const dur = this.duration;
1452
- for (const raw of this.spec.keyframes ?? []) {
1453
- const kf = normalizeKeyframe(raw, dur);
1454
- const set = kf.set;
1455
- const to = kf.to;
1456
- const from = kf.from;
1457
- if (kf.kind === "set" && set && "volume" in set) {
1458
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: set.volume });
1459
- } else if (kf.kind === "to" && to && "volume" in to) {
1460
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: to.volume });
1461
- } else if (kf.kind === "fromTo" && to && "volume" in to) {
1462
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: from?.volume ?? initialVolume });
1463
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: to.volume });
1464
- } else if (kf.kind === "from" && from && "volume" in from) {
1465
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: from.volume });
1466
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: initialVolume });
1467
- }
1468
- }
1469
1550
  out.push({
1470
- buffer: this._audioBuffer,
1471
- loop: !!this.spec.loop,
1472
- start: baseTime + this.at,
1473
- end: baseTime + this.at + dur,
1551
+ ...this._audioBuffer ? { buffer: this._audioBuffer } : { synth: this._synth },
1552
+ layer: describeLayer(this.spec),
1553
+ source: this._source,
1554
+ loop: !this._synth && !!this.spec.loop,
1555
+ start: t0,
1556
+ end: t0 + dur,
1474
1557
  initialVolume,
1475
- volumeKeyframes
1558
+ volumeKeyframes: this._volumePoints(t0, dur, initialVolume)
1476
1559
  });
1477
1560
  }
1561
+ _volumePoints(t0, dur, initialVolume) {
1562
+ const volumeKeyframes = [];
1563
+ 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);
1564
+ let current = initialVolume;
1565
+ const point = (time, value) => {
1566
+ volumeKeyframes.push({ time, value });
1567
+ };
1568
+ for (const kf of keyframes) {
1569
+ const start = t0 + kf.at;
1570
+ const end = start + kf.duration;
1571
+ const set = kf.set?.volume;
1572
+ const to = kf.to?.volume;
1573
+ const from = kf.from?.volume;
1574
+ point(start, current);
1575
+ if (kf.kind === "set" && set !== void 0) {
1576
+ point(start, set);
1577
+ current = set;
1578
+ } else if (kf.kind === "to" && to !== void 0) {
1579
+ point(end, to);
1580
+ current = to;
1581
+ } else if (kf.kind === "fromTo" && to !== void 0) {
1582
+ point(start, from ?? current);
1583
+ point(end, to);
1584
+ current = to;
1585
+ } else if (kf.kind === "from" && from !== void 0) {
1586
+ point(start, from);
1587
+ point(end, current);
1588
+ }
1589
+ }
1590
+ return volumeKeyframes;
1591
+ }
1478
1592
  destroy() {
1479
1593
  this._audioBuffer = null;
1594
+ this._synth = null;
1480
1595
  }
1481
1596
  };
1482
1597
 
@@ -1625,6 +1740,8 @@ var VideoSequence = class extends Sequence {
1625
1740
  const initialVolume = this.spec.volume ?? 1;
1626
1741
  out.push({
1627
1742
  buffer: this._audioBuffer,
1743
+ layer: describeLayer(this.spec),
1744
+ source: `video "${this.spec.asset}"`,
1628
1745
  loop: !!this.spec.loop,
1629
1746
  start: baseTime + this.at,
1630
1747
  end: baseTime + this.at + this.duration,
@@ -1713,11 +1830,12 @@ var GEOMETRY_KEYS_BY_SHAPE = {
1713
1830
  rect: ["width", "height", "cornerRadius", "anchorX", "anchorY"],
1714
1831
  circle: ["radius", "anchorX", "anchorY"],
1715
1832
  ellipse: ["radiusX", "radiusY", "anchorX", "anchorY"],
1833
+ arc: ["radius", "innerRadius", "startAngle", "endAngle"],
1716
1834
  line: [],
1717
1835
  polygon: [],
1718
1836
  path: []
1719
1837
  };
1720
- var ANCHORED_SHAPES = /* @__PURE__ */ new Set(["rect", "circle", "ellipse"]);
1838
+ var ANCHORED_SHAPES = /* @__PURE__ */ new Set(["rect", "circle", "ellipse", "arc"]);
1721
1839
  var ShapeSequence = class extends Sequence {
1722
1840
  _state = {
1723
1841
  fillColor: void 0,
@@ -1744,6 +1862,11 @@ var ShapeSequence = class extends Sequence {
1744
1862
  for (const k of geomKeys) this._liveKeys.add(k);
1745
1863
  seedGeometry(this._state, this.spec, scope);
1746
1864
  seedStyle(this._state, this.spec.initial ?? {}, scope);
1865
+ seedStrokeStyle(this._state, this.spec, this.spec.initial ?? {}, describeLayer(this.spec));
1866
+ if (this.spec.shape === "arc") {
1867
+ this._state.startAngle ??= 0;
1868
+ this._state.endAngle ??= 360;
1869
+ }
1747
1870
  const gradientSpec = this.spec.fillGradient ?? this.spec.initial?.fillGradient;
1748
1871
  if (gradientSpec) this._state.fillStyle = makeGradientFill(gradientSpec);
1749
1872
  this._drawGeometry = makeDrawGeometry(this.spec, scope, this._state);
@@ -1761,12 +1884,32 @@ var ShapeSequence = class extends Sequence {
1761
1884
  if (init.y === void 0) graphics.y = cy;
1762
1885
  }
1763
1886
  graphics.onRender = () => {
1764
- graphics.clear();
1765
- this._drawGeometry(graphics);
1766
- applyState(graphics, this._state);
1887
+ if (this._fresh) {
1888
+ this._fresh = false;
1889
+ return;
1890
+ }
1891
+ this._redraw();
1767
1892
  };
1768
1893
  this.buildFilters();
1769
1894
  }
1895
+ _fresh = false;
1896
+ _redraw() {
1897
+ const graphics = this.target;
1898
+ graphics.clear();
1899
+ this._drawGeometry(graphics);
1900
+ applyState(graphics, this._state);
1901
+ }
1902
+ /**
1903
+ * Draw from the tweened `_state` before the culler runs. The culler measures the geometry as last
1904
+ * drawn; a layer drawn only inside `onRender` is culled on stale bounds (a rect growing from
1905
+ * `width: 0` at the left edge was skipped after a jump seek, and one frame late in playback).
1906
+ */
1907
+ syncFrame() {
1908
+ super.syncFrame();
1909
+ if (!this.target?.renderable) return;
1910
+ this._redraw();
1911
+ this._fresh = true;
1912
+ }
1770
1913
  bindTimeline(timeline, offset = 0) {
1771
1914
  if (!this.target) return;
1772
1915
  const scope = this.scope();
@@ -1792,6 +1935,21 @@ function seedGeometry(state, spec, scope) {
1792
1935
  if (v !== void 0) state[k] = num(v, scope);
1793
1936
  }
1794
1937
  }
1938
+ var STROKE_CAPS = ["butt", "round", "square"];
1939
+ var STROKE_JOINS = ["miter", "round", "bevel"];
1940
+ function seedStrokeStyle(state, spec, initial, who) {
1941
+ const top = spec;
1942
+ const ini = initial;
1943
+ const pick = (key, valid) => {
1944
+ const v = top[key] ?? ini[key];
1945
+ if (v === void 0) return void 0;
1946
+ if (valid.includes(String(v))) return String(v);
1947
+ console.warn(`pixi-effects: ${who}: ${key} "${v}" is not one of ${valid.join(", ")}; ignored`);
1948
+ return void 0;
1949
+ };
1950
+ state.strokeCap = pick("strokeCap", STROKE_CAPS);
1951
+ state.strokeJoin = pick("strokeJoin", STROKE_JOINS);
1952
+ }
1795
1953
  function seedStyle(state, initial, scope) {
1796
1954
  for (const k of STYLE_KEYS) {
1797
1955
  const v = initial[k];
@@ -1800,8 +1958,8 @@ function seedStyle(state, initial, scope) {
1800
1958
  }
1801
1959
  }
1802
1960
  function stripGradient(props) {
1803
- if (!props || !("fillGradient" in props)) return props;
1804
- const { fillGradient: _drop, ...rest } = props;
1961
+ if (!props || !("fillGradient" in props || "strokeCap" in props || "strokeJoin" in props)) return props;
1962
+ const { fillGradient: _drop, strokeCap: _cap, strokeJoin: _join, ...rest } = props;
1805
1963
  return rest;
1806
1964
  }
1807
1965
  function stripLive(props, liveKeys) {
@@ -1848,7 +2006,13 @@ function applyState(g, s) {
1848
2006
  g.fill({ color: s.fillColor, alpha: s.fillAlpha });
1849
2007
  }
1850
2008
  if (s.strokeColor !== void 0 && s.strokeWidth > 0) {
1851
- g.stroke({ color: s.strokeColor, alpha: s.strokeAlpha, width: s.strokeWidth });
2009
+ g.stroke({
2010
+ color: s.strokeColor,
2011
+ alpha: s.strokeAlpha,
2012
+ width: s.strokeWidth,
2013
+ ...s.strokeCap ? { cap: s.strokeCap } : {},
2014
+ ...s.strokeJoin ? { join: s.strokeJoin } : {}
2015
+ });
1852
2016
  }
1853
2017
  }
1854
2018
  function drawRectFromState(g, s) {
@@ -1875,6 +2039,34 @@ function drawEllipseFromState(g, s) {
1875
2039
  const ay = s.anchorY ?? 0.5;
1876
2040
  g.ellipse((0.5 - ax) * 2 * rx, (0.5 - ay) * 2 * ry, rx, ry);
1877
2041
  }
2042
+ var DEG2 = Math.PI / 180;
2043
+ function drawArcFromState(g, s) {
2044
+ const r = s.radius ?? 0;
2045
+ const ri = Math.max(0, Math.min(s.innerRadius ?? 0, r));
2046
+ const startDeg = s.startAngle ?? 0;
2047
+ let endDeg = s.endAngle ?? 360;
2048
+ if (!(r > 0) || endDeg === startDeg) return;
2049
+ const filled = !!s.fillStyle || s.fillColor !== void 0;
2050
+ if (Math.abs(endDeg - startDeg) >= 360) {
2051
+ if (!filled) {
2052
+ g.circle(0, 0, r);
2053
+ return;
2054
+ }
2055
+ endDeg = startDeg + Math.sign(endDeg - startDeg) * 359.999;
2056
+ }
2057
+ const a0 = startDeg * DEG2, a1 = endDeg * DEG2;
2058
+ const ccw = a1 < a0;
2059
+ if (filled) g.moveTo(r * Math.cos(a0), r * Math.sin(a0));
2060
+ g.arc(0, 0, r, a0, a1, ccw);
2061
+ if (!filled) return;
2062
+ if (ri > 0) {
2063
+ g.lineTo(ri * Math.cos(a1), ri * Math.sin(a1));
2064
+ g.arc(0, 0, ri, a1, a0, !ccw);
2065
+ } else {
2066
+ g.lineTo(0, 0);
2067
+ }
2068
+ g.closePath();
2069
+ }
1878
2070
  function makeLineDraw(spec, scope) {
1879
2071
  const fx = num(spec.from[0], scope);
1880
2072
  const fy = num(spec.from[1], scope);
@@ -1903,7 +2095,8 @@ function makePathDraw(spec) {
1903
2095
  var STATE_DRAWERS = {
1904
2096
  rect: drawRectFromState,
1905
2097
  circle: drawCircleFromState,
1906
- ellipse: drawEllipseFromState
2098
+ ellipse: drawEllipseFromState,
2099
+ arc: drawArcFromState
1907
2100
  };
1908
2101
  function makeDrawGeometry(spec, scope, state) {
1909
2102
  const stateDrawer = STATE_DRAWERS[spec.shape];
@@ -1930,9 +2123,10 @@ function bindLiveKeyframes(timeline, state, liveKeys, keyframes, parentDuration,
1930
2123
  if (live) {
1931
2124
  for (const [k, v] of Object.entries(live)) {
1932
2125
  const resolved = resolveLiveValue(k, v, scope);
1933
- timeline.call(() => {
1934
- state[k] = resolved;
1935
- }, [], at);
2126
+ const live2 = state;
2127
+ revertibleSet(timeline, at, () => live2[k], (v2) => {
2128
+ live2[k] = v2;
2129
+ }, resolved);
1936
2130
  }
1937
2131
  }
1938
2132
  }
@@ -1991,7 +2185,7 @@ function numOrZero(v, scope) {
1991
2185
  var _CompositionSequence = null;
1992
2186
  async function getCompositionSequence() {
1993
2187
  if (!_CompositionSequence) {
1994
- _CompositionSequence = (await import('./Composition-6FDH5OPM.cjs')).CompositionSequence;
2188
+ _CompositionSequence = (await import('./Composition-63EXHNJJ.cjs')).CompositionSequence;
1995
2189
  }
1996
2190
  return _CompositionSequence;
1997
2191
  }
@@ -2031,6 +2225,7 @@ async function buildSequenceTree(specs, parent, root) {
2031
2225
  exports.CameraSequence = CameraSequence;
2032
2226
  exports.DEFAULT_FOV = DEFAULT_FOV;
2033
2227
  exports.DEG = DEG;
2228
+ exports.NEAR = NEAR;
2034
2229
  exports.Sequence = Sequence;
2035
2230
  exports.buildScope = buildScope;
2036
2231
  exports.buildSequenceTree = buildSequenceTree;
@@ -2045,5 +2240,6 @@ exports.lintTiming = lintTiming;
2045
2240
  exports.projectLayer = projectLayer;
2046
2241
  exports.registerSequenceType = registerSequenceType;
2047
2242
  exports.resolveAt = resolveAt;
2048
- //# sourceMappingURL=chunk-SIRVULFF.cjs.map
2049
- //# sourceMappingURL=chunk-SIRVULFF.cjs.map
2243
+ exports.summarizeWarnings = summarizeWarnings;
2244
+ //# sourceMappingURL=chunk-2RR43OIN.cjs.map
2245
+ //# sourceMappingURL=chunk-2RR43OIN.cjs.map