@pixodesk/svg-animator-rn 1.0.30 → 1.0.35
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 +36 -36
- package/dist/index.cjs +34 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -13
- package/dist/index.d.ts +16 -13
- package/dist/index.js +36 -46
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/PixodeskSvgAnimator.tsx +64 -66
- package/src/PxRnTracks.test.ts +8 -8
- package/src/PxRnTracks.ts +2 -2
|
@@ -3,19 +3,7 @@
|
|
|
3
3
|
* Licensed under the MIT License. See the LICENSE file in the project root for details.
|
|
4
4
|
*---------------------------------------------------------------------------------------*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
generateNewIds,
|
|
8
|
-
getAnimatorConfig,
|
|
9
|
-
getDefs,
|
|
10
|
-
materialiseAllInTree,
|
|
11
|
-
validateNodeEffects,
|
|
12
|
-
PxAnimatorEngine,
|
|
13
|
-
type FillMode,
|
|
14
|
-
type OutAction,
|
|
15
|
-
type PlaybackDirection,
|
|
16
|
-
type PxAnimatedSvgDocument,
|
|
17
|
-
type PxNode,
|
|
18
|
-
} from '@pixodesk/svg-animator-core';
|
|
6
|
+
import { reportDocumentDiagnostics, generateNewIds, getAnimatorConfig, getDefs, materialiseAllInTree, validateNodeEffects, PxTimelineEngine, type FillMode, type OutAction, type PlaybackDirection, type PxAnimatedSvgDocument, type PxAnimatorConfigPatch, type PxNode, type StartOn, applyAnimatorConfig, foldAnimatorConfigShortcuts } from '@pixodesk/svg-animator-core';
|
|
19
7
|
import React, { createElement, useEffect, useImperativeHandle, useMemo, useRef, useState, type ComponentType, type ReactElement, type ReactNode } from 'react';
|
|
20
8
|
import { Dimensions, Platform, Pressable, View } from 'react-native';
|
|
21
9
|
import Animated, {
|
|
@@ -83,20 +71,22 @@ export interface PixodeskSvgAnimatorProps {
|
|
|
83
71
|
/** Number of iterations, or 'infinite' for endless looping. */
|
|
84
72
|
iterations?: number | 'infinite';
|
|
85
73
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
/** Playback direction. */
|
|
90
|
-
direction?: PlaybackDirection;
|
|
91
|
-
|
|
92
|
-
/** Snap back to the start state after a natural finish. */
|
|
93
|
-
resetOnFinish?: boolean;
|
|
74
|
+
/** Shortcut for `config.timeline.trigger.startOn`. */
|
|
75
|
+
startOn?: StartOn;
|
|
94
76
|
|
|
95
77
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
78
|
+
* Per-instance override of the document's `animator` config — the same shape as `animator`
|
|
79
|
+
* in SCHEMA.md, deep-merged over what the document says; `null` at a slot deletes it.
|
|
80
|
+
* Replaces the former flat `fill` / `direction` / `resetOnFinish` / `outAction` props, so
|
|
81
|
+
* every surface takes one vocabulary. Also accepts a JSON string.
|
|
82
|
+
*
|
|
83
|
+
* `timeline.engine` is accepted but ignored here: React Native always materialises the
|
|
84
|
+
* WAAPI-style flattening, because react-native-svg has no `<use>` shadow-tree propagation.
|
|
98
85
|
*/
|
|
99
|
-
|
|
86
|
+
config?: PxAnimatorConfigPatch | string;
|
|
87
|
+
|
|
88
|
+
/** Start from the player's defaults instead of the document's playback settings. */
|
|
89
|
+
resetDocDefaults?: boolean;
|
|
100
90
|
|
|
101
91
|
// -- Declarative control --------------------------------------------------
|
|
102
92
|
|
|
@@ -117,10 +107,10 @@ export interface PixodeskSvgAnimatorProps {
|
|
|
117
107
|
// -- Controlled (external) time -------------------------------------------
|
|
118
108
|
|
|
119
109
|
/** Seek to a fraction (0–1) of the whole timeline (duration × iterations). */
|
|
120
|
-
|
|
110
|
+
progress?: number;
|
|
121
111
|
|
|
122
112
|
/** Seek to a specific time in milliseconds. */
|
|
123
|
-
|
|
113
|
+
time?: number;
|
|
124
114
|
|
|
125
115
|
// -- Callbacks ------------------------------------------------------------
|
|
126
116
|
|
|
@@ -285,12 +275,12 @@ function SampledSubtree({
|
|
|
285
275
|
|
|
286
276
|
/** Overrides that shadow the document's own `animator` config. */
|
|
287
277
|
interface ConfigOverrides {
|
|
278
|
+
config?: PxAnimatorConfigPatch | string;
|
|
279
|
+
resetDocDefaults?: boolean;
|
|
288
280
|
duration?: number;
|
|
289
281
|
delay?: number;
|
|
290
282
|
iterations?: number | 'infinite';
|
|
291
|
-
|
|
292
|
-
direction?: PlaybackDirection;
|
|
293
|
-
resetOnFinish?: boolean;
|
|
283
|
+
startOn?: StartOn;
|
|
294
284
|
}
|
|
295
285
|
|
|
296
286
|
interface Compiled {
|
|
@@ -312,9 +302,21 @@ const EMPTY_TRACKS: PxCompiledTracks = {
|
|
|
312
302
|
* whole thing sits behind one try/catch, and so it can be tested directly.
|
|
313
303
|
*/
|
|
314
304
|
function compileDocument(doc: PxAnimatedSvgDocument, overrides: ConfigOverrides): Compiled {
|
|
315
|
-
const {
|
|
305
|
+
const { config, resetDocDefaults, duration, delay, iterations, startOn } = overrides;
|
|
316
306
|
const warnings = validateNodeEffects(doc as PxNode);
|
|
317
307
|
for (const w of warnings) console.warn('[PixodeskSvgAnimator] effects shape warning:', w);
|
|
308
|
+
// The whole-document boundary diagnostic — see the note in the web player's entry.
|
|
309
|
+
reportDocumentDiagnostics(doc, '[PixodeskSvgAnimator]');
|
|
310
|
+
|
|
311
|
+
// The per-instance override, applied to the WIRE document BEFORE anything reads the
|
|
312
|
+
// config — `materialiseAllInTree` samples motion paths against `duration`, so a later
|
|
313
|
+
// patch would be read by none of the pipeline. Same call, same rules, on every surface.
|
|
314
|
+
const patch = foldAnimatorConfigShortcuts(config, { duration, delay, iterations, startOn });
|
|
315
|
+
if (patch !== undefined || resetDocDefaults) {
|
|
316
|
+
const applied = applyAnimatorConfig(doc, patch ?? {}, { resetDefaults: !!resetDocDefaults });
|
|
317
|
+
for (const w of applied.warnings) console.warn('[PixodeskSvgAnimator] config override:', w);
|
|
318
|
+
doc = applied.doc;
|
|
319
|
+
}
|
|
318
320
|
|
|
319
321
|
// `waapi` = the FULLY-FLATTENED materialisation: effects + loops +
|
|
320
322
|
// sampled motion paths + animated `<use>` inlined into real `<g>`
|
|
@@ -323,7 +325,7 @@ function compileDocument(doc: PxAnimatedSvgDocument, overrides: ConfigOverrides)
|
|
|
323
325
|
// live references, which only work because the DOM propagates
|
|
324
326
|
// attribute writes through `<use>` shadow trees. react-native-svg has
|
|
325
327
|
// no such live propagation, so an animated `<use>` would render frozen.
|
|
326
|
-
let prepared = materialiseAllInTree(doc,
|
|
328
|
+
let prepared = materialiseAllInTree(doc, PxTimelineEngine.native);
|
|
327
329
|
|
|
328
330
|
// Sidestep a react-native-svg NATIVE crash (see PxRnSafety). Guarded on
|
|
329
331
|
// the platform because the DOM renders this case correctly and the web
|
|
@@ -332,21 +334,6 @@ function compileDocument(doc: PxAnimatedSvgDocument, overrides: ConfigOverrides)
|
|
|
332
334
|
prepared = openClosedTextPathTargets(prepared as PxNode) as PxAnimatedSvgDocument;
|
|
333
335
|
}
|
|
334
336
|
|
|
335
|
-
// Apply prop overrides onto the animator config (mirrors the react wrapper).
|
|
336
|
-
const animator = getAnimatorConfig(prepared) || {};
|
|
337
|
-
prepared = {
|
|
338
|
-
...prepared,
|
|
339
|
-
animator: {
|
|
340
|
-
...animator,
|
|
341
|
-
duration: duration !== undefined ? duration : animator.duration,
|
|
342
|
-
delay: delay !== undefined ? delay : animator.delay,
|
|
343
|
-
iterations: iterations !== undefined ? iterations : animator.iterations,
|
|
344
|
-
fill: fill !== undefined ? fill : animator.fill,
|
|
345
|
-
direction: direction !== undefined ? direction : animator.direction,
|
|
346
|
-
resetOnFinish: resetOnFinish !== undefined ? resetOnFinish : animator.resetOnFinish,
|
|
347
|
-
},
|
|
348
|
-
};
|
|
349
|
-
|
|
350
337
|
prepared = generateNewIds(prepared);
|
|
351
338
|
const tracks = compileTracks(prepared, { native: NATIVE_SVG_VIEWS });
|
|
352
339
|
return { doc: prepared, tracks, error: null };
|
|
@@ -366,18 +353,25 @@ function compileDocument(doc: PxAnimatedSvgDocument, overrides: ConfigOverrides)
|
|
|
366
353
|
* indexing the precompiled tracks. No JS-thread frame loop.
|
|
367
354
|
*/
|
|
368
355
|
export function PixodeskSvgAnimator({
|
|
369
|
-
doc,
|
|
370
|
-
|
|
356
|
+
doc, config, resetDocDefaults, duration, delay, iterations, startOn,
|
|
357
|
+
// (`progress` prop aliased — the name is taken by the internal reanimated SharedValue)
|
|
358
|
+
autoplay, play, pause, apiRef, progress: progressProp, time,
|
|
371
359
|
onPlay, onStop, onPause, onCancel, onFinish, onError, fallback,
|
|
372
360
|
}: PixodeskSvgAnimatorProps): ReactElement | null {
|
|
373
361
|
|
|
374
362
|
// -- Compile the document (once per doc/override change) ------------------
|
|
375
363
|
|
|
364
|
+
// `config` is an object prop: a fresh literal every render would otherwise recompile the
|
|
365
|
+
// whole document (materialise + compile tracks), which is the expensive path. Key on its
|
|
366
|
+
// CONTENT instead — the override is small, the document is not.
|
|
367
|
+
const configKey = typeof config === 'string' ? config : JSON.stringify(config ?? null);
|
|
368
|
+
|
|
369
|
+
|
|
376
370
|
const compiled = useMemo((): Compiled => {
|
|
377
371
|
try {
|
|
378
372
|
return compileDocument(
|
|
379
373
|
doc,
|
|
380
|
-
{
|
|
374
|
+
{ config, resetDocDefaults, duration, delay, iterations, startOn }
|
|
381
375
|
);
|
|
382
376
|
} catch (e) {
|
|
383
377
|
// A malformed document must not take the host screen down with it.
|
|
@@ -385,7 +379,9 @@ export function PixodeskSvgAnimator({
|
|
|
385
379
|
console.warn('[PixodeskSvgAnimator] could not compile the document:', error.message);
|
|
386
380
|
return { doc: null, tracks: EMPTY_TRACKS, error };
|
|
387
381
|
}
|
|
388
|
-
|
|
382
|
+
// `config` is an object prop, so a fresh literal each render would recompile the whole
|
|
383
|
+
// document. Key on its CONTENT — the override is small, unlike the document.
|
|
384
|
+
}, [doc, configKey, resetDocDefaults, duration, delay, iterations, startOn]);
|
|
389
385
|
|
|
390
386
|
const tracks: PxCompiledTracks = compiled.tracks;
|
|
391
387
|
const totalDuration = tracks.duration * (tracks.iterations === Infinity ? 1 : tracks.iterations);
|
|
@@ -513,13 +509,15 @@ export function PixodeskSvgAnimator({
|
|
|
513
509
|
|
|
514
510
|
// -- Declarative control --------------------------------------------------
|
|
515
511
|
|
|
512
|
+
// The EFFECTIVE trigger, read back off the COMPILED document — so it already reflects the
|
|
513
|
+
// `config` override and the `startOn` shortcut, both merged in before compilation.
|
|
516
514
|
const trigger = compiled.doc ? getAnimatorConfig(compiled.doc)?.trigger : undefined;
|
|
517
|
-
const
|
|
518
|
-
const
|
|
515
|
+
const effectiveStartOn = trigger?.startOn ?? 'load';
|
|
516
|
+
const effectiveOutAction = trigger?.outAction ?? 'pause';
|
|
519
517
|
|
|
520
518
|
useEffect(() => {
|
|
521
|
-
if (
|
|
522
|
-
const seekMs =
|
|
519
|
+
if (progressProp !== undefined || time !== undefined) {
|
|
520
|
+
const seekMs = time !== undefined ? time : (progressProp ?? 0) * totalDuration;
|
|
523
521
|
api.setCurrentTime(seekMs);
|
|
524
522
|
return;
|
|
525
523
|
}
|
|
@@ -531,11 +529,11 @@ export function PixodeskSvgAnimator({
|
|
|
531
529
|
return;
|
|
532
530
|
}
|
|
533
531
|
// 'click' and 'scrollIntoView' start from their own handlers below.
|
|
534
|
-
if (autoplay &&
|
|
532
|
+
if (autoplay && effectiveStartOn === 'load') {
|
|
535
533
|
api.play();
|
|
536
534
|
}
|
|
537
535
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
538
|
-
}, [compiled, autoplay, play, pause,
|
|
536
|
+
}, [compiled, autoplay, play, pause, progressProp, time]);
|
|
539
537
|
|
|
540
538
|
// `startOn: 'scrollIntoView'` — react-native has no IntersectionObserver, so
|
|
541
539
|
// visibility is sampled by measuring the view against the window box. The
|
|
@@ -544,7 +542,7 @@ export function PixodeskSvgAnimator({
|
|
|
544
542
|
const scrollRef = useRef<View | null>(null);
|
|
545
543
|
const inViewRef = useRef(false);
|
|
546
544
|
useEffect(() => {
|
|
547
|
-
if (!autoplay ||
|
|
545
|
+
if (!autoplay || effectiveStartOn !== 'scrollIntoView') return;
|
|
548
546
|
const threshold = trigger?.scrollIntoViewThreshold ?? 0;
|
|
549
547
|
inViewRef.current = false;
|
|
550
548
|
|
|
@@ -562,9 +560,9 @@ export function PixodeskSvgAnimator({
|
|
|
562
560
|
if (isIn) {
|
|
563
561
|
if (rateRef.current < 0) api.setPlaybackRate(Math.abs(rateRef.current));
|
|
564
562
|
api.play();
|
|
565
|
-
} else if (
|
|
566
|
-
else if (
|
|
567
|
-
else if (
|
|
563
|
+
} else if (effectiveOutAction === 'reset') api.cancel();
|
|
564
|
+
else if (effectiveOutAction === 'reverse') { api.setPlaybackRate(-Math.abs(rateRef.current || 1)); api.play(); }
|
|
565
|
+
else if (effectiveOutAction !== 'continue') api.pause();
|
|
568
566
|
});
|
|
569
567
|
};
|
|
570
568
|
|
|
@@ -572,7 +570,7 @@ export function PixodeskSvgAnimator({
|
|
|
572
570
|
const id = setInterval(check, 200);
|
|
573
571
|
return () => clearInterval(id);
|
|
574
572
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
575
|
-
}, [compiled, autoplay,
|
|
573
|
+
}, [compiled, autoplay, effectiveStartOn, effectiveOutAction]);
|
|
576
574
|
|
|
577
575
|
// Stop cleanly on unmount / doc swap.
|
|
578
576
|
useEffect(() => {
|
|
@@ -671,17 +669,17 @@ export function PixodeskSvgAnimator({
|
|
|
671
669
|
// so both are left to the host app.
|
|
672
670
|
let content: ReactElement | null = root;
|
|
673
671
|
|
|
674
|
-
if (autoplay &&
|
|
672
|
+
if (autoplay && effectiveStartOn === 'scrollIntoView' && root) {
|
|
675
673
|
// `collapsable={false}` keeps the view in the native tree so it can be measured.
|
|
676
674
|
content = <View ref={scrollRef} collapsable={false}>{root}</View>;
|
|
677
|
-
} else if (autoplay &&
|
|
675
|
+
} else if (autoplay && effectiveStartOn === 'click' && root) {
|
|
678
676
|
content = (
|
|
679
677
|
<Pressable
|
|
680
678
|
onPress={() => {
|
|
681
679
|
if (playingRef.current) {
|
|
682
|
-
if (
|
|
683
|
-
else if (
|
|
684
|
-
else if (
|
|
680
|
+
if (effectiveOutAction === 'reset') api.cancel();
|
|
681
|
+
else if (effectiveOutAction === 'reverse') { api.setPlaybackRate(-Math.abs(rateRef.current || 1)); api.play(); }
|
|
682
|
+
else if (effectiveOutAction !== 'continue') api.pause();
|
|
685
683
|
} else {
|
|
686
684
|
if (rateRef.current < 0) api.setPlaybackRate(Math.abs(rateRef.current));
|
|
687
685
|
api.play();
|
package/src/PxRnTracks.test.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*---------------------------------------------------------------------------------------*/
|
|
5
5
|
|
|
6
6
|
import { describe, expect, it } from 'vitest';
|
|
7
|
-
import { generateNewIds, materialiseAllInTree,
|
|
7
|
+
import { generateNewIds, materialiseAllInTree, PxTimelineEngine, type PxAnimatedSvgDocument } from '@pixodesk/svg-animator-core';
|
|
8
8
|
import { compileTracks, sampleProps } from './PxRnTracks';
|
|
9
9
|
import { toRnPropName } from './PxRnPropNames';
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ function makeDoc(): PxAnimatedSvgDocument {
|
|
|
12
12
|
return {
|
|
13
13
|
type: 'svg',
|
|
14
14
|
viewBox: '0 0 200 200',
|
|
15
|
-
animator: {
|
|
15
|
+
animator: { timeline: { engine: 'js', duration: 1000, iterations: 2, direction: 'alternate' } },
|
|
16
16
|
children: [
|
|
17
17
|
{
|
|
18
18
|
type: 'rect',
|
|
@@ -37,7 +37,7 @@ function makeDoc(): PxAnimatedSvgDocument {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function compile(doc = makeDoc(), opts?: Parameters<typeof compileTracks>[1]) {
|
|
40
|
-
const materialised = generateNewIds(materialiseAllInTree(doc,
|
|
40
|
+
const materialised = generateNewIds(materialiseAllInTree(doc, PxTimelineEngine.js));
|
|
41
41
|
return compileTracks(materialised, opts);
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -150,13 +150,13 @@ describe('length-list props (stroke-dasharray)', () => {
|
|
|
150
150
|
it('compiles stroke-dasharray into number arrays (rn-svg native shape)', () => {
|
|
151
151
|
const doc: PxAnimatedSvgDocument = {
|
|
152
152
|
type: 'svg', viewBox: '0 0 100 100',
|
|
153
|
-
animator: {
|
|
153
|
+
animator: { timeline: { engine: 'js', duration: 1000 } },
|
|
154
154
|
children: [{
|
|
155
155
|
type: 'path', id: 'p', d: 'M 0 50 L 100 50', stroke: '#000', fill: 'none',
|
|
156
156
|
effects: { strokeTrim: { range: { keyframes: [{ time: 0, value: [0, 0.1] }, { time: 1000, value: [0, 1] }] } } },
|
|
157
157
|
}],
|
|
158
158
|
};
|
|
159
|
-
const materialised = generateNewIds(materialiseAllInTree(doc,
|
|
159
|
+
const materialised = generateNewIds(materialiseAllInTree(doc, PxTimelineEngine.native));
|
|
160
160
|
const tracks = compileTracks(materialised);
|
|
161
161
|
const el = tracks.elements.find(e => 'strokeDasharray' in e.props)!;
|
|
162
162
|
expect(el).toBeDefined();
|
|
@@ -171,17 +171,17 @@ describe('animated <use> flattening (waapi materialisation)', () => {
|
|
|
171
171
|
it('inlines animated <use> clones so no live references remain', () => {
|
|
172
172
|
const doc: PxAnimatedSvgDocument = {
|
|
173
173
|
type: 'svg', viewBox: '0 0 300 200',
|
|
174
|
-
animator: {
|
|
174
|
+
animator: { timeline: { engine: 'js', duration: 2000 } },
|
|
175
175
|
children: [
|
|
176
176
|
{ type: 'defs', children: [{ type: 'g', id: 'sym', children: [{
|
|
177
177
|
type: 'circle', id: 'c', cx: 30, cy: 40, r: 16, fill: '#f59e0b',
|
|
178
178
|
animate: { cy: { keyframes: [{ time: 0, value: 40 }, { time: 2000, value: 160 }] } },
|
|
179
179
|
}] }] },
|
|
180
180
|
{ type: 'use', id: 'u1', href: '#sym' },
|
|
181
|
-
{ type: 'use', id: 'u2', href: '#sym', x: 80, effects: { clone: {
|
|
181
|
+
{ type: 'use', id: 'u2', href: '#sym', x: 80, effects: { clone: { source: '#sym', retime: { start: -600 } } } },
|
|
182
182
|
],
|
|
183
183
|
};
|
|
184
|
-
const materialised = generateNewIds(materialiseAllInTree(doc,
|
|
184
|
+
const materialised = generateNewIds(materialiseAllInTree(doc, PxTimelineEngine.native));
|
|
185
185
|
const countUse = (n: any): number =>
|
|
186
186
|
(n.type === 'use' ? 1 : 0) + (n.children || []).reduce((s: number, c: any) => s + countUse(c), 0);
|
|
187
187
|
expect(countUse(materialised)).toBe(0);
|
package/src/PxRnTracks.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
getAnimatorConfig,
|
|
9
9
|
getNormalisedBindings,
|
|
10
10
|
DEFAULT_DURATION_MS,
|
|
11
|
-
|
|
11
|
+
PxTimelineEngine,
|
|
12
12
|
type PxAnimatedSvgDocument,
|
|
13
13
|
type PxAnimationDefinition,
|
|
14
14
|
} from '@pixodesk/svg-animator-core';
|
|
@@ -99,7 +99,7 @@ export function compileTracks(doc: PxAnimatedSvgDocument, opts?: CompileTracksOp
|
|
|
99
99
|
};
|
|
100
100
|
indexTags(doc);
|
|
101
101
|
|
|
102
|
-
const bindings = getNormalisedBindings(doc,
|
|
102
|
+
const bindings = getNormalisedBindings(doc, PxTimelineEngine.js) || [];
|
|
103
103
|
|
|
104
104
|
const elements: Array<PxElementTracks> = [];
|
|
105
105
|
for (const binding of bindings) {
|