@tamagui/animations-reanimated 2.4.5 → 2.5.0

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.
@@ -1,6 +1,7 @@
1
1
  import { getEffectiveAnimation, normalizeTransition } from "@tamagui/animation-helpers";
2
2
  import { getSplitStyles, hooks, isWeb, Text, useComposedRefs, useEvent, useIsomorphicLayoutEffect, useThemeWithState, View } from "@tamagui/core";
3
3
  import { ResetPresence, usePresence } from "@tamagui/use-presence";
4
+ import normalizeColor from "@react-native/normalize-colors";
4
5
  import React, { forwardRef, useMemo, useRef } from "react";
5
6
  import Animated_, { cancelAnimation, runOnJS, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withDelay, withSpring, withTiming } from "react-native-reanimated";
6
7
  import { jsx } from "react/jsx-runtime";
@@ -42,9 +43,162 @@ const cloneAnimationValue = value => {
42
43
  }
43
44
  return value;
44
45
  };
46
+ const cloneStyleRecord = style => {
47
+ const next = {};
48
+ for (const key in style) {
49
+ next[key] = cloneAnimationValue(style[key]);
50
+ }
51
+ return next;
52
+ };
45
53
  const cloneTransitionConfig = config => {
46
54
  return cloneAnimationValue(config);
47
55
  };
56
+ const getAnimatedTransforms = value => {
57
+ if (!Array.isArray(value)) return [];
58
+ return value.filter(item => item !== null && typeof item === "object" && !Array.isArray(item));
59
+ };
60
+ const getRemovedAnimatedKeys = (current, previous) => {
61
+ const removedKeys = {};
62
+ for (const key of previous) {
63
+ if (!current.has(key)) removedKeys[key] = true;
64
+ }
65
+ return removedKeys;
66
+ };
67
+ const getImplicitDefault = (key, targetValue) => {
68
+ "worklet";
69
+
70
+ const value = key === "opacity" || key === "scale" || key === "scaleX" || key === "scaleY" ? 1 : 0;
71
+ if (typeof targetValue !== "string") return value;
72
+ let suffixIndex = 0;
73
+ while (suffixIndex < targetValue.length) {
74
+ const character = targetValue[suffixIndex];
75
+ const code = character.charCodeAt(0);
76
+ if (code >= 48 && code <= 57 || character === "." || character === "-" || character === "+") {
77
+ suffixIndex++;
78
+ } else {
79
+ break;
80
+ }
81
+ }
82
+ return suffixIndex > 0 ? `${value}${targetValue.slice(suffixIndex)}` : value;
83
+ };
84
+ const COLOR_STYLE_KEYS = {
85
+ backgroundColor: true,
86
+ borderColor: true,
87
+ borderLeftColor: true,
88
+ borderRightColor: true,
89
+ borderTopColor: true,
90
+ borderBottomColor: true,
91
+ color: true
92
+ };
93
+ const toReanimatedColor = value => {
94
+ "worklet";
95
+
96
+ if (typeof value === "number") {
97
+ const color = value >>> 0;
98
+ const alpha = (color >>> 24 & 255) / 255;
99
+ const red = color >>> 16 & 255;
100
+ const green = color >>> 8 & 255;
101
+ const blue = color & 255;
102
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
103
+ }
104
+ if (typeof value !== "string") return value;
105
+ const trimmed = value.trim();
106
+ if (trimmed === "transparent") return "rgba(0, 0, 0, 0)";
107
+ return trimmed;
108
+ };
109
+ const isReanimatedColorHistory = value => {
110
+ "worklet";
111
+
112
+ const normalized = toReanimatedColor(value);
113
+ return typeof normalized === "string" && normalized.startsWith("rgba(");
114
+ };
115
+ const normalizeAnimationColor = value => {
116
+ if (typeof value === "number") {
117
+ return toReanimatedColor(value);
118
+ }
119
+ if (typeof value !== "string") return;
120
+ const color = normalizeColor(value);
121
+ if (color == null) return;
122
+ const red = Math.round((color & 4278190080) >>> 24);
123
+ const green = Math.round((color & 16711680) >>> 16);
124
+ const blue = Math.round((color & 65280) >>> 8);
125
+ const alpha = (color & 255) / 255;
126
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
127
+ };
128
+ const buildSnapshot = (animated, statics, transforms, previousKeys, lastPainted) => {
129
+ const snapshotAnimated = {};
130
+ const snapshotStatics = cloneStyleRecord(statics);
131
+ for (const key in snapshotStatics) {
132
+ if (!COLOR_STYLE_KEYS[key]) continue;
133
+ const normalized = normalizeAnimationColor(snapshotStatics[key]);
134
+ if (normalized !== void 0) snapshotStatics[key] = normalized;
135
+ }
136
+ for (const key in animated) {
137
+ if (key === "transform") continue;
138
+ const value = animated[key];
139
+ if (COLOR_STYLE_KEYS[key]) {
140
+ const normalized = normalizeAnimationColor(value);
141
+ if (normalized === void 0) {
142
+ snapshotStatics[key] = cloneAnimationValue(value);
143
+ continue;
144
+ }
145
+ snapshotAnimated[key] = normalized;
146
+ } else {
147
+ snapshotAnimated[key] = cloneAnimationValue(value);
148
+ }
149
+ }
150
+ const snapshotTransforms = transforms.map(cloneStyleRecord);
151
+ const keys = new Set(Object.keys(snapshotAnimated));
152
+ for (const transform of snapshotTransforms) {
153
+ const key = Object.keys(transform)[0];
154
+ const value = key ? transform[key] : void 0;
155
+ if (key && (typeof value === "number" || typeof value === "string")) {
156
+ keys.add(`transform:${key}`);
157
+ }
158
+ }
159
+ const seeds = {};
160
+ for (const key of keys) {
161
+ let value = lastPainted[key];
162
+ if (typeof value !== "number" && typeof value !== "string") continue;
163
+ if (value === "auto" || typeof value === "string" && value.startsWith("calc")) {
164
+ continue;
165
+ }
166
+ if (COLOR_STYLE_KEYS[key]) value = normalizeAnimationColor(value);
167
+ if (value !== void 0) seeds[key] = value;
168
+ }
169
+ const gatedKeys = {};
170
+ for (const key of keys) gatedKeys[key] = true;
171
+ const removedKeys = getRemovedAnimatedKeys(keys, previousKeys);
172
+ const painted = {
173
+ ...snapshotStatics,
174
+ ...snapshotAnimated
175
+ };
176
+ const staticTransforms = getAnimatedTransforms(snapshotStatics.transform);
177
+ delete painted.transform;
178
+ for (const transform of staticTransforms) {
179
+ const key = Object.keys(transform)[0];
180
+ if (key) painted[`transform:${key}`] = transform[key];
181
+ }
182
+ for (const transform of snapshotTransforms) {
183
+ const key = Object.keys(transform)[0];
184
+ if (key) painted[`transform:${key}`] = transform[key];
185
+ }
186
+ return {
187
+ value: {
188
+ animated: snapshotAnimated,
189
+ statics: snapshotStatics,
190
+ transforms: snapshotTransforms,
191
+ gatedKeys,
192
+ seeds,
193
+ removedKeys,
194
+ removeTransform: Object.keys(removedKeys).some(key => key.startsWith("transform:")),
195
+ clearValue: isWeb ? "" : null
196
+ },
197
+ painted,
198
+ keys
199
+ };
200
+ };
201
+ const getGatedKeys = snapshot => Object.keys(snapshot.gatedKeys);
48
202
  const createReanimatedConfig = config => {
49
203
  "worklet";
50
204
 
@@ -59,7 +213,7 @@ const createReanimatedConfig = config => {
59
213
  }
60
214
  return next;
61
215
  };
62
- const applyAnimation = (targetValue, config, callback) => {
216
+ const applyAnimation = (targetValue, config, callback, seedValue, validateStartAsColor = false) => {
63
217
  "worklet";
64
218
 
65
219
  const delay = config.delay;
@@ -70,11 +224,43 @@ const applyAnimation = (targetValue, config, callback) => {
70
224
  } else {
71
225
  animatedValue = withSpring(targetValue, reanimatedConfig, callback);
72
226
  }
227
+ if (seedValue !== void 0 || validateStartAsColor) {
228
+ const innerOnStart = animatedValue.onStart;
229
+ animatedValue.onStart = (animation, value, timestamp, previousAnimation) => {
230
+ "worklet";
231
+
232
+ const startValue = validateStartAsColor ? isReanimatedColorHistory(value) ? toReanimatedColor(value) : seedValue ?? targetValue : seedValue;
233
+ innerOnStart(animation, startValue, timestamp, previousAnimation);
234
+ };
235
+ }
73
236
  if (delay && delay > 0) {
74
237
  animatedValue = withDelay(delay, animatedValue);
75
238
  }
76
239
  return animatedValue;
77
240
  };
241
+ const animateSnapshotValue = (key, implicitKey, targetValue, config, previouslyEmitted, seedValue, gated, currentlyExiting, exitCycleId, currentlyCompletingAnimation, didAnimateCycleId, markExitKeyDone, markDidAnimateKeyDone, validateStartAsColor = false) => {
242
+ "worklet";
243
+
244
+ const cycleGated = gated && (currentlyExiting || currentlyCompletingAnimation);
245
+ if (!previouslyEmitted && seedValue === void 0 && !cycleGated) {
246
+ return targetValue;
247
+ }
248
+ let callback;
249
+ if (gated && currentlyExiting) {
250
+ callback = finished => {
251
+ "worklet";
252
+
253
+ runOnJS(markExitKeyDone)(key, exitCycleId, finished ?? false);
254
+ };
255
+ } else if (gated && currentlyCompletingAnimation) {
256
+ callback = finished => {
257
+ "worklet";
258
+
259
+ runOnJS(markDidAnimateKeyDone)(key, didAnimateCycleId, finished ?? false);
260
+ };
261
+ }
262
+ return applyAnimation(targetValue, config, callback, previouslyEmitted ? void 0 : seedValue ?? getImplicitDefault(implicitKey, targetValue), validateStartAsColor);
263
+ };
78
264
  const ANIMATABLE_PROPERTIES = {
79
265
  // Transform
80
266
  transform: true,
@@ -149,6 +335,23 @@ const canAnimateProperty = (key, value, animateOnly) => {
149
335
  if (animateOnly && !animateOnly.includes(key)) return false;
150
336
  return true;
151
337
  };
338
+ const splitAnimationStyles = (style, isDark, disableAnimation, animateOnly) => {
339
+ const animated = {};
340
+ const statics = {};
341
+ for (const key in style) {
342
+ const value = resolveDynamicValue(style[key], isDark);
343
+ if (value === void 0) continue;
344
+ if (!disableAnimation && canAnimateProperty(key, value, animateOnly)) {
345
+ animated[key] = cloneAnimationValue(value);
346
+ } else {
347
+ statics[key] = cloneAnimationValue(value);
348
+ }
349
+ }
350
+ return {
351
+ animated,
352
+ statics
353
+ };
354
+ };
152
355
  function createWebAnimatedComponent(defaultTag) {
153
356
  const isText = defaultTag === "span";
154
357
  const Component = Animated.createAnimatedComponent(forwardRef((propsIn, ref) => {
@@ -369,7 +572,8 @@ function createAnimations(animationsConfig) {
369
572
  useStyleEmitter,
370
573
  themeName,
371
574
  stateRef,
372
- styleState
575
+ styleState,
576
+ onDidAnimate
373
577
  } = animationProps;
374
578
  const isHydrating = componentState.unmounted === true;
375
579
  const isMounting = componentState.unmounted === "should-enter";
@@ -387,6 +591,25 @@ function createAnimations(animationsConfig) {
387
591
  const disableAnimation = isHydrating || !animationKey;
388
592
  const isDark = themeName?.startsWith("dark") || false;
389
593
  const sendExitComplete = presence?.[1];
594
+ const onDidAnimateRef = useRef(onDidAnimate);
595
+ useIsomorphicLayoutEffect(() => {
596
+ onDidAnimateRef.current = onDidAnimate;
597
+ }, [onDidAnimate]);
598
+ const didAnimateCycleIdRef = useRef(0);
599
+ const pendingDidAnimateKeysRef = useRef(/* @__PURE__ */new Set());
600
+ const didAnimateCompletedRef = useRef(false);
601
+ const isCompletingAnimationRef = useSharedValue(false);
602
+ const didAnimateCycleIdShared = useSharedValue(0);
603
+ const markDidAnimateKeyDone = useEvent((key, cycleId, finished) => {
604
+ if (!finished || cycleId !== didAnimateCycleIdRef.current) return;
605
+ if (didAnimateCompletedRef.current) return;
606
+ pendingDidAnimateKeysRef.current.delete(key);
607
+ if (pendingDidAnimateKeysRef.current.size === 0) {
608
+ didAnimateCompletedRef.current = true;
609
+ isCompletingAnimationRef.value = false;
610
+ onDidAnimateRef.current?.();
611
+ }
612
+ });
390
613
  const exitCycleIdRef = useRef(0);
391
614
  const pendingExitKeysRef = useRef(/* @__PURE__ */new Set());
392
615
  const exitCompletedRef = useRef(false);
@@ -408,6 +631,9 @@ function createAnimations(animationsConfig) {
408
631
  exitCycleIdRef.current++;
409
632
  exitCompletedRef.current = false;
410
633
  pendingExitKeysRef.current.clear();
634
+ didAnimateCycleIdRef.current++;
635
+ pendingDidAnimateKeysRef.current.clear();
636
+ didAnimateCompletedRef.current = true;
411
637
  }
412
638
  if (justStoppedExiting) {
413
639
  exitCycleIdRef.current++;
@@ -416,52 +642,83 @@ function createAnimations(animationsConfig) {
416
642
  useIsomorphicLayoutEffect(() => {
417
643
  isExitingRef.value = isExiting;
418
644
  exitCycleIdShared.value = exitCycleIdRef.current;
419
- }, [isExiting, exitCycleIdRef.current]);
645
+ if (justStartedExiting) {
646
+ isCompletingAnimationRef.value = false;
647
+ didAnimateCycleIdShared.value = didAnimateCycleIdRef.current;
648
+ }
649
+ }, [isExiting, exitCycleIdRef.current, justStartedExiting]);
420
650
  React.useEffect(() => {
421
651
  wasExitingRef.current = isExiting;
422
652
  });
423
- const animatedTargetsRef = useSharedValue(null);
424
- const staticTargetsRef = useSharedValue(null);
425
- const transformTargetsRef = useSharedValue(null);
653
+ const committedRenderKeysRef = useRef(/* @__PURE__ */new Set());
654
+ const lastPaintedRef = useRef({});
655
+ const carryRef = useRef({});
426
656
  const {
427
657
  animatedStyles,
428
- staticStyles
658
+ staticStyles,
659
+ nextCarry
429
660
  } = useMemo(() => {
430
- const animated = {};
431
- const staticStyles2 = {};
432
661
  const animateOnly = props.animateOnly;
433
- for (const key in style) {
434
- const rawValue = style[key];
435
- const value = resolveDynamicValue(rawValue, isDark);
436
- if (value === void 0) continue;
437
- if (disableAnimation) {
438
- staticStyles2[key] = cloneAnimationValue(value);
439
- continue;
440
- }
441
- if (canAnimateProperty(key, value, animateOnly)) {
442
- animated[key] = cloneAnimationValue(value);
443
- } else {
444
- staticStyles2[key] = cloneAnimationValue(value);
445
- }
662
+ const {
663
+ animated,
664
+ statics
665
+ } = splitAnimationStyles(style, isDark, disableAnimation, animateOnly);
666
+ const nextCarry2 = cloneStyleRecord(carryRef.current);
667
+ for (const key in nextCarry2) {
668
+ if (!(key in animated)) delete nextCarry2[key];
446
669
  }
447
- if (isMounting) {
448
- for (const key in animated) {
449
- staticStyles2[key] = animated[key];
670
+ for (const key in animated) {
671
+ if (!(key in nextCarry2)) {
672
+ nextCarry2[key] = cloneAnimationValue(animated[key]);
450
673
  }
674
+ statics[key] = nextCarry2[key];
451
675
  }
452
676
  return {
453
677
  animatedStyles: animated,
454
- staticStyles: staticStyles2
678
+ staticStyles: statics,
679
+ nextCarry: nextCarry2
455
680
  };
456
- }, [disableAnimation, style, isDark, isMounting, props.animateOnly]);
681
+ }, [disableAnimation, style, isDark, props.animateOnly]);
682
+ const renderSnapshot = useMemo(() => buildSnapshot(animatedStyles, staticStyles, getAnimatedTransforms(animatedStyles.transform), committedRenderKeysRef.current, lastPaintedRef.current), [animatedStyles, staticStyles]);
683
+ const renderSnapshotRef = useSharedValue({
684
+ animated: {},
685
+ statics: {},
686
+ transforms: [],
687
+ gatedKeys: {},
688
+ seeds: {},
689
+ removedKeys: {},
690
+ removeTransform: false,
691
+ clearValue: isWeb ? "" : null
692
+ });
693
+ const emitterSnapshotRef = useSharedValue(null);
694
+ const emitterKeysRef = useRef(null);
695
+ useIsomorphicLayoutEffect(() => {
696
+ carryRef.current = nextCarry;
697
+ committedRenderKeysRef.current = renderSnapshot.keys;
698
+ lastPaintedRef.current = renderSnapshot.painted;
699
+ }, [nextCarry, renderSnapshot]);
457
700
  const pseudoActiveRef = useRef(false);
458
701
  const isExitingJSRef = useRef(false);
459
702
  isExitingJSRef.current = isExiting;
703
+ const publishedSnapshotRef = useRef(null);
460
704
  useIsomorphicLayoutEffect(() => {
461
- if ((isExiting || !pseudoActiveRef.current) && animatedTargetsRef.value !== null) {
462
- animatedTargetsRef.value = null;
463
- staticTargetsRef.value = null;
464
- transformTargetsRef.value = null;
705
+ const droppingLatch = (isExiting || !pseudoActiveRef.current) && emitterSnapshotRef.value !== null;
706
+ const emitterKeys = droppingLatch ? emitterKeysRef.current : null;
707
+ if (droppingLatch || publishedSnapshotRef.current !== renderSnapshot.value) {
708
+ const removedKeys = emitterKeys ? {
709
+ ...renderSnapshot.value.removedKeys,
710
+ ...getRemovedAnimatedKeys(renderSnapshot.keys, emitterKeys)
711
+ } : renderSnapshot.value.removedKeys;
712
+ renderSnapshotRef.value = {
713
+ ...renderSnapshot.value,
714
+ removedKeys,
715
+ removeTransform: Object.keys(removedKeys).some(key => key.startsWith("transform:"))
716
+ };
717
+ publishedSnapshotRef.current = renderSnapshot.value;
718
+ }
719
+ if (droppingLatch) {
720
+ emitterSnapshotRef.value = null;
721
+ emitterKeysRef.current = null;
465
722
  }
466
723
  });
467
724
  const {
@@ -497,9 +754,6 @@ function createAnimations(animationsConfig) {
497
754
  if (isExitingJSRef.current) return;
498
755
  pseudoActiveRef.current = pseudoActive === true;
499
756
  const animateOnly = props.animateOnly;
500
- const animated = {};
501
- const statics = {};
502
- const transforms = [];
503
757
  const transitionToUse = effectiveTransition2 ?? props.transition;
504
758
  const {
505
759
  baseConfig: newBase,
@@ -511,69 +765,52 @@ function createAnimations(animationsConfig) {
511
765
  disableAnimation: configRef.value.disableAnimation,
512
766
  isHydrating: configRef.value.isHydrating
513
767
  };
514
- for (const key in nextStyle) {
515
- const rawValue = nextStyle[key];
516
- const value = resolveDynamicValue(rawValue, isDark);
517
- if (value == void 0) continue;
518
- if (configRef.value.disableAnimation) {
519
- statics[key] = cloneAnimationValue(value);
520
- continue;
521
- }
522
- if (key === "transform" && Array.isArray(value)) {
523
- for (const t of value) {
524
- if (t && typeof t === "object") {
525
- const tKey = Object.keys(t)[0];
526
- const tVal = t[tKey];
527
- if (typeof tVal === "number" || typeof tVal === "string") {
528
- transforms.push(cloneAnimationValue(t));
529
- }
530
- }
531
- }
532
- continue;
533
- }
534
- if (canAnimateProperty(key, value, animateOnly)) {
535
- animated[key] = cloneAnimationValue(value);
536
- } else {
537
- statics[key] = cloneAnimationValue(value);
538
- }
539
- }
540
- animatedTargetsRef.value = animated;
541
- staticTargetsRef.value = statics;
542
- transformTargetsRef.value = transforms;
768
+ const previousKeys = emitterKeysRef.current ?? committedRenderKeysRef.current;
769
+ const {
770
+ animated,
771
+ statics
772
+ } = splitAnimationStyles(nextStyle, isDark, configRef.value.disableAnimation, animateOnly);
773
+ const snapshot = buildSnapshot(animated, statics, getAnimatedTransforms(animated.transform), previousKeys, lastPaintedRef.current);
774
+ emitterSnapshotRef.value = snapshot.value;
775
+ emitterKeysRef.current = snapshot.keys;
776
+ lastPaintedRef.current = snapshot.painted;
543
777
  if (process.env.NODE_ENV === "development" && props.debug && props.debug !== "profile") {
544
778
  console.info("[animations-reanimated] useStyleEmitter update", {
545
779
  animated,
546
780
  statics,
547
- transforms
781
+ transforms: snapshot.value.transforms
548
782
  });
549
783
  }
550
784
  });
551
785
  const exitKeysRegistered = useRef(false);
552
786
  if (justStartedExiting && sendExitComplete) {
553
- const exitKeys = [];
554
- const animateOnly = props.animateOnly;
555
- for (const key in animatedStyles) {
556
- if (key === "transform") continue;
557
- if (canAnimateProperty(key, animatedStyles[key], animateOnly)) {
558
- exitKeys.push(key);
559
- }
560
- }
561
- const transforms = animatedStyles.transform;
562
- if (transforms && Array.isArray(transforms)) {
563
- for (const t of transforms) {
564
- if (!t) continue;
565
- const tKey = Object.keys(t)[0];
566
- if (tKey) {
567
- if (animateOnly && !animateOnly.includes(tKey)) {
568
- continue;
569
- }
570
- exitKeys.push(`transform:${tKey}`);
571
- }
572
- }
573
- }
787
+ const exitKeys = getGatedKeys(renderSnapshot.value);
574
788
  pendingExitKeysRef.current = new Set(exitKeys);
575
789
  exitKeysRegistered.current = exitKeys.length > 0;
576
790
  }
791
+ const previousDidAnimateStyleRef = useRef(null);
792
+ const didAnimateStyle = onDidAnimate ? JSON.stringify(style) : null;
793
+ const previousDidAnimateStyle = previousDidAnimateStyleRef.current;
794
+ const shouldRegisterDidAnimate = didAnimateStyle !== null && previousDidAnimateStyle !== null && previousDidAnimateStyle !== didAnimateStyle && !isExiting && !isEntering && !justFinishedEntering;
795
+ const didAnimateKeys = shouldRegisterDidAnimate ? getGatedKeys(renderSnapshot.value) : [];
796
+ useIsomorphicLayoutEffect(() => {
797
+ if (didAnimateStyle === null) {
798
+ previousDidAnimateStyleRef.current = null;
799
+ isCompletingAnimationRef.value = false;
800
+ return;
801
+ }
802
+ const previousStyleStillCurrent = previousDidAnimateStyleRef.current === previousDidAnimateStyle;
803
+ previousDidAnimateStyleRef.current = didAnimateStyle;
804
+ if (!shouldRegisterDidAnimate || !previousStyleStillCurrent) return;
805
+ const cycleId = ++didAnimateCycleIdRef.current;
806
+ pendingDidAnimateKeysRef.current = new Set(didAnimateKeys);
807
+ didAnimateCompletedRef.current = didAnimateKeys.length === 0;
808
+ isCompletingAnimationRef.value = didAnimateKeys.length > 0;
809
+ didAnimateCycleIdShared.value = cycleId;
810
+ if (didAnimateKeys.length === 0) {
811
+ onDidAnimateRef.current?.();
812
+ }
813
+ }, [didAnimateStyle, shouldRegisterDidAnimate]);
577
814
  React.useEffect(() => {
578
815
  if (!justStartedExiting || !sendExitComplete) return;
579
816
  if (!exitKeysRegistered.current && pendingExitKeysRef.current.size === 0) {
@@ -583,76 +820,76 @@ function createAnimations(animationsConfig) {
583
820
  }
584
821
  }
585
822
  }, [justStartedExiting, sendExitComplete]);
823
+ const mapperStateRef = useSharedValue({
824
+ emitted: {}
825
+ });
586
826
  const animatedStyle = useAnimatedStyle(() => {
587
827
  "worklet";
588
828
 
589
- if (disableAnimation || isHydrating) {
829
+ const mapperState = mapperStateRef.value;
830
+ const config = configRef.value;
831
+ if (config.disableAnimation || config.isHydrating) {
832
+ mapperState.emitted = {};
590
833
  return {};
591
834
  }
835
+ const previouslyEmitted = mapperState.emitted;
836
+ const emitted = {};
592
837
  const result = {};
593
- const config = configRef.value;
594
- const emitterAnimated = animatedTargetsRef.value;
595
- const emitterStatic = staticTargetsRef.value;
596
- const emitterTransforms = transformTargetsRef.value;
597
- const hasEmitterUpdates = emitterAnimated !== null;
598
- const animatedValues = hasEmitterUpdates ? emitterAnimated : animatedStyles;
599
- const staticValues = hasEmitterUpdates ? emitterStatic : staticStyles;
838
+ const emitterSnapshot = emitterSnapshotRef.value;
839
+ const snapshot = emitterSnapshot ?? renderSnapshotRef.value;
840
+ const animatedValues = snapshot.animated;
841
+ const staticValues = snapshot.statics;
600
842
  const currentlyExiting = isExitingRef.value;
601
843
  const currentCycleId = exitCycleIdShared.value;
844
+ const currentlyCompletingAnimation = isCompletingAnimationRef.value;
845
+ const currentDidAnimateCycleId = didAnimateCycleIdShared.value;
602
846
  for (const key in staticValues) {
603
847
  result[key] = staticValues[key];
604
848
  }
849
+ for (const key in snapshot.removedKeys) {
850
+ if (!key.startsWith("transform:") && !(key in staticValues)) {
851
+ result[key] = snapshot.clearValue;
852
+ }
853
+ }
605
854
  for (const key in animatedValues) {
606
855
  if (key === "transform") continue;
607
856
  const targetValue = animatedValues[key];
608
- const propConfig = config.propertyConfigs[key] ?? config.baseConfig;
609
- let callback;
610
- if (currentlyExiting) {
611
- const capturedKey = key;
612
- const capturedCycleId = currentCycleId;
613
- callback = finished => {
614
- "worklet";
615
-
616
- runOnJS(markExitKeyDone)(capturedKey, capturedCycleId, finished ?? false);
617
- };
857
+ emitted[key] = true;
858
+ if (typeof targetValue !== "number" && typeof targetValue !== "string") {
859
+ result[key] = targetValue;
860
+ continue;
618
861
  }
619
- result[key] = applyAnimation(targetValue, propConfig, callback);
862
+ result[key] = animateSnapshotValue(key, key, targetValue, config.propertyConfigs[key] ?? config.baseConfig, !!previouslyEmitted[key], snapshot.seeds[key], !!snapshot.gatedKeys[key], currentlyExiting, currentCycleId, currentlyCompletingAnimation, currentDidAnimateCycleId, markExitKeyDone, markDidAnimateKeyDone, !!COLOR_STYLE_KEYS[key]);
620
863
  }
621
- const transforms = hasEmitterUpdates ? emitterTransforms : animatedStyles.transform;
622
- if (transforms && Array.isArray(transforms)) {
864
+ const transforms = snapshot.transforms;
865
+ if (transforms.length > 0 || snapshot.removeTransform) {
623
866
  const validTransforms = [];
624
867
  for (const t of transforms) {
625
868
  if (!t) continue;
626
869
  const keys = Object.keys(t);
627
870
  if (keys.length === 0) continue;
628
- const value = t[keys[0]];
871
+ const transformKey = keys[0];
872
+ const value = t[transformKey];
629
873
  if (typeof value === "number" || typeof value === "string") {
630
- const transformKey = Object.keys(t)[0];
631
- const targetValue = t[transformKey];
632
874
  const propConfig = config.propertyConfigs[transformKey] ?? config.baseConfig;
633
- let callback;
634
- if (currentlyExiting) {
635
- const capturedKey = `transform:${transformKey}`;
636
- const capturedCycleId = currentCycleId;
637
- callback = finished => {
638
- "worklet";
639
-
640
- runOnJS(markExitKeyDone)(capturedKey, capturedCycleId, finished ?? false);
641
- };
642
- }
875
+ const subKey = `transform:${transformKey}`;
876
+ emitted[subKey] = true;
643
877
  validTransforms.push({
644
- [transformKey]: applyAnimation(targetValue, propConfig, callback)
878
+ [transformKey]: animateSnapshotValue(subKey, transformKey, value, propConfig, !!previouslyEmitted[subKey], snapshot.seeds[subKey], !!snapshot.gatedKeys[subKey], currentlyExiting, currentCycleId, currentlyCompletingAnimation, currentDidAnimateCycleId, markExitKeyDone, markDidAnimateKeyDone)
645
879
  });
880
+ } else {
881
+ validTransforms.push(t);
646
882
  }
647
883
  }
648
- if (validTransforms.length > 0) {
884
+ if (validTransforms.length > 0 || !("transform" in staticValues)) {
649
885
  result.transform = validTransforms;
650
886
  }
651
887
  }
888
+ mapperState.emitted = emitted;
652
889
  return result;
653
- }, isWeb ? [animatedStyles, staticStyles, baseConfig, propertyConfigs, disableAnimation, isHydrating,
654
- // pass SharedValues so the mapper watches them on web (no babel plugin)
655
- animatedTargetsRef, staticTargetsRef, transformTargetsRef, isExitingRef, exitCycleIdShared, markExitKeyDone] : void 0);
890
+ }, isWeb ? [
891
+ // pass SharedValues so the stable mapper watches them on web (no babel plugin)
892
+ renderSnapshotRef, emitterSnapshotRef, configRef, isExitingRef, exitCycleIdShared, markExitKeyDone, isCompletingAnimationRef, didAnimateCycleIdShared, markDidAnimateKeyDone] : void 0);
656
893
  silenceAnimatedComponentDevCheck(animatedStyle);
657
894
  if (process.env.NODE_ENV === "development" && props.debug && props.debug !== "profile") {
658
895
  console.info("[animations-reanimated] useAnimations", {