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
@@ -739,6 +739,11 @@ var Sequence = class {
739
739
  * (not a normal child) by PIXI when `target.mask = maskSequence.target`.
740
740
  */
741
741
  maskSequence = null;
742
+ /**
743
+ * Set by the parent composition on a sprite-based (image / video) mask. PIXI hides such a mask with
744
+ * `renderable = false`; the lifespan toggles must not switch it back on, or the mask is drawn as a picture.
745
+ */
746
+ keepHidden = false;
742
747
  /**
743
748
  * Absolute start time on the global timeline (offset + at), recorded by
744
749
  * bindTimeline. Movie._awaitVideoFrames uses it to determine the correct
@@ -783,6 +788,7 @@ var Sequence = class {
783
788
  applyKeyframes(timeline, this.target, this.spec.keyframes, this.duration, scope, [], startTime, routers);
784
789
  const endTime = startTime + this.duration;
785
790
  this.absoluteStart = startTime;
791
+ if (this.keepHidden) return;
786
792
  this.target.renderable = startTime <= 0;
787
793
  timeline.set(this.target, { renderable: true }, startTime);
788
794
  timeline.set(this.target, { renderable: false }, endTime);
@@ -796,6 +802,14 @@ var Sequence = class {
796
802
  */
797
803
  updateSpace(_t, _host) {
798
804
  }
805
+ /**
806
+ * Runs once per rendered frame, after the timeline seek and before the cull. Layers whose drawn
807
+ * geometry follows tweened state (shapes) redraw here, so the culler measures where the layer is NOW
808
+ * and not where it was drawn at the previous render.
809
+ */
810
+ syncFrame() {
811
+ this.maskSequence?.syncFrame();
812
+ }
799
813
  destroy() {
800
814
  this.maskSequence?.destroy();
801
815
  this.maskSequence = null;
@@ -827,18 +841,27 @@ function lintTiming(spec, parentDuration, warn = defaultWarn) {
827
841
  if (at >= parentDuration) {
828
842
  warn(`pixi-effects: ${who} starts at ${at}s, after its composition ends (${parentDuration}s), so it is never visible`);
829
843
  }
844
+ if (spec.type === "audio" && spec.sfx !== void 0 && spec.duration === void 0) return;
830
845
  const duration = spec.duration ?? parentDuration;
831
846
  (spec.keyframes ?? []).forEach((kf, i) => {
832
847
  const kat = kf.at ?? 0;
833
848
  if (kat >= duration) {
834
849
  warn(
835
- `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.`
850
+ `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.`,
851
+ "late-keyframe"
836
852
  );
837
853
  }
838
854
  });
839
855
  }
856
+ function summarizeWarnings(messages, shown = 3) {
857
+ if (messages.length <= shown) return messages;
858
+ 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.`];
859
+ }
840
860
  function lintText(spec, warn = defaultWarn) {
841
861
  if (spec.type !== "text") return;
862
+ if ((spec.text ?? "").includes("{value}") && !collectPropKeys(spec).has("value")) {
863
+ 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}").`);
864
+ }
842
865
  if (collectPropKeys(spec).has("value") && !(spec.text ?? "").includes("{value}")) {
843
866
  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').`);
844
867
  }
@@ -1011,7 +1034,30 @@ var CameraSequence = class extends Sequence {
1011
1034
  }
1012
1035
  };
1013
1036
 
1037
+ // src/core/revertibleSet.ts
1038
+ function revertibleSet(timeline, at, read, write, value) {
1039
+ let p = 0;
1040
+ let before;
1041
+ const proxy = {
1042
+ get p() {
1043
+ return p;
1044
+ },
1045
+ set p(next) {
1046
+ if (next >= 1 && p < 1) {
1047
+ before = read();
1048
+ write(value);
1049
+ } else if (next < 1 && p >= 1) {
1050
+ write(before);
1051
+ }
1052
+ p = next;
1053
+ }
1054
+ };
1055
+ timeline.set(proxy, { p: 1 }, at);
1056
+ }
1057
+
1014
1058
  // src/expr/colorInterp.ts
1059
+ var CHROMA_FLOOR = 0.03;
1060
+ var CHROMA_FULL = 0.08;
1015
1061
  function buildColorInterp(from, to, space) {
1016
1062
  const fromRGBA = parseToRGBA(from);
1017
1063
  const toRGBA = parseToRGBA(to);
@@ -1028,8 +1074,9 @@ function buildColorInterp(from, to, space) {
1028
1074
  }
1029
1075
  const fromLCH = oklabToOklch(fromOk);
1030
1076
  const toLCH = oklabToOklch(toOk);
1031
- if (fromLCH.C < 1e-4) fromLCH.h = toLCH.h;
1032
- if (toLCH.C < 1e-4) toLCH.h = fromLCH.h;
1077
+ const fromHue = fromLCH.h, toHue = toLCH.h;
1078
+ fromLCH.h = borrowHue(fromHue, fromLCH.C, toHue);
1079
+ toLCH.h = borrowHue(toHue, toLCH.C, fromHue);
1033
1080
  const hueDelta = shortestHueDelta(fromLCH.h, toLCH.h);
1034
1081
  return (t) => {
1035
1082
  const L = lerp(fromLCH.L, toLCH.L, t);
@@ -1040,6 +1087,11 @@ function buildColorInterp(from, to, space) {
1040
1087
  return rgbaString(oklabToRgb(ab), alpha);
1041
1088
  };
1042
1089
  }
1090
+ function borrowHue(own, chroma, other) {
1091
+ const u = Math.min(1, Math.max(0, (chroma - CHROMA_FLOOR) / (CHROMA_FULL - CHROMA_FLOOR)));
1092
+ const keep = u * u * (3 - 2 * u);
1093
+ return own + shortestHueDelta(own, other) * (1 - keep);
1094
+ }
1043
1095
  function parseToRGBA(input) {
1044
1096
  if (typeof input === "number") {
1045
1097
  return { r: (input >> 16 & 255) / 255, g: (input >> 8 & 255) / 255, b: (input & 255) / 255, a: 1 };
@@ -1201,9 +1253,10 @@ var ImageSequence = class extends Sequence {
1201
1253
  const toTint = kf.to?.tint;
1202
1254
  const setTint = kf.set?.tint;
1203
1255
  if (setTint !== void 0) {
1204
- timeline.call(() => {
1205
- this.target.tint = setTint;
1206
- }, [], at);
1256
+ const sprite = this.target;
1257
+ revertibleSet(timeline, at, () => sprite.tint, (c) => {
1258
+ sprite.tint = c;
1259
+ }, setTint);
1207
1260
  }
1208
1261
  if (toTint !== void 0) {
1209
1262
  tweenColor(
@@ -1223,6 +1276,7 @@ var ImageSequence = class extends Sequence {
1223
1276
  }
1224
1277
  this.absoluteStart = startTime;
1225
1278
  const endTime = startTime + this.duration;
1279
+ if (this.keepHidden) return;
1226
1280
  this.target.renderable = startTime <= 0;
1227
1281
  timeline.set(this.target, { renderable: true }, startTime);
1228
1282
  timeline.set(this.target, { renderable: false }, endTime);
@@ -1250,12 +1304,31 @@ function stripKeysInKeyframe(kf, keys) {
1250
1304
  from: stripKeys(kf.from, keys)
1251
1305
  };
1252
1306
  }
1253
- var STYLE_OPAQUE_KEYS = ["fontFamily", "fill", "align", "fontStyle", "fontWeight"];
1307
+ var STYLE_OPAQUE_KEYS = [
1308
+ "fontFamily",
1309
+ "fill",
1310
+ "align",
1311
+ "fontStyle",
1312
+ "fontWeight",
1313
+ "fontVariant",
1314
+ "textBaseline",
1315
+ "whiteSpace",
1316
+ "lineJoin",
1317
+ "join",
1318
+ "cap",
1319
+ "color",
1320
+ "stroke"
1321
+ ];
1254
1322
  function formatValue(v, f) {
1255
1323
  const fixed = v.toFixed(Math.max(0, Math.floor(f?.decimals ?? 0)));
1256
- if (!f?.grouping) return fixed;
1257
- const [int, frac] = fixed.split(".");
1258
- return int.replace(/\B(?=(\d{3})+(?!\d))/g, ",") + (frac !== void 0 ? "." + frac : "");
1324
+ const pad = Math.max(0, Math.floor(f?.pad ?? 0));
1325
+ if (!f?.grouping && !pad) return fixed;
1326
+ const [signed, frac] = fixed.split(".");
1327
+ const sign = signed.startsWith("-") ? "-" : "";
1328
+ let int = sign ? signed.slice(1) : signed;
1329
+ if (pad) int = int.padStart(pad, "0");
1330
+ if (f?.grouping) int = int.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1331
+ return sign + int + (frac !== void 0 ? "." + frac : "");
1259
1332
  }
1260
1333
  var TextSequence = class extends Sequence {
1261
1334
  /** Current number for the `{value}` placeholder (tweened by `value` keyframes). */
@@ -1365,10 +1438,10 @@ function bindValueKeyframes(timeline, state, refresh, keyframes, parentDuration,
1365
1438
  const loop = loopVars(kf);
1366
1439
  if (setV !== void 0) {
1367
1440
  const v = resolveNumber(setV, scope);
1368
- timeline.call(() => {
1369
- state.value = v;
1441
+ revertibleSet(timeline, at, () => state.value, (n) => {
1442
+ state.value = n;
1370
1443
  refresh();
1371
- }, [], at);
1444
+ }, v);
1372
1445
  }
1373
1446
  if (toV !== void 0 && fromV !== void 0) {
1374
1447
  timeline.fromTo(
@@ -1397,10 +1470,10 @@ function bindFillKeyframes(timeline, text, keyframes, parentDuration, offset, co
1397
1470
  const fromFill = pickFill(kf.from);
1398
1471
  const toFill = pickFill(kf.to);
1399
1472
  if (setFill !== void 0) {
1400
- timeline.call(() => {
1401
- state.fill = setFill;
1473
+ revertibleSet(timeline, at, () => state.fill, (c) => {
1474
+ state.fill = c;
1402
1475
  writeFill();
1403
- }, [], at);
1476
+ }, setFill);
1404
1477
  }
1405
1478
  if (toFill !== void 0) {
1406
1479
  tweenColor(
@@ -1427,54 +1500,96 @@ function pickFill(props) {
1427
1500
  }
1428
1501
  var AudioSequence = class extends Sequence {
1429
1502
  _audioBuffer = null;
1503
+ _synth = null;
1504
+ _source = "";
1430
1505
  async build() {
1431
- const data = await Assets.get(this.spec.asset);
1506
+ this.target = null;
1507
+ if (this.spec.sfx !== void 0) return this._buildSfx(this.spec);
1508
+ const spec = this.spec;
1509
+ this._source = `asset "${spec.asset}"`;
1510
+ const data = await Assets.get(spec.asset);
1432
1511
  this._audioBuffer = data.audioBuffer;
1433
1512
  if (this.duration === void 0) {
1434
- this.duration = this.spec.duration ?? data.duration ?? this.root.duration;
1513
+ const rest = Math.max(0, (this.parent?.duration ?? this.root.duration) - this.at);
1514
+ this.duration = spec.duration ?? (spec.loop ? rest : data.duration) ?? this.root.duration;
1435
1515
  }
1436
- if (!this.spec.loop && data.duration !== void 0 && this.duration > data.duration + 0.05) {
1516
+ if (!spec.loop && data.duration !== void 0 && this.duration > data.duration + 0.05) {
1437
1517
  console.warn(
1438
- `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.`
1518
+ `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.`
1439
1519
  );
1440
1520
  }
1441
- this.target = null;
1521
+ }
1522
+ async _buildSfx(spec) {
1523
+ const who = describeLayer(spec);
1524
+ const raw = spec;
1525
+ if (raw.asset !== void 0) console.warn(`pixi-effects: ${who}: has both asset and sfx \u2014 the sfx plays; remove one`);
1526
+ 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)`);
1527
+ const { resolveSfx, renderSfx, sfxKey } = await import('./sfx-S37DDC66.js');
1528
+ const sfx = resolveSfx(spec.sfx, spec.duration, who);
1529
+ if (!sfx) return;
1530
+ this.duration = sfx.length;
1531
+ this._source = `sfx "${sfx.preset ?? "custom"}"`;
1532
+ this._synth = { key: sfxKey(sfx), render: (sr) => renderSfx(sfx, sr) };
1533
+ (spec.keyframes ?? []).forEach((kf, i) => {
1534
+ if ((kf.at ?? 0) >= sfx.length) {
1535
+ console.warn(`pixi-effects: ${who}: keyframes[${i}] starts at ${kf.at}s, after the sound ends (${sfx.length}s), so it never plays`);
1536
+ } else if ((kf.at ?? 0) < 0 && sfx.length + (kf.at ?? 0) < 0) {
1537
+ 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\`.`);
1538
+ }
1539
+ });
1442
1540
  }
1443
1541
  bindTimeline(_timeline) {
1444
1542
  }
1445
1543
  collectAudio(out, baseTime) {
1446
- if (!this._audioBuffer) return;
1447
- const initialVolume = this.spec.volume ?? 1;
1448
- const volumeKeyframes = [];
1544
+ if (!this._audioBuffer && !this._synth) return;
1545
+ const initialVolume = this.spec.volume ?? this.spec.initial?.volume ?? 1;
1546
+ const t0 = baseTime + this.at;
1449
1547
  const dur = this.duration;
1450
- for (const raw of this.spec.keyframes ?? []) {
1451
- const kf = normalizeKeyframe(raw, dur);
1452
- const set = kf.set;
1453
- const to = kf.to;
1454
- const from = kf.from;
1455
- if (kf.kind === "set" && set && "volume" in set) {
1456
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: set.volume });
1457
- } else if (kf.kind === "to" && to && "volume" in to) {
1458
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: to.volume });
1459
- } else if (kf.kind === "fromTo" && to && "volume" in to) {
1460
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: from?.volume ?? initialVolume });
1461
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: to.volume });
1462
- } else if (kf.kind === "from" && from && "volume" in from) {
1463
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: from.volume });
1464
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: initialVolume });
1465
- }
1466
- }
1467
1548
  out.push({
1468
- buffer: this._audioBuffer,
1469
- loop: !!this.spec.loop,
1470
- start: baseTime + this.at,
1471
- end: baseTime + this.at + dur,
1549
+ ...this._audioBuffer ? { buffer: this._audioBuffer } : { synth: this._synth },
1550
+ layer: describeLayer(this.spec),
1551
+ source: this._source,
1552
+ loop: !this._synth && !!this.spec.loop,
1553
+ start: t0,
1554
+ end: t0 + dur,
1472
1555
  initialVolume,
1473
- volumeKeyframes
1556
+ volumeKeyframes: this._volumePoints(t0, dur, initialVolume)
1474
1557
  });
1475
1558
  }
1559
+ _volumePoints(t0, dur, initialVolume) {
1560
+ const volumeKeyframes = [];
1561
+ 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);
1562
+ let current = initialVolume;
1563
+ const point = (time, value) => {
1564
+ volumeKeyframes.push({ time, value });
1565
+ };
1566
+ for (const kf of keyframes) {
1567
+ const start = t0 + kf.at;
1568
+ const end = start + kf.duration;
1569
+ const set = kf.set?.volume;
1570
+ const to = kf.to?.volume;
1571
+ const from = kf.from?.volume;
1572
+ point(start, current);
1573
+ if (kf.kind === "set" && set !== void 0) {
1574
+ point(start, set);
1575
+ current = set;
1576
+ } else if (kf.kind === "to" && to !== void 0) {
1577
+ point(end, to);
1578
+ current = to;
1579
+ } else if (kf.kind === "fromTo" && to !== void 0) {
1580
+ point(start, from ?? current);
1581
+ point(end, to);
1582
+ current = to;
1583
+ } else if (kf.kind === "from" && from !== void 0) {
1584
+ point(start, from);
1585
+ point(end, current);
1586
+ }
1587
+ }
1588
+ return volumeKeyframes;
1589
+ }
1476
1590
  destroy() {
1477
1591
  this._audioBuffer = null;
1592
+ this._synth = null;
1478
1593
  }
1479
1594
  };
1480
1595
 
@@ -1623,6 +1738,8 @@ var VideoSequence = class extends Sequence {
1623
1738
  const initialVolume = this.spec.volume ?? 1;
1624
1739
  out.push({
1625
1740
  buffer: this._audioBuffer,
1741
+ layer: describeLayer(this.spec),
1742
+ source: `video "${this.spec.asset}"`,
1626
1743
  loop: !!this.spec.loop,
1627
1744
  start: baseTime + this.at,
1628
1745
  end: baseTime + this.at + this.duration,
@@ -1711,11 +1828,12 @@ var GEOMETRY_KEYS_BY_SHAPE = {
1711
1828
  rect: ["width", "height", "cornerRadius", "anchorX", "anchorY"],
1712
1829
  circle: ["radius", "anchorX", "anchorY"],
1713
1830
  ellipse: ["radiusX", "radiusY", "anchorX", "anchorY"],
1831
+ arc: ["radius", "innerRadius", "startAngle", "endAngle"],
1714
1832
  line: [],
1715
1833
  polygon: [],
1716
1834
  path: []
1717
1835
  };
1718
- var ANCHORED_SHAPES = /* @__PURE__ */ new Set(["rect", "circle", "ellipse"]);
1836
+ var ANCHORED_SHAPES = /* @__PURE__ */ new Set(["rect", "circle", "ellipse", "arc"]);
1719
1837
  var ShapeSequence = class extends Sequence {
1720
1838
  _state = {
1721
1839
  fillColor: void 0,
@@ -1742,6 +1860,11 @@ var ShapeSequence = class extends Sequence {
1742
1860
  for (const k of geomKeys) this._liveKeys.add(k);
1743
1861
  seedGeometry(this._state, this.spec, scope);
1744
1862
  seedStyle(this._state, this.spec.initial ?? {}, scope);
1863
+ seedStrokeStyle(this._state, this.spec, this.spec.initial ?? {}, describeLayer(this.spec));
1864
+ if (this.spec.shape === "arc") {
1865
+ this._state.startAngle ??= 0;
1866
+ this._state.endAngle ??= 360;
1867
+ }
1745
1868
  const gradientSpec = this.spec.fillGradient ?? this.spec.initial?.fillGradient;
1746
1869
  if (gradientSpec) this._state.fillStyle = makeGradientFill(gradientSpec);
1747
1870
  this._drawGeometry = makeDrawGeometry(this.spec, scope, this._state);
@@ -1759,12 +1882,32 @@ var ShapeSequence = class extends Sequence {
1759
1882
  if (init.y === void 0) graphics.y = cy;
1760
1883
  }
1761
1884
  graphics.onRender = () => {
1762
- graphics.clear();
1763
- this._drawGeometry(graphics);
1764
- applyState(graphics, this._state);
1885
+ if (this._fresh) {
1886
+ this._fresh = false;
1887
+ return;
1888
+ }
1889
+ this._redraw();
1765
1890
  };
1766
1891
  this.buildFilters();
1767
1892
  }
1893
+ _fresh = false;
1894
+ _redraw() {
1895
+ const graphics = this.target;
1896
+ graphics.clear();
1897
+ this._drawGeometry(graphics);
1898
+ applyState(graphics, this._state);
1899
+ }
1900
+ /**
1901
+ * Draw from the tweened `_state` before the culler runs. The culler measures the geometry as last
1902
+ * drawn; a layer drawn only inside `onRender` is culled on stale bounds (a rect growing from
1903
+ * `width: 0` at the left edge was skipped after a jump seek, and one frame late in playback).
1904
+ */
1905
+ syncFrame() {
1906
+ super.syncFrame();
1907
+ if (!this.target?.renderable) return;
1908
+ this._redraw();
1909
+ this._fresh = true;
1910
+ }
1768
1911
  bindTimeline(timeline, offset = 0) {
1769
1912
  if (!this.target) return;
1770
1913
  const scope = this.scope();
@@ -1790,6 +1933,21 @@ function seedGeometry(state, spec, scope) {
1790
1933
  if (v !== void 0) state[k] = num(v, scope);
1791
1934
  }
1792
1935
  }
1936
+ var STROKE_CAPS = ["butt", "round", "square"];
1937
+ var STROKE_JOINS = ["miter", "round", "bevel"];
1938
+ function seedStrokeStyle(state, spec, initial, who) {
1939
+ const top = spec;
1940
+ const ini = initial;
1941
+ const pick = (key, valid) => {
1942
+ const v = top[key] ?? ini[key];
1943
+ if (v === void 0) return void 0;
1944
+ if (valid.includes(String(v))) return String(v);
1945
+ console.warn(`pixi-effects: ${who}: ${key} "${v}" is not one of ${valid.join(", ")}; ignored`);
1946
+ return void 0;
1947
+ };
1948
+ state.strokeCap = pick("strokeCap", STROKE_CAPS);
1949
+ state.strokeJoin = pick("strokeJoin", STROKE_JOINS);
1950
+ }
1793
1951
  function seedStyle(state, initial, scope) {
1794
1952
  for (const k of STYLE_KEYS) {
1795
1953
  const v = initial[k];
@@ -1798,8 +1956,8 @@ function seedStyle(state, initial, scope) {
1798
1956
  }
1799
1957
  }
1800
1958
  function stripGradient(props) {
1801
- if (!props || !("fillGradient" in props)) return props;
1802
- const { fillGradient: _drop, ...rest } = props;
1959
+ if (!props || !("fillGradient" in props || "strokeCap" in props || "strokeJoin" in props)) return props;
1960
+ const { fillGradient: _drop, strokeCap: _cap, strokeJoin: _join, ...rest } = props;
1803
1961
  return rest;
1804
1962
  }
1805
1963
  function stripLive(props, liveKeys) {
@@ -1846,7 +2004,13 @@ function applyState(g, s) {
1846
2004
  g.fill({ color: s.fillColor, alpha: s.fillAlpha });
1847
2005
  }
1848
2006
  if (s.strokeColor !== void 0 && s.strokeWidth > 0) {
1849
- g.stroke({ color: s.strokeColor, alpha: s.strokeAlpha, width: s.strokeWidth });
2007
+ g.stroke({
2008
+ color: s.strokeColor,
2009
+ alpha: s.strokeAlpha,
2010
+ width: s.strokeWidth,
2011
+ ...s.strokeCap ? { cap: s.strokeCap } : {},
2012
+ ...s.strokeJoin ? { join: s.strokeJoin } : {}
2013
+ });
1850
2014
  }
1851
2015
  }
1852
2016
  function drawRectFromState(g, s) {
@@ -1873,6 +2037,34 @@ function drawEllipseFromState(g, s) {
1873
2037
  const ay = s.anchorY ?? 0.5;
1874
2038
  g.ellipse((0.5 - ax) * 2 * rx, (0.5 - ay) * 2 * ry, rx, ry);
1875
2039
  }
2040
+ var DEG2 = Math.PI / 180;
2041
+ function drawArcFromState(g, s) {
2042
+ const r = s.radius ?? 0;
2043
+ const ri = Math.max(0, Math.min(s.innerRadius ?? 0, r));
2044
+ const startDeg = s.startAngle ?? 0;
2045
+ let endDeg = s.endAngle ?? 360;
2046
+ if (!(r > 0) || endDeg === startDeg) return;
2047
+ const filled = !!s.fillStyle || s.fillColor !== void 0;
2048
+ if (Math.abs(endDeg - startDeg) >= 360) {
2049
+ if (!filled) {
2050
+ g.circle(0, 0, r);
2051
+ return;
2052
+ }
2053
+ endDeg = startDeg + Math.sign(endDeg - startDeg) * 359.999;
2054
+ }
2055
+ const a0 = startDeg * DEG2, a1 = endDeg * DEG2;
2056
+ const ccw = a1 < a0;
2057
+ if (filled) g.moveTo(r * Math.cos(a0), r * Math.sin(a0));
2058
+ g.arc(0, 0, r, a0, a1, ccw);
2059
+ if (!filled) return;
2060
+ if (ri > 0) {
2061
+ g.lineTo(ri * Math.cos(a1), ri * Math.sin(a1));
2062
+ g.arc(0, 0, ri, a1, a0, !ccw);
2063
+ } else {
2064
+ g.lineTo(0, 0);
2065
+ }
2066
+ g.closePath();
2067
+ }
1876
2068
  function makeLineDraw(spec, scope) {
1877
2069
  const fx = num(spec.from[0], scope);
1878
2070
  const fy = num(spec.from[1], scope);
@@ -1901,7 +2093,8 @@ function makePathDraw(spec) {
1901
2093
  var STATE_DRAWERS = {
1902
2094
  rect: drawRectFromState,
1903
2095
  circle: drawCircleFromState,
1904
- ellipse: drawEllipseFromState
2096
+ ellipse: drawEllipseFromState,
2097
+ arc: drawArcFromState
1905
2098
  };
1906
2099
  function makeDrawGeometry(spec, scope, state) {
1907
2100
  const stateDrawer = STATE_DRAWERS[spec.shape];
@@ -1928,9 +2121,10 @@ function bindLiveKeyframes(timeline, state, liveKeys, keyframes, parentDuration,
1928
2121
  if (live) {
1929
2122
  for (const [k, v] of Object.entries(live)) {
1930
2123
  const resolved = resolveLiveValue(k, v, scope);
1931
- timeline.call(() => {
1932
- state[k] = resolved;
1933
- }, [], at);
2124
+ const live2 = state;
2125
+ revertibleSet(timeline, at, () => live2[k], (v2) => {
2126
+ live2[k] = v2;
2127
+ }, resolved);
1934
2128
  }
1935
2129
  }
1936
2130
  }
@@ -1989,7 +2183,7 @@ function numOrZero(v, scope) {
1989
2183
  var _CompositionSequence = null;
1990
2184
  async function getCompositionSequence() {
1991
2185
  if (!_CompositionSequence) {
1992
- _CompositionSequence = (await import('./Composition-FEYAUMFI.js')).CompositionSequence;
2186
+ _CompositionSequence = (await import('./Composition-TUCRSMJU.js')).CompositionSequence;
1993
2187
  }
1994
2188
  return _CompositionSequence;
1995
2189
  }
@@ -2026,6 +2220,6 @@ async function buildSequenceTree(specs, parent, root) {
2026
2220
  return out;
2027
2221
  }
2028
2222
 
2029
- export { CameraSequence, DEFAULT_FOV, DEG, Sequence, buildScope, buildSequenceTree, cameraBasis, collectPropKeys, describeLayer, evaluateExpr, homeCamera, homeDistance, lintText, lintTiming, projectLayer, registerSequenceType, resolveAt };
2030
- //# sourceMappingURL=chunk-PN5A6QA7.js.map
2031
- //# sourceMappingURL=chunk-PN5A6QA7.js.map
2223
+ export { CameraSequence, DEFAULT_FOV, DEG, NEAR, Sequence, buildScope, buildSequenceTree, cameraBasis, collectPropKeys, describeLayer, evaluateExpr, homeCamera, homeDistance, lintText, lintTiming, projectLayer, registerSequenceType, resolveAt, summarizeWarnings };
2224
+ //# sourceMappingURL=chunk-UBPZLGLU.js.map
2225
+ //# sourceMappingURL=chunk-UBPZLGLU.js.map