@pixodesk/svg-animator-rn 1.0.35 → 1.0.40

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,22 @@ 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
- | `onError` | `(error, componentStack?) => void` | Called when a document cannot be compiled or rendered |
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) |
191
+ | `onError` | `(diagnostic) => void` | this instance will not play — the document could not be compiled or rendered; `fallback` shows instead. `diagnostic.error` is the Error, `diagnostic.detail.componentStack` when the error boundary caught it |
192
+ | `onWarn` | `(diagnostic) => void` | it plays, but something was ignored, degraded or misspelled; without it → `console.warn` |
193
+ | `muteWarn` | `boolean` | switch the `console.warn` fallback off — for when you know the player has something to say about this document and are prepared to tolerate it. `onWarn`, if you gave it, still fires: mute is about the console, not about you |
194
+ | `muteError` | `boolean` | the same switch for `console.error` |
190
195
  | `fallback` | `(error) => ReactElement \| null` | Rendered in place of a failed animation (default: nothing) |
191
196
 
192
197
  With none of `autoplay` / `play` / `pause` / `progress` / `time` set, the component
@@ -196,14 +201,14 @@ renders the animation statically (initial state, no playback).
196
201
 
197
202
  The component never throws for a bad document. Compilation and rendering both
198
203
  run inside `try`/`catch`, and the rendered tree sits behind an error boundary,
199
- so a failure is reported through `onError` and shows `fallback` while the
200
- surrounding screen keeps working.
204
+ so a failure is reported through `onError` — the shared diagnostic (`diagnostic.error` is the
205
+ Error), exactly as on the web — and shows `fallback` while the surrounding screen keeps working.
201
206
 
202
207
  ```tsx
203
208
  <PixodeskSvgAnimator
204
209
  doc={doc}
205
210
  autoplay
206
- onError={e => console.warn('animation failed:', e.message)}
211
+ onError={d => console.warn('animation failed:', d.message)}
207
212
  fallback={() => <Text>could not play this animation</Text>}
208
213
  />
209
214
  ```
@@ -214,11 +219,12 @@ renderer never reaches JavaScript and cannot be caught — see
214
219
 
215
220
  ### Differences from the React package
216
221
 
222
+ <!-- px-check off differences from React, prose -->
217
223
  | Prop | Why it differs |
218
224
  |---|---|
219
- | `timeline.engine` | Accepted inside `config` but ignored. There is no Web Animations API on React Native; playback is always native-driven. |
220
- | `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. |
225
+ | `timeline.engine` | Accepted inside `timeline` but ignored. There is no Web Animations API on React Native; playback is always native-driven. |
226
+ | `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, which the player fixes at 60. |
227
+ | `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
228
  | `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
229
  | `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
230
 
@@ -228,9 +234,9 @@ There is **no JavaScript frame loop** — the JS thread is idle while an
228
234
  animation runs.
229
235
 
230
236
  1. **Once per document:** the shared core flattens it
231
- (`materialiseAllInTree` → effects, loops, motion-path sampling, animated
237
+ (`materializeAllInTree` → effects, loops, motion-path sampling, animated
232
238
  `<use>` inlining), then a track compiler densely samples every animated
233
- property with `calcAnimationValues` — the same function the web frames engine
239
+ property with `calcAnimationValues` — the same function the web frame-loop engine
234
240
  renders with, so values match the web player exactly.
235
241
  2. **Per frame:** one reanimated progress value, driven by
236
242
  `withTiming`/`withRepeat` on the UI thread, and a tiny worklet per animated
@@ -243,73 +249,78 @@ converts it into plain values ahead of time instead of fighting the platform.
243
249
  ## Feature support
244
250
 
245
251
  Every row below was verified by running the document through the real
246
- pipeline (`materialiseAllInTree` → track compilation) and checking that the
252
+ pipeline (`materializeAllInTree` → track compilation) and checking that the
247
253
  element maps to a `react-native-svg` component and that its animated
248
254
  properties actually change over time.
249
255
 
250
256
  ### Elements
251
257
 
258
+ <!-- px-check off support matrix, prose -->
252
259
  | Element | Renders | Notes |
253
260
  |---|---|---|
254
261
  | `svg`, `g`, `defs` | ✅ | |
255
262
  | `rect`, `circle`, `ellipse`, `line`, `path`, `polygon`, `polyline` | ✅ | |
256
- | `text`, `tspan` | ✅ | content via the `text` attribute |
263
+ | `text`, `tspan` | ✅ | content via `textContent` |
257
264
  | `textPath` | ✅ | see *Text along a path* below |
258
- | `image` | ✅ | `href` accepts `data:` URIs; remote URLs are blocked by the sanitiser |
265
+ | `image` | ✅ | `href` accepts `data:` URIs; remote URLs are blocked by the sanitizer |
259
266
  | `use`, `symbol` | ✅ | animated targets are **inlined into real clones** before render — `<use>` does not propagate animation natively in React Native |
260
267
  | `linearGradient`, `radialGradient`, `stop` | ✅ | |
261
268
  | `pattern`, `marker` | ✅ | static geometry verified; complex cases unverified on device |
262
269
  | `mask`, `clipPath` | ✅ | |
263
270
  | `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 |
271
+ | `foreignObject` | ❌ | blocked by the shared sanitizer (embeds arbitrary host content) |
272
+ | `script` | ❌ | blocked by the shared sanitizer |
266
273
 
267
274
  ### Animatable attributes
268
275
 
276
+ <!-- px-check off support matrix, prose -->
269
277
  | Attribute | Animates | Notes |
270
278
  |---|---|---|
271
- | `opacity`, `fill-opacity`, `stroke-opacity` | ✅ | |
272
- | `fill`, `stroke`, `stop-color` | ✅ | interpolated as RGBA |
273
- | `stroke-width`, `stroke-dasharray`, `stroke-dashoffset` | ✅ | dash arrays are converted to the numeric form React Native expects |
279
+ | `opacity`, `fillOpacity`, `strokeOpacity` | ✅ | |
280
+ | `fill`, `stroke`, `stopColor` | ✅ | interpolated as RGBA |
281
+ | `strokeWidth`, `strokeDasharray`, `strokeDashoffset` | ✅ | dash arrays are converted to the numeric form React Native expects |
274
282
  | `x`, `y`, `width`, `height`, `cx`, `cy`, `r`, `rx`, `ry` | ✅ | |
275
283
  | `d` (**path morphing**) | ✅ | keyframes must share command structure |
276
284
  | `transform` (unified parts record) | ✅ | `translate`, `rotate`, `skew`, `scale`, `origin` |
277
285
  | `translate` / `rotate` / `scale` (legacy per-key form) | ✅ | |
278
- | `offset` and `stop-color` on gradient stops | ✅ | |
286
+ | `offset` and `stopColor` on gradient stops | ✅ | |
279
287
  | filter primitive attrs (e.g. `stdDeviation`) | ✅ | compiles correctly; on-device rendering unverified |
280
- | `font-size` | ✅ | |
288
+ | `fontSize` | ✅ | |
281
289
  | Any other numeric SVG attribute | ✅ | interpolated numerically and written straight through |
282
290
 
283
291
  ### Effects (`node.effects`)
284
292
 
285
- All effects are materialised by the shared core before rendering, so the React Native
293
+ All effects are materialized by the shared core before rendering, so the React Native
286
294
  player sees plain nodes. **All are supported:**
287
295
 
296
+ <!-- px-check schema PxEffectsSchema -->
288
297
  | Effect | Status | Notes |
289
298
  |---|---|---|
290
299
  | `transformBy` | ✅ | all parts animatable, including `skew` |
291
- | `repeater` | ✅ | copies materialised as real elements; per-copy params animatable |
300
+ | `repeater` | ✅ | copies materialized as real elements; per-copy params animatable |
292
301
  | `maskedBy` | ✅ | including an animated mask source |
293
302
  | `clipPath` | ✅ | including animated clip geometry |
294
303
  | `strokeTrim` | ✅ | incl. `offset` and `subPaths: 'combined'` |
295
304
  | `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) |
305
+ | `fillGradient` / `strokeGradient` | ✅ | animated stops **and animated geometry** (`animate.gradientX1`/`Cx`/`R`, …); `gradientTransform` is static (core-wide) | <!-- px names=fillGradient,strokeGradient -->
297
306
  | `textPath` | ✅ | incl. animated `startOffset` |
298
307
  | `text.useGlyphs` | ✅ | text becomes `<path>` outlines from `definitions.fonts` — no font needed |
299
308
 
300
309
  ### Motion, timing and references
301
310
 
311
+ <!-- px-check off support matrix, prose -->
302
312
  | Feature | Status | Notes |
303
313
  |---|---|---|
304
314
  | **Motion along a path** + `autoOrient` | ✅ | **sampled** by the core into plain transform keyframes — `react-native-svg` has no native path motion |
305
315
  | **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` |
306
316
  | Per-property `loop` (incl. `alternate` pingpong) | ✅ | expanded before playback |
307
317
  | Easing (cubic-bezier and named refs) | ✅ | baked into the sampled tracks |
308
- | `definitions.animations` / `easings` / `styles` / `fonts` | ✅ | named refs resolved; `style` presets applied as props |
309
- | `node.style` (inline or named) | ✅ | resolved to props — React Native has no CSS, so explicit attributes win |
318
+ | `definitions.animations` / `easings` / `fonts` | ✅ | named refs resolved |
319
+ | `node.style` (inline record) | ✅ | applied as props — React Native has no CSS, so explicit attributes win |
310
320
 
311
321
  ### Playback and triggers
312
322
 
323
+ <!-- px-check off support matrix, prose -->
313
324
  | Feature | Status | Notes |
314
325
  |---|---|---|
315
326
  | `duration`, `delay`, `iterations` (incl. `'infinite'`) | ✅ | |
@@ -321,9 +332,9 @@ player sees plain nodes. **All are supported:**
321
332
  | `setPlaybackRate` — faster, slower and **reverse** (negative) | ✅ | composes with `direction` |
322
333
  | Trigger `load` / `programmatic` | ✅ | |
323
334
  | 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` |
335
+ | Trigger `scrollIntoView` | ✅ | visibility sampled by measuring against the window (React Native has no `IntersectionObserver`); honors `scrollIntoViewThreshold` and `outAction` |
325
336
  | Trigger `mouseOver` | ❌ | no touch equivalent — use `click`, or drive `play` yourself |
326
- | `timeline.frameRate` | n/a | reanimated runs at the display refresh rate; use `compileTracks({sampleRate})` to trade memory for temporal precision |
337
+ | `timeline.frameRate` | n/a | reanimated runs at the display refresh rate; the player prepares 60 snapshots per second of animation |
327
338
  | `timeline.engine` (`auto` / `native` / `js`) | n/a | there is no Web Animations API on React Native — playback is always native-driven |
328
339
 
329
340
  ### Known limitations
@@ -342,7 +353,7 @@ player sees plain nodes. **All are supported:**
342
353
  glyph placement by `startOffset … startOffset + pathLength` instead of
343
354
  `0 … pathLength`, so glyphs past the end of the path reach a lookup that
344
355
  returns `NSNotFound`. On native the player gives such a `<textPath>` its own
345
- open copy of the path (`openClosedTextPathTargets`), which restores the
356
+ open copy of the path, which restores the
346
357
  correct bounds. Text that would have wrapped around past the end of the loop
347
358
  is clipped instead. Web is unaffected and left untouched.
348
359
 
@@ -381,25 +392,6 @@ config.resolver.resolveRequest = (context, moduleName, platform) => {
381
392
  A complete config is in
382
393
  [`examples/react-native-preview-player/metro.config.js`](../../examples/react-native-preview-player/metro.config.js).
383
394
 
384
- ## Advanced exports
385
-
386
- For custom rendering or diagnostics:
387
-
388
- - `renderRnNode(node, opts)` — render a `PxNode` tree to `react-native-svg`
389
- elements, with a `decorate` hook for wrapping animated elements.
390
- - `compileTracks(doc, { sampleRate, maxSamples, native })` — build the sampled
391
- tracks yourself; `sampleRate` trades memory for temporal precision
392
- (default 60/s). `native` selects the value form: the default is the SVG/DOM
393
- one, `true` gives what the native views want (a `transform` becomes a
394
- 6-number matrix).
395
- - `sampleProps(tracks, tMs, stepMs, sampleCount, native)` — the worklet-safe
396
- lookup. `native` renames `transform` to the native views' `matrix`; pass it
397
- only for values going through reanimated's animated-props path on a device.
398
- - `openClosedTextPathTargets(doc, warnings?)` — the closed-path `<textPath>`
399
- workaround described under [Known limitations](#known-limitations).
400
- - `PxRnErrorBoundary` — the boundary the component wraps itself in.
401
- - `RN_SVG_COMPONENTS`, `toRnPropName` — the tag and attribute maps.
402
-
403
395
  ## Example app
404
396
 
405
397
  A full preview player with six animations and transport controls: