@pixodesk/svg-animator-core 1.0.28 → 1.0.30

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/README.md CHANGED
@@ -56,7 +56,7 @@ runtime crash on a non-browser platform.
56
56
  | **Text** | `materialiseGlyphText`, `layoutGlyphTextChars`, `extendedPathForBrowser` |
57
57
  | **Node helpers** | `getNormalizedProps`, `sanitiseAttributeValue`, `resolveStyle`, `generateNewIds` |
58
58
  | **Playback engine** | `createBasicFrameLoopAnimator` + the `PxPlatformAdapter` interface |
59
- | **Wire enums** | `PxAnimatorMode`, `PxAnimatorEngine`, `PxLoopExtend`, `PxTrimSubPaths`, `PxMaskType`, `PxCloneType`, `PxUnits`, `PxGradientType`, `PxGradientUnits`, `PxGradientSpreadMethod`, `PxPathOverflow`, `PxLengthAdjust`, `PxTextPathMethod`, `PxTextPathSpacing` — every two-or-more-way wire selector is a named enum, not a bare string |
59
+ | **Wire enums** | `PxAnimatorMode`, `PxAnimatorEngine`, `PxLoopExtend`, `PxStrokeTrimSubPaths`, `PxMaskType`, `PxCloneType`, `PxUnits`, `PxGradientType`, `PxGradientUnits`, `PxGradientSpreadMethod`, `PxPathOverflow`, `PxLengthAdjust`, `PxTextPathMethod`, `PxTextPathSpacing` — every two-or-more-way wire selector is a named enum, not a bare string |
60
60
 
61
61
  ### Validating a document
62
62
 
@@ -69,7 +69,7 @@ import { PxAnimatedSvgDocumentSchema, type PxValidationContext } from '@pixodesk
69
69
 
70
70
  const ctx: PxValidationContext = { errors: [], warnings: [], strict: true };
71
71
  const ok = PxAnimatedSvgDocumentSchema.isValid(doc, ctx, []);
72
- if (!ok) console.error(ctx.errors); // ["children[0].effects.trimPath.range: …", …]
72
+ if (!ok) console.error(ctx.errors); // ["children[0].effects.strokeTrim.range: …", …]
73
73
  ```
74
74
 
75
75
  **Two modes, two different questions:**
@@ -90,7 +90,7 @@ in-memory object that produced it.
90
90
  `materialiseAllInTree(doc, engine)` is the single entry point that turns a
91
91
  lightweight editor document into a flat tree any renderer can walk:
92
92
 
93
- 1. **Effects** — `node.effects` (transformBy, repeater, maskedBy, trimPath,
93
+ 1. **Effects** — `node.effects` (transformBy, repeater, maskedBy, strokeTrim,
94
94
  clone/retime, gradients, textPath) become real nodes, wrappers and defs.
95
95
  2. **Loops** — each property's `loop` is expanded into explicit keyframes.
96
96
  3. **Motion paths** — tangented `transform` keyframes plus `autoOrient` are
package/dist/index.cjs CHANGED
@@ -56,6 +56,7 @@ __export(index_exports, {
56
56
  DISALLOWED_SVG_TAGS_LOWER: () => DISALLOWED_SVG_TAGS_LOWER,
57
57
  INTERNAL_ATTRS: () => INTERNAL_ATTRS,
58
58
  LOOP_JUMP_SHIFT_MS: () => LOOP_JUMP_SHIFT_MS,
59
+ MISSING_GLYPH_CLASS_NAME: () => MISSING_GLYPH_CLASS_NAME,
59
60
  PCT_BASED_ATTR_NAMES: () => PCT_BASED_ATTR_NAMES,
60
61
  PX_ANIM_ATTR_NAME: () => PX_ANIM_ATTR_NAME,
61
62
  PX_ANIM_SRC_ATTR_NAME: () => PX_ANIM_SRC_ATTR_NAME,
@@ -92,6 +93,8 @@ __export(index_exports, {
92
93
  PxScrollRangeSchema: () => PxScrollRangeSchema,
93
94
  PxScrollSchema: () => PxScrollSchema,
94
95
  PxStrokeGradientEffectSchema: () => PxStrokeGradientEffectSchema,
96
+ PxStrokeTrimEffectSchema: () => PxStrokeTrimEffectSchema,
97
+ PxStrokeTrimSubPaths: () => PxStrokeTrimSubPaths,
95
98
  PxSvgNodeExtra: () => PxSvgNodeExtra,
96
99
  PxTextEffectSchema: () => PxTextEffectSchema,
97
100
  PxTextPathEffectSchema: () => PxTextPathEffectSchema,
@@ -99,8 +102,6 @@ __export(index_exports, {
99
102
  PxTransformPartsSchema: () => PxTransformPartsSchema,
100
103
  PxTransformValueSchema: () => PxTransformValueSchema,
101
104
  PxTriggerSchema: () => PxTriggerSchema,
102
- PxTrimPathEffectSchema: () => PxTrimPathEffectSchema,
103
- PxTrimSubPaths: () => PxTrimSubPaths,
104
105
  STYLE_ATTR_NAMES: () => STYLE_ATTR_NAMES,
105
106
  TEXT_ATTR: () => TEXT_ATTR,
106
107
  TEXT_CONTENT_ATTR: () => TEXT_CONTENT_ATTR,
@@ -1161,7 +1162,7 @@ var PxTextPathSpacing = {
1161
1162
  auto: "auto",
1162
1163
  exact: "exact"
1163
1164
  };
1164
- var PxTrimSubPaths = {
1165
+ var PxStrokeTrimSubPaths = {
1165
1166
  separate: "separate",
1166
1167
  combined: "combined"
1167
1168
  };
@@ -1322,6 +1323,7 @@ var PxScrollSchema = implementsInterface()(px.object({
1322
1323
  subject: px.string().optional(),
1323
1324
  smoothing: px.number().optional(),
1324
1325
  pin: px.boolean().optional(),
1326
+ pinAlign: px.enum(["top", "center", "bottom"]).optional(),
1325
1327
  pinTop: px.number().optional(),
1326
1328
  pinDistance: px.number().optional(),
1327
1329
  range: PxScrollRangeSchema.optional()
@@ -1404,10 +1406,10 @@ var PxClipPathEffectSchema = implementsInterface()(px.object({
1404
1406
  d: PxAnimatableStringSchema.optional(),
1405
1407
  animate: PxPropertyAnimationSchema.optional()
1406
1408
  }));
1407
- var PxTrimPathEffectSchema = implementsInterface()(px.object({
1409
+ var PxStrokeTrimEffectSchema = implementsInterface()(px.object({
1408
1410
  offset: PxAnimatableNumberSchema.optional(),
1409
1411
  range: PxAnimatableVec2Schema.optional(),
1410
- subPaths: px.enum([PxTrimSubPaths.separate, PxTrimSubPaths.combined]).optional()
1412
+ subPaths: px.enum([PxStrokeTrimSubPaths.separate, PxStrokeTrimSubPaths.combined]).optional()
1411
1413
  }));
1412
1414
  var PxRetimeEffectSchema = implementsInterface()(px.object({
1413
1415
  sourceId: px.string().optional(),
@@ -1461,7 +1463,7 @@ var PxEffectsSchema = implementsInterface()(px.object({
1461
1463
  repeater: PxRepeaterEffectSchema.optional(),
1462
1464
  maskedBy: PxMaskedByEffectSchema.optional(),
1463
1465
  clipPath: PxClipPathEffectSchema.optional(),
1464
- trimPath: PxTrimPathEffectSchema.optional(),
1466
+ strokeTrim: PxStrokeTrimEffectSchema.optional(),
1465
1467
  clone: PxCloneEffectSchema.optional(),
1466
1468
  fillGradient: PxFillGradientEffectSchema.optional(),
1467
1469
  strokeGradient: PxStrokeGradientEffectSchema.optional(),
@@ -4595,6 +4597,19 @@ var TEXT_ATTR_KEYS = [
4595
4597
  TEXT_CONTENT_ATTR,
4596
4598
  "xml:space"
4597
4599
  ];
4600
+ var PAINT_STATIC_KEYS = [
4601
+ "fillOpacity",
4602
+ "fillRule",
4603
+ "strokeOpacity",
4604
+ "strokeDasharray",
4605
+ "strokeDashoffset",
4606
+ "strokeLinecap",
4607
+ "strokeLinejoin",
4608
+ "strokeMiterlimit",
4609
+ "mixBlendMode",
4610
+ "filter"
4611
+ ];
4612
+ var PAINT_ANIMATE_KEYS = ["fill", "stroke", "strokeWidth", "opacity", "fillOpacity", "strokeOpacity", "strokeDasharray", "strokeDashoffset"];
4598
4613
  function parseLen(v) {
4599
4614
  if (typeof v === "number") return v;
4600
4615
  if (typeof v === "string") {
@@ -4606,17 +4621,39 @@ function parseLen(v) {
4606
4621
  function str(v) {
4607
4622
  return typeof v === "string" ? v : void 0;
4608
4623
  }
4624
+ function paintAnimateOf(node) {
4625
+ const bag = node.animate;
4626
+ if (!bag || typeof bag !== "object") return void 0;
4627
+ let out;
4628
+ for (const k of PAINT_ANIMATE_KEYS) {
4629
+ if (bag[k] !== void 0) (out != null ? out : out = {})[k] = bag[k];
4630
+ }
4631
+ return out;
4632
+ }
4609
4633
  function resolveStyle2(node, parent) {
4610
- var _a, _b, _c, _d, _e, _f, _g;
4611
- return {
4634
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
4635
+ const isTextRoot = node.type === "text";
4636
+ const nodeStyle = node.style;
4637
+ const own = (key) => {
4638
+ var _a2;
4639
+ return isTextRoot ? void 0 : (_a2 = node[key]) != null ? _a2 : nodeStyle == null ? void 0 : nodeStyle[key];
4640
+ };
4641
+ const res = {
4612
4642
  fontFamily: (_a = str(node.fontFamily)) != null ? _a : parent.fontFamily,
4613
4643
  fontSize: (_b = parseLen(node.fontSize)) != null ? _b : parent.fontSize,
4614
4644
  fill: (_c = node.fill) != null ? _c : parent.fill,
4615
4645
  stroke: (_d = node.stroke) != null ? _d : parent.stroke,
4616
4646
  strokeWidth: (_e = node.strokeWidth) != null ? _e : parent.strokeWidth,
4617
4647
  letterSpacing: (_f = parseLen(node.letterSpacing)) != null ? _f : parent.letterSpacing,
4618
- wordSpacing: (_g = parseLen(node.wordSpacing)) != null ? _g : parent.wordSpacing
4648
+ wordSpacing: (_g = parseLen(node.wordSpacing)) != null ? _g : parent.wordSpacing,
4649
+ opacity: (_h = parseLen(own("opacity"))) != null ? _h : parent.opacity,
4650
+ animate: (_i = isTextRoot ? void 0 : paintAnimateOf(node)) != null ? _i : parent.animate
4619
4651
  };
4652
+ for (const key of PAINT_STATIC_KEYS) {
4653
+ const v = (_j = own(key)) != null ? _j : parent[key];
4654
+ if (v !== void 0) res[key] = v;
4655
+ }
4656
+ return res;
4620
4657
  }
4621
4658
  function rootStyleOf(node) {
4622
4659
  var _a, _b, _c;
@@ -4635,6 +4672,11 @@ function paintOf(s) {
4635
4672
  if (s.fill !== void 0) p.fill = s.fill;
4636
4673
  if (s.stroke !== void 0) p.stroke = s.stroke;
4637
4674
  if (s.strokeWidth !== void 0) p.strokeWidth = s.strokeWidth;
4675
+ if (s.opacity !== void 0 && s.opacity !== 1) p.opacity = s.opacity;
4676
+ for (const key of PAINT_STATIC_KEYS) {
4677
+ if (s[key] !== void 0) p[key] = s[key];
4678
+ }
4679
+ if (s.animate !== void 0) p.animate = s.animate;
4638
4680
  return p;
4639
4681
  }
4640
4682
  function glyphFontFor(s, glyphs, soleFont, warnings) {
@@ -4648,6 +4690,7 @@ function soleFontOf(glyphs) {
4648
4690
  return names.length === 1 ? glyphs[names[0]] : void 0;
4649
4691
  }
4650
4692
  var MISSING_GLYPH_ADVANCE_EM = 0.6;
4693
+ var MISSING_GLYPH_CLASS_NAME = "px-missing-glyph";
4651
4694
  function missingGlyphBoxEm(advanceEm, ascentEm) {
4652
4695
  if (advanceEm <= 0 || ascentEm <= 0) return "";
4653
4696
  const inset = 0.08;
@@ -4683,7 +4726,7 @@ function materialiseGlyphTextHorizontal(node, opts) {
4683
4726
  pen.x += g.width * scale;
4684
4727
  } else if (/\S/.test(ch)) {
4685
4728
  const advEm = g && g.width > 0 ? g.width : MISSING_GLYPH_ADVANCE_EM * upm;
4686
- placements.push({ glyphD: missingGlyphBoxEm(advEm, ascentEm), m: [scale, 0, 0, scale, pen.x, pen.y], paint, line, x: pen.x, y: pen.y, scale });
4729
+ placements.push({ glyphD: missingGlyphBoxEm(advEm, ascentEm), m: [scale, 0, 0, scale, pen.x, pen.y], paint, isMissing: true, line, x: pen.x, y: pen.y, scale });
4687
4730
  pen.x += advEm * scale;
4688
4731
  } else {
4689
4732
  pen.x += (g ? g.width : 0) * scale;
@@ -4874,7 +4917,7 @@ function collectAlongPathCells(node, glyphs, soleFont, warnings) {
4874
4917
  } else if (/\S/.test(ch)) {
4875
4918
  const wEm = g && g.width > 0 ? g.width : MISSING_GLYPH_ADVANCE_EM * gf.unitsPerEm;
4876
4919
  const boxAdv = wEm * scale;
4877
- cells.push({ glyphD: missingGlyphBoxEm(wEm, ascentEm), widthEm: wEm, scale, paint, midBase: adv + boxAdv / 2 });
4920
+ cells.push({ glyphD: missingGlyphBoxEm(wEm, ascentEm), widthEm: wEm, scale, paint, isMissing: true, midBase: adv + boxAdv / 2 });
4878
4921
  adv += boxAdv;
4879
4922
  } else {
4880
4923
  adv += (g ? g.width : 0) * scale;
@@ -4930,6 +4973,7 @@ function materialiseGlyphTextAlongPath(node, pathD, startOffset, opts, textLengt
4930
4973
  const placements = placeCells.map((c) => ({
4931
4974
  glyphD: c.glyphD,
4932
4975
  paint: c.paint,
4976
+ isMissing: c.isMissing,
4933
4977
  m: alongAffine(sampler, base + c.midBase * k, c.scale, c.widthEm, perp)
4934
4978
  }));
4935
4979
  return toGroup(node, buildPaths(placements, create, warnings), create);
@@ -5012,12 +5056,13 @@ function buildAnimatedAlongPath(cells, sampler, distOf, times, loop, create, isC
5012
5056
  const transform = { keyframes: kfs };
5013
5057
  if (loop !== void 0) transform.loop = loop;
5014
5058
  const animate = { transform };
5059
+ if (c.paint.animate) Object.assign(animate, c.paint.animate);
5015
5060
  if (isClip && opKfs.some((k) => k.value === 0)) {
5016
5061
  const op = { keyframes: opKfs };
5017
5062
  if (loop !== void 0) op.loop = loop;
5018
5063
  animate.opacity = op;
5019
5064
  }
5020
- out.push(create("path", __spreadProps(__spreadValues({ d }, paintProps(c.paint)), { animate }), []));
5065
+ out.push(create("path", __spreadProps(__spreadValues(__spreadValues({ d }, paintProps(c.paint)), missingGlyphProps(c.isMissing)), { animate }), []));
5021
5066
  }
5022
5067
  return out;
5023
5068
  }
@@ -5026,24 +5071,34 @@ function paintProps(paint) {
5026
5071
  if (paint.fill !== void 0) p.fill = paint.fill;
5027
5072
  if (paint.stroke !== void 0) p.stroke = paint.stroke;
5028
5073
  if (paint.strokeWidth !== void 0) p.strokeWidth = paint.strokeWidth;
5074
+ if (paint.opacity !== void 0) p.opacity = paint.opacity;
5075
+ for (const key of PAINT_STATIC_KEYS) {
5076
+ if (paint[key] !== void 0) p[key] = paint[key];
5077
+ }
5029
5078
  return p;
5030
5079
  }
5080
+ function missingGlyphProps(isMissing) {
5081
+ return isMissing ? { class: MISSING_GLYPH_CLASS_NAME } : {};
5082
+ }
5031
5083
  function buildPaths(placements, create, warnings) {
5032
- var _a, _b, _c;
5033
5084
  if (!placements.length) {
5034
5085
  warnings == null ? void 0 : warnings.push("textGlyphs: nothing to render");
5035
5086
  return [];
5036
5087
  }
5037
5088
  const byPaint = /* @__PURE__ */ new Map();
5038
5089
  for (const p of placements) {
5039
- const key = JSON.stringify([(_a = p.paint.fill) != null ? _a : null, (_b = p.paint.stroke) != null ? _b : null, (_c = p.paint.strokeWidth) != null ? _c : null]);
5090
+ const key = JSON.stringify([p.paint, !!p.isMissing]);
5040
5091
  const baked = transformPathData(p.glyphD, p.m);
5041
5092
  const entry = byPaint.get(key);
5042
5093
  if (entry) entry.d += baked;
5043
- else byPaint.set(key, { paint: p.paint, d: baked });
5094
+ else byPaint.set(key, { paint: p.paint, d: baked, isMissing: p.isMissing });
5044
5095
  }
5045
5096
  const out = [];
5046
- for (const { paint, d } of byPaint.values()) out.push(create("path", __spreadValues({ d }, paintProps(paint)), []));
5097
+ for (const { paint, d, isMissing } of byPaint.values()) {
5098
+ out.push(create("path", __spreadValues(__spreadValues(__spreadValues({
5099
+ d
5100
+ }, paintProps(paint)), missingGlyphProps(isMissing)), paint.animate !== void 0 ? { animate: __spreadValues({}, paint.animate) } : {}), []));
5101
+ }
5047
5102
  return out;
5048
5103
  }
5049
5104
  function toGroup(node, children, create) {
@@ -5080,10 +5135,10 @@ function applyTextGlyphsAlongPath(node, ctx, pathD, startOffset, textLength, pat
5080
5135
  return materialiseGlyphTextAlongPath(node, pathD, startOffset, { glyphs: ctx.glyphs, warnings: ctx.warnings }, textLength, pathOverflow);
5081
5136
  }
5082
5137
 
5083
- // src/effects/trimPathEffect.ts
5084
- function applyTrimPathEffect(node, trimPath, ctx) {
5085
- if (!trimPath) return node;
5086
- const combined = trimPath.subPaths === PxTrimSubPaths.combined;
5138
+ // src/effects/strokeTrimEffect.ts
5139
+ function applyStrokeTrimEffect(node, strokeTrim, ctx) {
5140
+ if (!strokeTrim) return node;
5141
+ const combined = strokeTrim.subPaths === PxStrokeTrimSubPaths.combined;
5087
5142
  const leafEntries = [];
5088
5143
  const measure = (n) => {
5089
5144
  if (Array.isArray(n.children) && n.children.length > 0) {
@@ -5114,9 +5169,9 @@ function applyTrimPathEffect(node, trimPath, ctx) {
5114
5169
  }
5115
5170
  const chainLengthPx = acc;
5116
5171
  if (combined && chainLengthPx < 1e-3) return node;
5117
- const offsetReadRaw = readAnimatable(trimPath.offset);
5172
+ const offsetReadRaw = readAnimatable(strokeTrim.offset);
5118
5173
  const offsetRead = offsetReadRaw.kind === "absent" /* Absent */ ? { kind: "static" /* Static */, value: 0 } : offsetReadRaw;
5119
- const rangeReadRaw = readRangeWithCrossings(trimPath.range);
5174
+ const rangeReadRaw = readRangeWithCrossings(strokeTrim.range);
5120
5175
  const rangeRead = rangeReadRaw.kind === "absent" /* Absent */ ? { kind: "static" /* Static */, value: [0, 1] } : rangeReadRaw;
5121
5176
  const offsetValues = readScalarValues(offsetRead);
5122
5177
  const minOffset = offsetValues.length ? Math.min(...offsetValues) : 0;
@@ -5448,7 +5503,7 @@ function applyPlayerEffects_exceptRetime(node, ctx) {
5448
5503
  const originalId = typeof node.id === "string" ? node.id : void 0;
5449
5504
  const innerIdForContentRef = originalId ? ctx.contentRefInnerIds.get(originalId) : void 0;
5450
5505
  if (!fx && !innerIdForContentRef) return node;
5451
- const { transformBy, repeater, maskedBy, clipPath, trimPath, clone: cloneFx, fillGradient, strokeGradient, textPath, text } = fx != null ? fx : {};
5506
+ const { transformBy, repeater, maskedBy, clipPath, strokeTrim, clone: cloneFx, fillGradient, strokeGradient, textPath, text } = fx != null ? fx : {};
5452
5507
  if (fx) delete node.effects;
5453
5508
  let n = node;
5454
5509
  let consumedByGlyphs = false;
@@ -5468,7 +5523,7 @@ function applyPlayerEffects_exceptRetime(node, ctx) {
5468
5523
  if (!consumedByGlyphs) n = applyTextPathEffect(n, textPath, ctx);
5469
5524
  n = applyFillGradientEffect(n, fillGradient, ctx);
5470
5525
  n = applyStrokeGradientEffect(n, strokeGradient, ctx);
5471
- n = applyTrimPathEffect(n, trimPath, ctx);
5526
+ n = applyStrokeTrimEffect(n, strokeTrim, ctx);
5472
5527
  n = applyRepeaterEffect(n, repeater, ctx);
5473
5528
  n = applyMaskedByEffect(n, maskedBy, transformBy, ctx);
5474
5529
  n = applyClipPathEffect(n, clipPath, ctx);
@@ -6155,6 +6210,7 @@ function subtractMultiset(a, b) {
6155
6210
  DISALLOWED_SVG_TAGS_LOWER,
6156
6211
  INTERNAL_ATTRS,
6157
6212
  LOOP_JUMP_SHIFT_MS,
6213
+ MISSING_GLYPH_CLASS_NAME,
6158
6214
  PCT_BASED_ATTR_NAMES,
6159
6215
  PX_ANIM_ATTR_NAME,
6160
6216
  PX_ANIM_SRC_ATTR_NAME,
@@ -6191,6 +6247,8 @@ function subtractMultiset(a, b) {
6191
6247
  PxScrollRangeSchema,
6192
6248
  PxScrollSchema,
6193
6249
  PxStrokeGradientEffectSchema,
6250
+ PxStrokeTrimEffectSchema,
6251
+ PxStrokeTrimSubPaths,
6194
6252
  PxSvgNodeExtra,
6195
6253
  PxTextEffectSchema,
6196
6254
  PxTextPathEffectSchema,
@@ -6198,8 +6256,6 @@ function subtractMultiset(a, b) {
6198
6256
  PxTransformPartsSchema,
6199
6257
  PxTransformValueSchema,
6200
6258
  PxTriggerSchema,
6201
- PxTrimPathEffectSchema,
6202
- PxTrimSubPaths,
6203
6259
  STYLE_ATTR_NAMES,
6204
6260
  TEXT_ATTR,
6205
6261
  TEXT_CONTENT_ATTR,