@pixodesk/svg-animator-rn 1.0.21 โ 1.0.22
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 +415 -0
- package/dist/index.cjs +512 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +103 -7
- package/dist/index.d.ts +103 -7
- package/dist/index.js +528 -84
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/PixodeskSvgAnimator.tsx +389 -69
- package/src/PxRnErrorBoundary.tsx +57 -0
- package/src/PxRnMatrix.test.ts +97 -0
- package/src/PxRnMatrix.ts +110 -0
- package/src/PxRnPropNames.ts +21 -1
- package/src/PxRnRender.tsx +53 -8
- package/src/PxRnSafety.test.ts +118 -0
- package/src/PxRnSafety.ts +123 -0
- package/src/PxRnTracks.test.ts +52 -4
- package/src/PxRnTracks.ts +57 -5
- package/src/PxRnTypeMap.ts +72 -5
- package/src/index.ts +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# animator-rn
|
|
2
|
+
|
|
3
|
+
[](https://github.com/pixodesk/pixodesk-svg-animator/actions/workflows/ci.yml)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
React Native component for rendering and controlling Pixodesk SVG animations,
|
|
7
|
+
built on `react-native-svg` and `react-native-reanimated`. Props mirror
|
|
8
|
+
[`@pixodesk/svg-animator-react`](../svg-animator-react/README.md).
|
|
9
|
+
|
|
10
|
+
# ๐งช **Status โ EXPERIMENTAL**
|
|
11
|
+
|
|
12
|
+
- API may change without a major version bump.
|
|
13
|
+
- A few things are unimplemented or unverified โ see [Feature support](#feature-support).
|
|
14
|
+
|
|
15
|
+
## What it does
|
|
16
|
+
|
|
17
|
+
`<PixodeskSvgAnimator doc={โฆ} />` takes an animation document exported from the
|
|
18
|
+
Pixodesk editor โ the **same `.json` the web player uses** โ and renders it as
|
|
19
|
+
native SVG, driven on the UI thread.
|
|
20
|
+
|
|
21
|
+
- **One prop to render:** put the `.json` next to your component, import it, pass
|
|
22
|
+
it as `doc`.
|
|
23
|
+
- **Playback control:** declarative props (`autoplay`, `play`, `pause`,
|
|
24
|
+
`timeMs`) or an imperative ref (`play()`, `pause()`, `setCurrentTime()`, โฆ).
|
|
25
|
+
- **Sized by its container:** wrap it in a `View` with the dimensions you want.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
Drop `animation.json` next to your component and render it:
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { View } from 'react-native';
|
|
33
|
+
import { PixodeskSvgAnimator } from '@pixodesk/svg-animator-rn';
|
|
34
|
+
import animation from './animation.json';
|
|
35
|
+
|
|
36
|
+
export function Logo() {
|
|
37
|
+
return (
|
|
38
|
+
<View style={{ width: 200, height: 200 }}>
|
|
39
|
+
<PixodeskSvgAnimator doc={animation} autoplay />
|
|
40
|
+
</View>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That's the whole common case: an animation that plays on mount and loops if the
|
|
46
|
+
document says so.
|
|
47
|
+
|
|
48
|
+
### Typical cases
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// Play once when a screen opens, then hold the last frame
|
|
52
|
+
<PixodeskSvgAnimator doc={doc} autoplay iterations={1} fill="forwards" />
|
|
53
|
+
|
|
54
|
+
// Loop forever regardless of what the document says
|
|
55
|
+
<PixodeskSvgAnimator doc={doc} autoplay iterations="infinite" />
|
|
56
|
+
|
|
57
|
+
// Static โ first frame only, no playback
|
|
58
|
+
<PixodeskSvgAnimator doc={doc} />
|
|
59
|
+
|
|
60
|
+
// Toggle from your own state
|
|
61
|
+
<PixodeskSvgAnimator doc={doc} play={isPlaying} />
|
|
62
|
+
|
|
63
|
+
// Tie progress to a gesture / slider (no playback, just a frame)
|
|
64
|
+
<PixodeskSvgAnimator doc={doc} time={scrollProgress} />
|
|
65
|
+
|
|
66
|
+
// Override the document's timing (duration in ms, delay before start)
|
|
67
|
+
<PixodeskSvgAnimator doc={doc} autoplay duration={4000} delay={500} />
|
|
68
|
+
|
|
69
|
+
// Do something when it finishes
|
|
70
|
+
<PixodeskSvgAnimator doc={doc} autoplay onFinish={() => setDone(true)} />
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
TypeScript: `import animation from './animation.json'` gives you a plain object.
|
|
74
|
+
If your `tsconfig` complains, cast it:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import type { PxAnimatedSvgDocument } from '@pixodesk/svg-animator-core';
|
|
78
|
+
const doc = animation as PxAnimatedSvgDocument;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Install
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm install @pixodesk/svg-animator-rn
|
|
85
|
+
# peers, if you don't have them already:
|
|
86
|
+
npx expo install react-native-svg react-native-reanimated
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Peer dependencies: `react >=18`, `react-native >=0.76`,
|
|
90
|
+
`react-native-svg >=15`, `react-native-reanimated >=3.16`.
|
|
91
|
+
|
|
92
|
+
Reanimated needs its Babel plugin. In `babel.config.js`:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
module.exports = function (api) {
|
|
96
|
+
api.cache(true);
|
|
97
|
+
return {
|
|
98
|
+
presets: ['babel-preset-expo'],
|
|
99
|
+
plugins: ['react-native-reanimated/plugin'], // must be last
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> **Monorepo users:** make sure `react-native-svg`, `react-native-reanimated`
|
|
105
|
+
> and `react` resolve to a **single copy**. Two copies produce
|
|
106
|
+
> `View config getter callback for component 'RNSVGLine' must be a function`
|
|
107
|
+
> at runtime. See [Monorepo setup](#monorepo-setup).
|
|
108
|
+
|
|
109
|
+
## Control modes
|
|
110
|
+
|
|
111
|
+
Four ways to drive playback. Pick one โ they are mutually exclusive.
|
|
112
|
+
|
|
113
|
+
### Autoplay
|
|
114
|
+
|
|
115
|
+
Uses the trigger defined in the animation document (`startOn: 'load'` plays on mount):
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
<PixodeskSvgAnimator doc={doc} autoplay />
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Declarative play/pause
|
|
122
|
+
|
|
123
|
+
Control playback with boolean props:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
const [play, setPlay] = useState(false);
|
|
127
|
+
const [pause, setPause] = useState(false);
|
|
128
|
+
|
|
129
|
+
<PixodeskSvgAnimator doc={doc} play={play} pause={pause} />
|
|
130
|
+
<Button title={play ? 'Play (on)' : 'Play (off)'} onPress={() => setPlay(p => !p)} />
|
|
131
|
+
<Button title={pause ? 'Pause (on)' : 'Pause (off)'} onPress={() => setPause(p => !p)} />
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Imperative API
|
|
135
|
+
|
|
136
|
+
Use a ref for full programmatic control:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { useRef } from 'react';
|
|
140
|
+
import type { RnAnimatorApi } from '@pixodesk/svg-animator-rn';
|
|
141
|
+
|
|
142
|
+
const api = useRef<RnAnimatorApi>(null);
|
|
143
|
+
|
|
144
|
+
<PixodeskSvgAnimator doc={doc} apiRef={api} />
|
|
145
|
+
<Button title="Play" onPress={() => api.current?.play()} />
|
|
146
|
+
<Button title="Pause" onPress={() => api.current?.pause()} />
|
|
147
|
+
<Button title="Cancel" onPress={() => api.current?.cancel()} />
|
|
148
|
+
<Button title="Finish" onPress={() => api.current?.finish()} />
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`RnAnimatorApi` methods: `play()`, `pause()`, `cancel()`, `finish()`,
|
|
152
|
+
`isPlaying()`, `setPlaybackRate(rate)`, `getCurrentTime()`, `setCurrentTime(ms)`.
|
|
153
|
+
|
|
154
|
+
### Controlled time
|
|
155
|
+
|
|
156
|
+
Scrub through the animation or pin a fixed frame:
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
const [timeMs, setTimeMs] = useState(0);
|
|
160
|
+
|
|
161
|
+
<PixodeskSvgAnimator doc={doc} timeMs={timeMs} />
|
|
162
|
+
<Slider minimumValue={0} maximumValue={2000} value={timeMs} onValueChange={setTimeMs} />
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Props
|
|
166
|
+
|
|
167
|
+
| Prop | Type | Description |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `doc` | `PxAnimatedSvgDocument` | The animation document to render (required) |
|
|
170
|
+
| `autoplay` | `boolean` | Use the trigger from the document |
|
|
171
|
+
| `play` | `boolean` | Start playback, ignoring document triggers |
|
|
172
|
+
| `pause` | `boolean` | Pause current playback |
|
|
173
|
+
| `apiRef` | `RefObject<RnAnimatorApi>` | Ref for imperative control |
|
|
174
|
+
| `time` | `number` | Seek to a fraction (0โ1) of the whole timeline (duration ร iterations) |
|
|
175
|
+
| `timeMs` | `number` | Seek to a time in milliseconds |
|
|
176
|
+
| `duration` | `number` | Duration override (ms) |
|
|
177
|
+
| `delay` | `number` | Delay before start (ms) |
|
|
178
|
+
| `iterations` | `number \| 'infinite'` | Loop count |
|
|
179
|
+
| `fill` | `FillMode` | Fill behaviour |
|
|
180
|
+
| `direction` | `PlaybackDirection` | Playback direction |
|
|
181
|
+
| `resetOnFinish` | `boolean` | Snap back to the start after a natural finish |
|
|
182
|
+
| `outAction` | `OutAction` | What a second tap does with the `click` trigger (default: the document's, else `pause`) |
|
|
183
|
+
| `onPlay` | `() => void` | Called on play/resume |
|
|
184
|
+
| `onPause` | `() => void` | Called on pause |
|
|
185
|
+
| `onFinish` | `() => void` | Called on natural finish |
|
|
186
|
+
| `onCancel` | `() => void` | Called on cancel |
|
|
187
|
+
| `onStop` | `() => void` | Called whenever playback halts (pause / cancel / finish) |
|
|
188
|
+
| `onError` | `(error, componentStack?) => void` | Called when a document cannot be compiled or rendered |
|
|
189
|
+
| `fallback` | `(error) => ReactElement \| null` | Rendered in place of a failed animation (default: nothing) |
|
|
190
|
+
|
|
191
|
+
With none of `autoplay` / `play` / `pause` / `time` / `timeMs` set, the component
|
|
192
|
+
renders the animation statically (initial state, no playback).
|
|
193
|
+
|
|
194
|
+
### Failure handling
|
|
195
|
+
|
|
196
|
+
The component never throws for a bad document. Compilation and rendering both
|
|
197
|
+
run inside `try`/`catch`, and the rendered tree sits behind an error boundary,
|
|
198
|
+
so a failure is reported through `onError` and shows `fallback` while the
|
|
199
|
+
surrounding screen keeps working.
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
<PixodeskSvgAnimator
|
|
203
|
+
doc={doc}
|
|
204
|
+
autoplay
|
|
205
|
+
onError={e => console.warn('animation failed:', e.message)}
|
|
206
|
+
fallback={() => <Text>could not play this animation</Text>}
|
|
207
|
+
/>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
This covers JavaScript errors. A crash inside react-native-svg's **native**
|
|
211
|
+
renderer never reaches JavaScript and cannot be caught โ see
|
|
212
|
+
[Known limitations](#known-limitations).
|
|
213
|
+
|
|
214
|
+
### Differences from the React package
|
|
215
|
+
|
|
216
|
+
| Prop | Why it differs |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `mode` | Not accepted. There is no Web Animations API on RN; playback is always native-driven. |
|
|
219
|
+
| `frameRate` | Not accepted. Reanimated runs at the display refresh rate. The analogous knob is sampling density โ see `compileTracks`. |
|
|
220
|
+
| `startOn` | Not accepted as a prop โ the document's trigger is honoured via `autoplay` (`load`, `click`, `scrollIntoView`, `programmatic`). `mouseOver` has no touch equivalent. |
|
|
221
|
+
| `className` / `style` | Not accepted. Size the animation with the container `View`. (`node.style` *inside* the document is supported.) |
|
|
222
|
+
| `onRemove` | Not emitted. Use React's own unmount cleanup. |
|
|
223
|
+
|
|
224
|
+
## How playback works
|
|
225
|
+
|
|
226
|
+
There is **no JavaScript frame loop** โ the JS thread is idle while an
|
|
227
|
+
animation runs.
|
|
228
|
+
|
|
229
|
+
1. **Once per document:** the shared core flattens it
|
|
230
|
+
(`materialiseAllInTree` โ effects, loops, motion-path sampling, animated
|
|
231
|
+
`<use>` inlining), then a track compiler densely samples every animated
|
|
232
|
+
property with `calcAnimationValues` โ the same function the web frames engine
|
|
233
|
+
renders with, so values match the web player exactly.
|
|
234
|
+
2. **Per frame:** one reanimated progress value, driven by
|
|
235
|
+
`withTiming`/`withRepeat` on the UI thread, and a tiny worklet per animated
|
|
236
|
+
element that indexes its precompiled track.
|
|
237
|
+
|
|
238
|
+
This is why sampling appears so often below: where `react-native-svg` cannot
|
|
239
|
+
express something directly (motion along a path, text on a path), the core
|
|
240
|
+
converts it into plain values ahead of time instead of fighting the platform.
|
|
241
|
+
|
|
242
|
+
## Feature support
|
|
243
|
+
|
|
244
|
+
Every row below was verified by running the document through the real
|
|
245
|
+
pipeline (`materialiseAllInTree` โ track compilation) and checking that the
|
|
246
|
+
element maps to a `react-native-svg` component and that its animated
|
|
247
|
+
properties actually change over time.
|
|
248
|
+
|
|
249
|
+
### Elements
|
|
250
|
+
|
|
251
|
+
| Element | Renders | Notes |
|
|
252
|
+
|---|---|---|
|
|
253
|
+
| `svg`, `g`, `defs` | โ
| |
|
|
254
|
+
| `rect`, `circle`, `ellipse`, `line`, `path`, `polygon`, `polyline` | โ
| |
|
|
255
|
+
| `text`, `tspan` | โ
| content via the `text` attribute |
|
|
256
|
+
| `textPath` | โ
| see *Text along a path* below |
|
|
257
|
+
| `image` | โ
| `href` accepts `data:` URIs; remote URLs are blocked by the sanitiser |
|
|
258
|
+
| `use`, `symbol` | โ
| animated targets are **inlined into real clones** before render โ `<use>` does not propagate animation natively in RN |
|
|
259
|
+
| `linearGradient`, `radialGradient`, `stop` | โ
| |
|
|
260
|
+
| `pattern`, `marker` | โ
| static geometry verified; complex cases unverified on device |
|
|
261
|
+
| `mask`, `clipPath` | โ
| |
|
|
262
|
+
| `filter` + all 22 `fe*` primitives | โ
| `feGaussianBlur`, `feDropShadow`, `feColorMatrix`, `feMerge`, `feComponentTransfer` + `feFunc*`, โฆ Requires the New Architecture; **visual parity with the web is unverified on device** |
|
|
263
|
+
| `foreignObject` | โ | blocked by the shared sanitiser (embeds arbitrary host content) |
|
|
264
|
+
| `script` | โ | blocked by the shared sanitiser |
|
|
265
|
+
|
|
266
|
+
### Animatable attributes
|
|
267
|
+
|
|
268
|
+
| Attribute | Animates | Notes |
|
|
269
|
+
|---|---|---|
|
|
270
|
+
| `opacity`, `fill-opacity`, `stroke-opacity` | โ
| |
|
|
271
|
+
| `fill`, `stroke`, `stop-color` | โ
| interpolated as RGBA |
|
|
272
|
+
| `stroke-width`, `stroke-dasharray`, `stroke-dashoffset` | โ
| dash arrays are converted to the numeric form RN expects |
|
|
273
|
+
| `x`, `y`, `width`, `height`, `cx`, `cy`, `r`, `rx`, `ry` | โ
| |
|
|
274
|
+
| `d` (**path morphing**) | โ
| keyframes must share command structure |
|
|
275
|
+
| `transform` (unified parts record) | โ
| `translate`, `rotate`, `skew`, `scale`, `origin` |
|
|
276
|
+
| `translate` / `rotate` / `scale` (legacy per-key form) | โ
| |
|
|
277
|
+
| `offset` and `stop-color` on gradient stops | โ
| |
|
|
278
|
+
| filter primitive attrs (e.g. `stdDeviation`) | โ
| compiles correctly; on-device rendering unverified |
|
|
279
|
+
| `font-size` | โ
| |
|
|
280
|
+
| Any other numeric SVG attribute | โ
| interpolated numerically and written straight through |
|
|
281
|
+
|
|
282
|
+
### Effects (`node.effects`)
|
|
283
|
+
|
|
284
|
+
All effects are materialised by the shared core before rendering, so the RN
|
|
285
|
+
player sees plain nodes. **All are supported:**
|
|
286
|
+
|
|
287
|
+
| Effect | Status | Notes |
|
|
288
|
+
|---|---|---|
|
|
289
|
+
| `transformation` | โ
| all parts animatable, including `skew` |
|
|
290
|
+
| `repeater` | โ
| copies materialised as real elements; per-copy params animatable |
|
|
291
|
+
| `maskedBy` | โ
| including an animated mask source |
|
|
292
|
+
| `clipPath` | โ
| including animated clip geometry |
|
|
293
|
+
| `trimPath` | โ
| incl. `offset` and `trimAllAsOne` |
|
|
294
|
+
| `clone` + `retime` | โ
| each clone keeps its own time shift. `retime.timeCrop` is not implemented (core-wide) |
|
|
295
|
+
| `fillGradient` / `strokeGradient` | โ
| animated stops **and animated geometry** (`animate.gradientX1`/`Cx`/`R`, โฆ); `gradientTransform` is static (core-wide) |
|
|
296
|
+
| `textPath` | โ
| incl. animated `startOffset` |
|
|
297
|
+
| `text.useGlyphs` | โ
| text becomes `<path>` outlines from `definitions.glyphs` โ no font needed |
|
|
298
|
+
| `isCombinedShape` | โ
| |
|
|
299
|
+
|
|
300
|
+
### Motion, timing and references
|
|
301
|
+
|
|
302
|
+
| Feature | Status | Notes |
|
|
303
|
+
|---|---|---|
|
|
304
|
+
| **Motion along a path** + `autoOrient` | โ
| **sampled** by the core into plain transform keyframes โ `react-native-svg` has no native path motion |
|
|
305
|
+
| **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
|
+
| Per-property `loop` (incl. `alternate` pingpong) | โ
| expanded before playback |
|
|
307
|
+
| Easing (cubic-bezier and named refs) | โ
| baked into the sampled tracks |
|
|
308
|
+
| `definitions.animations` / `easings` / `styles` / `glyphs` | โ
| named refs resolved; `style` presets applied as props |
|
|
309
|
+
| `node.style` (inline or named) | โ
| resolved to props โ RN has no CSS, so explicit attributes win |
|
|
310
|
+
|
|
311
|
+
### Playback and triggers
|
|
312
|
+
|
|
313
|
+
| Feature | Status | Notes |
|
|
314
|
+
|---|---|---|
|
|
315
|
+
| `duration`, `delay`, `iterations` (incl. `'infinite'`) | โ
| |
|
|
316
|
+
| `direction` โ all four values | โ
| |
|
|
317
|
+
| `fill` โ `forwards` / `backwards` / `both` / `none` | โ
| |
|
|
318
|
+
| `resetOnFinish` | โ
| |
|
|
319
|
+
| `play` / `pause` / `cancel` / `finish` | โ
| |
|
|
320
|
+
| `setCurrentTime` โ seek, including **while playing** | โ
| playback continues from the new position |
|
|
321
|
+
| `setPlaybackRate` โ faster, slower and **reverse** (negative) | โ
| composes with `direction` |
|
|
322
|
+
| Trigger `load` / `programmatic` | โ
| |
|
|
323
|
+
| Trigger `click` | โ
| wrapped in a `Pressable`; a second tap applies `outAction` |
|
|
324
|
+
| Trigger `scrollIntoView` | โ
| visibility sampled by measuring against the window (RN has no `IntersectionObserver`); honours `scrollIntoViewThreshold` and `outAction` |
|
|
325
|
+
| Trigger `mouseOver` | โ | no touch equivalent โ use `click`, or drive `play` yourself |
|
|
326
|
+
| `frameRate` | n/a | reanimated runs at the display refresh rate; use `compileTracks({sampleRate})` to trade memory for temporal precision |
|
|
327
|
+
| `mode` (`webapi` / `frames`) | n/a | there is no Web Animations API on RN โ playback is always native-driven |
|
|
328
|
+
|
|
329
|
+
### Known limitations
|
|
330
|
+
|
|
331
|
+
- **On-device verification is incomplete.** The pipeline, prop mapping and
|
|
332
|
+
driving logic are covered by unit tests and were exercised end-to-end
|
|
333
|
+
through `react-native-web`; the native reanimated โ `react-native-svg` prop
|
|
334
|
+
bridge (notably filters and `strokeDasharray`) still needs checking on real
|
|
335
|
+
iOS/Android.
|
|
336
|
+
- **`retime.timeCrop`** and **animated `gradientTransform`** are unimplemented
|
|
337
|
+
in the core, so they are unavailable here too.
|
|
338
|
+
- **`mouseOver`** has no touch analogue and will not be implemented.
|
|
339
|
+
- **Text on a closed path is worked around, not fixed.** react-native-svg's
|
|
340
|
+
native text-on-path layout crashes the app (an `NSRangeException` on iOS) when
|
|
341
|
+
a `<textPath>` has a non-zero `startOffset` on a *closed* path: it bounds
|
|
342
|
+
glyph placement by `startOffset โฆ startOffset + pathLength` instead of
|
|
343
|
+
`0 โฆ pathLength`, so glyphs past the end of the path reach a lookup that
|
|
344
|
+
returns `NSNotFound`. On native the player gives such a `<textPath>` its own
|
|
345
|
+
open copy of the path (`openClosedTextPathTargets`), which restores the
|
|
346
|
+
correct bounds. Text that would have wrapped around past the end of the loop
|
|
347
|
+
is clipped instead. Web is unaffected and left untouched.
|
|
348
|
+
|
|
349
|
+
## Monorepo setup
|
|
350
|
+
|
|
351
|
+
pnpm and yarn workspaces can install **two physical copies** of a native package
|
|
352
|
+
when peer versions differ even slightly. If the copy this player imports is not
|
|
353
|
+
the copy the app registered natively, you get:
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
Invariant Violation: View config getter callback for component `RNSVGLine`
|
|
357
|
+
must be a function (received `undefined`)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Two things prevent it:
|
|
361
|
+
|
|
362
|
+
1. Keep `@types/react`, `react` and `react-native` versions aligned across every
|
|
363
|
+
workspace package.
|
|
364
|
+
2. Force single instances in `metro.config.js`:
|
|
365
|
+
|
|
366
|
+
```js
|
|
367
|
+
const SINGLETONS = ['react', 'react-dom', 'react-native', 'react-native-svg',
|
|
368
|
+
'react-native-reanimated', 'react-native-worklets'];
|
|
369
|
+
|
|
370
|
+
const base = config.resolver.resolveRequest;
|
|
371
|
+
config.resolver.resolveRequest = (context, moduleName, platform) => {
|
|
372
|
+
if (SINGLETONS.some(n => moduleName === n || moduleName.startsWith(n + '/'))) {
|
|
373
|
+
return context.resolveRequest(
|
|
374
|
+
{ ...context, originModulePath: path.join(projectRoot, 'index.js') },
|
|
375
|
+
moduleName, platform);
|
|
376
|
+
}
|
|
377
|
+
return (base ?? context.resolveRequest)(context, moduleName, platform);
|
|
378
|
+
};
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
A complete config is in
|
|
382
|
+
[`examples/react-native-preview-player/metro.config.js`](../../examples/react-native-preview-player/metro.config.js).
|
|
383
|
+
|
|
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
|
+
## Example app
|
|
404
|
+
|
|
405
|
+
A full preview player with six animations and transport controls:
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
pnpm example:rn # or: cd examples/react-native-preview-player && npx expo start
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
See [`examples/react-native-preview-player`](../../examples/react-native-preview-player).
|
|
412
|
+
|
|
413
|
+
## License
|
|
414
|
+
|
|
415
|
+
[MIT](../../LICENSE) ยฉ [Pixodesk](https://pixodesk.com)
|