@pixodesk/svg-animator-rn 1.0.21 → 1.0.24
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 +54 -8
- package/src/PxRnSafety.test.ts +118 -0
- package/src/PxRnSafety.ts +123 -0
- package/src/PxRnTracks.test.ts +56 -8
- package/src/PxRnTracks.ts +57 -5
- package/src/PxRnTypeMap.ts +72 -5
- package/src/index.ts +2 -0
package/src/PxRnTracks.test.ts
CHANGED
|
@@ -84,11 +84,20 @@ describe('compileTracks', () => {
|
|
|
84
84
|
expect(rect.props.fill[0]).not.toBe(rect.props.fill[rect.props.fill.length - 1]);
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
it('samples transforms as
|
|
88
|
-
|
|
87
|
+
it('samples transforms as MATRICES when targeting native views', () => {
|
|
88
|
+
// react-native-svg parses a transform string into a matrix in JS during
|
|
89
|
+
// render — a step reanimated's animated-props path skips. Feeding the
|
|
90
|
+
// matrix directly is what makes animated transforms work on device.
|
|
91
|
+
const tracks = compile(makeDoc(), { native: true });
|
|
89
92
|
const g = tracks.elements.find(e => 'transform' in e.props)!;
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
const first = g.props.transform[0] as Array<number>;
|
|
94
|
+
const last = g.props.transform[g.props.transform.length - 1] as Array<number>;
|
|
95
|
+
|
|
96
|
+
expect(Array.isArray(first)).toBe(true);
|
|
97
|
+
expect(first).toHaveLength(6);
|
|
98
|
+
// translate(0,0) → identity; translate(100,100) → tx/ty in slots 4/5
|
|
99
|
+
expect(first).toEqual([1, 0, 0, 1, 0, 0]);
|
|
100
|
+
expect(last).toEqual([1, 0, 0, 1, 100, 100]);
|
|
92
101
|
});
|
|
93
102
|
|
|
94
103
|
it('respects sampleRate and maxSamples options', () => {
|
|
@@ -147,7 +156,7 @@ describe('length-list props (stroke-dasharray)', () => {
|
|
|
147
156
|
effects: { trimPath: { range: { keyframes: [{ time: 0, value: [0, 0.1] }, { time: 1000, value: [0, 1] }] } } },
|
|
148
157
|
}],
|
|
149
158
|
};
|
|
150
|
-
const materialised = generateNewIds(materialiseAllInTree(doc, PxAnimatorEngine.
|
|
159
|
+
const materialised = generateNewIds(materialiseAllInTree(doc, PxAnimatorEngine.waapi));
|
|
151
160
|
const tracks = compileTracks(materialised);
|
|
152
161
|
const el = tracks.elements.find(e => 'strokeDasharray' in e.props)!;
|
|
153
162
|
expect(el).toBeDefined();
|
|
@@ -158,7 +167,7 @@ describe('length-list props (stroke-dasharray)', () => {
|
|
|
158
167
|
});
|
|
159
168
|
});
|
|
160
169
|
|
|
161
|
-
describe('animated <use> flattening (
|
|
170
|
+
describe('animated <use> flattening (waapi materialisation)', () => {
|
|
162
171
|
it('inlines animated <use> clones so no live references remain', () => {
|
|
163
172
|
const doc: PxAnimatedSvgDocument = {
|
|
164
173
|
type: 'svg', viewBox: '0 0 300 200',
|
|
@@ -169,10 +178,10 @@ describe('animated <use> flattening (webapi materialisation)', () => {
|
|
|
169
178
|
animate: { cy: { keyframes: [{ time: 0, value: 40 }, { time: 2000, value: 160 }] } },
|
|
170
179
|
}] }] },
|
|
171
180
|
{ type: 'use', id: 'u1', href: '#sym' },
|
|
172
|
-
{ type: 'use', id: 'u2', href: '#sym', x: 80, effects: { clone: {
|
|
181
|
+
{ type: 'use', id: 'u2', href: '#sym', x: 80, effects: { clone: { sourceId: '#sym', retime: { start: -600 } } } },
|
|
173
182
|
],
|
|
174
183
|
};
|
|
175
|
-
const materialised = generateNewIds(materialiseAllInTree(doc, PxAnimatorEngine.
|
|
184
|
+
const materialised = generateNewIds(materialiseAllInTree(doc, PxAnimatorEngine.waapi));
|
|
176
185
|
const countUse = (n: any): number =>
|
|
177
186
|
(n.type === 'use' ? 1 : 0) + (n.children || []).reduce((s: number, c: any) => s + countUse(c), 0);
|
|
178
187
|
expect(countUse(materialised)).toBe(0);
|
|
@@ -183,3 +192,42 @@ describe('animated <use> flattening (webapi materialisation)', () => {
|
|
|
183
192
|
expect(cyTracks[0].props.cy[0]).not.toBe(cyTracks[1].props.cy[0]);
|
|
184
193
|
});
|
|
185
194
|
});
|
|
195
|
+
|
|
196
|
+
describe('compileTracks platform gating', () => {
|
|
197
|
+
/** The DEFAULT is the DOM form. react-native-web passes values straight to
|
|
198
|
+
* the DOM, where a matrix array serialises to `transform="1,0,0,1,x,y"` —
|
|
199
|
+
* invalid, so the element silently stops moving. Web is the priority
|
|
200
|
+
* behaviour; native is the one that has to opt in. */
|
|
201
|
+
it('samples transforms as SVG STRINGS by default', () => {
|
|
202
|
+
const tracks = compile();
|
|
203
|
+
const g = tracks.elements.find(e => 'transform' in e.props)!;
|
|
204
|
+
|
|
205
|
+
expect(typeof g.props.transform[0]).toBe('string');
|
|
206
|
+
expect(String(g.props.transform[g.props.transform.length - 1]))
|
|
207
|
+
.toContain('translate(100');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
/** The whole native path end to end: compile → sample. Both halves have to
|
|
211
|
+
* agree, and neither is observable without a device — hence the test. */
|
|
212
|
+
it('compile+sample delivers an animated `matrix` of numbers on native', () => {
|
|
213
|
+
const tracks = compile(makeDoc(), { native: true });
|
|
214
|
+
const g = tracks.elements.find(e => 'transform' in e.props)!;
|
|
215
|
+
|
|
216
|
+
const start = sampleProps(g, 0, tracks.stepMs, tracks.sampleCount, true);
|
|
217
|
+
const end = sampleProps(g, tracks.duration, tracks.stepMs, tracks.sampleCount, true);
|
|
218
|
+
|
|
219
|
+
expect(start.transform).toBeUndefined();
|
|
220
|
+
expect(start.matrix).toEqual([1, 0, 0, 1, 0, 0]);
|
|
221
|
+
expect(end.matrix).toEqual([1, 0, 0, 1, 100, 100]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
/** …and the same compile targeted at the DOM must never emit `matrix`. */
|
|
225
|
+
it('compile+sample delivers an animated `transform` string on web', () => {
|
|
226
|
+
const tracks = compile();
|
|
227
|
+
const g = tracks.elements.find(e => 'transform' in e.props)!;
|
|
228
|
+
|
|
229
|
+
const end = sampleProps(g, tracks.duration, tracks.stepMs, tracks.sampleCount, false);
|
|
230
|
+
expect(end.matrix).toBeUndefined();
|
|
231
|
+
expect(String(end.transform)).toContain('translate(100');
|
|
232
|
+
});
|
|
233
|
+
});
|
package/src/PxRnTracks.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface PxCompiledTracks {
|
|
|
41
41
|
delay: number;
|
|
42
42
|
/** WAAPI-style fill mode (default 'forwards'). */
|
|
43
43
|
fill: string;
|
|
44
|
+
/** When true, snap back to the start after a natural finish. */
|
|
45
|
+
resetOnFinish: boolean;
|
|
44
46
|
/** Sample step, ms. */
|
|
45
47
|
stepMs: number;
|
|
46
48
|
/** Number of samples per iteration (>= 2; sample i is at time i*stepMs). */
|
|
@@ -54,6 +56,15 @@ export interface CompileTracksOptions {
|
|
|
54
56
|
sampleRate?: number;
|
|
55
57
|
/** Hard cap on samples per iteration (memory guard). Default 600. */
|
|
56
58
|
maxSamples?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Sample values in the form the NATIVE react-native-svg views expect
|
|
61
|
+
* (currently: `transform` as a 6-number matrix rather than an SVG string).
|
|
62
|
+
*
|
|
63
|
+
* Defaults to `false` — the plain SVG form, which is what react-native-web
|
|
64
|
+
* hands straight to the DOM. Only the component knows the platform, so it
|
|
65
|
+
* decides; every consumer that does not opt in keeps DOM-compatible values.
|
|
66
|
+
*/
|
|
67
|
+
native?: boolean;
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
|
|
@@ -72,12 +83,22 @@ export function compileTracks(doc: PxAnimatedSvgDocument, opts?: CompileTracksOp
|
|
|
72
83
|
if (_iterations === 'infinite') iterations = Infinity;
|
|
73
84
|
if (iterations < 1) iterations = 1;
|
|
74
85
|
|
|
86
|
+
const native = opts?.native ?? false;
|
|
75
87
|
const sampleRate = opts?.sampleRate ?? 60;
|
|
76
88
|
const maxSamples = opts?.maxSamples ?? 600;
|
|
77
89
|
let sampleCount = Math.max(2, Math.round((duration / 1000) * sampleRate) + 1);
|
|
78
90
|
if (sampleCount > maxSamples) sampleCount = maxSamples;
|
|
79
91
|
const stepMs = duration / (sampleCount - 1);
|
|
80
92
|
|
|
93
|
+
// Element tag per id — `toRnPropValue` needs it to leave the root `<Svg>`'s
|
|
94
|
+
// transform as a string (see its `tag` parameter).
|
|
95
|
+
const tagById = new Map<string, string>();
|
|
96
|
+
const indexTags = (n: any): void => {
|
|
97
|
+
if (n && typeof n.id === 'string') tagById.set(n.id, String(n.type));
|
|
98
|
+
(n?.children ?? []).forEach(indexTags);
|
|
99
|
+
};
|
|
100
|
+
indexTags(doc);
|
|
101
|
+
|
|
81
102
|
const bindings = getNormalisedBindings(doc, PxAnimatorEngine.frames) || [];
|
|
82
103
|
|
|
83
104
|
const elements: Array<PxElementTracks> = [];
|
|
@@ -98,9 +119,9 @@ export function compileTracks(doc: PxAnimatedSvgDocument, opts?: CompileTracksOp
|
|
|
98
119
|
// A prop can appear late (e.g. attrs whose first kf is
|
|
99
120
|
// beyond t=0) — backfill earlier samples with the first
|
|
100
121
|
// computed value so the array is always fully populated.
|
|
101
|
-
for (let j = 0; j < i; j++) arr[j] = toRnPropValue(rnProp, value);
|
|
122
|
+
for (let j = 0; j < i; j++) arr[j] = toRnPropValue(rnProp, value, tagById.get(binding.id), native);
|
|
102
123
|
}
|
|
103
|
-
arr[i] = toRnPropValue(rnProp, value);
|
|
124
|
+
arr[i] = toRnPropValue(rnProp, value, tagById.get(binding.id), native);
|
|
104
125
|
}
|
|
105
126
|
}
|
|
106
127
|
// Forward-fill any holes (attr disappeared from a later sample).
|
|
@@ -121,30 +142,61 @@ export function compileTracks(doc: PxAnimatedSvgDocument, opts?: CompileTracksOp
|
|
|
121
142
|
direction: config.direction || 'normal',
|
|
122
143
|
delay: config.delay || 0,
|
|
123
144
|
fill: config.fill ?? 'forwards',
|
|
145
|
+
resetOnFinish: !!config.resetOnFinish,
|
|
124
146
|
stepMs,
|
|
125
147
|
sampleCount,
|
|
126
148
|
elements,
|
|
127
149
|
};
|
|
128
150
|
}
|
|
129
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Wire prop → the prop name the NATIVE view actually declares.
|
|
154
|
+
*
|
|
155
|
+
* react-native-svg's JS layer renames some props on the way down: a
|
|
156
|
+
* `transform` (string or matrix) is parsed by `extractProps` and handed to the
|
|
157
|
+
* native component as `matrix` — the Fabric spec has no `transform` prop at
|
|
158
|
+
* all. Values sent through reanimated's animated-props path skip that JS
|
|
159
|
+
* rename, so they must already use the native name or the native view drops
|
|
160
|
+
* them silently. This is ONLY for the animated path; plain React renders still
|
|
161
|
+
* go through the JS layer and must keep the wire name.
|
|
162
|
+
*/
|
|
163
|
+
export const NATIVE_PROP_NAME: Record<string, string> = {
|
|
164
|
+
transform: 'matrix',
|
|
165
|
+
};
|
|
166
|
+
|
|
130
167
|
/**
|
|
131
168
|
* Worklet-safe sample lookup: returns the per-prop values at time `tMs`
|
|
132
169
|
* (already mapped into a single iteration by the caller). Kept deliberately
|
|
133
170
|
* trivial — runs on the UI thread every frame.
|
|
171
|
+
*
|
|
172
|
+
* `native = true` applies {@link NATIVE_PROP_NAME} — pass it only on the
|
|
173
|
+
* reanimated animated-props path of a real device. Props that are applied by
|
|
174
|
+
* re-rendering through React (and therefore still pass through react-native-svg's
|
|
175
|
+
* JS layer), and every value on the web, must keep the wire name.
|
|
134
176
|
*/
|
|
135
177
|
export function sampleProps(
|
|
136
178
|
tracks: PxElementTracks,
|
|
137
179
|
tMs: number,
|
|
138
180
|
stepMs: number,
|
|
139
|
-
sampleCount: number
|
|
181
|
+
sampleCount: number,
|
|
182
|
+
native = false
|
|
140
183
|
): Record<string, string | number | Array<number>> {
|
|
141
184
|
'worklet';
|
|
185
|
+
const out: Record<string, string | number | Array<number>> = {};
|
|
186
|
+
// Defensive: this runs on the UI thread, where a throw is a hard crash
|
|
187
|
+
// with no React boundary to catch it.
|
|
188
|
+
if (!tracks || !tracks.props || !(stepMs > 0)) return out;
|
|
189
|
+
|
|
142
190
|
let idx = Math.round(tMs / stepMs);
|
|
143
191
|
if (idx < 0) idx = 0;
|
|
144
192
|
if (idx >= sampleCount) idx = sampleCount - 1;
|
|
145
|
-
const out: Record<string, string | number | Array<number>> = {};
|
|
146
193
|
for (const key in tracks.props) {
|
|
147
|
-
|
|
194
|
+
const values = tracks.props[key];
|
|
195
|
+
if (!values) continue;
|
|
196
|
+
const value = values[idx];
|
|
197
|
+
if (value === undefined) continue;
|
|
198
|
+
const name = native && key === 'transform' ? 'matrix' : key;
|
|
199
|
+
out[name] = value;
|
|
148
200
|
}
|
|
149
201
|
return out;
|
|
150
202
|
}
|
package/src/PxRnTypeMap.ts
CHANGED
|
@@ -8,6 +8,32 @@ import {
|
|
|
8
8
|
ClipPath,
|
|
9
9
|
Defs,
|
|
10
10
|
Ellipse,
|
|
11
|
+
FeBlend,
|
|
12
|
+
FeColorMatrix,
|
|
13
|
+
FeComponentTransfer,
|
|
14
|
+
FeComposite,
|
|
15
|
+
FeConvolveMatrix,
|
|
16
|
+
FeDiffuseLighting,
|
|
17
|
+
FeDisplacementMap,
|
|
18
|
+
FeDistantLight,
|
|
19
|
+
FeDropShadow,
|
|
20
|
+
FeFlood,
|
|
21
|
+
FeFuncA,
|
|
22
|
+
FeFuncB,
|
|
23
|
+
FeFuncG,
|
|
24
|
+
FeFuncR,
|
|
25
|
+
FeGaussianBlur,
|
|
26
|
+
FeImage,
|
|
27
|
+
FeMerge,
|
|
28
|
+
FeMergeNode,
|
|
29
|
+
FeMorphology,
|
|
30
|
+
FeOffset,
|
|
31
|
+
FePointLight,
|
|
32
|
+
FeSpecularLighting,
|
|
33
|
+
FeSpotLight,
|
|
34
|
+
FeTile,
|
|
35
|
+
FeTurbulence,
|
|
36
|
+
Filter,
|
|
11
37
|
G,
|
|
12
38
|
Image,
|
|
13
39
|
Line,
|
|
@@ -34,10 +60,19 @@ import type { ComponentType } from 'react';
|
|
|
34
60
|
* SVG tag → react-native-svg component. Tags not in this map are skipped at
|
|
35
61
|
* render time (with a warning collected by the renderer) — they go on the
|
|
36
62
|
* feature-gap list rather than crashing the tree.
|
|
63
|
+
*
|
|
64
|
+
* Keys are the wire-format `node.type` values, which follow SVG's own casing
|
|
65
|
+
* (`clipPath`, `feGaussianBlur`, `linearGradient`, …).
|
|
37
66
|
*/
|
|
38
67
|
export const RN_SVG_COMPONENTS: Record<string, ComponentType<any>> = {
|
|
68
|
+
// Root & containers
|
|
39
69
|
svg: Svg,
|
|
40
70
|
g: G,
|
|
71
|
+
defs: Defs,
|
|
72
|
+
symbol: SvgSymbol,
|
|
73
|
+
use: Use,
|
|
74
|
+
|
|
75
|
+
// Shapes
|
|
41
76
|
rect: Rect,
|
|
42
77
|
circle: Circle,
|
|
43
78
|
ellipse: Ellipse,
|
|
@@ -45,20 +80,52 @@ export const RN_SVG_COMPONENTS: Record<string, ComponentType<any>> = {
|
|
|
45
80
|
path: Path,
|
|
46
81
|
polygon: Polygon,
|
|
47
82
|
polyline: Polyline,
|
|
83
|
+
image: Image,
|
|
84
|
+
|
|
85
|
+
// Text
|
|
48
86
|
text: SvgText,
|
|
49
87
|
tspan: TSpan,
|
|
50
88
|
textPath: TextPath,
|
|
51
|
-
|
|
89
|
+
|
|
90
|
+
// Paint servers & clipping
|
|
52
91
|
linearGradient: LinearGradient,
|
|
53
92
|
radialGradient: RadialGradient,
|
|
54
93
|
stop: Stop,
|
|
55
|
-
|
|
56
|
-
symbol: SvgSymbol,
|
|
94
|
+
pattern: Pattern,
|
|
57
95
|
mask: Mask,
|
|
58
96
|
clipPath: ClipPath,
|
|
59
|
-
pattern: Pattern,
|
|
60
97
|
marker: Marker,
|
|
61
|
-
|
|
98
|
+
|
|
99
|
+
// Filters — react-native-svg implements the full primitive set.
|
|
100
|
+
// NOTE: filter rendering requires the New Architecture (Fabric) and is
|
|
101
|
+
// newer than the rest of react-native-svg; treat visual parity with the
|
|
102
|
+
// web player as unverified until checked on a device.
|
|
103
|
+
filter: Filter,
|
|
104
|
+
feBlend: FeBlend,
|
|
105
|
+
feColorMatrix: FeColorMatrix,
|
|
106
|
+
feComponentTransfer: FeComponentTransfer,
|
|
107
|
+
feComposite: FeComposite,
|
|
108
|
+
feConvolveMatrix: FeConvolveMatrix,
|
|
109
|
+
feDiffuseLighting: FeDiffuseLighting,
|
|
110
|
+
feDisplacementMap: FeDisplacementMap,
|
|
111
|
+
feDistantLight: FeDistantLight,
|
|
112
|
+
feDropShadow: FeDropShadow,
|
|
113
|
+
feFlood: FeFlood,
|
|
114
|
+
feFuncA: FeFuncA,
|
|
115
|
+
feFuncB: FeFuncB,
|
|
116
|
+
feFuncG: FeFuncG,
|
|
117
|
+
feFuncR: FeFuncR,
|
|
118
|
+
feGaussianBlur: FeGaussianBlur,
|
|
119
|
+
feImage: FeImage,
|
|
120
|
+
feMerge: FeMerge,
|
|
121
|
+
feMergeNode: FeMergeNode,
|
|
122
|
+
feMorphology: FeMorphology,
|
|
123
|
+
feOffset: FeOffset,
|
|
124
|
+
fePointLight: FePointLight,
|
|
125
|
+
feSpecularLighting: FeSpecularLighting,
|
|
126
|
+
feSpotLight: FeSpotLight,
|
|
127
|
+
feTile: FeTile,
|
|
128
|
+
feTurbulence: FeTurbulence,
|
|
62
129
|
};
|
|
63
130
|
|
|
64
131
|
export { toRnPropName } from './PxRnPropNames';
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ export { PixodeskSvgAnimator, default } from './PixodeskSvgAnimator';
|
|
|
7
7
|
export type { PixodeskSvgAnimatorProps, RnAnimatorApi } from './PixodeskSvgAnimator';
|
|
8
8
|
|
|
9
9
|
export { renderRnNode, toRnProps } from './PxRnRender';
|
|
10
|
+
export { PxRnErrorBoundary, type PxRnErrorBoundaryProps } from './PxRnErrorBoundary';
|
|
11
|
+
export { openClosedTextPathTargets } from './PxRnSafety';
|
|
10
12
|
export type { RenderRnNodeOptions } from './PxRnRender';
|
|
11
13
|
|
|
12
14
|
export { compileTracks, sampleProps } from './PxRnTracks';
|