@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.cjs CHANGED
@@ -62,6 +62,7 @@ __export(index_exports, {
62
62
  PxAttrValueSchema: () => PxAttrValueSchema,
63
63
  PxBezierPathSchema: () => PxBezierPathSchema,
64
64
  PxBindingSchema: () => PxBindingSchema,
65
+ PxCloneEffectSchema: () => PxCloneEffectSchema,
65
66
  PxDefsSchema: () => PxDefsSchema,
66
67
  PxEasingOrRefSchema: () => PxEasingOrRefSchema,
67
68
  PxEffectsSchema: () => PxEffectsSchema,
@@ -77,7 +78,6 @@ __export(index_exports, {
77
78
  PxNodeBase: () => PxNodeBase,
78
79
  PxNodeSchema: () => PxNodeSchema,
79
80
  PxPropertyAnimationSchema: () => PxPropertyAnimationSchema,
80
- PxRefEffectSchema: () => PxRefEffectSchema,
81
81
  PxRepeaterEffectSchema: () => PxRepeaterEffectSchema,
82
82
  PxRetimeEffectSchema: () => PxRetimeEffectSchema,
83
83
  PxStrokeGradientEffectSchema: () => PxStrokeGradientEffectSchema,
@@ -144,7 +144,7 @@ function readAnimatable(raw) {
144
144
  if (typeof raw === "object") {
145
145
  const obj = raw;
146
146
  if (obj.keyframes) {
147
- return { kind: "animated" /* Animated */, keyframes: obj.keyframes, autoOrient: obj.autoOrient };
147
+ return { kind: "animated" /* Animated */, keyframes: obj.keyframes, autoOrient: obj.autoOrient, loop: obj.loop };
148
148
  }
149
149
  if (obj.value !== void 0) return { kind: "static" /* Static */, value: obj.value };
150
150
  }
@@ -234,6 +234,7 @@ function wrapTransformPart(inner, part, raw, ctx) {
234
234
  if (v.kind === "animated" /* Animated */) {
235
235
  const animTr = { keyframes: v.keyframes.map((kf) => keyframeWith(kf, partsRecord(part, kf.value, void 0))) };
236
236
  if (v.autoOrient) animTr.autoOrient = true;
237
+ if (v.loop !== void 0) animTr.loop = v.loop;
237
238
  return {
238
239
  type: "g",
239
240
  animate: { transform: animTr },
@@ -252,9 +253,11 @@ function wrapOrigin(inner, raw, invert) {
252
253
  return { type: "g", transform: { value: { translate: sign(v.value) } }, children: [inner] };
253
254
  }
254
255
  if (v.kind === "animated" /* Animated */) {
256
+ const animTr = { keyframes: v.keyframes.map((kf) => keyframeWith(kf, { translate: sign(kf.value) })) };
257
+ if (v.loop !== void 0) animTr.loop = v.loop;
255
258
  return {
256
259
  type: "g",
257
- animate: { transform: { keyframes: v.keyframes.map((kf) => keyframeWith(kf, { translate: sign(kf.value) })) } },
260
+ animate: { transform: animTr },
258
261
  children: [inner]
259
262
  };
260
263
  }
@@ -264,8 +267,8 @@ function wrapOrigin(inner, raw, invert) {
264
267
  // src/effects/contentRefSplit.ts
265
268
  function identifyContentRefTargets(node, ctx, allocator) {
266
269
  var _a, _b, _c;
267
- if (node.type === "use" && ((_b = (_a = node.effects) == null ? void 0 : _a.ref) == null ? void 0 : _b.type) === "content") {
268
- const baseId = node.effects.ref.baseId;
270
+ if (node.type === "use" && ((_b = (_a = node.effects) == null ? void 0 : _a.clone) == null ? void 0 : _b.type) === "content") {
271
+ const baseId = node.effects.clone.baseId;
269
272
  if (typeof baseId === "string" && baseId && !ctx.contentRefInnerIds.has(baseId)) {
270
273
  ctx.contentRefInnerIds.set(baseId, allocator(baseId));
271
274
  }
@@ -313,6 +316,8 @@ function liftBodyTranslate(node, transformation) {
313
316
  });
314
317
  const outerAnimTr = { keyframes: outerKfs };
315
318
  if (animTr.autoOrient) outerAnimTr.autoOrient = true;
319
+ const srcLoop = animTr.loop;
320
+ if (srcLoop !== void 0) outerAnimTr.loop = srcLoop;
316
321
  out.animate = { transform: outerAnimTr };
317
322
  const innerHasPivotedPart = kfs.some((kf) => {
318
323
  const v = kf.value || {};
@@ -334,7 +339,9 @@ function liftBodyTranslate(node, transformation) {
334
339
  delete node.animate.transform;
335
340
  if (node.animate && Object.keys(node.animate).length === 0) delete node.animate;
336
341
  } else {
337
- node.animate.transform = { keyframes: innerKfs };
342
+ const innerAnimTr = { keyframes: innerKfs };
343
+ if (srcLoop !== void 0) innerAnimTr.loop = srcLoop;
344
+ node.animate.transform = innerAnimTr;
338
345
  }
339
346
  didLiftAnimate = true;
340
347
  }
@@ -1074,9 +1081,10 @@ var PxRetimeEffectSchema = implementsInterface()(px.object({
1074
1081
  stretch: px.number().optional(),
1075
1082
  timeCrop: px.tuple([px.number(), px.number()]).optional()
1076
1083
  }));
1077
- var PxRefEffectSchema = implementsInterface()(px.object({
1084
+ var PxCloneEffectSchema = implementsInterface()(px.object({
1085
+ type: px.string().optional(),
1078
1086
  baseId: px.string().optional(),
1079
- type: px.string().optional()
1087
+ retime: PxRetimeEffectSchema.optional()
1080
1088
  }));
1081
1089
  var PxGradientUnits = {
1082
1090
  userSpaceOnUse: "userSpaceOnUse",
@@ -1126,9 +1134,8 @@ var PxEffectsSchema = implementsInterface()(px.object({
1126
1134
  repeater: PxRepeaterEffectSchema.optional(),
1127
1135
  maskedBy: PxMaskedByEffectSchema.optional(),
1128
1136
  trimPath: PxTrimPathEffectSchema.optional(),
1129
- retime: PxRetimeEffectSchema.optional(),
1137
+ clone: PxCloneEffectSchema.optional(),
1130
1138
  isCombinedShape: px.boolean().optional(),
1131
- ref: PxRefEffectSchema.optional(),
1132
1139
  fillGradient: PxFillGradientEffectSchema.optional(),
1133
1140
  strokeGradient: PxStrokeGradientEffectSchema.optional(),
1134
1141
  textAlongPath: PxTextAlongPathEffectSchema.optional()
@@ -1356,6 +1363,7 @@ function buildStopChildren(stops, ctx) {
1356
1363
  const animBlock = stops;
1357
1364
  const kfs = animBlock.keyframes;
1358
1365
  if (!Array.isArray(kfs) || !kfs.length) return [];
1366
+ const loopFromSource = animBlock.loop;
1359
1367
  let stopCount = 0;
1360
1368
  for (const kf of kfs) {
1361
1369
  const v = (_a = kf.value) != null ? _a : kf.v;
@@ -1368,7 +1376,7 @@ function buildStopChildren(stops, ctx) {
1368
1376
  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" };
1369
1377
  baselineStops.push({ offset: s.offset, color: s.color });
1370
1378
  }
1371
- return baselineStops.map((bs, i) => animatedStopNode(bs, kfs, i, ctx));
1379
+ return baselineStops.map((bs, i) => animatedStopNode(bs, kfs, i, ctx, loopFromSource));
1372
1380
  }
1373
1381
  function staticStopNode(s) {
1374
1382
  return {
@@ -1377,7 +1385,7 @@ function staticStopNode(s) {
1377
1385
  stopColor: s.color
1378
1386
  };
1379
1387
  }
1380
- function animatedStopNode(baseline, kfs, stopIdx, _ctx) {
1388
+ function animatedStopNode(baseline, kfs, stopIdx, _ctx, loop) {
1381
1389
  var _a, _b, _c, _d, _e;
1382
1390
  const colorKfs = [];
1383
1391
  const offsetKfs = [];
@@ -1402,8 +1410,14 @@ function animatedStopNode(baseline, kfs, stopIdx, _ctx) {
1402
1410
  stopColor: baseline.color
1403
1411
  };
1404
1412
  const animate = {};
1405
- if (colorKfs.length) animate.stopColor = { keyframes: colorKfs };
1406
- if (offsetVaries && offsetKfs.length) animate.offset = { keyframes: offsetKfs };
1413
+ if (colorKfs.length) {
1414
+ animate.stopColor = { keyframes: colorKfs };
1415
+ if (loop !== void 0) animate.stopColor.loop = loop;
1416
+ }
1417
+ if (offsetVaries && offsetKfs.length) {
1418
+ animate.offset = { keyframes: offsetKfs };
1419
+ if (loop !== void 0) animate.offset.loop = loop;
1420
+ }
1407
1421
  if (Object.keys(animate).length) stop.animate = animate;
1408
1422
  return stop;
1409
1423
  }
@@ -1468,9 +1482,11 @@ function wrapInversePart(inner, part, raw, origin, ctx) {
1468
1482
  return { type: "g", transform: { value: partsRecord(part, invertPartValue(part, v.value), origin) }, children: [inner] };
1469
1483
  }
1470
1484
  if (v.kind === "animated" /* Animated */) {
1485
+ const animTr = { keyframes: v.keyframes.map((kf) => keyframeWith(kf, partsRecord(part, invertPartValue(part, kf.value), origin))) };
1486
+ if (v.loop !== void 0) animTr.loop = v.loop;
1471
1487
  return {
1472
1488
  type: "g",
1473
- animate: { transform: { keyframes: v.keyframes.map((kf) => keyframeWith(kf, partsRecord(part, invertPartValue(part, kf.value), origin))) } },
1489
+ animate: { transform: animTr },
1474
1490
  children: [inner]
1475
1491
  };
1476
1492
  }
@@ -1507,10 +1523,16 @@ function wrapInverseAnimatedBodyTransform(inner, node, _ctx) {
1507
1523
  scaleKfs.push(__spreadProps(__spreadValues({}, baseKf), { value: rec }));
1508
1524
  }
1509
1525
  }
1526
+ const srcLoop = animTr == null ? void 0 : animTr.loop;
1527
+ const withLoop = (kfs2) => {
1528
+ const block = { keyframes: kfs2 };
1529
+ if (srcLoop !== void 0) block.loop = srcLoop;
1530
+ return block;
1531
+ };
1510
1532
  let n = inner;
1511
- if (translateKfs.length) n = { type: "g", animate: { transform: { keyframes: translateKfs } }, children: [n] };
1512
- if (rotateKfs.length) n = { type: "g", animate: { transform: { keyframes: rotateKfs } }, children: [n] };
1513
- if (scaleKfs.length) n = { type: "g", animate: { transform: { keyframes: scaleKfs } }, children: [n] };
1533
+ if (translateKfs.length) n = { type: "g", animate: { transform: withLoop(translateKfs) }, children: [n] };
1534
+ if (rotateKfs.length) n = { type: "g", animate: { transform: withLoop(rotateKfs) }, children: [n] };
1535
+ if (scaleKfs.length) n = { type: "g", animate: { transform: withLoop(scaleKfs) }, children: [n] };
1514
1536
  return n;
1515
1537
  }
1516
1538
  function nodeHasBodyTransform(node) {
@@ -1744,16 +1766,18 @@ function parseTranslateOnlyFromString(s, ctx) {
1744
1766
 
1745
1767
  // src/effects/refEffect.ts
1746
1768
  var CONTENT_SUBREF = "content";
1747
- function applyRefAndTransformationEffect(node, ref, transformation, ctx) {
1748
- if (ref) {
1749
- const baseId = ref.baseId;
1750
- if (!baseId) {
1751
- ctx.errors.push("ref: missing baseId");
1752
- } else {
1753
- const targetId = ref.type === CONTENT_SUBREF ? ctx.contentRefInnerIds.get(baseId) || baseId : baseId;
1754
- node.href = "#" + targetId;
1755
- }
1769
+ function applyRefHref(node, clone2, ctx) {
1770
+ if (!clone2) return;
1771
+ const baseId = clone2.baseId;
1772
+ if (!baseId) {
1773
+ if (clone2.type === CONTENT_SUBREF) ctx.errors.push("clone: content ref missing baseId");
1774
+ return;
1756
1775
  }
1776
+ const targetId = clone2.type === CONTENT_SUBREF ? ctx.contentRefInnerIds.get(baseId) || baseId : baseId;
1777
+ node.href = "#" + targetId;
1778
+ }
1779
+ function applyRefAndTransformationEffect(node, clone2, transformation, ctx) {
1780
+ applyRefHref(node, clone2, ctx);
1757
1781
  return applyTransformationEffect(node, transformation, ctx);
1758
1782
  }
1759
1783
 
@@ -1864,22 +1888,32 @@ function concatRetime(child, parent) {
1864
1888
  stretch: parent.stretch * child.stretch
1865
1889
  };
1866
1890
  }
1867
- function applyAllRetimeEffects(root, ctx) {
1891
+ function readCloneRetime(n) {
1892
+ var _a, _b;
1893
+ return (_b = (_a = n.effects) == null ? void 0 : _a.clone) == null ? void 0 : _b.retime;
1894
+ }
1895
+ function clearCloneRetime(n) {
1868
1896
  var _a;
1897
+ const clone2 = (_a = n.effects) == null ? void 0 : _a.clone;
1898
+ if (!clone2) return;
1899
+ delete clone2.retime;
1900
+ if (Object.keys(clone2).length === 0) delete n.effects.clone;
1901
+ if (n.effects && Object.keys(n.effects).length === 0) delete n.effects;
1902
+ }
1903
+ function applyAllRetimeEffects(root, ctx) {
1869
1904
  ctx.idMap.clear();
1870
1905
  indexById(root, ctx.idMap);
1871
1906
  const sites = [];
1872
1907
  const collect = (n) => {
1873
- var _a2, _b;
1874
- if ((_a2 = n.effects) == null ? void 0 : _a2.retime) sites.push(n);
1875
- (_b = n.children) == null ? void 0 : _b.forEach(collect);
1908
+ var _a;
1909
+ if (readCloneRetime(n)) sites.push(n);
1910
+ (_a = n.children) == null ? void 0 : _a.forEach(collect);
1876
1911
  };
1877
1912
  collect(root);
1878
1913
  for (const useNode of sites) {
1879
- const retime = (_a = useNode.effects) == null ? void 0 : _a.retime;
1914
+ const retime = readCloneRetime(useNode);
1880
1915
  if (!retime) continue;
1881
- delete useNode.effects.retime;
1882
- if (Object.keys(useNode.effects).length === 0) delete useNode.effects;
1916
+ clearCloneRetime(useNode);
1883
1917
  materialiseRetime(useNode, asRetime(retime), ctx);
1884
1918
  }
1885
1919
  }
@@ -1903,7 +1937,6 @@ function materialiseRetime(useNode, retime, ctx) {
1903
1937
  }
1904
1938
  }
1905
1939
  function buildChainClone(targetId, accum, ctx, chain) {
1906
- var _a, _b;
1907
1940
  if (chain.has(targetId)) {
1908
1941
  ctx.errors.push('retime: loop via "' + targetId + '"');
1909
1942
  return void 0;
@@ -1920,25 +1953,43 @@ function buildChainClone(targetId, accum, ctx, chain) {
1920
1953
  } else {
1921
1954
  remapKeyframeTimes(cloneNode, accum.start, accum.stretch);
1922
1955
  }
1923
- if ((_a = cloneNode.effects) == null ? void 0 : _a.retime) {
1924
- delete cloneNode.effects.retime;
1925
- if (Object.keys(cloneNode.effects).length === 0) delete cloneNode.effects;
1926
- }
1956
+ clearCloneRetime(cloneNode);
1927
1957
  if (target.type === "use" && target.href) {
1928
1958
  const subId = stripHash(target.href);
1929
1959
  if (subId) {
1930
- const innerRetime = (_b = target.effects) == null ? void 0 : _b.retime;
1960
+ const innerRetime = readCloneRetime(target);
1931
1961
  const subAccum = innerRetime ? concatRetime(asRetime(innerRetime), accum) : accum;
1932
1962
  const subChain = new Set(chain);
1933
1963
  subChain.add(targetId);
1934
1964
  const subId2 = buildChainClone(subId, subAccum, ctx, subChain);
1935
1965
  if (subId2) cloneNode.href = "#" + subId2;
1936
1966
  }
1967
+ } else {
1968
+ materialiseNestedRetimeUses(cloneNode, accum, ctx, chain, targetId);
1937
1969
  }
1938
1970
  ctx.defs.push(cloneNode);
1939
1971
  if (typeof cloneNode.id === "string") ctx.idMap.set(cloneNode.id, cloneNode);
1940
1972
  return typeof cloneNode.id === "string" ? cloneNode.id : void 0;
1941
1973
  }
1974
+ function materialiseNestedRetimeUses(node, accum, ctx, chain, parentTargetId) {
1975
+ const visit = (n) => {
1976
+ var _a;
1977
+ const retime = readCloneRetime(n);
1978
+ if (n.type === "use" && retime && n.href) {
1979
+ const subId = stripHash(n.href);
1980
+ if (subId) {
1981
+ const subAccum = concatRetime(asRetime(retime), accum);
1982
+ const subChain = new Set(chain);
1983
+ subChain.add(parentTargetId);
1984
+ const subId2 = buildChainClone(subId, subAccum, ctx, subChain);
1985
+ if (subId2) n.href = "#" + subId2;
1986
+ }
1987
+ clearCloneRetime(n);
1988
+ }
1989
+ (_a = n.children) == null ? void 0 : _a.forEach(visit);
1990
+ };
1991
+ visit(node);
1992
+ }
1942
1993
  function remapKeyframeTimes(node, start, stretch) {
1943
1994
  var _a;
1944
1995
  remapKeyframeTimesOnly(node, start, stretch);
@@ -1983,12 +2034,14 @@ function applyAnimatableNumber(node, attrName, raw) {
1983
2034
  node[attrName] = String(raw);
1984
2035
  return;
1985
2036
  }
1986
- if (typeof raw === "object") {
2037
+ if (typeof raw === "object" && raw !== null) {
1987
2038
  const obj = raw;
1988
2039
  if (Array.isArray(obj.keyframes)) {
1989
2040
  const prevAnimate = node.animate && typeof node.animate === "object" && !Array.isArray(node.animate) ? node.animate : void 0;
1990
2041
  const animate = __spreadValues({}, prevAnimate || {});
1991
- animate[attrName] = { keyframes: obj.keyframes };
2042
+ const block = { keyframes: obj.keyframes };
2043
+ if (obj.loop !== void 0) block.loop = obj.loop;
2044
+ animate[attrName] = block;
1992
2045
  node.animate = animate;
1993
2046
  return;
1994
2047
  }
@@ -3051,16 +3104,67 @@ function expandLoopKeyframes(propName, keyframes, loop, duration) {
3051
3104
  looped.push(pushed);
3052
3105
  }
3053
3106
  }
3054
- for (let rep = 0; rep < fullReps; rep++) {
3055
- const distFromBoundary = loop.before ? fullReps - 1 - rep : rep;
3056
- const isReversed = !!loop.alternate && distFromBoundary % 2 === 0;
3057
- const repStart = fillStart + rep * segDuration;
3058
- appendRep(repStart, isReversed);
3107
+ function appendRepTail(repStart, isReversed, tailFraction) {
3108
+ let entries;
3109
+ if (isReversed) {
3110
+ entries = [];
3111
+ for (let i = template.length - 1; i >= 0; i--) {
3112
+ entries.push({
3113
+ relT: 1 - template[i].relT,
3114
+ v: template[i].v,
3115
+ e: i > 0 ? reverseEasing(template[i - 1].e) : void 0,
3116
+ tangentIn: template[i].tangentOut,
3117
+ tangentOut: template[i].tangentIn
3118
+ });
3119
+ }
3120
+ } else {
3121
+ entries = template;
3122
+ }
3123
+ const startRelT = 1 - tailFraction;
3124
+ for (let i = 0; i < entries.length; i++) {
3125
+ const entry = entries[i];
3126
+ if (entry.relT < startRelT - 1e-9) continue;
3127
+ const prev = entries[i - 1];
3128
+ if (prev && prev.relT < startRelT - 1e-9 && entry.relT > startRelT + 1e-9) {
3129
+ const intervalSpan = entry.relT - prev.relT;
3130
+ const localFrac = (startRelT - prev.relT) / intervalSpan;
3131
+ const easedFrac = prev.e ? cubicBezier(prev.e)(localFrac) : localFrac;
3132
+ const startValue = interpolateValue(propName, prev.v, entry.v, easedFrac);
3133
+ const { right: rightEasing } = splitEasing(prev.e, localFrac);
3134
+ looped.push({ t: repStart, v: startValue, e: rightEasing });
3135
+ }
3136
+ const pushed = {
3137
+ t: repStart + (entry.relT - startRelT) * segDuration,
3138
+ v: entry.v,
3139
+ e: i < entries.length - 1 ? entry.e : void 0
3140
+ };
3141
+ if (entry.tangentIn) pushed.tangentIn = entry.tangentIn;
3142
+ if (entry.tangentOut) pushed.tangentOut = entry.tangentOut;
3143
+ looped.push(pushed);
3144
+ }
3059
3145
  }
3060
- if (partialFraction > 1e-9) {
3061
- const isReversed = !!loop.alternate && fullReps % 2 === 0;
3062
- const repStart = fillStart + fullReps * segDuration;
3063
- appendRep(repStart, isReversed, partialFraction);
3146
+ if (loop.before) {
3147
+ if (partialFraction > 1e-9) {
3148
+ const isReversed = !!loop.alternate && fullReps % 2 === 0;
3149
+ appendRepTail(fillStart, isReversed, partialFraction);
3150
+ }
3151
+ for (let rep = 0; rep < fullReps; rep++) {
3152
+ const distFromBoundary = fullReps - 1 - rep;
3153
+ const isReversed = !!loop.alternate && distFromBoundary % 2 === 0;
3154
+ const repStart = fillStart + remainder + rep * segDuration;
3155
+ appendRep(repStart, isReversed);
3156
+ }
3157
+ } else {
3158
+ for (let rep = 0; rep < fullReps; rep++) {
3159
+ const isReversed = !!loop.alternate && rep % 2 === 0;
3160
+ const repStart = fillStart + rep * segDuration;
3161
+ appendRep(repStart, isReversed);
3162
+ }
3163
+ if (partialFraction > 1e-9) {
3164
+ const isReversed = !!loop.alternate && fullReps % 2 === 0;
3165
+ const repStart = fillStart + fullReps * segDuration;
3166
+ appendRep(repStart, isReversed, partialFraction);
3167
+ }
3064
3168
  }
3065
3169
  if (loop.before) {
3066
3170
  return [...looped, ...keyframes];
@@ -3379,7 +3483,6 @@ function applyTrimPathEffect(node, trimPath, isCombinedShape, ctx) {
3379
3483
  if (!trimPath) return node;
3380
3484
  const trimAllAsOne = !!trimPath.trimAllAsOne;
3381
3485
  const leafEntries = [];
3382
- let acc = 0;
3383
3486
  const measure = (n) => {
3384
3487
  if (Array.isArray(n.children) && n.children.length > 0) {
3385
3488
  for (const ch of n.children) measure(ch);
@@ -3391,15 +3494,22 @@ function applyTrimPathEffect(node, trimPath, isCombinedShape, ctx) {
3391
3494
  if (!subpaths.length) return;
3392
3495
  const entry = { leaf: n, subpaths: [] };
3393
3496
  for (const sp of subpaths) {
3394
- if (!trimAllAsOne) acc = 0;
3395
3497
  const lengthPx = pxBezierPathLength(sp);
3396
- entry.subpaths.push({ subpath: sp, lengthPx, startOffsetPx: acc });
3397
- acc += lengthPx;
3498
+ entry.subpaths.push({ subpath: sp, lengthPx, startOffsetPx: 0 });
3398
3499
  }
3399
3500
  leafEntries.push(entry);
3400
3501
  };
3401
3502
  measure(node);
3402
3503
  if (!leafEntries.length) return node;
3504
+ let acc = 0;
3505
+ const iterOrder = trimAllAsOne ? [...leafEntries].reverse() : leafEntries;
3506
+ for (const entry of iterOrder) {
3507
+ for (const sp of entry.subpaths) {
3508
+ if (!trimAllAsOne) acc = 0;
3509
+ sp.startOffsetPx = acc;
3510
+ acc += sp.lengthPx;
3511
+ }
3512
+ }
3403
3513
  const chainLengthPx = acc;
3404
3514
  if (trimAllAsOne && chainLengthPx < 1e-3) return node;
3405
3515
  const offsetReadRaw = readAnimatable(trimPath.offset);
@@ -3526,10 +3636,12 @@ function computeAnimAttr(read, map) {
3526
3636
  value: map((_c = kf.value) != null ? _c : kf.v),
3527
3637
  easing: (_d = kf.easing) != null ? _d : kf.e
3528
3638
  };
3529
- })
3639
+ }),
3640
+ loop: read.loop
3530
3641
  };
3531
3642
  }
3532
3643
  function applyAttr(node, attrName, attr) {
3644
+ var _a, _b;
3533
3645
  if (!attr) return;
3534
3646
  if (attr.kind === "static" /* Static */) {
3535
3647
  node[attrName] = attr.value;
@@ -3537,9 +3649,16 @@ function applyAttr(node, attrName, attr) {
3537
3649
  }
3538
3650
  const prevAnimate = node.animate && typeof node.animate === "object" && !Array.isArray(node.animate) ? node.animate : void 0;
3539
3651
  const animate = __spreadValues({}, prevAnimate || {});
3540
- animate[attrName] = { keyframes: attr.keyframes };
3652
+ const block = { keyframes: attr.keyframes };
3653
+ if (attr.loop !== void 0) block.loop = attr.loop;
3654
+ animate[attrName] = block;
3541
3655
  node.animate = animate;
3656
+ const firstKf = attr.keyframes[0];
3657
+ if (firstKf && ((_a = firstKf.value) != null ? _a : firstKf.v) !== void 0) {
3658
+ node[attrName] = (_b = firstKf.value) != null ? _b : firstKf.v;
3659
+ }
3542
3660
  }
3661
+ var OPACITY_STEP_MS = 10;
3543
3662
  function computeOpacityFromRange(rangeRead) {
3544
3663
  var _a, _b, _c, _d, _e, _f;
3545
3664
  const hide = (v) => v[0] === v[1];
@@ -3565,9 +3684,9 @@ function computeOpacityFromRange(rangeRead) {
3565
3684
  const nextHide = nextKf ? thisHide && hide((_f = nextKf.value) != null ? _f : nextKf.v) : thisHide;
3566
3685
  if (prevHide && !nextHide) {
3567
3686
  out.push({ time: t, value: 0 });
3568
- out.push({ time: t + 1, value: 1 });
3687
+ out.push({ time: t + OPACITY_STEP_MS, value: 1 });
3569
3688
  } else if (!prevHide && nextHide) {
3570
- out.push({ time: t - 1, value: 1 });
3689
+ out.push({ time: t - OPACITY_STEP_MS, value: 1 });
3571
3690
  out.push({ time: t, value: 0 });
3572
3691
  }
3573
3692
  }
@@ -3700,6 +3819,7 @@ function rectToPathD(node) {
3700
3819
 
3701
3820
  // src/effects/PlayerEffectsUtil.ts
3702
3821
  function applyPlayerEffects(root) {
3822
+ var _a;
3703
3823
  const ctx = {
3704
3824
  defs: [],
3705
3825
  warnings: [],
@@ -3707,7 +3827,10 @@ function applyPlayerEffects(root) {
3707
3827
  idMap: /* @__PURE__ */ new Map(),
3708
3828
  nextId: 0,
3709
3829
  contentRefInnerIds: /* @__PURE__ */ new Map(),
3710
- maskAncestorChains: /* @__PURE__ */ new Map()
3830
+ maskAncestorChains: /* @__PURE__ */ new Map(),
3831
+ // Resolved engine: `frames` ONLY when explicitly set; auto/webapi/unset →
3832
+ // webapi (we're not 100% sure it's frames, and CSS/WAAPI need the inline form).
3833
+ engine: ((_a = getAnimatorConfig(root)) == null ? void 0 : _a.mode) === PxAnimatorMode.frames ? PxAnimatorEngine.frames : PxAnimatorEngine.webapi
3711
3834
  };
3712
3835
  const working = clone(root);
3713
3836
  indexById(working, ctx.idMap);
@@ -3724,7 +3847,7 @@ function applyPlayerEffects_exceptRetime(node, ctx) {
3724
3847
  const originalId = typeof node.id === "string" ? node.id : void 0;
3725
3848
  const innerIdForContentRef = originalId ? ctx.contentRefInnerIds.get(originalId) : void 0;
3726
3849
  if (!fx && !innerIdForContentRef) return node;
3727
- const { transformation, repeater, maskedBy, trimPath, retime, ref, fillGradient, strokeGradient, textAlongPath } = fx != null ? fx : {};
3850
+ const { transformation, repeater, maskedBy, trimPath, clone: cloneFx, fillGradient, strokeGradient, textAlongPath } = fx != null ? fx : {};
3728
3851
  const isCombinedShape = fx == null ? void 0 : fx.isCombinedShape;
3729
3852
  if (fx) delete node.effects;
3730
3853
  let n = node;
@@ -3735,11 +3858,12 @@ function applyPlayerEffects_exceptRetime(node, ctx) {
3735
3858
  n = applyRepeaterEffect(n, repeater, ctx);
3736
3859
  n = applyMaskedByEffect(n, maskedBy, transformation, ctx);
3737
3860
  if (innerIdForContentRef) {
3861
+ applyRefHref(n, cloneFx, ctx);
3738
3862
  n = splitForContentRef(n, transformation, originalId, innerIdForContentRef, ctx);
3739
3863
  } else {
3740
- n = applyRefAndTransformationEffect(n, ref, transformation, ctx);
3864
+ n = applyRefAndTransformationEffect(n, cloneFx, transformation, ctx);
3741
3865
  }
3742
- if (retime) node.effects = { retime };
3866
+ if (cloneFx == null ? void 0 : cloneFx.retime) node.effects = { clone: { retime: cloneFx.retime } };
3743
3867
  if (originalId) ctx.idMap.set(originalId, n);
3744
3868
  return n;
3745
3869
  }
@@ -3756,13 +3880,30 @@ function materialiseAnimatedUseInstances(root) {
3756
3880
  if (animatedIds.size === 0) return root;
3757
3881
  let idCounter = 0;
3758
3882
  const genId3 = () => "_lw_use_mat_" + ++idCounter;
3883
+ const rootViewport = readRootViewport(root);
3759
3884
  const defsCollector = [];
3760
- const walked = walkAndMaterialise2(root, idMap, animatedIds, genId3, defsCollector);
3885
+ const walked = walkAndMaterialise2(root, idMap, animatedIds, genId3, defsCollector, rootViewport);
3761
3886
  if (defsCollector.length === 0) return walked;
3762
3887
  const defsNode = { type: "defs", children: defsCollector };
3763
3888
  const newChildren = [...(_a = walked.children) != null ? _a : [], defsNode];
3764
3889
  return __spreadProps(__spreadValues({}, walked), { children: newChildren });
3765
3890
  }
3891
+ function readRootViewport(root) {
3892
+ const vb = parseViewBox(root.viewBox);
3893
+ if (vb) return [vb[2], vb[3]];
3894
+ const w = numericAttr(root.width);
3895
+ const h = numericAttr(root.height);
3896
+ if (w !== void 0 && h !== void 0) return [w, h];
3897
+ return [1, 1];
3898
+ }
3899
+ function numericAttr(v) {
3900
+ if (typeof v === "number") return v;
3901
+ if (typeof v === "string") {
3902
+ const n = parseFloat(v);
3903
+ return Number.isFinite(n) ? n : void 0;
3904
+ }
3905
+ return void 0;
3906
+ }
3766
3907
  function buildIdMap(root) {
3767
3908
  const map = /* @__PURE__ */ new Map();
3768
3909
  const visit = (n) => {
@@ -3814,16 +3955,22 @@ function stripHash2(href) {
3814
3955
  if (typeof href !== "string") return void 0;
3815
3956
  return href.startsWith("#") ? href.slice(1) : href;
3816
3957
  }
3817
- function materialiseOneUse(useNode, target, idMap, animatedIds, genId3, defsCollector) {
3958
+ function materialiseOneUse(useNode, target, idMap, animatedIds, genId3, defsCollector, rootViewport) {
3959
+ var _a, _b, _c, _d;
3818
3960
  const clone2 = deepClonePxNode(target);
3819
3961
  regenerateIdsAndRewriteRefs(clone2, genId3);
3820
- const rewrittenClone = clone2.type === "symbol" ? rewriteSymbolRootToGroup(clone2, genId3, defsCollector) : clone2;
3821
- const materialisedClone = walkAndMaterialise2(rewrittenClone, idMap, animatedIds, genId3, defsCollector);
3962
+ const symbolViewBox = clone2.type === "symbol" ? parseViewBox(clone2.viewBox) : void 0;
3963
+ const useW = (_b = (_a = numericAttr(useNode.width)) != null ? _a : symbolViewBox == null ? void 0 : symbolViewBox[2]) != null ? _b : rootViewport[0];
3964
+ const useH = (_d = (_c = numericAttr(useNode.height)) != null ? _c : symbolViewBox == null ? void 0 : symbolViewBox[3]) != null ? _d : rootViewport[1];
3965
+ const rewrittenClone = clone2.type === "symbol" ? rewriteSymbolRootToGroup(clone2, genId3, defsCollector, useW, useH) : clone2;
3966
+ const materialisedClone = walkAndMaterialise2(rewrittenClone, idMap, animatedIds, genId3, defsCollector, rootViewport);
3822
3967
  const newNode = __spreadProps(__spreadValues({}, useNode), { type: "g", children: [materialisedClone] });
3823
3968
  delete newNode.href;
3969
+ delete newNode.width;
3970
+ delete newNode.height;
3824
3971
  return applyUseOffsetToG(newNode);
3825
3972
  }
3826
- function rewriteSymbolRootToGroup(symbolNode, genId3, defsCollector) {
3973
+ function rewriteSymbolRootToGroup(symbolNode, genId3, defsCollector, useW, useH) {
3827
3974
  const viewBox = parseViewBox(symbolNode.viewBox);
3828
3975
  const g = __spreadProps(__spreadValues({}, symbolNode), { type: "g" });
3829
3976
  delete g.viewBox;
@@ -3832,9 +3979,14 @@ function rewriteSymbolRootToGroup(symbolNode, genId3, defsCollector) {
3832
3979
  delete g.height;
3833
3980
  if (!viewBox) return g;
3834
3981
  const [vbX, vbY, vbW, vbH] = viewBox;
3835
- if (vbX !== 0 || vbY !== 0) {
3836
- g.transform = "translate(" + -vbX + "," + -vbY + ")";
3837
- }
3982
+ const scale = vbW > 0 && vbH > 0 ? Math.min(useW / vbW, useH / vbH) : 1;
3983
+ const xOff = (useW - vbW * scale) / 2;
3984
+ const yOff = (useH - vbH * scale) / 2;
3985
+ const parts = [];
3986
+ if (xOff !== 0 || yOff !== 0) parts.push("translate(" + xOff + "," + yOff + ")");
3987
+ if (scale !== 1) parts.push("scale(" + scale + ")");
3988
+ if (vbX !== 0 || vbY !== 0) parts.push("translate(" + -vbX + "," + -vbY + ")");
3989
+ if (parts.length) g.transform = parts.join("");
3838
3990
  const clipId = genId3();
3839
3991
  defsCollector.push({
3840
3992
  type: "clipPath",
@@ -3850,18 +4002,18 @@ function parseViewBox(v) {
3850
4002
  if (parts.length < 4 || parts.some((n) => !Number.isFinite(n))) return void 0;
3851
4003
  return [parts[0], parts[1], parts[2], parts[3]];
3852
4004
  }
3853
- function walkAndMaterialise2(node, idMap, animatedIds, genId3, defsCollector) {
4005
+ function walkAndMaterialise2(node, idMap, animatedIds, genId3, defsCollector, rootViewport) {
3854
4006
  if (node.type === "use" && typeof node.href === "string") {
3855
4007
  const targetId = stripHash2(node.href);
3856
4008
  if (targetId && animatedIds.has(targetId)) {
3857
4009
  const target = idMap.get(targetId);
3858
- if (target) return materialiseOneUse(node, target, idMap, animatedIds, genId3, defsCollector);
4010
+ if (target) return materialiseOneUse(node, target, idMap, animatedIds, genId3, defsCollector, rootViewport);
3859
4011
  }
3860
4012
  }
3861
4013
  if (!node.children) return node;
3862
4014
  let changed = false;
3863
4015
  const newChildren = node.children.map((ch) => {
3864
- const m = walkAndMaterialise2(ch, idMap, animatedIds, genId3, defsCollector);
4016
+ const m = walkAndMaterialise2(ch, idMap, animatedIds, genId3, defsCollector, rootViewport);
3865
4017
  if (m !== ch) changed = true;
3866
4018
  return m;
3867
4019
  });
@@ -3877,6 +4029,36 @@ function materialiseAllInTree(doc, engine, opts) {
3877
4029
  if (engine === PxAnimatorEngine.webapi) {
3878
4030
  root = materialiseMotionPathsInTree(root, opts == null ? void 0 : opts.motionPath);
3879
4031
  root = materialiseAnimatedUseInstances(root);
4032
+ root = pruneUnreferencedDefs(root);
4033
+ }
4034
+ return root;
4035
+ }
4036
+ function pruneUnreferencedDefs(root) {
4037
+ const stripHash3 = (h) => h.startsWith("#") ? h.slice(1) : h;
4038
+ const walk = (n, fn) => {
4039
+ var _a;
4040
+ fn(n);
4041
+ (_a = n.children) == null ? void 0 : _a.forEach((c) => walk(c, fn));
4042
+ };
4043
+ let changed = true;
4044
+ while (changed) {
4045
+ changed = false;
4046
+ const referenced = /* @__PURE__ */ new Set();
4047
+ walk(root, (n) => {
4048
+ if (n.type === "use" && typeof n.href === "string") referenced.add(stripHash3(n.href));
4049
+ });
4050
+ walk(root, (n) => {
4051
+ if (!n.children) return;
4052
+ let kept = n.children;
4053
+ if (n.type === "defs") {
4054
+ kept = kept.filter((c) => !((c.type === "g" || c.type === "symbol") && typeof c.id === "string" && !referenced.has(c.id)));
4055
+ }
4056
+ kept = kept.filter((c) => !(c.type === "defs" && (!c.children || c.children.length === 0)));
4057
+ if (kept.length !== n.children.length) {
4058
+ n.children = kept;
4059
+ changed = true;
4060
+ }
4061
+ });
3880
4062
  }
3881
4063
  return root;
3882
4064
  }
@@ -3909,6 +4091,13 @@ var URL_VALUE_ATTRS_LOWER = /* @__PURE__ */ new Set([
3909
4091
  ]);
3910
4092
  var IMAGE_REF_ATTRS_LOWER = /* @__PURE__ */ new Set(["href", "xlink:href", "src"]);
3911
4093
  var DATA_RASTER_IMAGE_RE = /^data:image\/(?:png|jpe?g|gif|webp|bmp);base64,/i;
4094
+ var DATA_SVG_IMAGE_RE = /^data:image\/svg\+xml(?:;[^,]*)?,/i;
4095
+ var DATA_IMAGE_BASE64_PAYLOAD_RE = /^data:image\/[^;,]*;base64,([A-Za-z0-9+/]{8})/i;
4096
+ var BASE64_RASTER_MAGICS = ["iVBORw0K", "/9j/", "R0lGOD", "UklGR", "Qk"];
4097
+ function isContentSniffedRasterImage(str) {
4098
+ const m = DATA_IMAGE_BASE64_PAYLOAD_RE.exec(str);
4099
+ return !!m && BASE64_RASTER_MAGICS.some((magic) => m[1].startsWith(magic));
4100
+ }
3912
4101
  function isDangerousAttrName(nameLower) {
3913
4102
  if (nameLower.startsWith("on")) return true;
3914
4103
  return false;
@@ -3931,8 +4120,8 @@ function sanitiseAttributeValue(name, value) {
3931
4120
  const str = String(value);
3932
4121
  if (str.startsWith("#")) return value;
3933
4122
  if (/^url\(#[^)]+\)$/.test(str)) return value;
3934
- if (IMAGE_REF_ATTRS_LOWER.has(nameLower) && DATA_RASTER_IMAGE_RE.test(str)) return value;
3935
- console.warn('Attribute "' + nameLower + '" blocked: must be #id, url(#id), or base64 raster data: URI, got:', value);
4123
+ if (IMAGE_REF_ATTRS_LOWER.has(nameLower) && (DATA_RASTER_IMAGE_RE.test(str) || DATA_SVG_IMAGE_RE.test(str) || isContentSniffedRasterImage(str))) return value;
4124
+ console.warn('Attribute "' + nameLower + '" blocked: must be #id, url(#id), or data:image/\u2026 URI, got:', value);
3936
4125
  return void 0;
3937
4126
  }
3938
4127
  return value;
@@ -5030,6 +5219,7 @@ function subtractMultiset(a, b) {
5030
5219
  PxAttrValueSchema,
5031
5220
  PxBezierPathSchema,
5032
5221
  PxBindingSchema,
5222
+ PxCloneEffectSchema,
5033
5223
  PxDefsSchema,
5034
5224
  PxEasingOrRefSchema,
5035
5225
  PxEffectsSchema,
@@ -5045,7 +5235,6 @@ function subtractMultiset(a, b) {
5045
5235
  PxNodeBase,
5046
5236
  PxNodeSchema,
5047
5237
  PxPropertyAnimationSchema,
5048
- PxRefEffectSchema,
5049
5238
  PxRepeaterEffectSchema,
5050
5239
  PxRetimeEffectSchema,
5051
5240
  PxStrokeGradientEffectSchema,