@pixodesk/svg-animator-web 1.0.16 → 1.0.17

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.
package/dist/index.js CHANGED
@@ -45,7 +45,7 @@ function readAnimatable(raw) {
45
45
  if (typeof raw === "object") {
46
46
  const obj = raw;
47
47
  if (obj.keyframes) {
48
- return { kind: "animated" /* Animated */, keyframes: obj.keyframes, autoOrient: obj.autoOrient };
48
+ return { kind: "animated" /* Animated */, keyframes: obj.keyframes, autoOrient: obj.autoOrient, loop: obj.loop };
49
49
  }
50
50
  if (obj.value !== void 0) return { kind: "static" /* Static */, value: obj.value };
51
51
  }
@@ -135,6 +135,7 @@ function wrapTransformPart(inner, part, raw, ctx) {
135
135
  if (v.kind === "animated" /* Animated */) {
136
136
  const animTr = { keyframes: v.keyframes.map((kf) => keyframeWith(kf, partsRecord(part, kf.value, void 0))) };
137
137
  if (v.autoOrient) animTr.autoOrient = true;
138
+ if (v.loop !== void 0) animTr.loop = v.loop;
138
139
  return {
139
140
  type: "g",
140
141
  animate: { transform: animTr },
@@ -153,9 +154,11 @@ function wrapOrigin(inner, raw, invert) {
153
154
  return { type: "g", transform: { value: { translate: sign(v.value) } }, children: [inner] };
154
155
  }
155
156
  if (v.kind === "animated" /* Animated */) {
157
+ const animTr = { keyframes: v.keyframes.map((kf) => keyframeWith(kf, { translate: sign(kf.value) })) };
158
+ if (v.loop !== void 0) animTr.loop = v.loop;
156
159
  return {
157
160
  type: "g",
158
- animate: { transform: { keyframes: v.keyframes.map((kf) => keyframeWith(kf, { translate: sign(kf.value) })) } },
161
+ animate: { transform: animTr },
159
162
  children: [inner]
160
163
  };
161
164
  }
@@ -165,8 +168,8 @@ function wrapOrigin(inner, raw, invert) {
165
168
  // src/effects/contentRefSplit.ts
166
169
  function identifyContentRefTargets(node, ctx, allocator) {
167
170
  var _a, _b, _c;
168
- if (node.type === "use" && ((_b = (_a = node.effects) == null ? void 0 : _a.ref) == null ? void 0 : _b.type) === "content") {
169
- const baseId = node.effects.ref.baseId;
171
+ if (node.type === "use" && ((_b = (_a = node.effects) == null ? void 0 : _a.clone) == null ? void 0 : _b.type) === "content") {
172
+ const baseId = node.effects.clone.baseId;
170
173
  if (typeof baseId === "string" && baseId && !ctx.contentRefInnerIds.has(baseId)) {
171
174
  ctx.contentRefInnerIds.set(baseId, allocator(baseId));
172
175
  }
@@ -214,6 +217,8 @@ function liftBodyTranslate(node, transformation) {
214
217
  });
215
218
  const outerAnimTr = { keyframes: outerKfs };
216
219
  if (animTr.autoOrient) outerAnimTr.autoOrient = true;
220
+ const srcLoop = animTr.loop;
221
+ if (srcLoop !== void 0) outerAnimTr.loop = srcLoop;
217
222
  out.animate = { transform: outerAnimTr };
218
223
  const innerHasPivotedPart = kfs.some((kf) => {
219
224
  const v = kf.value || {};
@@ -235,7 +240,9 @@ function liftBodyTranslate(node, transformation) {
235
240
  delete node.animate.transform;
236
241
  if (node.animate && Object.keys(node.animate).length === 0) delete node.animate;
237
242
  } else {
238
- node.animate.transform = { keyframes: innerKfs };
243
+ const innerAnimTr = { keyframes: innerKfs };
244
+ if (srcLoop !== void 0) innerAnimTr.loop = srcLoop;
245
+ node.animate.transform = innerAnimTr;
239
246
  }
240
247
  didLiftAnimate = true;
241
248
  }
@@ -975,9 +982,10 @@ var PxRetimeEffectSchema = implementsInterface()(px.object({
975
982
  stretch: px.number().optional(),
976
983
  timeCrop: px.tuple([px.number(), px.number()]).optional()
977
984
  }));
978
- var PxRefEffectSchema = implementsInterface()(px.object({
985
+ var PxCloneEffectSchema = implementsInterface()(px.object({
986
+ type: px.string().optional(),
979
987
  baseId: px.string().optional(),
980
- type: px.string().optional()
988
+ retime: PxRetimeEffectSchema.optional()
981
989
  }));
982
990
  var PxGradientUnits = {
983
991
  userSpaceOnUse: "userSpaceOnUse",
@@ -1027,9 +1035,8 @@ var PxEffectsSchema = implementsInterface()(px.object({
1027
1035
  repeater: PxRepeaterEffectSchema.optional(),
1028
1036
  maskedBy: PxMaskedByEffectSchema.optional(),
1029
1037
  trimPath: PxTrimPathEffectSchema.optional(),
1030
- retime: PxRetimeEffectSchema.optional(),
1038
+ clone: PxCloneEffectSchema.optional(),
1031
1039
  isCombinedShape: px.boolean().optional(),
1032
- ref: PxRefEffectSchema.optional(),
1033
1040
  fillGradient: PxFillGradientEffectSchema.optional(),
1034
1041
  strokeGradient: PxStrokeGradientEffectSchema.optional(),
1035
1042
  textAlongPath: PxTextAlongPathEffectSchema.optional()
@@ -1257,6 +1264,7 @@ function buildStopChildren(stops, ctx) {
1257
1264
  const animBlock = stops;
1258
1265
  const kfs = animBlock.keyframes;
1259
1266
  if (!Array.isArray(kfs) || !kfs.length) return [];
1267
+ const loopFromSource = animBlock.loop;
1260
1268
  let stopCount = 0;
1261
1269
  for (const kf of kfs) {
1262
1270
  const v = (_a = kf.value) != null ? _a : kf.v;
@@ -1269,7 +1277,7 @@ function buildStopChildren(stops, ctx) {
1269
1277
  const s = (_d = (_c = firstKfValue == null ? void 0 : firstKfValue[i]) != null ? _c : prevDefinedStop(kfs, 0, i)) != null ? _d : { offset: i / Math.max(1, stopCount - 1), color: "#000000" };
1270
1278
  baselineStops.push({ offset: s.offset, color: s.color });
1271
1279
  }
1272
- return baselineStops.map((bs, i) => animatedStopNode(bs, kfs, i, ctx));
1280
+ return baselineStops.map((bs, i) => animatedStopNode(bs, kfs, i, ctx, loopFromSource));
1273
1281
  }
1274
1282
  function staticStopNode(s) {
1275
1283
  return {
@@ -1278,7 +1286,7 @@ function staticStopNode(s) {
1278
1286
  stopColor: s.color
1279
1287
  };
1280
1288
  }
1281
- function animatedStopNode(baseline, kfs, stopIdx, _ctx) {
1289
+ function animatedStopNode(baseline, kfs, stopIdx, _ctx, loop) {
1282
1290
  var _a, _b, _c, _d, _e;
1283
1291
  const colorKfs = [];
1284
1292
  const offsetKfs = [];
@@ -1303,8 +1311,14 @@ function animatedStopNode(baseline, kfs, stopIdx, _ctx) {
1303
1311
  stopColor: baseline.color
1304
1312
  };
1305
1313
  const animate = {};
1306
- if (colorKfs.length) animate.stopColor = { keyframes: colorKfs };
1307
- if (offsetVaries && offsetKfs.length) animate.offset = { keyframes: offsetKfs };
1314
+ if (colorKfs.length) {
1315
+ animate.stopColor = { keyframes: colorKfs };
1316
+ if (loop !== void 0) animate.stopColor.loop = loop;
1317
+ }
1318
+ if (offsetVaries && offsetKfs.length) {
1319
+ animate.offset = { keyframes: offsetKfs };
1320
+ if (loop !== void 0) animate.offset.loop = loop;
1321
+ }
1308
1322
  if (Object.keys(animate).length) stop.animate = animate;
1309
1323
  return stop;
1310
1324
  }
@@ -1369,9 +1383,11 @@ function wrapInversePart(inner, part, raw, origin, ctx) {
1369
1383
  return { type: "g", transform: { value: partsRecord(part, invertPartValue(part, v.value), origin) }, children: [inner] };
1370
1384
  }
1371
1385
  if (v.kind === "animated" /* Animated */) {
1386
+ const animTr = { keyframes: v.keyframes.map((kf) => keyframeWith(kf, partsRecord(part, invertPartValue(part, kf.value), origin))) };
1387
+ if (v.loop !== void 0) animTr.loop = v.loop;
1372
1388
  return {
1373
1389
  type: "g",
1374
- animate: { transform: { keyframes: v.keyframes.map((kf) => keyframeWith(kf, partsRecord(part, invertPartValue(part, kf.value), origin))) } },
1390
+ animate: { transform: animTr },
1375
1391
  children: [inner]
1376
1392
  };
1377
1393
  }
@@ -1408,10 +1424,16 @@ function wrapInverseAnimatedBodyTransform(inner, node, _ctx) {
1408
1424
  scaleKfs.push(__spreadProps(__spreadValues({}, baseKf), { value: rec }));
1409
1425
  }
1410
1426
  }
1427
+ const srcLoop = animTr == null ? void 0 : animTr.loop;
1428
+ const withLoop = (kfs2) => {
1429
+ const block = { keyframes: kfs2 };
1430
+ if (srcLoop !== void 0) block.loop = srcLoop;
1431
+ return block;
1432
+ };
1411
1433
  let n = inner;
1412
- if (translateKfs.length) n = { type: "g", animate: { transform: { keyframes: translateKfs } }, children: [n] };
1413
- if (rotateKfs.length) n = { type: "g", animate: { transform: { keyframes: rotateKfs } }, children: [n] };
1414
- if (scaleKfs.length) n = { type: "g", animate: { transform: { keyframes: scaleKfs } }, children: [n] };
1434
+ if (translateKfs.length) n = { type: "g", animate: { transform: withLoop(translateKfs) }, children: [n] };
1435
+ if (rotateKfs.length) n = { type: "g", animate: { transform: withLoop(rotateKfs) }, children: [n] };
1436
+ if (scaleKfs.length) n = { type: "g", animate: { transform: withLoop(scaleKfs) }, children: [n] };
1415
1437
  return n;
1416
1438
  }
1417
1439
  function nodeHasBodyTransform(node) {
@@ -1645,16 +1667,18 @@ function parseTranslateOnlyFromString(s, ctx) {
1645
1667
 
1646
1668
  // src/effects/refEffect.ts
1647
1669
  var CONTENT_SUBREF = "content";
1648
- function applyRefAndTransformationEffect(node, ref, transformation, ctx) {
1649
- if (ref) {
1650
- const baseId = ref.baseId;
1651
- if (!baseId) {
1652
- ctx.errors.push("ref: missing baseId");
1653
- } else {
1654
- const targetId = ref.type === CONTENT_SUBREF ? ctx.contentRefInnerIds.get(baseId) || baseId : baseId;
1655
- node.href = "#" + targetId;
1656
- }
1670
+ function applyRefHref(node, clone2, ctx) {
1671
+ if (!clone2) return;
1672
+ const baseId = clone2.baseId;
1673
+ if (!baseId) {
1674
+ if (clone2.type === CONTENT_SUBREF) ctx.errors.push("clone: content ref missing baseId");
1675
+ return;
1657
1676
  }
1677
+ const targetId = clone2.type === CONTENT_SUBREF ? ctx.contentRefInnerIds.get(baseId) || baseId : baseId;
1678
+ node.href = "#" + targetId;
1679
+ }
1680
+ function applyRefAndTransformationEffect(node, clone2, transformation, ctx) {
1681
+ applyRefHref(node, clone2, ctx);
1658
1682
  return applyTransformationEffect(node, transformation, ctx);
1659
1683
  }
1660
1684
 
@@ -1765,22 +1789,32 @@ function concatRetime(child, parent) {
1765
1789
  stretch: parent.stretch * child.stretch
1766
1790
  };
1767
1791
  }
1768
- function applyAllRetimeEffects(root, ctx) {
1792
+ function readCloneRetime(n) {
1793
+ var _a, _b;
1794
+ return (_b = (_a = n.effects) == null ? void 0 : _a.clone) == null ? void 0 : _b.retime;
1795
+ }
1796
+ function clearCloneRetime(n) {
1769
1797
  var _a;
1798
+ const clone2 = (_a = n.effects) == null ? void 0 : _a.clone;
1799
+ if (!clone2) return;
1800
+ delete clone2.retime;
1801
+ if (Object.keys(clone2).length === 0) delete n.effects.clone;
1802
+ if (n.effects && Object.keys(n.effects).length === 0) delete n.effects;
1803
+ }
1804
+ function applyAllRetimeEffects(root, ctx) {
1770
1805
  ctx.idMap.clear();
1771
1806
  indexById(root, ctx.idMap);
1772
1807
  const sites = [];
1773
1808
  const collect = (n) => {
1774
- var _a2, _b;
1775
- if ((_a2 = n.effects) == null ? void 0 : _a2.retime) sites.push(n);
1776
- (_b = n.children) == null ? void 0 : _b.forEach(collect);
1809
+ var _a;
1810
+ if (readCloneRetime(n)) sites.push(n);
1811
+ (_a = n.children) == null ? void 0 : _a.forEach(collect);
1777
1812
  };
1778
1813
  collect(root);
1779
1814
  for (const useNode of sites) {
1780
- const retime = (_a = useNode.effects) == null ? void 0 : _a.retime;
1815
+ const retime = readCloneRetime(useNode);
1781
1816
  if (!retime) continue;
1782
- delete useNode.effects.retime;
1783
- if (Object.keys(useNode.effects).length === 0) delete useNode.effects;
1817
+ clearCloneRetime(useNode);
1784
1818
  materialiseRetime(useNode, asRetime(retime), ctx);
1785
1819
  }
1786
1820
  }
@@ -1804,7 +1838,6 @@ function materialiseRetime(useNode, retime, ctx) {
1804
1838
  }
1805
1839
  }
1806
1840
  function buildChainClone(targetId, accum, ctx, chain) {
1807
- var _a, _b;
1808
1841
  if (chain.has(targetId)) {
1809
1842
  ctx.errors.push('retime: loop via "' + targetId + '"');
1810
1843
  return void 0;
@@ -1821,25 +1854,43 @@ function buildChainClone(targetId, accum, ctx, chain) {
1821
1854
  } else {
1822
1855
  remapKeyframeTimes(cloneNode, accum.start, accum.stretch);
1823
1856
  }
1824
- if ((_a = cloneNode.effects) == null ? void 0 : _a.retime) {
1825
- delete cloneNode.effects.retime;
1826
- if (Object.keys(cloneNode.effects).length === 0) delete cloneNode.effects;
1827
- }
1857
+ clearCloneRetime(cloneNode);
1828
1858
  if (target.type === "use" && target.href) {
1829
1859
  const subId = stripHash(target.href);
1830
1860
  if (subId) {
1831
- const innerRetime = (_b = target.effects) == null ? void 0 : _b.retime;
1861
+ const innerRetime = readCloneRetime(target);
1832
1862
  const subAccum = innerRetime ? concatRetime(asRetime(innerRetime), accum) : accum;
1833
1863
  const subChain = new Set(chain);
1834
1864
  subChain.add(targetId);
1835
1865
  const subId2 = buildChainClone(subId, subAccum, ctx, subChain);
1836
1866
  if (subId2) cloneNode.href = "#" + subId2;
1837
1867
  }
1868
+ } else {
1869
+ materialiseNestedRetimeUses(cloneNode, accum, ctx, chain, targetId);
1838
1870
  }
1839
1871
  ctx.defs.push(cloneNode);
1840
1872
  if (typeof cloneNode.id === "string") ctx.idMap.set(cloneNode.id, cloneNode);
1841
1873
  return typeof cloneNode.id === "string" ? cloneNode.id : void 0;
1842
1874
  }
1875
+ function materialiseNestedRetimeUses(node, accum, ctx, chain, parentTargetId) {
1876
+ const visit = (n) => {
1877
+ var _a;
1878
+ const retime = readCloneRetime(n);
1879
+ if (n.type === "use" && retime && n.href) {
1880
+ const subId = stripHash(n.href);
1881
+ if (subId) {
1882
+ const subAccum = concatRetime(asRetime(retime), accum);
1883
+ const subChain = new Set(chain);
1884
+ subChain.add(parentTargetId);
1885
+ const subId2 = buildChainClone(subId, subAccum, ctx, subChain);
1886
+ if (subId2) n.href = "#" + subId2;
1887
+ }
1888
+ clearCloneRetime(n);
1889
+ }
1890
+ (_a = n.children) == null ? void 0 : _a.forEach(visit);
1891
+ };
1892
+ visit(node);
1893
+ }
1843
1894
  function remapKeyframeTimes(node, start, stretch) {
1844
1895
  var _a;
1845
1896
  remapKeyframeTimesOnly(node, start, stretch);
@@ -1884,12 +1935,14 @@ function applyAnimatableNumber(node, attrName, raw) {
1884
1935
  node[attrName] = String(raw);
1885
1936
  return;
1886
1937
  }
1887
- if (typeof raw === "object") {
1938
+ if (typeof raw === "object" && raw !== null) {
1888
1939
  const obj = raw;
1889
1940
  if (Array.isArray(obj.keyframes)) {
1890
1941
  const prevAnimate = node.animate && typeof node.animate === "object" && !Array.isArray(node.animate) ? node.animate : void 0;
1891
1942
  const animate = __spreadValues({}, prevAnimate || {});
1892
- animate[attrName] = { keyframes: obj.keyframes };
1943
+ const block = { keyframes: obj.keyframes };
1944
+ if (obj.loop !== void 0) block.loop = obj.loop;
1945
+ animate[attrName] = block;
1893
1946
  node.animate = animate;
1894
1947
  return;
1895
1948
  }
@@ -2952,16 +3005,67 @@ function expandLoopKeyframes(propName, keyframes, loop, duration) {
2952
3005
  looped.push(pushed);
2953
3006
  }
2954
3007
  }
2955
- for (let rep = 0; rep < fullReps; rep++) {
2956
- const distFromBoundary = loop.before ? fullReps - 1 - rep : rep;
2957
- const isReversed = !!loop.alternate && distFromBoundary % 2 === 0;
2958
- const repStart = fillStart + rep * segDuration;
2959
- appendRep(repStart, isReversed);
3008
+ function appendRepTail(repStart, isReversed, tailFraction) {
3009
+ let entries;
3010
+ if (isReversed) {
3011
+ entries = [];
3012
+ for (let i = template.length - 1; i >= 0; i--) {
3013
+ entries.push({
3014
+ relT: 1 - template[i].relT,
3015
+ v: template[i].v,
3016
+ e: i > 0 ? reverseEasing(template[i - 1].e) : void 0,
3017
+ tangentIn: template[i].tangentOut,
3018
+ tangentOut: template[i].tangentIn
3019
+ });
3020
+ }
3021
+ } else {
3022
+ entries = template;
3023
+ }
3024
+ const startRelT = 1 - tailFraction;
3025
+ for (let i = 0; i < entries.length; i++) {
3026
+ const entry = entries[i];
3027
+ if (entry.relT < startRelT - 1e-9) continue;
3028
+ const prev = entries[i - 1];
3029
+ if (prev && prev.relT < startRelT - 1e-9 && entry.relT > startRelT + 1e-9) {
3030
+ const intervalSpan = entry.relT - prev.relT;
3031
+ const localFrac = (startRelT - prev.relT) / intervalSpan;
3032
+ const easedFrac = prev.e ? cubicBezier(prev.e)(localFrac) : localFrac;
3033
+ const startValue = interpolateValue(propName, prev.v, entry.v, easedFrac);
3034
+ const { right: rightEasing } = splitEasing(prev.e, localFrac);
3035
+ looped.push({ t: repStart, v: startValue, e: rightEasing });
3036
+ }
3037
+ const pushed = {
3038
+ t: repStart + (entry.relT - startRelT) * segDuration,
3039
+ v: entry.v,
3040
+ e: i < entries.length - 1 ? entry.e : void 0
3041
+ };
3042
+ if (entry.tangentIn) pushed.tangentIn = entry.tangentIn;
3043
+ if (entry.tangentOut) pushed.tangentOut = entry.tangentOut;
3044
+ looped.push(pushed);
3045
+ }
2960
3046
  }
2961
- if (partialFraction > 1e-9) {
2962
- const isReversed = !!loop.alternate && fullReps % 2 === 0;
2963
- const repStart = fillStart + fullReps * segDuration;
2964
- appendRep(repStart, isReversed, partialFraction);
3047
+ if (loop.before) {
3048
+ if (partialFraction > 1e-9) {
3049
+ const isReversed = !!loop.alternate && fullReps % 2 === 0;
3050
+ appendRepTail(fillStart, isReversed, partialFraction);
3051
+ }
3052
+ for (let rep = 0; rep < fullReps; rep++) {
3053
+ const distFromBoundary = fullReps - 1 - rep;
3054
+ const isReversed = !!loop.alternate && distFromBoundary % 2 === 0;
3055
+ const repStart = fillStart + remainder + rep * segDuration;
3056
+ appendRep(repStart, isReversed);
3057
+ }
3058
+ } else {
3059
+ for (let rep = 0; rep < fullReps; rep++) {
3060
+ const isReversed = !!loop.alternate && rep % 2 === 0;
3061
+ const repStart = fillStart + rep * segDuration;
3062
+ appendRep(repStart, isReversed);
3063
+ }
3064
+ if (partialFraction > 1e-9) {
3065
+ const isReversed = !!loop.alternate && fullReps % 2 === 0;
3066
+ const repStart = fillStart + fullReps * segDuration;
3067
+ appendRep(repStart, isReversed, partialFraction);
3068
+ }
2965
3069
  }
2966
3070
  if (loop.before) {
2967
3071
  return [...looped, ...keyframes];
@@ -3280,7 +3384,6 @@ function applyTrimPathEffect(node, trimPath, isCombinedShape, ctx) {
3280
3384
  if (!trimPath) return node;
3281
3385
  const trimAllAsOne = !!trimPath.trimAllAsOne;
3282
3386
  const leafEntries = [];
3283
- let acc = 0;
3284
3387
  const measure = (n) => {
3285
3388
  if (Array.isArray(n.children) && n.children.length > 0) {
3286
3389
  for (const ch of n.children) measure(ch);
@@ -3292,15 +3395,22 @@ function applyTrimPathEffect(node, trimPath, isCombinedShape, ctx) {
3292
3395
  if (!subpaths.length) return;
3293
3396
  const entry = { leaf: n, subpaths: [] };
3294
3397
  for (const sp of subpaths) {
3295
- if (!trimAllAsOne) acc = 0;
3296
3398
  const lengthPx = pxBezierPathLength(sp);
3297
- entry.subpaths.push({ subpath: sp, lengthPx, startOffsetPx: acc });
3298
- acc += lengthPx;
3399
+ entry.subpaths.push({ subpath: sp, lengthPx, startOffsetPx: 0 });
3299
3400
  }
3300
3401
  leafEntries.push(entry);
3301
3402
  };
3302
3403
  measure(node);
3303
3404
  if (!leafEntries.length) return node;
3405
+ let acc = 0;
3406
+ const iterOrder = trimAllAsOne ? [...leafEntries].reverse() : leafEntries;
3407
+ for (const entry of iterOrder) {
3408
+ for (const sp of entry.subpaths) {
3409
+ if (!trimAllAsOne) acc = 0;
3410
+ sp.startOffsetPx = acc;
3411
+ acc += sp.lengthPx;
3412
+ }
3413
+ }
3304
3414
  const chainLengthPx = acc;
3305
3415
  if (trimAllAsOne && chainLengthPx < 1e-3) return node;
3306
3416
  const offsetReadRaw = readAnimatable(trimPath.offset);
@@ -3427,10 +3537,12 @@ function computeAnimAttr(read, map) {
3427
3537
  value: map((_c = kf.value) != null ? _c : kf.v),
3428
3538
  easing: (_d = kf.easing) != null ? _d : kf.e
3429
3539
  };
3430
- })
3540
+ }),
3541
+ loop: read.loop
3431
3542
  };
3432
3543
  }
3433
3544
  function applyAttr(node, attrName, attr) {
3545
+ var _a, _b;
3434
3546
  if (!attr) return;
3435
3547
  if (attr.kind === "static" /* Static */) {
3436
3548
  node[attrName] = attr.value;
@@ -3438,9 +3550,16 @@ function applyAttr(node, attrName, attr) {
3438
3550
  }
3439
3551
  const prevAnimate = node.animate && typeof node.animate === "object" && !Array.isArray(node.animate) ? node.animate : void 0;
3440
3552
  const animate = __spreadValues({}, prevAnimate || {});
3441
- animate[attrName] = { keyframes: attr.keyframes };
3553
+ const block = { keyframes: attr.keyframes };
3554
+ if (attr.loop !== void 0) block.loop = attr.loop;
3555
+ animate[attrName] = block;
3442
3556
  node.animate = animate;
3557
+ const firstKf = attr.keyframes[0];
3558
+ if (firstKf && ((_a = firstKf.value) != null ? _a : firstKf.v) !== void 0) {
3559
+ node[attrName] = (_b = firstKf.value) != null ? _b : firstKf.v;
3560
+ }
3443
3561
  }
3562
+ var OPACITY_STEP_MS = 10;
3444
3563
  function computeOpacityFromRange(rangeRead) {
3445
3564
  var _a, _b, _c, _d, _e, _f;
3446
3565
  const hide = (v) => v[0] === v[1];
@@ -3466,9 +3585,9 @@ function computeOpacityFromRange(rangeRead) {
3466
3585
  const nextHide = nextKf ? thisHide && hide((_f = nextKf.value) != null ? _f : nextKf.v) : thisHide;
3467
3586
  if (prevHide && !nextHide) {
3468
3587
  out.push({ time: t, value: 0 });
3469
- out.push({ time: t + 1, value: 1 });
3588
+ out.push({ time: t + OPACITY_STEP_MS, value: 1 });
3470
3589
  } else if (!prevHide && nextHide) {
3471
- out.push({ time: t - 1, value: 1 });
3590
+ out.push({ time: t - OPACITY_STEP_MS, value: 1 });
3472
3591
  out.push({ time: t, value: 0 });
3473
3592
  }
3474
3593
  }
@@ -3601,6 +3720,7 @@ function rectToPathD(node) {
3601
3720
 
3602
3721
  // src/effects/PlayerEffectsUtil.ts
3603
3722
  function applyPlayerEffects(root) {
3723
+ var _a;
3604
3724
  const ctx = {
3605
3725
  defs: [],
3606
3726
  warnings: [],
@@ -3608,7 +3728,10 @@ function applyPlayerEffects(root) {
3608
3728
  idMap: /* @__PURE__ */ new Map(),
3609
3729
  nextId: 0,
3610
3730
  contentRefInnerIds: /* @__PURE__ */ new Map(),
3611
- maskAncestorChains: /* @__PURE__ */ new Map()
3731
+ maskAncestorChains: /* @__PURE__ */ new Map(),
3732
+ // Resolved engine: `frames` ONLY when explicitly set; auto/webapi/unset →
3733
+ // webapi (we're not 100% sure it's frames, and CSS/WAAPI need the inline form).
3734
+ engine: ((_a = getAnimatorConfig(root)) == null ? void 0 : _a.mode) === PxAnimatorMode.frames ? PxAnimatorEngine.frames : PxAnimatorEngine.webapi
3612
3735
  };
3613
3736
  const working = clone(root);
3614
3737
  indexById(working, ctx.idMap);
@@ -3625,7 +3748,7 @@ function applyPlayerEffects_exceptRetime(node, ctx) {
3625
3748
  const originalId = typeof node.id === "string" ? node.id : void 0;
3626
3749
  const innerIdForContentRef = originalId ? ctx.contentRefInnerIds.get(originalId) : void 0;
3627
3750
  if (!fx && !innerIdForContentRef) return node;
3628
- const { transformation, repeater, maskedBy, trimPath, retime, ref, fillGradient, strokeGradient, textAlongPath } = fx != null ? fx : {};
3751
+ const { transformation, repeater, maskedBy, trimPath, clone: cloneFx, fillGradient, strokeGradient, textAlongPath } = fx != null ? fx : {};
3629
3752
  const isCombinedShape = fx == null ? void 0 : fx.isCombinedShape;
3630
3753
  if (fx) delete node.effects;
3631
3754
  let n = node;
@@ -3636,11 +3759,12 @@ function applyPlayerEffects_exceptRetime(node, ctx) {
3636
3759
  n = applyRepeaterEffect(n, repeater, ctx);
3637
3760
  n = applyMaskedByEffect(n, maskedBy, transformation, ctx);
3638
3761
  if (innerIdForContentRef) {
3762
+ applyRefHref(n, cloneFx, ctx);
3639
3763
  n = splitForContentRef(n, transformation, originalId, innerIdForContentRef, ctx);
3640
3764
  } else {
3641
- n = applyRefAndTransformationEffect(n, ref, transformation, ctx);
3765
+ n = applyRefAndTransformationEffect(n, cloneFx, transformation, ctx);
3642
3766
  }
3643
- if (retime) node.effects = { retime };
3767
+ if (cloneFx == null ? void 0 : cloneFx.retime) node.effects = { clone: { retime: cloneFx.retime } };
3644
3768
  if (originalId) ctx.idMap.set(originalId, n);
3645
3769
  return n;
3646
3770
  }
@@ -3657,13 +3781,30 @@ function materialiseAnimatedUseInstances(root) {
3657
3781
  if (animatedIds.size === 0) return root;
3658
3782
  let idCounter = 0;
3659
3783
  const genId3 = () => "_lw_use_mat_" + ++idCounter;
3784
+ const rootViewport = readRootViewport(root);
3660
3785
  const defsCollector = [];
3661
- const walked = walkAndMaterialise2(root, idMap, animatedIds, genId3, defsCollector);
3786
+ const walked = walkAndMaterialise2(root, idMap, animatedIds, genId3, defsCollector, rootViewport);
3662
3787
  if (defsCollector.length === 0) return walked;
3663
3788
  const defsNode = { type: "defs", children: defsCollector };
3664
3789
  const newChildren = [...(_a = walked.children) != null ? _a : [], defsNode];
3665
3790
  return __spreadProps(__spreadValues({}, walked), { children: newChildren });
3666
3791
  }
3792
+ function readRootViewport(root) {
3793
+ const vb = parseViewBox(root.viewBox);
3794
+ if (vb) return [vb[2], vb[3]];
3795
+ const w = numericAttr(root.width);
3796
+ const h = numericAttr(root.height);
3797
+ if (w !== void 0 && h !== void 0) return [w, h];
3798
+ return [1, 1];
3799
+ }
3800
+ function numericAttr(v) {
3801
+ if (typeof v === "number") return v;
3802
+ if (typeof v === "string") {
3803
+ const n = parseFloat(v);
3804
+ return Number.isFinite(n) ? n : void 0;
3805
+ }
3806
+ return void 0;
3807
+ }
3667
3808
  function buildIdMap(root) {
3668
3809
  const map = /* @__PURE__ */ new Map();
3669
3810
  const visit = (n) => {
@@ -3715,16 +3856,22 @@ function stripHash2(href) {
3715
3856
  if (typeof href !== "string") return void 0;
3716
3857
  return href.startsWith("#") ? href.slice(1) : href;
3717
3858
  }
3718
- function materialiseOneUse(useNode, target, idMap, animatedIds, genId3, defsCollector) {
3859
+ function materialiseOneUse(useNode, target, idMap, animatedIds, genId3, defsCollector, rootViewport) {
3860
+ var _a, _b, _c, _d;
3719
3861
  const clone2 = deepClonePxNode(target);
3720
3862
  regenerateIdsAndRewriteRefs(clone2, genId3);
3721
- const rewrittenClone = clone2.type === "symbol" ? rewriteSymbolRootToGroup(clone2, genId3, defsCollector) : clone2;
3722
- const materialisedClone = walkAndMaterialise2(rewrittenClone, idMap, animatedIds, genId3, defsCollector);
3863
+ const symbolViewBox = clone2.type === "symbol" ? parseViewBox(clone2.viewBox) : void 0;
3864
+ const useW = (_b = (_a = numericAttr(useNode.width)) != null ? _a : symbolViewBox == null ? void 0 : symbolViewBox[2]) != null ? _b : rootViewport[0];
3865
+ const useH = (_d = (_c = numericAttr(useNode.height)) != null ? _c : symbolViewBox == null ? void 0 : symbolViewBox[3]) != null ? _d : rootViewport[1];
3866
+ const rewrittenClone = clone2.type === "symbol" ? rewriteSymbolRootToGroup(clone2, genId3, defsCollector, useW, useH) : clone2;
3867
+ const materialisedClone = walkAndMaterialise2(rewrittenClone, idMap, animatedIds, genId3, defsCollector, rootViewport);
3723
3868
  const newNode = __spreadProps(__spreadValues({}, useNode), { type: "g", children: [materialisedClone] });
3724
3869
  delete newNode.href;
3870
+ delete newNode.width;
3871
+ delete newNode.height;
3725
3872
  return applyUseOffsetToG(newNode);
3726
3873
  }
3727
- function rewriteSymbolRootToGroup(symbolNode, genId3, defsCollector) {
3874
+ function rewriteSymbolRootToGroup(symbolNode, genId3, defsCollector, useW, useH) {
3728
3875
  const viewBox = parseViewBox(symbolNode.viewBox);
3729
3876
  const g = __spreadProps(__spreadValues({}, symbolNode), { type: "g" });
3730
3877
  delete g.viewBox;
@@ -3733,9 +3880,14 @@ function rewriteSymbolRootToGroup(symbolNode, genId3, defsCollector) {
3733
3880
  delete g.height;
3734
3881
  if (!viewBox) return g;
3735
3882
  const [vbX, vbY, vbW, vbH] = viewBox;
3736
- if (vbX !== 0 || vbY !== 0) {
3737
- g.transform = "translate(" + -vbX + "," + -vbY + ")";
3738
- }
3883
+ const scale = vbW > 0 && vbH > 0 ? Math.min(useW / vbW, useH / vbH) : 1;
3884
+ const xOff = (useW - vbW * scale) / 2;
3885
+ const yOff = (useH - vbH * scale) / 2;
3886
+ const parts = [];
3887
+ if (xOff !== 0 || yOff !== 0) parts.push("translate(" + xOff + "," + yOff + ")");
3888
+ if (scale !== 1) parts.push("scale(" + scale + ")");
3889
+ if (vbX !== 0 || vbY !== 0) parts.push("translate(" + -vbX + "," + -vbY + ")");
3890
+ if (parts.length) g.transform = parts.join("");
3739
3891
  const clipId = genId3();
3740
3892
  defsCollector.push({
3741
3893
  type: "clipPath",
@@ -3751,18 +3903,18 @@ function parseViewBox(v) {
3751
3903
  if (parts.length < 4 || parts.some((n) => !Number.isFinite(n))) return void 0;
3752
3904
  return [parts[0], parts[1], parts[2], parts[3]];
3753
3905
  }
3754
- function walkAndMaterialise2(node, idMap, animatedIds, genId3, defsCollector) {
3906
+ function walkAndMaterialise2(node, idMap, animatedIds, genId3, defsCollector, rootViewport) {
3755
3907
  if (node.type === "use" && typeof node.href === "string") {
3756
3908
  const targetId = stripHash2(node.href);
3757
3909
  if (targetId && animatedIds.has(targetId)) {
3758
3910
  const target = idMap.get(targetId);
3759
- if (target) return materialiseOneUse(node, target, idMap, animatedIds, genId3, defsCollector);
3911
+ if (target) return materialiseOneUse(node, target, idMap, animatedIds, genId3, defsCollector, rootViewport);
3760
3912
  }
3761
3913
  }
3762
3914
  if (!node.children) return node;
3763
3915
  let changed = false;
3764
3916
  const newChildren = node.children.map((ch) => {
3765
- const m = walkAndMaterialise2(ch, idMap, animatedIds, genId3, defsCollector);
3917
+ const m = walkAndMaterialise2(ch, idMap, animatedIds, genId3, defsCollector, rootViewport);
3766
3918
  if (m !== ch) changed = true;
3767
3919
  return m;
3768
3920
  });
@@ -3778,6 +3930,36 @@ function materialiseAllInTree(doc, engine, opts) {
3778
3930
  if (engine === PxAnimatorEngine.webapi) {
3779
3931
  root = materialiseMotionPathsInTree(root, opts == null ? void 0 : opts.motionPath);
3780
3932
  root = materialiseAnimatedUseInstances(root);
3933
+ root = pruneUnreferencedDefs(root);
3934
+ }
3935
+ return root;
3936
+ }
3937
+ function pruneUnreferencedDefs(root) {
3938
+ const stripHash3 = (h) => h.startsWith("#") ? h.slice(1) : h;
3939
+ const walk = (n, fn) => {
3940
+ var _a;
3941
+ fn(n);
3942
+ (_a = n.children) == null ? void 0 : _a.forEach((c) => walk(c, fn));
3943
+ };
3944
+ let changed = true;
3945
+ while (changed) {
3946
+ changed = false;
3947
+ const referenced = /* @__PURE__ */ new Set();
3948
+ walk(root, (n) => {
3949
+ if (n.type === "use" && typeof n.href === "string") referenced.add(stripHash3(n.href));
3950
+ });
3951
+ walk(root, (n) => {
3952
+ if (!n.children) return;
3953
+ let kept = n.children;
3954
+ if (n.type === "defs") {
3955
+ kept = kept.filter((c) => !((c.type === "g" || c.type === "symbol") && typeof c.id === "string" && !referenced.has(c.id)));
3956
+ }
3957
+ kept = kept.filter((c) => !(c.type === "defs" && (!c.children || c.children.length === 0)));
3958
+ if (kept.length !== n.children.length) {
3959
+ n.children = kept;
3960
+ changed = true;
3961
+ }
3962
+ });
3781
3963
  }
3782
3964
  return root;
3783
3965
  }
@@ -3810,6 +3992,13 @@ var URL_VALUE_ATTRS_LOWER = /* @__PURE__ */ new Set([
3810
3992
  ]);
3811
3993
  var IMAGE_REF_ATTRS_LOWER = /* @__PURE__ */ new Set(["href", "xlink:href", "src"]);
3812
3994
  var DATA_RASTER_IMAGE_RE = /^data:image\/(?:png|jpe?g|gif|webp|bmp);base64,/i;
3995
+ var DATA_SVG_IMAGE_RE = /^data:image\/svg\+xml(?:;[^,]*)?,/i;
3996
+ var DATA_IMAGE_BASE64_PAYLOAD_RE = /^data:image\/[^;,]*;base64,([A-Za-z0-9+/]{8})/i;
3997
+ var BASE64_RASTER_MAGICS = ["iVBORw0K", "/9j/", "R0lGOD", "UklGR", "Qk"];
3998
+ function isContentSniffedRasterImage(str) {
3999
+ const m = DATA_IMAGE_BASE64_PAYLOAD_RE.exec(str);
4000
+ return !!m && BASE64_RASTER_MAGICS.some((magic) => m[1].startsWith(magic));
4001
+ }
3813
4002
  function isDangerousAttrName(nameLower) {
3814
4003
  if (nameLower.startsWith("on")) return true;
3815
4004
  return false;
@@ -3832,8 +4021,8 @@ function sanitiseAttributeValue(name, value) {
3832
4021
  const str = String(value);
3833
4022
  if (str.startsWith("#")) return value;
3834
4023
  if (/^url\(#[^)]+\)$/.test(str)) return value;
3835
- if (IMAGE_REF_ATTRS_LOWER.has(nameLower) && DATA_RASTER_IMAGE_RE.test(str)) return value;
3836
- console.warn('Attribute "' + nameLower + '" blocked: must be #id, url(#id), or base64 raster data: URI, got:', value);
4024
+ if (IMAGE_REF_ATTRS_LOWER.has(nameLower) && (DATA_RASTER_IMAGE_RE.test(str) || DATA_SVG_IMAGE_RE.test(str) || isContentSniffedRasterImage(str))) return value;
4025
+ console.warn('Attribute "' + nameLower + '" blocked: must be #id, url(#id), or data:image/\u2026 URI, got:', value);
3837
4026
  return void 0;
3838
4027
  }
3839
4028
  return value;
@@ -4930,6 +5119,7 @@ export {
4930
5119
  PxAttrValueSchema,
4931
5120
  PxBezierPathSchema,
4932
5121
  PxBindingSchema,
5122
+ PxCloneEffectSchema,
4933
5123
  PxDefsSchema,
4934
5124
  PxEasingOrRefSchema,
4935
5125
  PxEffectsSchema,
@@ -4945,7 +5135,6 @@ export {
4945
5135
  PxNodeBase,
4946
5136
  PxNodeSchema,
4947
5137
  PxPropertyAnimationSchema,
4948
- PxRefEffectSchema,
4949
5138
  PxRepeaterEffectSchema,
4950
5139
  PxRetimeEffectSchema,
4951
5140
  PxStrokeGradientEffectSchema,