@pixodesk/svg-animator-rn 1.0.34 → 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 +16 -18
- package/dist/index.cjs +29 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -11
- package/dist/index.d.ts +14 -11
- package/dist/index.js +31 -42
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/PixodeskSvgAnimator.tsx +57 -60
- 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
|
|
|
@@ -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,7 +353,7 @@ 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,
|
|
356
|
+
doc, config, resetDocDefaults, duration, delay, iterations, startOn,
|
|
370
357
|
// (`progress` prop aliased — the name is taken by the internal reanimated SharedValue)
|
|
371
358
|
autoplay, play, pause, apiRef, progress: progressProp, time,
|
|
372
359
|
onPlay, onStop, onPause, onCancel, onFinish, onError, fallback,
|
|
@@ -374,11 +361,17 @@ export function PixodeskSvgAnimator({
|
|
|
374
361
|
|
|
375
362
|
// -- Compile the document (once per doc/override change) ------------------
|
|
376
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
|
+
|
|
377
370
|
const compiled = useMemo((): Compiled => {
|
|
378
371
|
try {
|
|
379
372
|
return compileDocument(
|
|
380
373
|
doc,
|
|
381
|
-
{
|
|
374
|
+
{ config, resetDocDefaults, duration, delay, iterations, startOn }
|
|
382
375
|
);
|
|
383
376
|
} catch (e) {
|
|
384
377
|
// A malformed document must not take the host screen down with it.
|
|
@@ -386,7 +379,9 @@ export function PixodeskSvgAnimator({
|
|
|
386
379
|
console.warn('[PixodeskSvgAnimator] could not compile the document:', error.message);
|
|
387
380
|
return { doc: null, tracks: EMPTY_TRACKS, error };
|
|
388
381
|
}
|
|
389
|
-
|
|
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]);
|
|
390
385
|
|
|
391
386
|
const tracks: PxCompiledTracks = compiled.tracks;
|
|
392
387
|
const totalDuration = tracks.duration * (tracks.iterations === Infinity ? 1 : tracks.iterations);
|
|
@@ -514,9 +509,11 @@ export function PixodeskSvgAnimator({
|
|
|
514
509
|
|
|
515
510
|
// -- Declarative control --------------------------------------------------
|
|
516
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.
|
|
517
514
|
const trigger = compiled.doc ? getAnimatorConfig(compiled.doc)?.trigger : undefined;
|
|
518
|
-
const
|
|
519
|
-
const
|
|
515
|
+
const effectiveStartOn = trigger?.startOn ?? 'load';
|
|
516
|
+
const effectiveOutAction = trigger?.outAction ?? 'pause';
|
|
520
517
|
|
|
521
518
|
useEffect(() => {
|
|
522
519
|
if (progressProp !== undefined || time !== undefined) {
|
|
@@ -532,7 +529,7 @@ export function PixodeskSvgAnimator({
|
|
|
532
529
|
return;
|
|
533
530
|
}
|
|
534
531
|
// 'click' and 'scrollIntoView' start from their own handlers below.
|
|
535
|
-
if (autoplay &&
|
|
532
|
+
if (autoplay && effectiveStartOn === 'load') {
|
|
536
533
|
api.play();
|
|
537
534
|
}
|
|
538
535
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -545,7 +542,7 @@ export function PixodeskSvgAnimator({
|
|
|
545
542
|
const scrollRef = useRef<View | null>(null);
|
|
546
543
|
const inViewRef = useRef(false);
|
|
547
544
|
useEffect(() => {
|
|
548
|
-
if (!autoplay ||
|
|
545
|
+
if (!autoplay || effectiveStartOn !== 'scrollIntoView') return;
|
|
549
546
|
const threshold = trigger?.scrollIntoViewThreshold ?? 0;
|
|
550
547
|
inViewRef.current = false;
|
|
551
548
|
|
|
@@ -563,9 +560,9 @@ export function PixodeskSvgAnimator({
|
|
|
563
560
|
if (isIn) {
|
|
564
561
|
if (rateRef.current < 0) api.setPlaybackRate(Math.abs(rateRef.current));
|
|
565
562
|
api.play();
|
|
566
|
-
} else if (
|
|
567
|
-
else if (
|
|
568
|
-
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();
|
|
569
566
|
});
|
|
570
567
|
};
|
|
571
568
|
|
|
@@ -573,7 +570,7 @@ export function PixodeskSvgAnimator({
|
|
|
573
570
|
const id = setInterval(check, 200);
|
|
574
571
|
return () => clearInterval(id);
|
|
575
572
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
576
|
-
}, [compiled, autoplay,
|
|
573
|
+
}, [compiled, autoplay, effectiveStartOn, effectiveOutAction]);
|
|
577
574
|
|
|
578
575
|
// Stop cleanly on unmount / doc swap.
|
|
579
576
|
useEffect(() => {
|
|
@@ -672,17 +669,17 @@ export function PixodeskSvgAnimator({
|
|
|
672
669
|
// so both are left to the host app.
|
|
673
670
|
let content: ReactElement | null = root;
|
|
674
671
|
|
|
675
|
-
if (autoplay &&
|
|
672
|
+
if (autoplay && effectiveStartOn === 'scrollIntoView' && root) {
|
|
676
673
|
// `collapsable={false}` keeps the view in the native tree so it can be measured.
|
|
677
674
|
content = <View ref={scrollRef} collapsable={false}>{root}</View>;
|
|
678
|
-
} else if (autoplay &&
|
|
675
|
+
} else if (autoplay && effectiveStartOn === 'click' && root) {
|
|
679
676
|
content = (
|
|
680
677
|
<Pressable
|
|
681
678
|
onPress={() => {
|
|
682
679
|
if (playingRef.current) {
|
|
683
|
-
if (
|
|
684
|
-
else if (
|
|
685
|
-
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();
|
|
686
683
|
} else {
|
|
687
684
|
if (rateRef.current < 0) api.setPlaybackRate(Math.abs(rateRef.current));
|
|
688
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) {
|