@pixodesk/svg-animator-rn 1.0.34 → 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} fill="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,19 +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
- | `duration` | `number` | Duration override (ms) |
179
- | `delay` | `number` | Delay before start (ms) |
180
- | `iterations` | `number \| 'infinite'` | Loop count |
181
- | `fill` | `FillMode` | Fill behaviour |
182
- | `direction` | `PlaybackDirection` | Playback direction |
183
- | `resetOnFinish` | `boolean` | Snap back to the start after a natural finish |
184
- | `outAction` | `OutAction` | What a second tap does with the `click` trigger (default: the document's, else `pause`) |
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 |
185
185
  | `onPlay` | `() => void` | Called on play/resume |
186
186
  | `onPause` | `() => void` | Called on pause |
187
- | `onFinish` | `() => void` | Called on natural finish |
187
+ | `onFinish` | `() => void` | Called when the animation reaches its end — every iteration played, or `finish()` was called |
188
188
  | `onCancel` | `() => void` | Called on cancel |
189
- | `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) |
190
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 |
191
194
  | `fallback` | `(error) => ReactElement \| null` | Rendered in place of a failed animation (default: nothing) |
192
195
 
193
196
  With none of `autoplay` / `play` / `pause` / `progress` / `time` set, the component
@@ -215,11 +218,12 @@ renderer never reaches JavaScript and cannot be caught — see
215
218
 
216
219
  ### Differences from the React package
217
220
 
221
+ <!-- px-check off differences from React, prose -->
218
222
  | Prop | Why it differs |
219
223
  |---|---|
220
- | `mode` | Not accepted. There is no Web Animations API on React Native; playback is always native-driven. |
221
- | `frameRate` | Not accepted. 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. |
222
- | `startOn` | Not accepted as a prop — the document's trigger is honoured via `autoplay` (`load`, `click`, `scrollIntoView`, `programmatic`). `mouseOver` has no touch equivalent. |
224
+ | `timeline.engine` | Accepted inside `timeline` but ignored. There is no Web Animations API on React Native; playback is always native-driven. |
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. |
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. |
223
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. |
224
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. |
225
229
 
@@ -229,9 +233,9 @@ There is **no JavaScript frame loop** — the JS thread is idle while an
229
233
  animation runs.
230
234
 
231
235
  1. **Once per document:** the shared core flattens it
232
- (`materialiseAllInTree` → effects, loops, motion-path sampling, animated
236
+ (`materializeAllInTree` → effects, loops, motion-path sampling, animated
233
237
  `<use>` inlining), then a track compiler densely samples every animated
234
- property with `calcAnimationValues` — the same function the web frames engine
238
+ property with `calcAnimationValues` — the same function the web frame-loop engine
235
239
  renders with, so values match the web player exactly.
236
240
  2. **Per frame:** one reanimated progress value, driven by
237
241
  `withTiming`/`withRepeat` on the UI thread, and a tiny worklet per animated
@@ -244,29 +248,31 @@ converts it into plain values ahead of time instead of fighting the platform.
244
248
  ## Feature support
245
249
 
246
250
  Every row below was verified by running the document through the real
247
- pipeline (`materialiseAllInTree` → track compilation) and checking that the
251
+ pipeline (`materializeAllInTree` → track compilation) and checking that the
248
252
  element maps to a `react-native-svg` component and that its animated
249
253
  properties actually change over time.
250
254
 
251
255
  ### Elements
252
256
 
257
+ <!-- px-check off support matrix, prose -->
253
258
  | Element | Renders | Notes |
254
259
  |---|---|---|
255
260
  | `svg`, `g`, `defs` | ✅ | |
256
261
  | `rect`, `circle`, `ellipse`, `line`, `path`, `polygon`, `polyline` | ✅ | |
257
262
  | `text`, `tspan` | ✅ | content via the `text` attribute |
258
263
  | `textPath` | ✅ | see *Text along a path* below |
259
- | `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 |
260
265
  | `use`, `symbol` | ✅ | animated targets are **inlined into real clones** before render — `<use>` does not propagate animation natively in React Native |
261
266
  | `linearGradient`, `radialGradient`, `stop` | ✅ | |
262
267
  | `pattern`, `marker` | ✅ | static geometry verified; complex cases unverified on device |
263
268
  | `mask`, `clipPath` | ✅ | |
264
269
  | `filter` + all 22 `fe*` primitives | ✅ | `feGaussianBlur`, `feDropShadow`, `feColorMatrix`, `feMerge`, `feComponentTransfer` + `feFunc*`, … **The visual result has not yet been checked on a real device** |
265
- | `foreignObject` | ❌ | blocked by the shared sanitiser (embeds arbitrary host content) |
266
- | `script` | ❌ | blocked by the shared sanitiser |
270
+ | `foreignObject` | ❌ | blocked by the shared sanitizer (embeds arbitrary host content) |
271
+ | `script` | ❌ | blocked by the shared sanitizer |
267
272
 
268
273
  ### Animatable attributes
269
274
 
275
+ <!-- px-check off support matrix, prose -->
270
276
  | Attribute | Animates | Notes |
271
277
  |---|---|---|
272
278
  | `opacity`, `fill-opacity`, `stroke-opacity` | ✅ | |
@@ -283,50 +289,52 @@ properties actually change over time.
283
289
 
284
290
  ### Effects (`node.effects`)
285
291
 
286
- 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
287
293
  player sees plain nodes. **All are supported:**
288
294
 
295
+ <!-- px-check schema PxEffectsSchema -->
289
296
  | Effect | Status | Notes |
290
297
  |---|---|---|
291
298
  | `transformBy` | ✅ | all parts animatable, including `skew` |
292
- | `repeater` | ✅ | copies materialised as real elements; per-copy params animatable |
299
+ | `repeater` | ✅ | copies materialized as real elements; per-copy params animatable |
293
300
  | `maskedBy` | ✅ | including an animated mask source |
294
301
  | `clipPath` | ✅ | including animated clip geometry |
295
302
  | `strokeTrim` | ✅ | incl. `offset` and `subPaths: 'combined'` |
296
303
  | `clone` + `retime` | ✅ | each clone keeps its own time shift, incl. `retime.timeCrop` (a visibility window on the document timeline) |
297
- | `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 -->
298
305
  | `textPath` | ✅ | incl. animated `startOffset` |
299
- | `text.useGlyphs` | ✅ | text becomes `<path>` outlines from `definitions.glyphs` — no font needed |
300
- | `isCombinedShape` | ✅ | |
306
+ | `text.useGlyphs` | ✅ | text becomes `<path>` outlines from `definitions.fonts` — no font needed |
301
307
 
302
308
  ### Motion, timing and references
303
309
 
310
+ <!-- px-check off support matrix, prose -->
304
311
  | Feature | Status | Notes |
305
312
  |---|---|---|
306
313
  | **Motion along a path** + `autoOrient` | ✅ | **sampled** by the core into plain transform keyframes — `react-native-svg` has no native path motion |
307
314
  | **Text along a path** | ✅ two ways | native `textPath` (incl. animated `startOffset`), or **per-letter motion paths** for smooth results — the example app uses the latter, since animating native `startOffset` is janky in `react-native-svg` |
308
315
  | Per-property `loop` (incl. `alternate` pingpong) | ✅ | expanded before playback |
309
316
  | Easing (cubic-bezier and named refs) | ✅ | baked into the sampled tracks |
310
- | `definitions.animations` / `easings` / `styles` / `glyphs` | ✅ | named refs resolved; `style` presets applied as props |
317
+ | `definitions.animations` / `easings` / `styles` / `fonts` | ✅ | named refs resolved; `style` presets applied as props |
311
318
  | `node.style` (inline or named) | ✅ | resolved to props — React Native has no CSS, so explicit attributes win |
312
319
 
313
320
  ### Playback and triggers
314
321
 
322
+ <!-- px-check off support matrix, prose -->
315
323
  | Feature | Status | Notes |
316
324
  |---|---|---|
317
325
  | `duration`, `delay`, `iterations` (incl. `'infinite'`) | ✅ | |
318
326
  | `direction` — all four values | ✅ | |
319
- | `fill` — `forwards` / `backwards` / `both` / `none` | ✅ | |
320
- | `resetOnFinish` | ✅ | |
327
+ | `fillMode` — `forwards` / `backwards` / `both` / `none` | ✅ | |
328
+ | `trigger.finishAction: 'reset'` | ✅ | |
321
329
  | `play` / `pause` / `cancel` / `finish` | ✅ | |
322
330
  | `setCurrentTime` — jump to a time, including **while playing** | ✅ | playback continues from the new point |
323
331
  | `setPlaybackRate` — faster, slower and **reverse** (negative) | ✅ | composes with `direction` |
324
332
  | Trigger `load` / `programmatic` | ✅ | |
325
333
  | Trigger `click` | ✅ | wrapped in a `Pressable`; a second tap applies `outAction` |
326
- | 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` |
327
335
  | Trigger `mouseOver` | ❌ | no touch equivalent — use `click`, or drive `play` yourself |
328
- | `frameRate` | n/a | reanimated runs at the display refresh rate; use `compileTracks({sampleRate})` to trade memory for temporal precision |
329
- | `mode` (`waapi` / `frames`) | n/a | there is no Web Animations API on React Native — playback is always native-driven |
336
+ | `timeline.frameRate` | n/a | reanimated runs at the display refresh rate; use `compileTracks({sampleRate})` to trade memory for temporal precision |
337
+ | `timeline.engine` (`auto` / `native` / `js`) | n/a | there is no Web Animations API on React Native — playback is always native-driven |
330
338
 
331
339
  ### Known limitations
332
340
 
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.PxAnimatorEngine.frames) || [];
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,38 +562,33 @@ var EMPTY_TRACKS = {
561
562
  sampleCount: 2,
562
563
  elements: []
563
564
  };
564
- function compileDocument(doc, overrides) {
565
- const { duration, delay, iterations, fill, direction, resetOnFinish } = 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
- let prepared = (0, import_svg_animator_core5.materialiseAllInTree)(doc, import_svg_animator_core5.PxAnimatorEngine.waapi);
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);
574
+ doc = applied.doc;
575
+ }
576
+ let prepared = (0, import_svg_animator_core6.materializeAllInTree)(doc, import_svg_animator_core6.PxTimelineEngine.native);
569
577
  if (NATIVE_SVG_VIEWS) {
570
578
  prepared = openClosedTextPathTargets(prepared);
571
579
  }
572
- const animator = (0, import_svg_animator_core5.getAnimatorConfig)(prepared) || {};
573
- prepared = __spreadProps(__spreadValues({}, prepared), {
574
- animator: __spreadProps(__spreadValues({}, animator), {
575
- duration: duration !== void 0 ? duration : animator.duration,
576
- delay: delay !== void 0 ? delay : animator.delay,
577
- iterations: iterations !== void 0 ? iterations : animator.iterations,
578
- fill: fill !== void 0 ? fill : animator.fill,
579
- direction: direction !== void 0 ? direction : animator.direction,
580
- resetOnFinish: resetOnFinish !== void 0 ? resetOnFinish : animator.resetOnFinish
581
- })
582
- });
583
- prepared = (0, import_svg_animator_core5.generateNewIds)(prepared);
580
+ prepared = (0, import_svg_animator_core6.generateNewIds)(prepared);
584
581
  const tracks = compileTracks(prepared, { native: NATIVE_SVG_VIEWS });
585
582
  return { doc: prepared, tracks, error: null };
586
583
  }
587
584
  function PixodeskSvgAnimator({
588
585
  doc,
586
+ timeline,
587
+ resetTimeline,
589
588
  duration,
590
589
  delay,
591
590
  iterations,
592
- fill,
593
- direction,
594
- resetOnFinish,
595
- outAction: outActionProp,
591
+ startOn,
596
592
  // (`progress` prop aliased — the name is taken by the internal reanimated SharedValue)
597
593
  autoplay,
598
594
  play,
@@ -605,24 +601,42 @@ function PixodeskSvgAnimator({
605
601
  onPause,
606
602
  onCancel,
607
603
  onFinish,
604
+ onRemove,
608
605
  onError,
609
- fallback
606
+ fallback,
607
+ onWarn,
608
+ silent
610
609
  }) {
611
- var _a, _b, _c, _d;
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]");
612
623
  const compiled = (0, import_react3.useMemo)(() => {
613
624
  try {
614
625
  return compileDocument(
615
626
  doc,
616
- { duration, delay, iterations, fill, direction, resetOnFinish }
627
+ { timeline, resetTimeline, duration, delay, iterations, startOn },
628
+ makeDiag()
617
629
  );
618
630
  } catch (e) {
619
631
  const error = e instanceof Error ? e : new Error(String(e));
620
- 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);
621
633
  return { doc: null, tracks: EMPTY_TRACKS, error };
622
634
  }
623
- }, [doc, duration, delay, iterations, fill, direction, resetOnFinish]);
635
+ }, [doc, timelineKey, resetTimeline, duration, delay, iterations, startOn]);
624
636
  const tracks = compiled.tracks;
625
- 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]);
626
640
  const progress = (0, import_react_native_reanimated.useSharedValue)(0);
627
641
  const playingRef = (0, import_react3.useRef)(false);
628
642
  const rateRef = (0, import_react3.useRef)(1);
@@ -634,6 +648,8 @@ function PixodeskSvgAnimator({
634
648
  };
635
649
  const notifyFinish = () => {
636
650
  playingRef.current = false;
651
+ runClock.seek(Number.isFinite(seekCeiling) ? seekCeiling : tracks.duration);
652
+ runClock.stop();
637
653
  progress.value = restingPosition();
638
654
  onFinish == null ? void 0 : onFinish();
639
655
  onStop == null ? void 0 : onStop();
@@ -670,12 +686,15 @@ function PixodeskSvgAnimator({
670
686
  const api = {
671
687
  isPlaying: () => playingRef.current,
672
688
  play: () => {
689
+ if (Number.isFinite(seekCeiling) && runClock.now() >= seekCeiling) runClock.seek(0);
690
+ runClock.start(rateRef.current);
673
691
  startFrom(progress.value);
674
692
  onPlay == null ? void 0 : onPlay();
675
693
  },
676
694
  pause: () => {
677
695
  (0, import_react_native_reanimated.cancelAnimation)(progress);
678
696
  playingRef.current = false;
697
+ runClock.stop();
679
698
  onPause == null ? void 0 : onPause();
680
699
  onStop == null ? void 0 : onStop();
681
700
  },
@@ -683,62 +702,82 @@ function PixodeskSvgAnimator({
683
702
  (0, import_react_native_reanimated.cancelAnimation)(progress);
684
703
  progress.value = 0;
685
704
  playingRef.current = false;
705
+ runClock.seek(0);
706
+ runClock.stop();
686
707
  onCancel == null ? void 0 : onCancel();
687
708
  onStop == null ? void 0 : onStop();
688
709
  },
689
710
  finish: () => {
690
711
  (0, import_react_native_reanimated.cancelAnimation)(progress);
691
712
  playingRef.current = false;
713
+ runClock.seek(Number.isFinite(seekCeiling) ? seekCeiling : tracks.duration);
714
+ runClock.stop();
692
715
  progress.value = restingPosition();
693
716
  onFinish == null ? void 0 : onFinish();
694
717
  onStop == null ? void 0 : onStop();
695
718
  },
696
719
  setPlaybackRate: (rate) => {
697
- if (!isFinite(rate) || rate === 0) {
698
- 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);
699
722
  return;
700
723
  }
701
724
  rateRef.current = rate;
702
- if (playingRef.current) startFrom(progress.value);
725
+ if (playingRef.current) {
726
+ runClock.start(rate);
727
+ startFrom(progress.value);
728
+ }
703
729
  },
704
- 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(),
705
734
  setCurrentTime: (t) => {
706
735
  const wasPlaying = playingRef.current;
707
736
  (0, import_react_native_reanimated.cancelAnimation)(progress);
708
737
  playingRef.current = false;
709
- const clamped = Math.max(0, Math.min(t, totalDuration));
738
+ const clamped = (0, import_svg_animator_core6.clampSeekMs)(t, seekCeiling);
739
+ runClock.seek(clamped);
710
740
  const withinIteration = tracks.duration > 0 ? clamped % tracks.duration || (clamped === 0 ? 0 : tracks.duration) : 0;
711
741
  progress.value = withinIteration;
712
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));
713
748
  }
714
749
  };
715
750
  (0, import_react3.useImperativeHandle)(apiRef, () => api, [compiled]);
716
- const trigger = compiled.doc ? (_a = (0, import_svg_animator_core5.getAnimatorConfig)(compiled.doc)) == null ? void 0 : _a.trigger : void 0;
717
- const startOn = (_b = trigger == null ? void 0 : trigger.startOn) != null ? _b : "load";
718
- const outAction = (_c = outActionProp != null ? outActionProp : 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 });
719
755
  (0, import_react3.useEffect)(() => {
720
- if (progressProp !== void 0 || time !== void 0) {
721
- const seekMs = time !== void 0 ? time : (progressProp != null ? progressProp : 0) * totalDuration;
756
+ const diag = makeDiag();
757
+ for (const w of modeWarnings) diag.warn(import_svg_animator_core6.PxDiagnosticKind.usage, w);
758
+ }, [modeWarnings.join("|")]);
759
+ (0, import_react3.useEffect)(() => {
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);
722
762
  api.setCurrentTime(seekMs);
723
763
  return;
724
764
  }
725
- if (play !== void 0 || pause !== void 0) {
765
+ if (compMode === import_svg_animator_core6.PxControlMode.play) {
726
766
  if (play && !pause) api.play();
727
767
  else if (pause) api.pause();
728
- else if (play === false) api.finish();
768
+ else if (play === false) api.pause();
729
769
  else api.play();
730
770
  return;
731
771
  }
732
- if (autoplay && startOn === "load") {
772
+ if (compMode === import_svg_animator_core6.PxControlMode.autoplay && effectiveStartOn === "load") {
733
773
  api.play();
734
774
  }
735
- }, [compiled, autoplay, play, pause, progressProp, time]);
775
+ }, [compiled, compMode, autoplay, play, pause, progressProp, time]);
736
776
  const scrollRef = (0, import_react3.useRef)(null);
737
777
  const inViewRef = (0, import_react3.useRef)(false);
738
778
  (0, import_react3.useEffect)(() => {
739
- var _a2;
740
- if (!autoplay || startOn !== "scrollIntoView") return;
741
- const threshold = (_a2 = trigger == null ? void 0 : trigger.scrollIntoViewThreshold) != null ? _a2 : 0;
779
+ if (!autoplay || effectiveStartOn !== "scrollIntoView") return;
780
+ const threshold = trigger.scrollIntoViewThreshold;
742
781
  inViewRef.current = false;
743
782
  const check = () => {
744
783
  const node = scrollRef.current;
@@ -754,21 +793,23 @@ function PixodeskSvgAnimator({
754
793
  if (isIn) {
755
794
  if (rateRef.current < 0) api.setPlaybackRate(Math.abs(rateRef.current));
756
795
  api.play();
757
- } else if (outAction === "reset") api.cancel();
758
- else if (outAction === "reverse") {
796
+ } else if (effectiveOutAction === "reset") api.cancel();
797
+ else if (effectiveOutAction === "reverse") {
759
798
  api.setPlaybackRate(-Math.abs(rateRef.current || 1));
760
799
  api.play();
761
- } else if (outAction !== "continue") api.pause();
800
+ } else if (effectiveOutAction !== "continue") api.pause();
762
801
  });
763
802
  };
764
803
  check();
765
804
  const id = setInterval(check, 200);
766
805
  return () => clearInterval(id);
767
- }, [compiled, autoplay, startOn, outAction]);
806
+ }, [compiled, autoplay, effectiveStartOn, effectiveOutAction]);
768
807
  (0, import_react3.useEffect)(() => {
769
808
  return () => {
770
809
  (0, import_react_native_reanimated.cancelAnimation)(progress);
771
810
  playingRef.current = false;
811
+ onRemove == null ? void 0 : onRemove();
812
+ onStop == null ? void 0 : onStop();
772
813
  };
773
814
  }, [compiled]);
774
815
  const trackById = (0, import_react3.useMemo)(() => {
@@ -784,7 +825,7 @@ function PixodeskSvgAnimator({
784
825
  if (!compiled.doc) return null;
785
826
  const renderOpts = {
786
827
  warnings: warningsRef.current,
787
- defs: (0, import_svg_animator_core5.getDefs)(compiled.doc)
828
+ defs: (0, import_svg_animator_core6.getDefs)(compiled.doc)
788
829
  };
789
830
  try {
790
831
  return renderRnNode(compiled.doc, __spreadProps(__spreadValues({}, renderOpts), {
@@ -824,32 +865,33 @@ function PixodeskSvgAnimator({
824
865
  } catch (e) {
825
866
  const error = e instanceof Error ? e : new Error(String(e));
826
867
  renderErrorRef.current = error;
827
- 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);
828
869
  return null;
829
870
  }
830
871
  }, [compiled, trackById]);
831
872
  (0, import_react3.useEffect)(() => {
832
- 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);
833
875
  }, [root]);
834
- const failure = (_d = compiled.error) != null ? _d : renderErrorRef.current;
876
+ const failure = (_b = compiled.error) != null ? _b : renderErrorRef.current;
835
877
  (0, import_react3.useEffect)(() => {
836
878
  if (failure) onError == null ? void 0 : onError(failure);
837
879
  }, [failure]);
838
880
  if (failure) return fallback ? fallback(failure) : null;
839
881
  let content = root;
840
- if (autoplay && startOn === "scrollIntoView" && root) {
882
+ if (autoplay && effectiveStartOn === "scrollIntoView" && root) {
841
883
  content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { ref: scrollRef, collapsable: false, children: root });
842
- } else if (autoplay && startOn === "click" && root) {
884
+ } else if (autoplay && effectiveStartOn === "click" && root) {
843
885
  content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
844
886
  import_react_native.Pressable,
845
887
  {
846
888
  onPress: () => {
847
889
  if (playingRef.current) {
848
- if (outAction === "reset") api.cancel();
849
- else if (outAction === "reverse") {
890
+ if (effectiveOutAction === "reset") api.cancel();
891
+ else if (effectiveOutAction === "reverse") {
850
892
  api.setPlaybackRate(-Math.abs(rateRef.current || 1));
851
893
  api.play();
852
- } else if (outAction !== "continue") api.pause();
894
+ } else if (effectiveOutAction !== "continue") api.pause();
853
895
  } else {
854
896
  if (rateRef.current < 0) api.setPlaybackRate(Math.abs(rateRef.current));
855
897
  api.play();
@@ -859,7 +901,7 @@ function PixodeskSvgAnimator({
859
901
  }
860
902
  );
861
903
  }
862
- 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 });
863
905
  }
864
906
  var PixodeskSvgAnimator_default = PixodeskSvgAnimator;
865
907
  // Annotate the CommonJS export names for ESM import in node: