@pixodesk/svg-animator-rn 1.0.35 → 1.0.39

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
@@ -51,7 +51,7 @@ document says so.
51
51
 
52
52
  ```tsx
53
53
  // Play once when a screen opens, then hold the last frame
54
- <PixodeskSvgAnimator doc={doc} autoplay iterations={1} config={{ timeline: { fillMode: 'forwards' } }} />
54
+ <PixodeskSvgAnimator doc={doc} autoplay iterations={1} timeline={{ fillMode: 'forwards' }} />
55
55
 
56
56
  // Loop forever regardless of what the document says
57
57
  <PixodeskSvgAnimator doc={doc} autoplay iterations="infinite" />
@@ -166,6 +166,7 @@ const [time, setTime] = useState(0);
166
166
 
167
167
  ## Props
168
168
 
169
+ <!-- px-check props PixodeskSvgAnimatorProps pkg=rn -->
169
170
  | Prop | Type | Description |
170
171
  |---|---|---|
171
172
  | `doc` | `PxAnimatedSvgDocument` | The animation document to render (required) |
@@ -175,18 +176,21 @@ const [time, setTime] = useState(0);
175
176
  | `apiRef` | `RefObject<RnAnimatorApi>` | Ref for imperative control |
176
177
  | `progress` | `number` | show the frame at this position in the whole timeline (duration × iterations): `0` is the first frame, `0.5` the middle, `1` the last |
177
178
  | `time` | `number` | show the frame at that time, in milliseconds from the start |
178
- | `config` | `object \| string` | Per-instance override of the document's `animator` block, deep-merged over it — same shape as the file (`{ timeline: { fillMode, direction, trigger: { outAction, finishAction, … } } }`); `null` at a slot deletes that key. A JSON string is accepted too. `timeline.engine` is accepted but ignored here |
179
- | `resetDocDefaults` | `boolean` | Ignore the document's playback settings and start from the player's defaults, with `config` on top |
180
- | `duration` | `number` | Shortcut for `config.timeline.duration` (ms) |
181
- | `delay` | `number` | Shortcut for `config.timeline.delay` (ms) |
182
- | `iterations` | `number \| 'infinite'` | Shortcut for `config.timeline.iterations` |
183
- | `startOn` | `StartOn` | Shortcut for `config.timeline.trigger.startOn`. `mouseOver` has no touch equivalent and is ignored |
179
+ | `timeline` | `object \| string` | per-instance override of the document's `timeline` block, deep-merged over it — same shape as the file; `null` at any slot deletes that key. A JSON string is accepted too. See [Playback overrides](#playback-overrides) |
180
+ | `resetTimeline` | `boolean` | ignore the document's own timeline and start from the player's default timeline, with `timeline` on top |
181
+ | `duration` | `number` | Shortcut for `timeline.duration` (ms) |
182
+ | `delay` | `number` | Shortcut for `timeline.delay` (ms) |
183
+ | `iterations` | `number \| 'infinite'` | Shortcut for `timeline.iterations` |
184
+ | `startOn` | `PxStartOn` | Shortcut for `timeline.trigger.startOn`. `mouseOver` has no touch equivalent and is ignored |
184
185
  | `onPlay` | `() => void` | Called on play/resume |
185
186
  | `onPause` | `() => void` | Called on pause |
186
- | `onFinish` | `() => void` | Called on natural finish |
187
+ | `onFinish` | `() => void` | Called when the animation reaches its end — every iteration played, or `finish()` was called |
187
188
  | `onCancel` | `() => void` | Called on cancel |
188
- | `onStop` | `() => void` | Called whenever playback halts (pause / cancel / finish) |
189
+ | `onRemove` | `() => void` | Called when the animator is thrown away — the component unmounted, or a new `doc` replaced it |
190
+ | `onStop` | `() => void` | Called whenever playback halts (pause / cancel / finish / remove) |
189
191
  | `onError` | `(error, componentStack?) => void` | Called when a document cannot be compiled or rendered |
192
+ | `onWarn` | `(diagnostic) => void` | something is off but the animation still plays; without it → `console.warn` |
193
+ | `silent` | `boolean \| PxDiagnosticKind[]` | silences the console fallback — everything, or just the kinds listed |
190
194
  | `fallback` | `(error) => ReactElement \| null` | Rendered in place of a failed animation (default: nothing) |
191
195
 
192
196
  With none of `autoplay` / `play` / `pause` / `progress` / `time` set, the component
@@ -214,11 +218,12 @@ renderer never reaches JavaScript and cannot be caught — see
214
218
 
215
219
  ### Differences from the React package
216
220
 
221
+ <!-- px-check off differences from React, prose -->
217
222
  | Prop | Why it differs |
218
223
  |---|---|
219
- | `timeline.engine` | Accepted inside `config` but ignored. There is no Web Animations API on React Native; playback is always native-driven. |
224
+ | `timeline.engine` | Accepted inside `timeline` but ignored. There is no Web Animations API on React Native; playback is always native-driven. |
220
225
  | `timeline.frameRate` | Ignored. The screen's own refresh rate is used. The player does not compute values frame by frame: when the document loads it works out the animated values in advance, as a list of snapshots (60 per second of animation), and each screen refresh shows the nearest one. The closest thing to a frame rate is how many snapshots per second are prepared — `compileTracks({ sampleRate })`, only available when you use the lower-level API instead of the component. |
221
- | `startOn: 'mouseOver'` | Has no touch equivalent, so it is not honoured. The other four values (`load`, `click`, `scrollIntoView`, `programmatic`) work as they do on the web, from the file or from the `startOn` prop. |
226
+ | `startOn: 'mouseOver'` | Has no touch equivalent, so it is not honored. The other four values (`load`, `click`, `scrollIntoView`, `programmatic`) work as they do on the web, from the file or from the `startOn` prop. |
222
227
  | `className` / `style` | Not accepted — you cannot style the component itself. It fills whatever `View` you put it in, so to set its size, give that `View` a `width` and `height`. Styling *inside* the document (`style` on an element in the JSON) is supported. |
223
228
  | `onRemove` | Never called. On the web it tells you the animator was thrown away; here there is nothing to tell — when the component leaves the screen, React removes it and everything it created. To run code at that moment, use a `useEffect` cleanup function in your own component. |
224
229
 
@@ -228,9 +233,9 @@ There is **no JavaScript frame loop** — the JS thread is idle while an
228
233
  animation runs.
229
234
 
230
235
  1. **Once per document:** the shared core flattens it
231
- (`materialiseAllInTree` → effects, loops, motion-path sampling, animated
236
+ (`materializeAllInTree` → effects, loops, motion-path sampling, animated
232
237
  `<use>` inlining), then a track compiler densely samples every animated
233
- property with `calcAnimationValues` — the same function the web frames engine
238
+ property with `calcAnimationValues` — the same function the web frame-loop engine
234
239
  renders with, so values match the web player exactly.
235
240
  2. **Per frame:** one reanimated progress value, driven by
236
241
  `withTiming`/`withRepeat` on the UI thread, and a tiny worklet per animated
@@ -243,29 +248,31 @@ converts it into plain values ahead of time instead of fighting the platform.
243
248
  ## Feature support
244
249
 
245
250
  Every row below was verified by running the document through the real
246
- pipeline (`materialiseAllInTree` → track compilation) and checking that the
251
+ pipeline (`materializeAllInTree` → track compilation) and checking that the
247
252
  element maps to a `react-native-svg` component and that its animated
248
253
  properties actually change over time.
249
254
 
250
255
  ### Elements
251
256
 
257
+ <!-- px-check off support matrix, prose -->
252
258
  | Element | Renders | Notes |
253
259
  |---|---|---|
254
260
  | `svg`, `g`, `defs` | ✅ | |
255
261
  | `rect`, `circle`, `ellipse`, `line`, `path`, `polygon`, `polyline` | ✅ | |
256
262
  | `text`, `tspan` | ✅ | content via the `text` attribute |
257
263
  | `textPath` | ✅ | see *Text along a path* below |
258
- | `image` | ✅ | `href` accepts `data:` URIs; remote URLs are blocked by the sanitiser |
264
+ | `image` | ✅ | `href` accepts `data:` URIs; remote URLs are blocked by the sanitizer |
259
265
  | `use`, `symbol` | ✅ | animated targets are **inlined into real clones** before render — `<use>` does not propagate animation natively in React Native |
260
266
  | `linearGradient`, `radialGradient`, `stop` | ✅ | |
261
267
  | `pattern`, `marker` | ✅ | static geometry verified; complex cases unverified on device |
262
268
  | `mask`, `clipPath` | ✅ | |
263
269
  | `filter` + all 22 `fe*` primitives | ✅ | `feGaussianBlur`, `feDropShadow`, `feColorMatrix`, `feMerge`, `feComponentTransfer` + `feFunc*`, … **The visual result has not yet been checked on a real device** |
264
- | `foreignObject` | ❌ | blocked by the shared sanitiser (embeds arbitrary host content) |
265
- | `script` | ❌ | blocked by the shared sanitiser |
270
+ | `foreignObject` | ❌ | blocked by the shared sanitizer (embeds arbitrary host content) |
271
+ | `script` | ❌ | blocked by the shared sanitizer |
266
272
 
267
273
  ### Animatable attributes
268
274
 
275
+ <!-- px-check off support matrix, prose -->
269
276
  | Attribute | Animates | Notes |
270
277
  |---|---|---|
271
278
  | `opacity`, `fill-opacity`, `stroke-opacity` | ✅ | |
@@ -282,23 +289,25 @@ properties actually change over time.
282
289
 
283
290
  ### Effects (`node.effects`)
284
291
 
285
- All effects are materialised by the shared core before rendering, so the React Native
292
+ All effects are materialized by the shared core before rendering, so the React Native
286
293
  player sees plain nodes. **All are supported:**
287
294
 
295
+ <!-- px-check schema PxEffectsSchema -->
288
296
  | Effect | Status | Notes |
289
297
  |---|---|---|
290
298
  | `transformBy` | ✅ | all parts animatable, including `skew` |
291
- | `repeater` | ✅ | copies materialised as real elements; per-copy params animatable |
299
+ | `repeater` | ✅ | copies materialized as real elements; per-copy params animatable |
292
300
  | `maskedBy` | ✅ | including an animated mask source |
293
301
  | `clipPath` | ✅ | including animated clip geometry |
294
302
  | `strokeTrim` | ✅ | incl. `offset` and `subPaths: 'combined'` |
295
303
  | `clone` + `retime` | ✅ | each clone keeps its own time shift, incl. `retime.timeCrop` (a visibility window on the document timeline) |
296
- | `fillGradient` / `strokeGradient` | ✅ | animated stops **and animated geometry** (`animate.gradientX1`/`Cx`/`R`, …); `gradientTransform` is static (core-wide) |
304
+ | `fillGradient` / `strokeGradient` | ✅ | animated stops **and animated geometry** (`animate.gradientX1`/`Cx`/`R`, …); `gradientTransform` is static (core-wide) | <!-- px names=fillGradient,strokeGradient -->
297
305
  | `textPath` | ✅ | incl. animated `startOffset` |
298
306
  | `text.useGlyphs` | ✅ | text becomes `<path>` outlines from `definitions.fonts` — no font needed |
299
307
 
300
308
  ### Motion, timing and references
301
309
 
310
+ <!-- px-check off support matrix, prose -->
302
311
  | Feature | Status | Notes |
303
312
  |---|---|---|
304
313
  | **Motion along a path** + `autoOrient` | ✅ | **sampled** by the core into plain transform keyframes — `react-native-svg` has no native path motion |
@@ -310,6 +319,7 @@ player sees plain nodes. **All are supported:**
310
319
 
311
320
  ### Playback and triggers
312
321
 
322
+ <!-- px-check off support matrix, prose -->
313
323
  | Feature | Status | Notes |
314
324
  |---|---|---|
315
325
  | `duration`, `delay`, `iterations` (incl. `'infinite'`) | ✅ | |
@@ -321,7 +331,7 @@ player sees plain nodes. **All are supported:**
321
331
  | `setPlaybackRate` — faster, slower and **reverse** (negative) | ✅ | composes with `direction` |
322
332
  | Trigger `load` / `programmatic` | ✅ | |
323
333
  | Trigger `click` | ✅ | wrapped in a `Pressable`; a second tap applies `outAction` |
324
- | Trigger `scrollIntoView` | ✅ | visibility sampled by measuring against the window (React Native has no `IntersectionObserver`); honours `scrollIntoViewThreshold` and `outAction` |
334
+ | Trigger `scrollIntoView` | ✅ | visibility sampled by measuring against the window (React Native has no `IntersectionObserver`); honors `scrollIntoViewThreshold` and `outAction` |
325
335
  | Trigger `mouseOver` | ❌ | no touch equivalent — use `click`, or drive `play` yourself |
326
336
  | `timeline.frameRate` | n/a | reanimated runs at the display refresh rate; use `compileTracks({sampleRate})` to trade memory for temporal precision |
327
337
  | `timeline.engine` (`auto` / `native` / `js`) | n/a | there is no Web Animations API on React Native — playback is always native-driven |
package/dist/index.cjs CHANGED
@@ -73,7 +73,7 @@ __export(index_exports, {
73
73
  module.exports = __toCommonJS(index_exports);
74
74
 
75
75
  // src/PixodeskSvgAnimator.tsx
76
- var import_svg_animator_core5 = require("@pixodesk/svg-animator-core");
76
+ var import_svg_animator_core6 = require("@pixodesk/svg-animator-core");
77
77
  var import_react3 = require("react");
78
78
  var import_react_native = require("react-native");
79
79
  var import_react_native_reanimated = __toESM(require("react-native-reanimated"), 1);
@@ -199,7 +199,7 @@ function compileTracks(doc, opts) {
199
199
  ((_a2 = n == null ? void 0 : n.children) != null ? _a2 : []).forEach(indexTags);
200
200
  };
201
201
  indexTags(doc);
202
- const bindings = (0, import_svg_animator_core2.getNormalisedBindings)(doc, import_svg_animator_core2.PxTimelineEngine.js) || [];
202
+ const bindings = (0, import_svg_animator_core2.getNormalizedBindings)(doc, import_svg_animator_core2.PxTimelineEngine.js) || [];
203
203
  const elements = [];
204
204
  for (const binding of bindings) {
205
205
  const animDef = binding.animate;
@@ -326,14 +326,14 @@ var RN_SVG_COMPONENTS = {
326
326
 
327
327
  // src/PxRnRender.tsx
328
328
  function toRnProps(props, warnings, tag) {
329
- const normalised = (0, import_svg_animator_core3.getNormalizedProps)(props);
329
+ const normalized = (0, import_svg_animator_core3.getNormalizedProps)(props);
330
330
  const out = {};
331
- for (const key of Object.keys(normalised)) {
332
- const sanitised = (0, import_svg_animator_core3.sanitiseAttributeValue)(key, normalised[key]);
333
- if (sanitised === void 0) continue;
331
+ for (const key of Object.keys(normalized)) {
332
+ const sanitized = (0, import_svg_animator_core3.sanitizeAttributeValue)(key, normalized[key]);
333
+ if (sanitized === void 0) continue;
334
334
  const rnKey = toRnPropName(key);
335
335
  if (!rnKey) continue;
336
- out[rnKey] = toRnPropValue(rnKey, String(sanitised), tag);
336
+ out[rnKey] = toRnPropValue(rnKey, String(sanitized), tag);
337
337
  }
338
338
  return out;
339
339
  }
@@ -360,12 +360,12 @@ function renderRnNode(node, opts = {}, key) {
360
360
  for (const [k, v] of Object.entries(resolved)) {
361
361
  const rnKey = toRnPropName(k);
362
362
  if (!rnKey || rnKey in rnProps) continue;
363
- const sanitised = (0, import_svg_animator_core3.sanitiseAttributeValue)(rnKey, v);
364
- if (sanitised === void 0) continue;
365
- rnProps[rnKey] = toRnPropValue(rnKey, String(sanitised), tag);
363
+ const sanitized = (0, import_svg_animator_core3.sanitizeAttributeValue)(rnKey, v);
364
+ if (sanitized === void 0) continue;
365
+ rnProps[rnKey] = toRnPropValue(rnKey, String(sanitized), tag);
366
366
  }
367
367
  }
368
- const textContent = props[import_svg_animator_core3.TEXT_ATTR] || props[import_svg_animator_core3.TEXT_CONTENT_ATTR];
368
+ const textContent = props[import_svg_animator_core3.TEXT_CONTENT_ATTR];
369
369
  let childElements = void 0;
370
370
  if (Array.isArray(children) && children.length > 0) {
371
371
  childElements = children.map((ch, i) => {
@@ -391,6 +391,7 @@ function renderRnNode(node, opts = {}, key) {
391
391
 
392
392
  // src/PxRnErrorBoundary.tsx
393
393
  var import_react2 = require("react");
394
+ var import_svg_animator_core4 = require("@pixodesk/svg-animator-core");
394
395
  var PxRnErrorBoundary = class extends import_react2.Component {
395
396
  constructor() {
396
397
  super(...arguments);
@@ -400,9 +401,9 @@ var PxRnErrorBoundary = class extends import_react2.Component {
400
401
  return { error };
401
402
  }
402
403
  componentDidCatch(error, info) {
403
- var _a, _b, _c, _d;
404
+ var _a, _b, _c, _d, _e;
404
405
  (_c = (_b = this.props).onError) == null ? void 0 : _c.call(_b, error, (_a = info == null ? void 0 : info.componentStack) != null ? _a : void 0);
405
- console.warn("[PixodeskSvgAnimator] render failed:", (_d = error == null ? void 0 : error.message) != null ? _d : error);
406
+ ((_d = this.props.diag) != null ? _d : (0, import_svg_animator_core4.createDiagnostics)(void 0, "[PixodeskSvgAnimator]")).warn(import_svg_animator_core4.PxDiagnosticKind.internal, "render failed: " + ((_e = error == null ? void 0 : error.message) != null ? _e : String(error)));
406
407
  }
407
408
  componentDidUpdate(prev) {
408
409
  if (this.state.error && prev.children !== this.props.children) {
@@ -417,7 +418,7 @@ var PxRnErrorBoundary = class extends import_react2.Component {
417
418
  };
418
419
 
419
420
  // src/PxRnSafety.ts
420
- var import_svg_animator_core4 = require("@pixodesk/svg-animator-core");
421
+ var import_svg_animator_core5 = require("@pixodesk/svg-animator-core");
421
422
  function isClosedPath(d) {
422
423
  return typeof d === "string" && /[zZ]/.test(d);
423
424
  }
@@ -442,7 +443,7 @@ function openClosedTextPathTargets(doc, warnings) {
442
443
  if (String(n.type) === "textPath") hasTextPath = true;
443
444
  });
444
445
  if (!hasTextPath) return doc;
445
- const result = (0, import_svg_animator_core4.deepClone)(doc);
446
+ const result = (0, import_svg_animator_core5.deepClone)(doc);
446
447
  const byId = /* @__PURE__ */ new Map();
447
448
  forEachNode(result, (n) => {
448
449
  const id = n.id;
@@ -469,8 +470,8 @@ function openClosedTextPathTargets(doc, warnings) {
469
470
  if (!target || !isClosedPath(target.d)) return;
470
471
  let copyId = openCopies.get(targetId);
471
472
  if (!copyId) {
472
- const copy = (0, import_svg_animator_core4.deepClone)(target);
473
- copyId = "px_open_" + (0, import_svg_animator_core4.generateUniqueId)();
473
+ const copy = (0, import_svg_animator_core5.deepClone)(target);
474
+ copyId = "px_open_" + (0, import_svg_animator_core5.generateUniqueId)();
474
475
  copy.id = copyId;
475
476
  copy.d = openPath(String(copy.d));
476
477
  delete copy.animate;
@@ -561,29 +562,29 @@ var EMPTY_TRACKS = {
561
562
  sampleCount: 2,
562
563
  elements: []
563
564
  };
564
- function compileDocument(doc, overrides) {
565
- const { config, resetDocDefaults, duration, delay, iterations, startOn } = overrides;
566
- const warnings = (0, import_svg_animator_core5.validateNodeEffects)(doc);
567
- for (const w of warnings) console.warn("[PixodeskSvgAnimator] effects shape warning:", w);
568
- (0, import_svg_animator_core5.reportDocumentDiagnostics)(doc, "[PixodeskSvgAnimator]");
569
- const patch = (0, import_svg_animator_core5.foldAnimatorConfigShortcuts)(config, { duration, delay, iterations, startOn });
570
- if (patch !== void 0 || resetDocDefaults) {
571
- const applied = (0, import_svg_animator_core5.applyAnimatorConfig)(doc, patch != null ? patch : {}, { resetDefaults: !!resetDocDefaults });
572
- for (const w of applied.warnings) console.warn("[PixodeskSvgAnimator] config override:", w);
565
+ function compileDocument(doc, overrides, diag) {
566
+ const { timeline, resetTimeline, duration, delay, iterations, startOn } = overrides;
567
+ const warnings = (0, import_svg_animator_core6.validateNodeEffects)(doc);
568
+ for (const w of warnings) diag.warn(import_svg_animator_core6.PxDiagnosticKind.document, "effects shape: " + w);
569
+ (0, import_svg_animator_core6.reportDocumentDiagnostics)(doc, "[PixodeskSvgAnimator]");
570
+ const patch = (0, import_svg_animator_core6.foldTimelineOverride)(timeline, { duration, delay, iterations, startOn });
571
+ if (patch !== void 0 || resetTimeline) {
572
+ const applied = (0, import_svg_animator_core6.applyAnimatorConfig)(doc, patch != null ? patch : {}, { resetDefaults: !!resetTimeline });
573
+ for (const w of applied.warnings) diag.warn(import_svg_animator_core6.PxDiagnosticKind.usage, "timeline override: " + w);
573
574
  doc = applied.doc;
574
575
  }
575
- let prepared = (0, import_svg_animator_core5.materialiseAllInTree)(doc, import_svg_animator_core5.PxTimelineEngine.native);
576
+ let prepared = (0, import_svg_animator_core6.materializeAllInTree)(doc, import_svg_animator_core6.PxTimelineEngine.native);
576
577
  if (NATIVE_SVG_VIEWS) {
577
578
  prepared = openClosedTextPathTargets(prepared);
578
579
  }
579
- prepared = (0, import_svg_animator_core5.generateNewIds)(prepared);
580
+ prepared = (0, import_svg_animator_core6.generateNewIds)(prepared);
580
581
  const tracks = compileTracks(prepared, { native: NATIVE_SVG_VIEWS });
581
582
  return { doc: prepared, tracks, error: null };
582
583
  }
583
584
  function PixodeskSvgAnimator({
584
585
  doc,
585
- config,
586
- resetDocDefaults,
586
+ timeline,
587
+ resetTimeline,
587
588
  duration,
588
589
  delay,
589
590
  iterations,
@@ -600,25 +601,42 @@ function PixodeskSvgAnimator({
600
601
  onPause,
601
602
  onCancel,
602
603
  onFinish,
604
+ onRemove,
603
605
  onError,
604
- fallback
606
+ fallback,
607
+ onWarn,
608
+ silent
605
609
  }) {
606
- var _a, _b, _c, _d;
607
- const configKey = typeof config === "string" ? config : JSON.stringify(config != null ? config : null);
610
+ var _a, _b;
611
+ const timelineKey = typeof timeline === "string" ? timeline : JSON.stringify(timeline != null ? timeline : null);
612
+ const makeDiag = () => (0, import_svg_animator_core6.createDiagnostics)({
613
+ onWarn,
614
+ // This component's public `onError` is richer — `(error, componentStack?)` — and the
615
+ // error boundary hands it a component stack. Adapt rather than narrow it; the ternary
616
+ // keeps "not given" as undefined, so the console fallback still fires.
617
+ onError: onError ? (d) => {
618
+ var _a2;
619
+ return onError((_a2 = d.error) != null ? _a2 : new Error(d.message));
620
+ } : void 0,
621
+ silent
622
+ }, "[PixodeskSvgAnimator]");
608
623
  const compiled = (0, import_react3.useMemo)(() => {
609
624
  try {
610
625
  return compileDocument(
611
626
  doc,
612
- { config, resetDocDefaults, duration, delay, iterations, startOn }
627
+ { timeline, resetTimeline, duration, delay, iterations, startOn },
628
+ makeDiag()
613
629
  );
614
630
  } catch (e) {
615
631
  const error = e instanceof Error ? e : new Error(String(e));
616
- console.warn("[PixodeskSvgAnimator] could not compile the document:", error.message);
632
+ makeDiag().warn(import_svg_animator_core6.PxDiagnosticKind.internal, "could not compile the document: " + error.message);
617
633
  return { doc: null, tracks: EMPTY_TRACKS, error };
618
634
  }
619
- }, [doc, configKey, resetDocDefaults, duration, delay, iterations, startOn]);
635
+ }, [doc, timelineKey, resetTimeline, duration, delay, iterations, startOn]);
620
636
  const tracks = compiled.tracks;
621
- const totalDuration = tracks.duration * (tracks.iterations === Infinity ? 1 : tracks.iterations);
637
+ const totalDuration = (0, import_svg_animator_core6.progressSpanMs)(tracks.duration, tracks.iterations);
638
+ const seekCeiling = (0, import_svg_animator_core6.seekCeilingMs)(tracks.duration, tracks.iterations);
639
+ const runClock = (0, import_react3.useMemo)(() => (0, import_svg_animator_core6.createRunClock)(seekCeiling), [seekCeiling]);
622
640
  const progress = (0, import_react_native_reanimated.useSharedValue)(0);
623
641
  const playingRef = (0, import_react3.useRef)(false);
624
642
  const rateRef = (0, import_react3.useRef)(1);
@@ -630,6 +648,8 @@ function PixodeskSvgAnimator({
630
648
  };
631
649
  const notifyFinish = () => {
632
650
  playingRef.current = false;
651
+ runClock.seek(Number.isFinite(seekCeiling) ? seekCeiling : tracks.duration);
652
+ runClock.stop();
633
653
  progress.value = restingPosition();
634
654
  onFinish == null ? void 0 : onFinish();
635
655
  onStop == null ? void 0 : onStop();
@@ -666,12 +686,15 @@ function PixodeskSvgAnimator({
666
686
  const api = {
667
687
  isPlaying: () => playingRef.current,
668
688
  play: () => {
689
+ if (Number.isFinite(seekCeiling) && runClock.now() >= seekCeiling) runClock.seek(0);
690
+ runClock.start(rateRef.current);
669
691
  startFrom(progress.value);
670
692
  onPlay == null ? void 0 : onPlay();
671
693
  },
672
694
  pause: () => {
673
695
  (0, import_react_native_reanimated.cancelAnimation)(progress);
674
696
  playingRef.current = false;
697
+ runClock.stop();
675
698
  onPause == null ? void 0 : onPause();
676
699
  onStop == null ? void 0 : onStop();
677
700
  },
@@ -679,62 +702,82 @@ function PixodeskSvgAnimator({
679
702
  (0, import_react_native_reanimated.cancelAnimation)(progress);
680
703
  progress.value = 0;
681
704
  playingRef.current = false;
705
+ runClock.seek(0);
706
+ runClock.stop();
682
707
  onCancel == null ? void 0 : onCancel();
683
708
  onStop == null ? void 0 : onStop();
684
709
  },
685
710
  finish: () => {
686
711
  (0, import_react_native_reanimated.cancelAnimation)(progress);
687
712
  playingRef.current = false;
713
+ runClock.seek(Number.isFinite(seekCeiling) ? seekCeiling : tracks.duration);
714
+ runClock.stop();
688
715
  progress.value = restingPosition();
689
716
  onFinish == null ? void 0 : onFinish();
690
717
  onStop == null ? void 0 : onStop();
691
718
  },
692
719
  setPlaybackRate: (rate) => {
693
- if (!isFinite(rate) || rate === 0) {
694
- console.warn("setPlaybackRate: rate must be finite and non-zero");
720
+ if (!(0, import_svg_animator_core6.isValidPlaybackRate)(rate)) {
721
+ makeDiag().warn(import_svg_animator_core6.PxDiagnosticKind.usage, import_svg_animator_core6.PX_RATE_REJECTED);
695
722
  return;
696
723
  }
697
724
  rateRef.current = rate;
698
- if (playingRef.current) startFrom(progress.value);
725
+ if (playingRef.current) {
726
+ runClock.start(rate);
727
+ startFrom(progress.value);
728
+ }
699
729
  },
700
- getCurrentTime: () => progress.value,
730
+ // Ms from the start of the WHOLE run, like every other engine (review §3). This used
731
+ // to return `progress.value`, which is ms within the current iteration — so a slider
732
+ // built on it jumped back to 0 every time the animation repeated.
733
+ getCurrentTime: () => runClock.now(),
701
734
  setCurrentTime: (t) => {
702
735
  const wasPlaying = playingRef.current;
703
736
  (0, import_react_native_reanimated.cancelAnimation)(progress);
704
737
  playingRef.current = false;
705
- const clamped = Math.max(0, Math.min(t, totalDuration));
738
+ const clamped = (0, import_svg_animator_core6.clampSeekMs)(t, seekCeiling);
739
+ runClock.seek(clamped);
706
740
  const withinIteration = tracks.duration > 0 ? clamped % tracks.duration || (clamped === 0 ? 0 : tracks.duration) : 0;
707
741
  progress.value = withinIteration;
708
742
  if (wasPlaying) startFrom(withinIteration);
743
+ else runClock.stop();
744
+ },
745
+ getCurrentProgress: () => (0, import_svg_animator_core6.timeToProgress)(runClock.now(), tracks.duration, tracks.iterations),
746
+ setCurrentProgress: (p) => {
747
+ api.setCurrentTime((0, import_svg_animator_core6.progressToTimeMs)(p, tracks.duration, tracks.iterations));
709
748
  }
710
749
  };
711
750
  (0, import_react3.useImperativeHandle)(apiRef, () => api, [compiled]);
712
- const trigger = compiled.doc ? (_a = (0, import_svg_animator_core5.getAnimatorConfig)(compiled.doc)) == null ? void 0 : _a.trigger : void 0;
713
- const effectiveStartOn = (_b = trigger == null ? void 0 : trigger.startOn) != null ? _b : "load";
714
- const effectiveOutAction = (_c = trigger == null ? void 0 : trigger.outAction) != null ? _c : "pause";
751
+ const trigger = (0, import_svg_animator_core6.resolveTrigger)(compiled.doc ? (_a = (0, import_svg_animator_core6.getAnimatorConfig)(compiled.doc)) == null ? void 0 : _a.trigger : void 0);
752
+ const effectiveStartOn = trigger.startOn;
753
+ const effectiveOutAction = trigger.outAction;
754
+ const { mode: compMode, warnings: modeWarnings } = (0, import_svg_animator_core6.resolveControlMode)({ progress: progressProp, time, play, pause, autoplay });
755
+ (0, import_react3.useEffect)(() => {
756
+ const diag = makeDiag();
757
+ for (const w of modeWarnings) diag.warn(import_svg_animator_core6.PxDiagnosticKind.usage, w);
758
+ }, [modeWarnings.join("|")]);
715
759
  (0, import_react3.useEffect)(() => {
716
- if (progressProp !== void 0 || time !== void 0) {
717
- const seekMs = time !== void 0 ? time : (progressProp != null ? progressProp : 0) * totalDuration;
760
+ if (compMode === import_svg_animator_core6.PxControlMode.fixedTime) {
761
+ const seekMs = time !== void 0 ? time : (0, import_svg_animator_core6.progressToTimeMs)(progressProp != null ? progressProp : 0, tracks.duration, tracks.iterations);
718
762
  api.setCurrentTime(seekMs);
719
763
  return;
720
764
  }
721
- if (play !== void 0 || pause !== void 0) {
765
+ if (compMode === import_svg_animator_core6.PxControlMode.play) {
722
766
  if (play && !pause) api.play();
723
767
  else if (pause) api.pause();
724
- else if (play === false) api.finish();
768
+ else if (play === false) api.pause();
725
769
  else api.play();
726
770
  return;
727
771
  }
728
- if (autoplay && effectiveStartOn === "load") {
772
+ if (compMode === import_svg_animator_core6.PxControlMode.autoplay && effectiveStartOn === "load") {
729
773
  api.play();
730
774
  }
731
- }, [compiled, autoplay, play, pause, progressProp, time]);
775
+ }, [compiled, compMode, autoplay, play, pause, progressProp, time]);
732
776
  const scrollRef = (0, import_react3.useRef)(null);
733
777
  const inViewRef = (0, import_react3.useRef)(false);
734
778
  (0, import_react3.useEffect)(() => {
735
- var _a2;
736
779
  if (!autoplay || effectiveStartOn !== "scrollIntoView") return;
737
- const threshold = (_a2 = trigger == null ? void 0 : trigger.scrollIntoViewThreshold) != null ? _a2 : 0;
780
+ const threshold = trigger.scrollIntoViewThreshold;
738
781
  inViewRef.current = false;
739
782
  const check = () => {
740
783
  const node = scrollRef.current;
@@ -765,6 +808,8 @@ function PixodeskSvgAnimator({
765
808
  return () => {
766
809
  (0, import_react_native_reanimated.cancelAnimation)(progress);
767
810
  playingRef.current = false;
811
+ onRemove == null ? void 0 : onRemove();
812
+ onStop == null ? void 0 : onStop();
768
813
  };
769
814
  }, [compiled]);
770
815
  const trackById = (0, import_react3.useMemo)(() => {
@@ -780,7 +825,7 @@ function PixodeskSvgAnimator({
780
825
  if (!compiled.doc) return null;
781
826
  const renderOpts = {
782
827
  warnings: warningsRef.current,
783
- defs: (0, import_svg_animator_core5.getDefs)(compiled.doc)
828
+ defs: (0, import_svg_animator_core6.getDefs)(compiled.doc)
784
829
  };
785
830
  try {
786
831
  return renderRnNode(compiled.doc, __spreadProps(__spreadValues({}, renderOpts), {
@@ -820,14 +865,15 @@ function PixodeskSvgAnimator({
820
865
  } catch (e) {
821
866
  const error = e instanceof Error ? e : new Error(String(e));
822
867
  renderErrorRef.current = error;
823
- console.warn("[PixodeskSvgAnimator] could not render the document:", error.message);
868
+ makeDiag().warn(import_svg_animator_core6.PxDiagnosticKind.internal, "could not render the document: " + error.message);
824
869
  return null;
825
870
  }
826
871
  }, [compiled, trackById]);
827
872
  (0, import_react3.useEffect)(() => {
828
- for (const w of warningsRef.current) console.warn("[PixodeskSvgAnimator]", w);
873
+ const diag = makeDiag();
874
+ for (const w of warningsRef.current) diag.warn(import_svg_animator_core6.PxDiagnosticKind.platform, w);
829
875
  }, [root]);
830
- const failure = (_d = compiled.error) != null ? _d : renderErrorRef.current;
876
+ const failure = (_b = compiled.error) != null ? _b : renderErrorRef.current;
831
877
  (0, import_react3.useEffect)(() => {
832
878
  if (failure) onError == null ? void 0 : onError(failure);
833
879
  }, [failure]);
@@ -855,7 +901,7 @@ function PixodeskSvgAnimator({
855
901
  }
856
902
  );
857
903
  }
858
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PxRnErrorBoundary, { onError, fallback, children: content });
904
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PxRnErrorBoundary, { onError, fallback, diag: makeDiag(), children: content });
859
905
  }
860
906
  var PixodeskSvgAnimator_default = PixodeskSvgAnimator;
861
907
  // Annotate the CommonJS export names for ESM import in node: