@pixodesk/svg-animator-rn 1.0.41 → 1.0.44

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixodesk/svg-animator-rn",
3
- "version": "1.0.41",
3
+ "version": "1.0.44",
4
4
  "description": "Pixodesk SVG animator for React Native — renders animator documents via react-native-svg, driven natively by react-native-reanimated",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,7 +27,7 @@
27
27
  "src"
28
28
  ],
29
29
  "dependencies": {
30
- "@pixodesk/svg-animator-core": "^1.0.41"
30
+ "@pixodesk/svg-animator-core": "^1.0.44"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=18",
@@ -3,7 +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 { clampSeekMs, createRunClock, isValidPlaybackRate, progressSpanMs, progressToTimeMs, PX_RATE_REJECTED, PxDiagnosticKind, seekCeilingMs, timeToProgress, type PxAnimatorHandle, type PxAnimatorCallbacks, type PxControlProps, type PxPlaybackOverride, type PxDiagnostics, generateNewIds, getAnimatorConfig, getDefinitions, materializeAllInTree, resolveTrigger, validateNodeEffects, PxTimelineEngine, PxControlMode, resolveControlMode, type PxFillMode, type PxOutAction, type PxPlaybackDirection, type PxAnimatedSvgDocument, type PxTimelinePatch, type PxNode, applyAnimatorConfig, foldTimelineOverride } from '@pixodesk/svg-animator-core';
6
+ import { clampSeekMs, createRunClock, isValidPlaybackRate, progressSpanMs, progressToTimeMs, PxDiagnosticCode, PxDiagnosticKind, seekCeilingMs, timeToProgress, type PxAnimatorHandle, type PxAnimatorCallbacks, type PxControlProps, type PxPlaybackOverride, type PxDiagnostics, generateNewIds, getAnimatorConfig, getDefinitions, materializeAllInTree, resolveTrigger, validateNodeEffects, PxTimelineEngine, PxControlMode, resolveControlMode, type PxFillMode, type PxMouseOutAction, type PxPlaybackDirection, type PxAnimatedSvgDocument, type PxTimelinePatch, type PxNode, applyAnimatorConfig, foldTimelineOverride } from '@pixodesk/svg-animator-core';
7
7
  import { createDiagnostics, reportDocumentDiagnostics } from '@pixodesk/svg-animator-core/internal';
8
8
  import React, { createElement, useEffect, useImperativeHandle, useMemo, useRef, useState, type ComponentType, type ReactElement, type ReactNode } from 'react';
9
9
  import { Dimensions, Platform, Pressable, View } from 'react-native';
@@ -70,7 +70,7 @@ export interface PixodeskSvgAnimatorProps
70
70
  // -- Failure handling -----------------------------------------------------
71
71
  // A document that cannot be compiled or rendered is reported through `onError` — the same
72
72
  // `(diagnostic) => void` as every other player (review §25.1), with `diagnostic.error` the
73
- // thrown Error and, when the error boundary caught it, `diagnostic.detail.componentStack`.
73
+ // thrown Error and, when the error boundary caught it, the component stack in `diagnostic.data`.
74
74
  // The component renders `fallback` instead of throwing, so one broken animation never
75
75
  // takes down the screen around it. Only JavaScript failures reach this — a crash inside
76
76
  // react-native-svg's native renderer bypasses JavaScript entirely.
@@ -238,19 +238,19 @@ const EMPTY_TRACKS: PxCompiledTracks = {
238
238
  * whole thing sits behind one try/catch, and so it can be tested directly.
239
239
  */
240
240
  function compileDocument(doc: PxAnimatedSvgDocument, overrides: ConfigOverrides, diag: PxDiagnostics): Compiled {
241
- const { timeline, resetTimeline, duration, delay, iterations, startOn } = overrides;
241
+ const { timeline, resetTimeline, duration, delay, iterations, start } = overrides;
242
242
  const warnings = validateNodeEffects(doc as PxNode);
243
- for (const w of warnings) diag.warn(PxDiagnosticKind.document, 'effects shape: ' + w);
243
+ for (const w of warnings) diag.warn(PxDiagnosticKind.document, PxDiagnosticCode.effectsShape, w);
244
244
  // The whole-document boundary diagnostic — see the note in the web player's entry.
245
245
  reportDocumentDiagnostics(doc, '[PixodeskSvgAnimator]');
246
246
 
247
247
  // The per-instance override, applied to the WIRE document BEFORE anything reads the
248
248
  // config — `materializeAllInTree` samples motion paths against `duration`, so a later
249
249
  // patch would be read by none of the pipeline. Same call, same rules, on every surface.
250
- const patch = foldTimelineOverride(timeline, { duration, delay, iterations, startOn });
250
+ const patch = foldTimelineOverride(timeline, { duration, delay, iterations, start });
251
251
  if (patch !== undefined || resetTimeline) {
252
252
  const applied = applyAnimatorConfig(doc, patch ?? {}, { resetTimeline: !!resetTimeline });
253
- for (const w of applied.warnings) diag.warn(PxDiagnosticKind.usage, 'timeline override: ' + w);
253
+ for (const w of applied.warnings) diag.warn(PxDiagnosticKind.usage, PxDiagnosticCode.timelineOverrideIgnored, w);
254
254
  doc = applied.doc;
255
255
  }
256
256
 
@@ -291,7 +291,7 @@ function compileDocument(doc: PxAnimatedSvgDocument, overrides: ConfigOverrides,
291
291
  * @public
292
292
  */
293
293
  export function PixodeskSvgAnimator({
294
- doc, timeline, resetTimeline, duration, delay, iterations, startOn,
294
+ doc, timeline, resetTimeline, duration, delay, iterations, start,
295
295
  // (`progress` prop aliased — the name is taken by the internal reanimated SharedValue)
296
296
  autoplay, play, pause, apiRef, progress: progressProp, time,
297
297
  onPlay, onStop, onPause, onCancel, onFinish, onRemove, onError, fallback, onWarn, muteWarn, muteError,
@@ -318,19 +318,19 @@ export function PixodeskSvgAnimator({
318
318
  try {
319
319
  return compileDocument(
320
320
  doc,
321
- { timeline, resetTimeline, duration, delay, iterations, startOn },
321
+ { timeline, resetTimeline, duration, delay, iterations, start },
322
322
  makeDiag(),
323
323
  );
324
324
  } catch (e) {
325
325
  // A malformed document must not take the host screen down with it. This instance
326
326
  // will not play — an ERROR, reported once, here (review §25.1).
327
327
  const error = e instanceof Error ? e : new Error(String(e));
328
- makeDiag().error(PxDiagnosticKind.internal, error, { phase: 'compile' });
328
+ makeDiag().error(PxDiagnosticKind.internal, PxDiagnosticCode.rnCompileFailed, error);
329
329
  return { doc: null, tracks: EMPTY_TRACKS, error };
330
330
  }
331
331
  // `timeline` is an object prop, so a fresh literal each render would recompile the whole
332
332
  // document. Key on its CONTENT — the override is small, unlike the document.
333
- }, [doc, timelineKey, resetTimeline, duration, delay, iterations, startOn]);
333
+ }, [doc, timelineKey, resetTimeline, duration, delay, iterations, start]);
334
334
 
335
335
  const tracks: PxCompiledTracks = compiled.tracks;
336
336
  // The span `progress` 0–1 covers (ONE iteration when endless) — NOT the seek ceiling.
@@ -450,7 +450,7 @@ export function PixodeskSvgAnimator({
450
450
  },
451
451
  setPlaybackRate: (rate: number) => {
452
452
  if (!isValidPlaybackRate(rate)) {
453
- makeDiag().warn(PxDiagnosticKind.usage, PX_RATE_REJECTED);
453
+ makeDiag().warn(PxDiagnosticKind.usage, PxDiagnosticCode.rateRejected);
454
454
  return;
455
455
  }
456
456
  rateRef.current = rate;
@@ -495,12 +495,14 @@ export function PixodeskSvgAnimator({
495
495
  // -- Declarative control --------------------------------------------------
496
496
 
497
497
  // The EFFECTIVE trigger, read back off the COMPILED document — so it already reflects the
498
- // `timeline` override and the `startOn` shortcut, both merged in before compilation.
498
+ // `timeline` override and the `start` shortcut, both merged in before compilation.
499
499
  // Resolved through core's one table, so a document means the same here as on the web:
500
- // no `startOn` = 'load', no `outAction` = 'continue'.
500
+ // no `start` = 'load', no `offScreen` = 'pause'.
501
501
  const trigger = resolveTrigger(compiled.doc ? getAnimatorConfig(compiled.doc)?.trigger : undefined);
502
- const effectiveStartOn = trigger.startOn;
503
- const effectiveOutAction = trigger.outAction;
502
+ const effectiveStart = trigger.start;
503
+ const effectiveOffScreen = trigger.offScreen;
504
+ /** Whether visibility governs this document. `continue` means "run wherever it is". */
505
+ const gated = effectiveOffScreen !== 'continue';
504
506
 
505
507
  // ONE control-mode rule, decided in core and shared with React and Vue (API review §1/§7).
506
508
  // This component always had the right ORDER but no name for it, and never told anyone when
@@ -510,7 +512,7 @@ export function PixodeskSvgAnimator({
510
512
 
511
513
  useEffect(() => {
512
514
  const diag = makeDiag();
513
- for (const w of modeWarnings) diag.warn(PxDiagnosticKind.usage, w);
515
+ for (const w of modeWarnings) diag.warn(PxDiagnosticKind.usage, PxDiagnosticCode.controlPropsConflict, w);
514
516
  // eslint-disable-next-line react-hooks/exhaustive-deps
515
517
  }, [modeWarnings.join('|')]);
516
518
 
@@ -529,22 +531,23 @@ export function PixodeskSvgAnimator({
529
531
  else api.play();
530
532
  return;
531
533
  }
532
- // 'click' and 'scrollIntoView' start from their own handlers below.
533
- if (compMode === PxControlMode.autoplay && effectiveStartOn === 'load') {
534
+ // 'click' starts from its own handler below, and a GATED document starts from the
535
+ // visibility poll — calling play() here as well would defeat the gate.
536
+ if (compMode === PxControlMode.autoplay && effectiveStart === 'load' && !gated) {
534
537
  api.play();
535
538
  }
536
539
  // eslint-disable-next-line react-hooks/exhaustive-deps
537
540
  }, [compiled, compMode, autoplay, play, pause, progressProp, time]);
538
541
 
539
- // `startOn: 'scrollIntoView'` — react-native has no IntersectionObserver, so
540
- // visibility is sampled by measuring the view against the window box. The
541
- // poll is cheap (a native measure every 200ms) and only runs while this
542
- // trigger is active; `outAction` decides what leaving the viewport does.
542
+ // THE VISIBILITY GATE — permission to run, whatever started the animation, exactly as on the
543
+ // web (`PxVisibilityGate`). React Native has no IntersectionObserver, so visibility is sampled
544
+ // by measuring the view against the window box. The poll is cheap (a native measure every
545
+ // 200ms) and only runs while the gate is active, which `offScreen: 'continue'` turns off.
543
546
  const scrollRef = useRef<View | null>(null);
544
547
  const inViewRef = useRef(false);
545
548
  useEffect(() => {
546
- if (!autoplay || effectiveStartOn !== 'scrollIntoView') return;
547
- const threshold = trigger.scrollIntoViewThreshold;
549
+ if (!autoplay || !gated) return;
550
+ const threshold = trigger.visibilityThreshold;
548
551
  inViewRef.current = false;
549
552
 
550
553
  const check = () => {
@@ -561,9 +564,8 @@ export function PixodeskSvgAnimator({
561
564
  if (isIn) {
562
565
  if (rateRef.current < 0) api.setPlaybackRate(Math.abs(rateRef.current));
563
566
  api.play();
564
- } else if (effectiveOutAction === 'reset') api.cancel();
565
- else if (effectiveOutAction === 'reverse') { api.setPlaybackRate(-Math.abs(rateRef.current || 1)); api.play(); }
566
- else if (effectiveOutAction !== 'continue') api.pause();
567
+ } else if (effectiveOffScreen === 'reset') api.cancel();
568
+ else api.pause(); // 'continue' never reaches here — the poll does not run
567
569
  });
568
570
  };
569
571
 
@@ -571,7 +573,7 @@ export function PixodeskSvgAnimator({
571
573
  const id = setInterval(check, 200);
572
574
  return () => clearInterval(id);
573
575
  // eslint-disable-next-line react-hooks/exhaustive-deps
574
- }, [compiled, autoplay, effectiveStartOn, effectiveOutAction]);
576
+ }, [compiled, autoplay, gated, effectiveOffScreen]);
575
577
 
576
578
  // Stop cleanly on unmount / doc swap — and say so, the way the web's destroy() does (§18):
577
579
  // both are "the animator was thrown away", so `onRemove` fires, and `onStop` with it.
@@ -648,7 +650,7 @@ export function PixodeskSvgAnimator({
648
650
  // (an ERROR, review §25.1) and render nothing rather than unmount the host screen.
649
651
  const error = e instanceof Error ? e : new Error(String(e));
650
652
  renderErrorRef.current = error;
651
- makeDiag().error(PxDiagnosticKind.internal, error, { phase: 'render' });
653
+ makeDiag().error(PxDiagnosticKind.internal, PxDiagnosticCode.rnRenderFailed, error);
652
654
  return null;
653
655
  }
654
656
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -658,7 +660,7 @@ export function PixodeskSvgAnimator({
658
660
  const diag = makeDiag();
659
661
  // `platform`: these come from the react-native-svg prop mapper — shapes this renderer
660
662
  // cannot express, rather than anything wrong with the file.
661
- for (const w of warningsRef.current) diag.warn(PxDiagnosticKind.platform, w);
663
+ for (const w of warningsRef.current) diag.warn(PxDiagnosticKind.platform, PxDiagnosticCode.rnUnsupported, w);
662
664
  // eslint-disable-next-line react-hooks/exhaustive-deps
663
665
  }, [root]);
664
666
 
@@ -667,23 +669,17 @@ export function PixodeskSvgAnimator({
667
669
  const failure = compiled.error ?? renderErrorRef.current;
668
670
  if (failure) return fallback ? fallback(failure) : null;
669
671
 
670
- // `startOn: 'click'` — the touch analogue of the web player's click trigger:
671
- // tap to start, tap again to apply `outAction`. Hover (`mouseOver`) has no
672
- // touch equivalent and `scrollIntoView` needs the surrounding scroll view,
673
- // so both are left to the host app.
672
+ // `start: 'click'` — the touch analogue of the web player's click trigger: a plain toggle,
673
+ // tap to play and tap again to pause. Hover (`mouseOver`) has no touch equivalent, so it is
674
+ // left to the host app.
674
675
  let content: ReactElement | null = root;
675
676
 
676
- if (autoplay && effectiveStartOn === 'scrollIntoView' && root) {
677
- // `collapsable={false}` keeps the view in the native tree so it can be measured.
678
- content = <View ref={scrollRef} collapsable={false}>{root}</View>;
679
- } else if (autoplay && effectiveStartOn === 'click' && root) {
677
+ if (autoplay && effectiveStart === 'click' && root) {
680
678
  content = (
681
679
  <Pressable
682
680
  onPress={() => {
683
681
  if (playingRef.current) {
684
- if (effectiveOutAction === 'reset') api.cancel();
685
- else if (effectiveOutAction === 'reverse') { api.setPlaybackRate(-Math.abs(rateRef.current || 1)); api.play(); }
686
- else if (effectiveOutAction !== 'continue') api.pause();
682
+ api.pause();
687
683
  } else {
688
684
  if (rateRef.current < 0) api.setPlaybackRate(Math.abs(rateRef.current));
689
685
  api.play();
@@ -695,6 +691,12 @@ export function PixodeskSvgAnimator({
695
691
  );
696
692
  }
697
693
 
694
+ // The gate measures the view, so it must survive in the native tree:
695
+ // `collapsable={false}` keeps it there.
696
+ if (autoplay && gated && content) {
697
+ content = <View ref={scrollRef} collapsable={false}>{content}</View>;
698
+ }
699
+
698
700
  // Catches what the try/catch above cannot: throws during React's own render
699
701
  // and commit of the tree — react-native-svg internals, reanimated failing
700
702
  // to attach to a component that turns out not to be a host view, and so on.
@@ -4,7 +4,7 @@
4
4
  *---------------------------------------------------------------------------------------*/
5
5
 
6
6
  import { Component, type ErrorInfo, type ReactNode } from 'react';
7
- import { PxDiagnosticKind, type PxDiagnostics } from '@pixodesk/svg-animator-core';
7
+ import { PxDiagnosticCode, PxDiagnosticKind, type PxDiagnostics } from '@pixodesk/svg-animator-core';
8
8
  import { createDiagnostics } from '@pixodesk/svg-animator-core/internal';
9
9
 
10
10
  /** @public @advanced */
@@ -14,7 +14,7 @@ export interface PxRnErrorBoundaryProps {
14
14
  fallback?: (error: Error) => ReactNode;
15
15
  /**
16
16
  * Where the failure is reported (API review §5, §25.1): `error(internal, …)` with
17
- * `{ componentStack }` as the detail — so it reaches `onError` like every other failure.
17
+ * the component stack passed as data — so it reaches `onError` like every other failure.
18
18
  * Defaults to the console.
19
19
  */
20
20
  diag?: PxDiagnostics;
@@ -47,7 +47,7 @@ export class PxRnErrorBoundary extends Component<PxRnErrorBoundaryProps, State>
47
47
 
48
48
  override componentDidCatch(error: Error, info: ErrorInfo): void {
49
49
  (this.props.diag ?? createDiagnostics(undefined, '[PixodeskSvgAnimator]'))
50
- .error(PxDiagnosticKind.internal, error, { componentStack: info?.componentStack ?? undefined });
50
+ .error(PxDiagnosticKind.internal, PxDiagnosticCode.rnBoundaryCaught, error, info?.componentStack ?? undefined);
51
51
  }
52
52
 
53
53
  override componentDidUpdate(prev: PxRnErrorBoundaryProps): void {