@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.
@@ -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
 
@@ -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,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, duration, delay, iterations, fill, direction, resetOnFinish, outAction: outActionProp,
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
- { duration, delay, iterations, fill, direction, resetOnFinish }
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
- }, [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]);
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 startOn = trigger?.startOn ?? 'load';
519
- const outAction = outActionProp ?? trigger?.outAction ?? 'pause';
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 && startOn === 'load') {
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 || startOn !== 'scrollIntoView') return;
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 (outAction === 'reset') api.cancel();
567
- else if (outAction === 'reverse') { api.setPlaybackRate(-Math.abs(rateRef.current || 1)); api.play(); }
568
- 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();
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, startOn, outAction]);
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 && startOn === 'scrollIntoView' && root) {
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 && startOn === 'click' && root) {
675
+ } else if (autoplay && effectiveStartOn === 'click' && root) {
679
676
  content = (
680
677
  <Pressable
681
678
  onPress={() => {
682
679
  if (playingRef.current) {
683
- if (outAction === 'reset') api.cancel();
684
- else if (outAction === 'reverse') { api.setPlaybackRate(-Math.abs(rateRef.current || 1)); api.play(); }
685
- 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();
686
683
  } else {
687
684
  if (rateRef.current < 0) api.setPlaybackRate(Math.abs(rateRef.current));
688
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) {