pixi-effects 0.4.0 → 0.6.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 +45 -1
  2. package/README.md +11 -6
  3. package/ai/SKILL.md +7 -4
  4. package/ai/reference/cheatsheet.md +18 -5
  5. package/ai/reference/pitfalls.md +16 -11
  6. package/ai/reference/recipes.md +153 -1
  7. package/ai/template.html +6 -3
  8. package/ai/tools/check.mjs +385 -0
  9. package/dist/{Base-DhY9oMxr.d.ts → Base-4vNLl4tH.d.cts} +6 -1
  10. package/dist/{Base-5R7C5pHR.d.cts → Base-CD4AoT5-.d.ts} +6 -1
  11. package/dist/Composition-7SO5WYHU.js +4 -0
  12. package/dist/{Composition-RGTRZ2JX.js.map → Composition-7SO5WYHU.js.map} +1 -1
  13. package/dist/Composition-Y5VZSDDP.cjs +13 -0
  14. package/dist/{Composition-D7RCAGEA.cjs.map → Composition-Y5VZSDDP.cjs.map} +1 -1
  15. package/dist/Controller.cjs +47 -15
  16. package/dist/Controller.cjs.map +1 -1
  17. package/dist/Controller.d.cts +5 -2
  18. package/dist/Controller.d.ts +5 -2
  19. package/dist/Controller.js +47 -15
  20. package/dist/Controller.js.map +1 -1
  21. package/dist/{Movie-DRZtIGU7.d.ts → Movie-CADDBfpl.d.cts} +58 -2
  22. package/dist/{Movie-CghFXprP.d.cts → Movie-Ch2SUI1h.d.ts} +58 -2
  23. package/dist/{chunk-QGBOC5YL.cjs → chunk-4AGJF5RK.cjs} +33 -21
  24. package/dist/chunk-4AGJF5RK.cjs.map +1 -0
  25. package/dist/{chunk-ZQX7WIYL.js → chunk-7IJHJXDI.js} +380 -64
  26. package/dist/chunk-7IJHJXDI.js.map +1 -0
  27. package/dist/{chunk-A574IA4F.js → chunk-B2FTJLNB.js} +18 -6
  28. package/dist/chunk-B2FTJLNB.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-6BQ6IEW2.cjs → chunk-KQM2ZZMK.cjs} +380 -63
  32. package/dist/chunk-KQM2ZZMK.cjs.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 +333 -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 +317 -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-CRE9WKd4.d.ts → types-DrW9QeKU.d.cts} +118 -12
  50. package/dist/{types-CRE9WKd4.d.cts → types-DrW9QeKU.d.ts} +118 -12
  51. package/docs/api.md +37 -0
  52. package/docs/dsl.md +70 -10
  53. package/llms-full.txt +300 -30
  54. package/llms.txt +1 -1
  55. package/package.json +2 -1
  56. package/dist/Composition-D7RCAGEA.cjs +0 -13
  57. package/dist/Composition-RGTRZ2JX.js +0 -4
  58. package/dist/chunk-6BQ6IEW2.cjs.map +0 -1
  59. package/dist/chunk-A574IA4F.js.map +0 -1
  60. package/dist/chunk-QGBOC5YL.cjs.map +0 -1
  61. package/dist/chunk-ZQX7WIYL.js.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);
@@ -835,6 +841,7 @@ function lintTiming(spec, parentDuration, warn = defaultWarn) {
835
841
  if (at >= parentDuration) {
836
842
  warn(`pixi-effects: ${who} starts at ${at}s, after its composition ends (${parentDuration}s), so it is never visible`);
837
843
  }
844
+ if (spec.type === "audio" && spec.sfx !== void 0 && spec.duration === void 0) return;
838
845
  const duration = spec.duration ?? parentDuration;
839
846
  (spec.keyframes ?? []).forEach((kf, i) => {
840
847
  const kat = kf.at ?? 0;
@@ -852,7 +859,12 @@ function summarizeWarnings(messages, shown = 3) {
852
859
  }
853
860
  function lintText(spec, warn = defaultWarn) {
854
861
  if (spec.type !== "text") return;
855
- if (collectPropKeys(spec).has("value") && !(spec.text ?? "").includes("{value}")) {
862
+ const strings = [spec.text ?? "", ...(spec.keyframes ?? []).map((kf) => String(kf.set?.text ?? ""))];
863
+ const hasPlaceholder = strings.some((t) => t.includes("{value}"));
864
+ if (hasPlaceholder && !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
+ }
867
+ if (collectPropKeys(spec).has("value") && !hasPlaceholder) {
856
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').`);
857
869
  }
858
870
  }
@@ -1046,6 +1058,8 @@ function revertibleSet(timeline, at, read, write, value) {
1046
1058
  }
1047
1059
 
1048
1060
  // src/expr/colorInterp.ts
1061
+ var CHROMA_FLOOR = 0.03;
1062
+ var CHROMA_FULL = 0.08;
1049
1063
  function buildColorInterp(from, to, space) {
1050
1064
  const fromRGBA = parseToRGBA(from);
1051
1065
  const toRGBA = parseToRGBA(to);
@@ -1062,8 +1076,9 @@ function buildColorInterp(from, to, space) {
1062
1076
  }
1063
1077
  const fromLCH = oklabToOklch(fromOk);
1064
1078
  const toLCH = oklabToOklch(toOk);
1065
- if (fromLCH.C < 1e-4) fromLCH.h = toLCH.h;
1066
- 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);
1067
1082
  const hueDelta = shortestHueDelta(fromLCH.h, toLCH.h);
1068
1083
  return (t) => {
1069
1084
  const L = lerp(fromLCH.L, toLCH.L, t);
@@ -1074,6 +1089,11 @@ function buildColorInterp(from, to, space) {
1074
1089
  return rgbaString(oklabToRgb(ab), alpha);
1075
1090
  };
1076
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
+ }
1077
1097
  function parseToRGBA(input) {
1078
1098
  if (typeof input === "number") {
1079
1099
  return { r: (input >> 16 & 255) / 255, g: (input >> 8 & 255) / 255, b: (input & 255) / 255, a: 1 };
@@ -1258,6 +1278,7 @@ var ImageSequence = class extends Sequence {
1258
1278
  }
1259
1279
  this.absoluteStart = startTime;
1260
1280
  const endTime = startTime + this.duration;
1281
+ if (this.keepHidden) return;
1261
1282
  this.target.renderable = startTime <= 0;
1262
1283
  timeline.set(this.target, { renderable: true }, startTime);
1263
1284
  timeline.set(this.target, { renderable: false }, endTime);
@@ -1302,19 +1323,31 @@ var STYLE_OPAQUE_KEYS = [
1302
1323
  ];
1303
1324
  function formatValue(v, f) {
1304
1325
  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 : "");
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 : "");
1308
1334
  }
1309
1335
  var TextSequence = class extends Sequence {
1310
- /** Current number for the `{value}` placeholder (tweened by `value` keyframes). */
1311
- _value = { value: 0 };
1336
+ /** `value`: the number for the `{value}` placeholder; `visibleChars`: how many characters show (typewriter). Both tweened by keyframes. */
1337
+ _counters = { value: 0, visibleChars: 0 };
1338
+ /** The string now on the layer (before `{value}` / `visibleChars`): `text`, or the last `set: { text }`. */
1339
+ _template = "";
1340
+ /** `visibleChars` is in play (initial or animated): otherwise the whole text shows. */
1341
+ _typed = false;
1312
1342
  _lastText = "";
1313
- /** Rewrite the displayed string from the template and the current `value`. */
1343
+ /** A typewriter that has typed nothing yet: the layer is empty on purpose (inspect must not call that "no size"). */
1344
+ get showsNothingYet() {
1345
+ return this._typed && this._lastText === "" && this._template !== "";
1346
+ }
1347
+ /** Rewrite the displayed string from the template, the current `value` and `visibleChars`. */
1314
1348
  _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));
1349
+ let next = this._template.includes("{value}") ? this._template.split("{value}").join(formatValue(this._counters.value, this.spec.format)) : this._template;
1350
+ if (this._typed) next = Array.from(next).slice(0, Math.max(0, Math.floor(this._counters.visibleChars))).join("");
1318
1351
  if (next === this._lastText) return;
1319
1352
  this._lastText = next;
1320
1353
  this.target.text = next;
@@ -1326,8 +1359,10 @@ var TextSequence = class extends Sequence {
1326
1359
  fill: "#ffffff",
1327
1360
  align: "center"
1328
1361
  };
1362
+ const initialProps = this.spec.initial ?? {};
1363
+ this._template = String(initialProps.text ?? this.spec.text ?? "");
1329
1364
  const text = new Text({
1330
- text: this.spec.text ?? "",
1365
+ text: this._template,
1331
1366
  style: baseStyle,
1332
1367
  label: this.spec.name
1333
1368
  });
@@ -1339,10 +1374,10 @@ var TextSequence = class extends Sequence {
1339
1374
  this.duration = this.parent?.duration ?? this.root.duration;
1340
1375
  }
1341
1376
  if (this.spec.style) {
1342
- const scope = this.scope();
1377
+ const scope2 = this.scope();
1343
1378
  const resolved = normalizeProps(
1344
1379
  this.spec.style,
1345
- scope,
1380
+ scope2,
1346
1381
  { skipKeys: STYLE_OPAQUE_KEYS }
1347
1382
  );
1348
1383
  for (const k of Object.keys(resolved)) {
@@ -1351,22 +1386,36 @@ var TextSequence = class extends Sequence {
1351
1386
  this.intrinsicWidth = text.width;
1352
1387
  this.intrinsicHeight = text.height;
1353
1388
  }
1354
- const initialValue = this.spec.initial?.value;
1355
- if (initialValue !== void 0) this._value.value = resolveNumber(initialValue, this.scope());
1389
+ const scope = this.scope();
1390
+ if (initialProps.value !== void 0) this._counters.value = resolveNumber(initialProps.value, scope);
1391
+ this._typed = initialProps.visibleChars !== void 0 || animates(this.spec.keyframes, "visibleChars");
1392
+ if (initialProps.visibleChars !== void 0) this._counters.visibleChars = resolveNumber(initialProps.visibleChars, scope);
1393
+ const typed = this._typed;
1394
+ this._typed = false;
1356
1395
  this._refreshText();
1357
1396
  if (this._lastText !== "") {
1358
1397
  this.intrinsicWidth = text.width;
1359
1398
  this.intrinsicHeight = text.height;
1360
1399
  }
1400
+ this._typed = typed;
1401
+ this._refreshText();
1361
1402
  this.buildFilters();
1362
1403
  }
1363
1404
  bindTimeline(timeline, offset = 0) {
1364
1405
  if (!this.target) return;
1365
1406
  const colorSpace = this.spec.colorSpace ?? "rgb";
1366
- const stripped = stripKeys2(this.spec, ["fill", "value"]);
1407
+ const stripped = stripKeys2(this.spec, ["fill", "value", "text", "visibleChars"]);
1367
1408
  runSuperWithStrippedSpec(this, stripped, timeline, offset);
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());
1409
+ const keyframes = this.spec.keyframes ?? [];
1410
+ const origin = offset + this.at;
1411
+ bindFillKeyframes(timeline, this.target, keyframes, this.duration, origin, colorSpace);
1412
+ const refresh = () => this._refreshText();
1413
+ bindCounterKeyframes(timeline, this._counters, "value", refresh, keyframes, this.duration, origin, this.scope());
1414
+ bindCounterKeyframes(timeline, this._counters, "visibleChars", refresh, keyframes, this.duration, origin, this.scope());
1415
+ bindTextKeyframes(timeline, keyframes, this.duration, origin, () => this._template, (t) => {
1416
+ this._template = t;
1417
+ refresh();
1418
+ }, describeLayer(this.spec));
1370
1419
  }
1371
1420
  };
1372
1421
  function runSuperWithStrippedSpec(inst, strippedSpec, timeline, offset) {
@@ -1402,11 +1451,11 @@ function resolveNumber(v, scope) {
1402
1451
  const out = normalizeProps({ v }, scope).v;
1403
1452
  return typeof out === "number" ? out : Number(out) || 0;
1404
1453
  }
1405
- function bindValueKeyframes(timeline, state, refresh, keyframes, parentDuration, origin, scope) {
1454
+ function bindCounterKeyframes(timeline, state, key, refresh, keyframes, parentDuration, origin, scope) {
1406
1455
  for (const kf of keyframes) {
1407
- const setV = kf.set?.value;
1408
- const fromV = kf.from?.value;
1409
- const toV = kf.to?.value;
1456
+ const setV = kf.set?.[key];
1457
+ const fromV = kf.from?.[key];
1458
+ const toV = kf.to?.[key];
1410
1459
  if (setV === void 0 && fromV === void 0 && toV === void 0) continue;
1411
1460
  const at = origin + resolveAt(kf.at, parentDuration);
1412
1461
  const duration = kf.duration ?? 0;
@@ -1414,25 +1463,40 @@ function bindValueKeyframes(timeline, state, refresh, keyframes, parentDuration,
1414
1463
  const loop = loopVars(kf);
1415
1464
  if (setV !== void 0) {
1416
1465
  const v = resolveNumber(setV, scope);
1417
- revertibleSet(timeline, at, () => state.value, (n) => {
1418
- state.value = n;
1466
+ revertibleSet(timeline, at, () => state[key], (n) => {
1467
+ state[key] = n;
1419
1468
  refresh();
1420
1469
  }, v);
1421
1470
  }
1422
1471
  if (toV !== void 0 && fromV !== void 0) {
1423
1472
  timeline.fromTo(
1424
1473
  state,
1425
- { value: resolveNumber(fromV, scope) },
1426
- { value: resolveNumber(toV, scope), duration, ease, ...loop, onUpdate: refresh },
1474
+ { [key]: resolveNumber(fromV, scope) },
1475
+ { [key]: resolveNumber(toV, scope), duration, ease, ...loop, onUpdate: refresh },
1427
1476
  at
1428
1477
  );
1429
1478
  } else if (toV !== void 0) {
1430
- timeline.to(state, { value: resolveNumber(toV, scope), duration, ease, ...loop, onUpdate: refresh }, at);
1479
+ timeline.to(state, { [key]: resolveNumber(toV, scope), duration, ease, ...loop, onUpdate: refresh }, at);
1431
1480
  } else if (fromV !== void 0) {
1432
- timeline.from(state, { value: resolveNumber(fromV, scope), duration, ease, ...loop, onUpdate: refresh }, at);
1481
+ timeline.from(state, { [key]: resolveNumber(fromV, scope), duration, ease, ...loop, onUpdate: refresh }, at);
1433
1482
  }
1434
1483
  }
1435
1484
  }
1485
+ function bindTextKeyframes(timeline, keyframes, parentDuration, origin, read, write, who) {
1486
+ for (const kf of keyframes) {
1487
+ for (const [what, props] of [["to", kf.to], ["from", kf.from]]) {
1488
+ if (props && props.text !== void 0) {
1489
+ console.warn(`pixi-effects: ${who}: a keyframe's ${what}: { text } cannot tween a string: use set: { text: '\u2026' } at the time it should change`);
1490
+ }
1491
+ }
1492
+ const t = kf.set?.text;
1493
+ if (t === void 0) continue;
1494
+ revertibleSet(timeline, origin + resolveAt(kf.at, parentDuration), read, write, String(t));
1495
+ }
1496
+ }
1497
+ function animates(keyframes, key) {
1498
+ return (keyframes ?? []).some((kf) => [kf.set, kf.to, kf.from].some((p) => p && p[key] !== void 0));
1499
+ }
1436
1500
  function bindFillKeyframes(timeline, text, keyframes, parentDuration, offset, colorSpace) {
1437
1501
  const state = { fill: text.style.fill };
1438
1502
  const writeFill = () => {
@@ -1476,28 +1540,64 @@ function pickFill(props) {
1476
1540
  }
1477
1541
  var AudioSequence = class extends Sequence {
1478
1542
  _audioBuffer = null;
1543
+ _synth = null;
1544
+ _source = "";
1479
1545
  async build() {
1480
- const data = await Assets.get(this.spec.asset);
1546
+ this.target = null;
1547
+ if (this.spec.sfx !== void 0) return this._buildSfx(this.spec);
1548
+ const spec = this.spec;
1549
+ this._source = `asset "${spec.asset}"`;
1550
+ const data = await Assets.get(spec.asset);
1481
1551
  this._audioBuffer = data.audioBuffer;
1482
1552
  if (this.duration === void 0) {
1483
1553
  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;
1554
+ this.duration = spec.duration ?? (spec.loop ? rest : data.duration) ?? this.root.duration;
1485
1555
  }
1486
- if (!this.spec.loop && data.duration !== void 0 && this.duration > data.duration + 0.05) {
1556
+ if (!spec.loop && data.duration !== void 0 && this.duration > data.duration + 0.05) {
1487
1557
  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.`
1558
+ `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.`
1489
1559
  );
1490
1560
  }
1491
- this.target = null;
1561
+ }
1562
+ async _buildSfx(spec) {
1563
+ const who = describeLayer(spec);
1564
+ const raw = spec;
1565
+ if (raw.asset !== void 0) console.warn(`pixi-effects: ${who}: has both asset and sfx \u2014 the sfx plays; remove one`);
1566
+ 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)`);
1567
+ const { resolveSfx, renderSfx, sfxKey } = await import('./sfx-S37DDC66.js');
1568
+ const sfx = resolveSfx(spec.sfx, spec.duration, who);
1569
+ if (!sfx) return;
1570
+ this.duration = sfx.length;
1571
+ this._source = `sfx "${sfx.preset ?? "custom"}"`;
1572
+ this._synth = { key: sfxKey(sfx), render: (sr) => renderSfx(sfx, sr) };
1573
+ (spec.keyframes ?? []).forEach((kf, i) => {
1574
+ if ((kf.at ?? 0) >= sfx.length) {
1575
+ console.warn(`pixi-effects: ${who}: keyframes[${i}] starts at ${kf.at}s, after the sound ends (${sfx.length}s), so it never plays`);
1576
+ } else if ((kf.at ?? 0) < 0 && sfx.length + (kf.at ?? 0) < 0) {
1577
+ 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\`.`);
1578
+ }
1579
+ });
1492
1580
  }
1493
1581
  bindTimeline(_timeline) {
1494
1582
  }
1495
1583
  collectAudio(out, baseTime) {
1496
- if (!this._audioBuffer) return;
1497
- const initialVolume = this.spec.volume ?? 1;
1498
- const volumeKeyframes = [];
1499
- const dur = this.duration;
1584
+ if (!this._audioBuffer && !this._synth) return;
1585
+ const initialVolume = this.spec.volume ?? this.spec.initial?.volume ?? 1;
1500
1586
  const t0 = baseTime + this.at;
1587
+ const dur = this.duration;
1588
+ out.push({
1589
+ ...this._audioBuffer ? { buffer: this._audioBuffer } : { synth: this._synth },
1590
+ layer: describeLayer(this.spec),
1591
+ source: this._source,
1592
+ loop: !this._synth && !!this.spec.loop,
1593
+ start: t0,
1594
+ end: t0 + dur,
1595
+ initialVolume,
1596
+ volumeKeyframes: this._volumePoints(t0, dur, initialVolume)
1597
+ });
1598
+ }
1599
+ _volumePoints(t0, dur, initialVolume) {
1600
+ const volumeKeyframes = [];
1501
1601
  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
1602
  let current = initialVolume;
1503
1603
  const point = (time, value) => {
@@ -1525,17 +1625,11 @@ var AudioSequence = class extends Sequence {
1525
1625
  point(end, current);
1526
1626
  }
1527
1627
  }
1528
- out.push({
1529
- buffer: this._audioBuffer,
1530
- loop: !!this.spec.loop,
1531
- start: baseTime + this.at,
1532
- end: baseTime + this.at + dur,
1533
- initialVolume,
1534
- volumeKeyframes
1535
- });
1628
+ return volumeKeyframes;
1536
1629
  }
1537
1630
  destroy() {
1538
1631
  this._audioBuffer = null;
1632
+ this._synth = null;
1539
1633
  }
1540
1634
  };
1541
1635
 
@@ -1684,6 +1778,8 @@ var VideoSequence = class extends Sequence {
1684
1778
  const initialVolume = this.spec.volume ?? 1;
1685
1779
  out.push({
1686
1780
  buffer: this._audioBuffer,
1781
+ layer: describeLayer(this.spec),
1782
+ source: `video "${this.spec.asset}"`,
1687
1783
  loop: !!this.spec.loop,
1688
1784
  start: baseTime + this.at,
1689
1785
  end: baseTime + this.at + this.duration,
@@ -1764,19 +1860,107 @@ function makeGradientFill(g) {
1764
1860
  }
1765
1861
  return new FillGradient(gradientOptions(g));
1766
1862
  }
1863
+ var CURVE_STEPS = 24;
1864
+ var ELLIPSE_STEPS = 128;
1865
+ function flattenSvgPath(d) {
1866
+ const prims = new GraphicsPath(d).shapePath.shapePrimitives;
1867
+ const out = [];
1868
+ for (const { shape } of prims) {
1869
+ const poly = shape;
1870
+ if (poly.points && poly.points.length >= 4) out.push({ pts: poly.points.slice(), closed: poly.closePath === true });
1871
+ }
1872
+ return out;
1873
+ }
1874
+ function rectOutline(x, y, w, h, radius) {
1875
+ const r = Math.max(0, Math.min(radius, Math.abs(w) / 2, Math.abs(h) / 2));
1876
+ if (r === 0) return { pts: [x, y, x + w, y, x + w, y + h, x, y + h], closed: true };
1877
+ const pts = [];
1878
+ const corner = (cx, cy, from) => {
1879
+ for (let i = 0; i <= CURVE_STEPS; i++) {
1880
+ const a = from + i / CURVE_STEPS * (Math.PI / 2);
1881
+ pts.push(cx + r * Math.cos(a), cy + r * Math.sin(a));
1882
+ }
1883
+ };
1884
+ corner(x + w - r, y + r, -Math.PI / 2);
1885
+ corner(x + w - r, y + h - r, 0);
1886
+ corner(x + r, y + h - r, Math.PI / 2);
1887
+ corner(x + r, y + r, Math.PI);
1888
+ const startAt = pts.length - 2;
1889
+ const rotated = pts.slice(startAt).concat(pts.slice(0, startAt));
1890
+ return { pts: rotated, closed: true };
1891
+ }
1892
+ function ellipseOutline(cx, cy, rx, ry) {
1893
+ const pts = [];
1894
+ for (let i = 0; i < ELLIPSE_STEPS; i++) {
1895
+ const a = -Math.PI / 2 + i / ELLIPSE_STEPS * Math.PI * 2;
1896
+ pts.push(cx + rx * Math.cos(a), cy + ry * Math.sin(a));
1897
+ }
1898
+ return { pts, closed: true };
1899
+ }
1900
+ function trimPolylines(lines, start, end) {
1901
+ const s = Math.max(0, Math.min(1, start)), e = Math.max(0, Math.min(1, end));
1902
+ if (!(e > s)) return [];
1903
+ const lengths = lines.map(polylineLength);
1904
+ const total = lengths.reduce((a, b) => a + b, 0);
1905
+ if (!(total > 0)) return [];
1906
+ const from = s * total, to = e * total;
1907
+ const out = [];
1908
+ let offset = 0;
1909
+ lines.forEach((line, i) => {
1910
+ const len = lengths[i];
1911
+ const a = Math.max(from - offset, 0), b = Math.min(to - offset, len);
1912
+ offset += len;
1913
+ if (!(b > a) || len === 0) return;
1914
+ if (line.closed && a <= 0 && b >= len) {
1915
+ out.push({ pts: line.pts.slice(), closed: true });
1916
+ return;
1917
+ }
1918
+ out.push({ pts: slice(line, a, b), closed: false });
1919
+ });
1920
+ return out;
1921
+ }
1922
+ function points(line) {
1923
+ return line.closed ? [...line.pts, line.pts[0], line.pts[1]] : line.pts;
1924
+ }
1925
+ function polylineLength(line) {
1926
+ const p = points(line);
1927
+ let len = 0;
1928
+ for (let i = 2; i < p.length; i += 2) len += Math.hypot(p[i] - p[i - 2], p[i + 1] - p[i - 1]);
1929
+ return len;
1930
+ }
1931
+ function slice(line, a, b) {
1932
+ const p = points(line);
1933
+ const out = [];
1934
+ let at = 0;
1935
+ for (let i = 2; i < p.length; i += 2) {
1936
+ const x0 = p[i - 2], y0 = p[i - 1], x1 = p[i], y1 = p[i + 1];
1937
+ const seg = Math.hypot(x1 - x0, y1 - y0);
1938
+ const segEnd = at + seg;
1939
+ if (segEnd > a && at < b && seg > 0) {
1940
+ const t0 = Math.max(a - at, 0) / seg, t1 = Math.min(b - at, seg) / seg;
1941
+ if (out.length === 0) out.push(x0 + (x1 - x0) * t0, y0 + (y1 - y0) * t0);
1942
+ out.push(x0 + (x1 - x0) * t1, y0 + (y1 - y0) * t1);
1943
+ }
1944
+ at = segEnd;
1945
+ if (at >= b) break;
1946
+ }
1947
+ return out;
1948
+ }
1767
1949
 
1768
1950
  // src/sequences/Shape.ts
1769
1951
  var STYLE_KEYS = ["fillColor", "fillAlpha", "strokeColor", "strokeAlpha", "strokeWidth"];
1770
1952
  var COLOR_KEYS = /* @__PURE__ */ new Set(["fillColor", "strokeColor"]);
1953
+ var TRIM_KEYS = ["trimStart", "trimEnd"];
1771
1954
  var GEOMETRY_KEYS_BY_SHAPE = {
1772
- rect: ["width", "height", "cornerRadius", "anchorX", "anchorY"],
1773
- circle: ["radius", "anchorX", "anchorY"],
1774
- ellipse: ["radiusX", "radiusY", "anchorX", "anchorY"],
1775
- line: [],
1776
- polygon: [],
1777
- path: []
1955
+ rect: ["width", "height", "cornerRadius", "anchorX", "anchorY", ...TRIM_KEYS],
1956
+ circle: ["radius", "anchorX", "anchorY", ...TRIM_KEYS],
1957
+ ellipse: ["radiusX", "radiusY", "anchorX", "anchorY", ...TRIM_KEYS],
1958
+ arc: ["radius", "innerRadius", "startAngle", "endAngle"],
1959
+ line: [...TRIM_KEYS],
1960
+ polygon: [...TRIM_KEYS],
1961
+ path: [...TRIM_KEYS]
1778
1962
  };
1779
- var ANCHORED_SHAPES = /* @__PURE__ */ new Set(["rect", "circle", "ellipse"]);
1963
+ var ANCHORED_SHAPES = /* @__PURE__ */ new Set(["rect", "circle", "ellipse", "arc"]);
1780
1964
  var ShapeSequence = class extends Sequence {
1781
1965
  _state = {
1782
1966
  fillColor: void 0,
@@ -1803,12 +1987,30 @@ var ShapeSequence = class extends Sequence {
1803
1987
  for (const k of geomKeys) this._liveKeys.add(k);
1804
1988
  seedGeometry(this._state, this.spec, scope);
1805
1989
  seedStyle(this._state, this.spec.initial ?? {}, scope);
1990
+ seedStrokeStyle(this._state, this.spec, this.spec.initial ?? {}, describeLayer(this.spec));
1991
+ if (this.spec.shape === "arc") {
1992
+ this._state.startAngle ??= 0;
1993
+ this._state.endAngle ??= 360;
1994
+ const top = this.spec, ini = this.spec.initial ?? {};
1995
+ if ("trimStart" in top || "trimEnd" in top || "trimStart" in ini || "trimEnd" in ini) {
1996
+ console.warn(`pixi-effects: ${describeLayer(this.spec)}: trimStart / trimEnd do not apply to an arc: animate its endAngle (and startAngle) instead`);
1997
+ }
1998
+ } else {
1999
+ this._state.trimStart ??= 0;
2000
+ this._state.trimEnd ??= 1;
2001
+ }
1806
2002
  const gradientSpec = this.spec.fillGradient ?? this.spec.initial?.fillGradient;
1807
2003
  if (gradientSpec) this._state.fillStyle = makeGradientFill(gradientSpec);
1808
2004
  this._drawGeometry = makeDrawGeometry(this.spec, scope, this._state);
2005
+ this._outline = makeOutline(this.spec, scope, this._state);
2006
+ const { trimStart, trimEnd } = this._state;
2007
+ this._state.trimStart = 0;
2008
+ this._state.trimEnd = 1;
1809
2009
  this._drawGeometry(graphics);
1810
2010
  applyState(graphics, this._state);
1811
2011
  const bounds = graphics.getLocalBounds();
2012
+ this._state.trimStart = trimStart;
2013
+ this._state.trimEnd = trimEnd;
1812
2014
  this.intrinsicWidth = bounds.width;
1813
2015
  this.intrinsicHeight = bounds.height;
1814
2016
  if (!ANCHORED_SHAPES.has(this.spec.shape)) {
@@ -1826,14 +2028,37 @@ var ShapeSequence = class extends Sequence {
1826
2028
  }
1827
2029
  this._redraw();
1828
2030
  };
2031
+ this._redraw();
1829
2032
  this.buildFilters();
1830
2033
  }
1831
2034
  _fresh = false;
2035
+ _outline = null;
1832
2036
  _redraw() {
1833
2037
  const graphics = this.target;
2038
+ const s = this._state;
1834
2039
  graphics.clear();
1835
- this._drawGeometry(graphics);
1836
- applyState(graphics, this._state);
2040
+ const trimmed = (s.trimStart ?? 0) > 0 || (s.trimEnd ?? 1) < 1;
2041
+ if (!trimmed || !this._outline || !(s.strokeColor !== void 0 && s.strokeWidth > 0)) {
2042
+ this._drawGeometry(graphics);
2043
+ applyState(graphics, s);
2044
+ return;
2045
+ }
2046
+ if (s.fillStyle || s.fillColor !== void 0) {
2047
+ this._drawGeometry(graphics);
2048
+ applyFill(graphics, s);
2049
+ graphics.beginPath();
2050
+ }
2051
+ const parts = trimPolylines(this._outline(), s.trimStart ?? 0, s.trimEnd ?? 1);
2052
+ if (parts.length === 0) return;
2053
+ for (const part of parts) {
2054
+ if (part.closed) {
2055
+ graphics.poly(part.pts, true);
2056
+ continue;
2057
+ }
2058
+ graphics.moveTo(part.pts[0], part.pts[1]);
2059
+ for (let i = 2; i < part.pts.length; i += 2) graphics.lineTo(part.pts[i], part.pts[i + 1]);
2060
+ }
2061
+ applyStroke(graphics, s);
1837
2062
  }
1838
2063
  /**
1839
2064
  * Draw from the tweened `_state` before the culler runs. The culler measures the geometry as last
@@ -1871,6 +2096,21 @@ function seedGeometry(state, spec, scope) {
1871
2096
  if (v !== void 0) state[k] = num(v, scope);
1872
2097
  }
1873
2098
  }
2099
+ var STROKE_CAPS = ["butt", "round", "square"];
2100
+ var STROKE_JOINS = ["miter", "round", "bevel"];
2101
+ function seedStrokeStyle(state, spec, initial, who) {
2102
+ const top = spec;
2103
+ const ini = initial;
2104
+ const pick = (key, valid) => {
2105
+ const v = top[key] ?? ini[key];
2106
+ if (v === void 0) return void 0;
2107
+ if (valid.includes(String(v))) return String(v);
2108
+ console.warn(`pixi-effects: ${who}: ${key} "${v}" is not one of ${valid.join(", ")}; ignored`);
2109
+ return void 0;
2110
+ };
2111
+ state.strokeCap = pick("strokeCap", STROKE_CAPS);
2112
+ state.strokeJoin = pick("strokeJoin", STROKE_JOINS);
2113
+ }
1874
2114
  function seedStyle(state, initial, scope) {
1875
2115
  for (const k of STYLE_KEYS) {
1876
2116
  const v = initial[k];
@@ -1879,8 +2119,8 @@ function seedStyle(state, initial, scope) {
1879
2119
  }
1880
2120
  }
1881
2121
  function stripGradient(props) {
1882
- if (!props || !("fillGradient" in props)) return props;
1883
- const { fillGradient: _drop, ...rest } = props;
2122
+ if (!props || !("fillGradient" in props || "strokeCap" in props || "strokeJoin" in props)) return props;
2123
+ const { fillGradient: _drop, strokeCap: _cap, strokeJoin: _join, ...rest } = props;
1884
2124
  return rest;
1885
2125
  }
1886
2126
  function stripLive(props, liveKeys) {
@@ -1921,13 +2161,25 @@ function resolveLiveValue(key, value, scope) {
1921
2161
  return numOrZero(value, scope);
1922
2162
  }
1923
2163
  function applyState(g, s) {
2164
+ applyFill(g, s);
2165
+ applyStroke(g, s);
2166
+ }
2167
+ function applyFill(g, s) {
1924
2168
  if (s.fillStyle) {
1925
2169
  g.fill(s.fillStyle);
1926
2170
  } else if (s.fillColor !== void 0) {
1927
2171
  g.fill({ color: s.fillColor, alpha: s.fillAlpha });
1928
2172
  }
2173
+ }
2174
+ function applyStroke(g, s) {
1929
2175
  if (s.strokeColor !== void 0 && s.strokeWidth > 0) {
1930
- g.stroke({ color: s.strokeColor, alpha: s.strokeAlpha, width: s.strokeWidth });
2176
+ g.stroke({
2177
+ color: s.strokeColor,
2178
+ alpha: s.strokeAlpha,
2179
+ width: s.strokeWidth,
2180
+ ...s.strokeCap ? { cap: s.strokeCap } : {},
2181
+ ...s.strokeJoin ? { join: s.strokeJoin } : {}
2182
+ });
1931
2183
  }
1932
2184
  }
1933
2185
  function drawRectFromState(g, s) {
@@ -1954,6 +2206,34 @@ function drawEllipseFromState(g, s) {
1954
2206
  const ay = s.anchorY ?? 0.5;
1955
2207
  g.ellipse((0.5 - ax) * 2 * rx, (0.5 - ay) * 2 * ry, rx, ry);
1956
2208
  }
2209
+ var DEG2 = Math.PI / 180;
2210
+ function drawArcFromState(g, s) {
2211
+ const r = s.radius ?? 0;
2212
+ const ri = Math.max(0, Math.min(s.innerRadius ?? 0, r));
2213
+ const startDeg = s.startAngle ?? 0;
2214
+ let endDeg = s.endAngle ?? 360;
2215
+ if (!(r > 0) || endDeg === startDeg) return;
2216
+ const filled = !!s.fillStyle || s.fillColor !== void 0;
2217
+ if (Math.abs(endDeg - startDeg) >= 360) {
2218
+ if (!filled) {
2219
+ g.circle(0, 0, r);
2220
+ return;
2221
+ }
2222
+ endDeg = startDeg + Math.sign(endDeg - startDeg) * 359.999;
2223
+ }
2224
+ const a0 = startDeg * DEG2, a1 = endDeg * DEG2;
2225
+ const ccw = a1 < a0;
2226
+ if (filled) g.moveTo(r * Math.cos(a0), r * Math.sin(a0));
2227
+ g.arc(0, 0, r, a0, a1, ccw);
2228
+ if (!filled) return;
2229
+ if (ri > 0) {
2230
+ g.lineTo(ri * Math.cos(a1), ri * Math.sin(a1));
2231
+ g.arc(0, 0, ri, a1, a0, !ccw);
2232
+ } else {
2233
+ g.lineTo(0, 0);
2234
+ }
2235
+ g.closePath();
2236
+ }
1957
2237
  function makeLineDraw(spec, scope) {
1958
2238
  const fx = num(spec.from[0], scope);
1959
2239
  const fy = num(spec.from[1], scope);
@@ -1982,7 +2262,8 @@ function makePathDraw(spec) {
1982
2262
  var STATE_DRAWERS = {
1983
2263
  rect: drawRectFromState,
1984
2264
  circle: drawCircleFromState,
1985
- ellipse: drawEllipseFromState
2265
+ ellipse: drawEllipseFromState,
2266
+ arc: drawArcFromState
1986
2267
  };
1987
2268
  function makeDrawGeometry(spec, scope, state) {
1988
2269
  const stateDrawer = STATE_DRAWERS[spec.shape];
@@ -1998,6 +2279,41 @@ function makeDrawGeometry(spec, scope, state) {
1998
2279
  return () => {
1999
2280
  };
2000
2281
  }
2282
+ function makeOutline(spec, scope, s) {
2283
+ switch (spec.shape) {
2284
+ case "rect":
2285
+ return () => {
2286
+ const w = s.width ?? 0, h = s.height ?? 0;
2287
+ return [rectOutline(0 - (s.anchorX ?? 0.5) * w, 0 - (s.anchorY ?? 0.5) * h, w, h, s.cornerRadius ?? 0)];
2288
+ };
2289
+ case "circle":
2290
+ return () => {
2291
+ const r = s.radius ?? 0;
2292
+ return [ellipseOutline((0.5 - (s.anchorX ?? 0.5)) * 2 * r, (0.5 - (s.anchorY ?? 0.5)) * 2 * r, r, r)];
2293
+ };
2294
+ case "ellipse":
2295
+ return () => {
2296
+ const rx = s.radiusX ?? 0, ry = s.radiusY ?? 0;
2297
+ return [ellipseOutline((0.5 - (s.anchorX ?? 0.5)) * 2 * rx, (0.5 - (s.anchorY ?? 0.5)) * 2 * ry, rx, ry)];
2298
+ };
2299
+ case "line": {
2300
+ const lines = [{ pts: [num(spec.from[0], scope), num(spec.from[1], scope), num(spec.to[0], scope), num(spec.to[1], scope)], closed: false }];
2301
+ return () => lines;
2302
+ }
2303
+ case "polygon": {
2304
+ const pts = [];
2305
+ for (const [x, y] of spec.points) pts.push(num(x, scope), num(y, scope));
2306
+ const lines = pts.length >= 4 ? [{ pts, closed: !spec.open }] : [];
2307
+ return () => lines;
2308
+ }
2309
+ case "path": {
2310
+ let lines = null;
2311
+ return () => lines ??= flattenSvgPath(spec.d);
2312
+ }
2313
+ default:
2314
+ return null;
2315
+ }
2316
+ }
2001
2317
  function bindLiveKeyframes(timeline, state, liveKeys, keyframes, parentDuration, scope, offset, colorSpace) {
2002
2318
  for (const kf of keyframes) {
2003
2319
  const at = offset + resolveAt(kf.at, parentDuration);
@@ -2071,7 +2387,7 @@ function numOrZero(v, scope) {
2071
2387
  var _CompositionSequence = null;
2072
2388
  async function getCompositionSequence() {
2073
2389
  if (!_CompositionSequence) {
2074
- _CompositionSequence = (await import('./Composition-RGTRZ2JX.js')).CompositionSequence;
2390
+ _CompositionSequence = (await import('./Composition-7SO5WYHU.js')).CompositionSequence;
2075
2391
  }
2076
2392
  return _CompositionSequence;
2077
2393
  }
@@ -2108,6 +2424,6 @@ async function buildSequenceTree(specs, parent, root) {
2108
2424
  return out;
2109
2425
  }
2110
2426
 
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
2427
+ export { CameraSequence, DEFAULT_FOV, DEG, NEAR, Sequence, buildScope, buildSequenceTree, cameraBasis, collectPropKeys, describeLayer, evaluateExpr, homeCamera, homeDistance, lintText, lintTiming, projectLayer, registerSequenceType, resolveAt, summarizeWarnings };
2428
+ //# sourceMappingURL=chunk-7IJHJXDI.js.map
2429
+ //# sourceMappingURL=chunk-7IJHJXDI.js.map