@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.
@@ -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
- /** Defines the element's state when the animation is not active. */
87
- fill?: FillMode;
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
- * What a second tap does when `startOn: 'click'` is active.
97
- * Defaults to the document's `trigger.outAction`, else `'pause'`.
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
- outAction?: OutAction;
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
- time?: number;
110
+ progress?: number;
121
111
 
122
112
  /** Seek to a specific time in milliseconds. */
123
- timeMs?: number;
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
- fill?: FillMode;
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 { duration, delay, iterations, fill, direction, resetOnFinish } = overrides;
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, PxAnimatorEngine.waapi);
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, duration, delay, iterations, fill, direction, resetOnFinish, outAction: outActionProp,
370
- autoplay, play, pause, apiRef, time, timeMs,
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
- { duration, delay, iterations, fill, direction, resetOnFinish }
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
- }, [doc, duration, delay, iterations, fill, direction, resetOnFinish]);
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 startOn = trigger?.startOn ?? 'load';
518
- const outAction = outActionProp ?? trigger?.outAction ?? 'pause';
515
+ const effectiveStartOn = trigger?.startOn ?? 'load';
516
+ const effectiveOutAction = trigger?.outAction ?? 'pause';
519
517
 
520
518
  useEffect(() => {
521
- if (time !== undefined || timeMs !== undefined) {
522
- const seekMs = timeMs !== undefined ? timeMs : (time ?? 0) * totalDuration;
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 && startOn === 'load') {
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, time, timeMs]);
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 || startOn !== 'scrollIntoView') return;
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 (outAction === 'reset') api.cancel();
566
- else if (outAction === 'reverse') { api.setPlaybackRate(-Math.abs(rateRef.current || 1)); api.play(); }
567
- else if (outAction !== 'continue') api.pause();
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, startOn, outAction]);
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 && startOn === 'scrollIntoView' && root) {
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 && startOn === 'click' && root) {
675
+ } else if (autoplay && effectiveStartOn === 'click' && root) {
678
676
  content = (
679
677
  <Pressable
680
678
  onPress={() => {
681
679
  if (playingRef.current) {
682
- if (outAction === 'reset') api.cancel();
683
- else if (outAction === 'reverse') { api.setPlaybackRate(-Math.abs(rateRef.current || 1)); api.play(); }
684
- else if (outAction !== 'continue') api.pause();
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();
@@ -4,7 +4,7 @@
4
4
  *---------------------------------------------------------------------------------------*/
5
5
 
6
6
  import { describe, expect, it } from 'vitest';
7
- import { generateNewIds, materialiseAllInTree, PxAnimatorEngine, type PxAnimatedSvgDocument } from '@pixodesk/svg-animator-core';
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: { mode: 'frames', duration: 1000, iterations: 2, direction: 'alternate' },
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, PxAnimatorEngine.frames));
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: { mode: 'frames', duration: 1000 },
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, PxAnimatorEngine.waapi));
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: { mode: 'frames', duration: 2000 },
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: { sourceId: '#sym', retime: { start: -600 } } } },
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, PxAnimatorEngine.waapi));
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
- PxAnimatorEngine,
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, PxAnimatorEngine.frames) || [];
102
+ const bindings = getNormalisedBindings(doc, PxTimelineEngine.js) || [];
103
103
 
104
104
  const elements: Array<PxElementTracks> = [];
105
105
  for (const binding of bindings) {