@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.
@@ -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 composed transform strings', () => {
88
- const tracks = compile();
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
- expect(String(g.props.transform[0])).toContain('translate(0');
91
- expect(String(g.props.transform[g.props.transform.length - 1])).toContain('translate(100');
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.webapi));
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 (webapi materialisation)', () => {
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: { baseId: 'sym', retime: { start: -600 } } } },
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.webapi));
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
- out[key] = tracks.props[key][idx];
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
  }
@@ -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
- defs: Defs,
89
+
90
+ // Paint servers & clipping
52
91
  linearGradient: LinearGradient,
53
92
  radialGradient: RadialGradient,
54
93
  stop: Stop,
55
- use: Use,
56
- symbol: SvgSymbol,
94
+ pattern: Pattern,
57
95
  mask: Mask,
58
96
  clipPath: ClipPath,
59
- pattern: Pattern,
60
97
  marker: Marker,
61
- image: Image,
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';