@hanzogui/animations-reanimated 7.3.1 → 8.0.1

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 "@hanzogui/animation-helpers";
2
2
  import { getSplitStyles, hooks, isWeb, Text, useComposedRefs, useEvent, useIsomorphicLayoutEffect, useThemeWithState, View } from "@hanzogui/core";
3
3
  import { ResetPresence, usePresence } from "@hanzogui/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";
@@ -12,6 +13,16 @@ const getDefaultExport = module => {
12
13
  return mod;
13
14
  };
14
15
  const Animated = getDefaultExport(Animated_);
16
+ const silenceAnimatedComponentDevCheck = style => {
17
+ if (process.env.NODE_ENV !== "development" || isWeb || !style || typeof style !== "object") {
18
+ return;
19
+ }
20
+ Object.defineProperty(style, "_requiresAnimatedComponent", {
21
+ configurable: true,
22
+ enumerable: false,
23
+ value: true
24
+ });
25
+ };
15
26
  const resolveDynamicValue = (value, isDark) => {
16
27
  if (value !== null && typeof value === "object" && "dynamic" in value && typeof value.dynamic === "object") {
17
28
  const dynamic = value.dynamic;
@@ -19,21 +30,248 @@ const resolveDynamicValue = (value, isDark) => {
19
30
  }
20
31
  return value;
21
32
  };
22
- const applyAnimation = (targetValue, config, callback) => {
33
+ const updateMapperState = isWeb ? (state, emitted) => {
34
+ state.value.emitted = emitted;
35
+ } : (state, emitted) => {
36
+ "worklet";
37
+
38
+ if (!globalThis._WORKLET) return;
39
+ state.modify(current => {
40
+ current.emitted = emitted;
41
+ return current;
42
+ }, false);
43
+ };
44
+ const cloneAnimationValue = value => {
45
+ if (Array.isArray(value)) {
46
+ return value.map(cloneAnimationValue);
47
+ }
48
+ if (value && typeof value === "object") {
49
+ const next = {};
50
+ for (const key in value) {
51
+ next[key] = cloneAnimationValue(value[key]);
52
+ }
53
+ return next;
54
+ }
55
+ return value;
56
+ };
57
+ const cloneStyleRecord = style => {
58
+ const next = {};
59
+ for (const key in style) {
60
+ next[key] = cloneAnimationValue(style[key]);
61
+ }
62
+ return next;
63
+ };
64
+ const cloneTransitionConfig = config => {
65
+ return cloneAnimationValue(config);
66
+ };
67
+ const getAnimatedTransforms = value => {
68
+ if (!Array.isArray(value)) return [];
69
+ return value.filter(item => item !== null && typeof item === "object" && !Array.isArray(item));
70
+ };
71
+ const getRemovedAnimatedKeys = (current, previous) => {
72
+ const removedKeys = {};
73
+ for (const key of previous) {
74
+ if (!current.has(key)) removedKeys[key] = true;
75
+ }
76
+ return removedKeys;
77
+ };
78
+ const getImplicitDefault = (key, targetValue) => {
79
+ "worklet";
80
+
81
+ const value = key === "opacity" || key === "scale" || key === "scaleX" || key === "scaleY" ? 1 : 0;
82
+ if (typeof targetValue !== "string") return value;
83
+ let suffixIndex = 0;
84
+ while (suffixIndex < targetValue.length) {
85
+ const character = targetValue[suffixIndex];
86
+ const code = character.charCodeAt(0);
87
+ if (code >= 48 && code <= 57 || character === "." || character === "-" || character === "+") {
88
+ suffixIndex++;
89
+ } else {
90
+ break;
91
+ }
92
+ }
93
+ return suffixIndex > 0 ? `${value}${targetValue.slice(suffixIndex)}` : value;
94
+ };
95
+ const COLOR_STYLE_KEYS = {
96
+ backgroundColor: true,
97
+ borderColor: true,
98
+ borderLeftColor: true,
99
+ borderRightColor: true,
100
+ borderTopColor: true,
101
+ borderBottomColor: true,
102
+ color: true
103
+ };
104
+ const toReanimatedColor = value => {
105
+ "worklet";
106
+
107
+ if (typeof value === "number") {
108
+ const color = value >>> 0;
109
+ const alpha = (color >>> 24 & 255) / 255;
110
+ const red = color >>> 16 & 255;
111
+ const green = color >>> 8 & 255;
112
+ const blue = color & 255;
113
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
114
+ }
115
+ if (typeof value !== "string") return value;
116
+ const trimmed = value.trim();
117
+ if (trimmed === "transparent") return "rgba(0, 0, 0, 0)";
118
+ return trimmed;
119
+ };
120
+ const isReanimatedColorHistory = value => {
121
+ "worklet";
122
+
123
+ const normalized = toReanimatedColor(value);
124
+ return typeof normalized === "string" && normalized.startsWith("rgba(");
125
+ };
126
+ const normalizeAnimationColor = value => {
127
+ if (typeof value === "number") {
128
+ return toReanimatedColor(value);
129
+ }
130
+ if (typeof value !== "string") return;
131
+ const color = normalizeColor(value);
132
+ if (color == null) return;
133
+ const red = Math.round((color & 4278190080) >>> 24);
134
+ const green = Math.round((color & 16711680) >>> 16);
135
+ const blue = Math.round((color & 65280) >>> 8);
136
+ const alpha = (color & 255) / 255;
137
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
138
+ };
139
+ const buildSnapshot = (animated, statics, transforms, previousKeys, lastPainted) => {
140
+ const snapshotAnimated = {};
141
+ const snapshotStatics = cloneStyleRecord(statics);
142
+ for (const key in snapshotStatics) {
143
+ if (!COLOR_STYLE_KEYS[key]) continue;
144
+ const normalized = normalizeAnimationColor(snapshotStatics[key]);
145
+ if (normalized !== void 0) snapshotStatics[key] = normalized;
146
+ }
147
+ for (const key in animated) {
148
+ if (key === "transform") continue;
149
+ const value = animated[key];
150
+ if (COLOR_STYLE_KEYS[key]) {
151
+ const normalized = normalizeAnimationColor(value);
152
+ if (normalized === void 0) {
153
+ snapshotStatics[key] = cloneAnimationValue(value);
154
+ continue;
155
+ }
156
+ snapshotAnimated[key] = normalized;
157
+ } else {
158
+ snapshotAnimated[key] = cloneAnimationValue(value);
159
+ }
160
+ }
161
+ const snapshotTransforms = transforms.map(cloneStyleRecord);
162
+ const keys = new Set(Object.keys(snapshotAnimated));
163
+ for (const transform of snapshotTransforms) {
164
+ const key = Object.keys(transform)[0];
165
+ const value = key ? transform[key] : void 0;
166
+ if (key && (typeof value === "number" || typeof value === "string")) {
167
+ keys.add(`transform:${key}`);
168
+ }
169
+ }
170
+ const seeds = {};
171
+ for (const key of keys) {
172
+ let value = lastPainted[key];
173
+ if (typeof value !== "number" && typeof value !== "string") continue;
174
+ if (value === "auto" || typeof value === "string" && value.startsWith("calc")) {
175
+ continue;
176
+ }
177
+ if (COLOR_STYLE_KEYS[key]) value = normalizeAnimationColor(value);
178
+ if (value !== void 0) seeds[key] = value;
179
+ }
180
+ const gatedKeys = {};
181
+ for (const key of keys) gatedKeys[key] = true;
182
+ const removedKeys = getRemovedAnimatedKeys(keys, previousKeys);
183
+ const painted = {
184
+ ...snapshotStatics,
185
+ ...snapshotAnimated
186
+ };
187
+ const staticTransforms = getAnimatedTransforms(snapshotStatics.transform);
188
+ delete painted.transform;
189
+ for (const transform of staticTransforms) {
190
+ const key = Object.keys(transform)[0];
191
+ if (key) painted[`transform:${key}`] = transform[key];
192
+ }
193
+ for (const transform of snapshotTransforms) {
194
+ const key = Object.keys(transform)[0];
195
+ if (key) painted[`transform:${key}`] = transform[key];
196
+ }
197
+ return {
198
+ value: {
199
+ animated: snapshotAnimated,
200
+ statics: snapshotStatics,
201
+ transforms: snapshotTransforms,
202
+ gatedKeys,
203
+ seeds,
204
+ removedKeys,
205
+ removeTransform: Object.keys(removedKeys).some(key => key.startsWith("transform:")),
206
+ clearValue: isWeb ? "" : null
207
+ },
208
+ painted,
209
+ keys
210
+ };
211
+ };
212
+ const getGatedKeys = snapshot => Object.keys(snapshot.gatedKeys);
213
+ const createReanimatedConfig = config => {
214
+ "worklet";
215
+
216
+ const next = {};
217
+ const source = config;
218
+ for (const key in source) {
219
+ if (key === "type" || key === "delay") continue;
220
+ const value = source[key];
221
+ if (value !== void 0) {
222
+ next[key] = value;
223
+ }
224
+ }
225
+ return next;
226
+ };
227
+ const applyAnimation = (targetValue, config, callback, seedValue, validateStartAsColor = false) => {
23
228
  "worklet";
24
229
 
25
230
  const delay = config.delay;
231
+ const reanimatedConfig = createReanimatedConfig(config);
26
232
  let animatedValue;
27
233
  if (config.type === "timing") {
28
- animatedValue = withTiming(targetValue, config, callback);
234
+ animatedValue = withTiming(targetValue, reanimatedConfig, callback);
29
235
  } else {
30
- animatedValue = withSpring(targetValue, config, callback);
236
+ animatedValue = withSpring(targetValue, reanimatedConfig, callback);
237
+ }
238
+ if (seedValue !== void 0 || validateStartAsColor) {
239
+ const innerOnStart = animatedValue.onStart;
240
+ animatedValue.onStart = (animation, value, timestamp, previousAnimation) => {
241
+ "worklet";
242
+
243
+ const startValue = validateStartAsColor ? isReanimatedColorHistory(value) ? toReanimatedColor(value) : seedValue ?? targetValue : seedValue;
244
+ innerOnStart(animation, startValue, timestamp, previousAnimation);
245
+ };
31
246
  }
32
247
  if (delay && delay > 0) {
33
248
  animatedValue = withDelay(delay, animatedValue);
34
249
  }
35
250
  return animatedValue;
36
251
  };
252
+ const animateSnapshotValue = (key, implicitKey, targetValue, config, previouslyEmitted, seedValue, gated, currentlyExiting, exitCycleId, currentlyCompletingAnimation, didAnimateCycleId, markExitKeyDone, markDidAnimateKeyDone, validateStartAsColor = false) => {
253
+ "worklet";
254
+
255
+ const cycleGated = gated && (currentlyExiting || currentlyCompletingAnimation);
256
+ if (!previouslyEmitted && seedValue === void 0 && !cycleGated) {
257
+ return targetValue;
258
+ }
259
+ let callback;
260
+ if (gated && currentlyExiting) {
261
+ callback = finished => {
262
+ "worklet";
263
+
264
+ runOnJS(markExitKeyDone)(key, exitCycleId, finished ?? false);
265
+ };
266
+ } else if (gated && currentlyCompletingAnimation) {
267
+ callback = finished => {
268
+ "worklet";
269
+
270
+ runOnJS(markDidAnimateKeyDone)(key, didAnimateCycleId, finished ?? false);
271
+ };
272
+ }
273
+ return applyAnimation(targetValue, config, callback, previouslyEmitted ? void 0 : seedValue ?? getImplicitDefault(implicitKey, targetValue), validateStartAsColor);
274
+ };
37
275
  const ANIMATABLE_PROPERTIES = {
38
276
  // Transform
39
277
  transform: true,
@@ -108,6 +346,23 @@ const canAnimateProperty = (key, value, animateOnly) => {
108
346
  if (animateOnly && !animateOnly.includes(key)) return false;
109
347
  return true;
110
348
  };
349
+ const splitAnimationStyles = (style, isDark, disableAnimation, animateOnly) => {
350
+ const animated = {};
351
+ const statics = {};
352
+ for (const key in style) {
353
+ const value = resolveDynamicValue(style[key], isDark);
354
+ if (value === void 0) continue;
355
+ if (!disableAnimation && canAnimateProperty(key, value, animateOnly)) {
356
+ animated[key] = cloneAnimationValue(value);
357
+ } else {
358
+ statics[key] = cloneAnimationValue(value);
359
+ }
360
+ }
361
+ return {
362
+ animated,
363
+ statics
364
+ };
365
+ };
111
366
  function createWebAnimatedComponent(defaultTag) {
112
367
  const isText = defaultTag === "span";
113
368
  const Component = Animated.createAnimatedComponent(forwardRef((propsIn, ref) => {
@@ -146,43 +401,43 @@ const AnimatedText = createWebAnimatedComponent("span");
146
401
  function buildTransitionConfig(transition, animations, animationState, styleKeys) {
147
402
  const normalized = normalizeTransition(transition);
148
403
  const effectiveKey = getEffectiveAnimation(normalized, animationState);
149
- let base = effectiveKey ? animations[effectiveKey] ?? {
404
+ let base = cloneTransitionConfig(effectiveKey ? animations[effectiveKey] ?? {
150
405
  type: "spring"
151
406
  } : {
152
407
  type: "spring"
153
- };
408
+ });
154
409
  if (normalized.delay) {
155
- base = {
410
+ base = cloneTransitionConfig({
156
411
  ...base,
157
412
  delay: normalized.delay
158
- };
413
+ });
159
414
  }
160
415
  if (normalized.config) {
161
- base = {
416
+ base = cloneTransitionConfig({
162
417
  ...base,
163
418
  ...normalized.config
164
- };
419
+ });
165
420
  if (base.type !== "timing" && normalized.config.duration !== void 0 && normalized.config.damping === void 0 && normalized.config.stiffness === void 0 && normalized.config.mass === void 0) {
166
- base = {
421
+ base = cloneTransitionConfig({
167
422
  ...base,
168
423
  type: "timing"
169
- };
424
+ });
170
425
  }
171
426
  }
172
427
  const propertyConfigs = {};
173
428
  for (const key of styleKeys) {
174
429
  const propAnimation = normalized.properties[key];
175
430
  if (typeof propAnimation === "string") {
176
- propertyConfigs[key] = animations[propAnimation] ?? base;
431
+ propertyConfigs[key] = cloneTransitionConfig(animations[propAnimation] ?? base);
177
432
  } else if (propAnimation && typeof propAnimation === "object") {
178
433
  const configType = propAnimation.type;
179
434
  const baseForProp = configType ? animations[configType] ?? base : base;
180
- propertyConfigs[key] = {
435
+ propertyConfigs[key] = cloneTransitionConfig({
181
436
  ...baseForProp,
182
437
  ...propAnimation
183
- };
438
+ });
184
439
  } else {
185
- propertyConfigs[key] = base;
440
+ propertyConfigs[key] = cloneTransitionConfig(base);
186
441
  }
187
442
  }
188
443
  return {
@@ -204,10 +459,10 @@ function getStyleKeys(style) {
204
459
  function createAnimations(animationsConfig) {
205
460
  const animations = {};
206
461
  for (const key in animationsConfig) {
207
- animations[key] = {
462
+ animations[key] = cloneTransitionConfig({
208
463
  type: "spring",
209
464
  ...animationsConfig[key]
210
- };
465
+ });
211
466
  }
212
467
  return {
213
468
  needsCustomComponent: true,
@@ -243,19 +498,22 @@ function createAnimations(animationsConfig) {
243
498
  sharedValue.value = next;
244
499
  onFinish?.();
245
500
  } else {
246
- const cb = onFinish ? () => {
501
+ const cb = onFinish ? finished => {
247
502
  "worklet";
248
503
 
249
- runOnJS(onFinish)();
504
+ if (finished !== false) {
505
+ runOnJS(onFinish)();
506
+ }
250
507
  } : void 0;
508
+ const animationConfig = cloneTransitionConfig(config);
251
509
  if (isWeb) {
252
- sharedValue.value = config.type === "spring" ? withSpring(next, config, cb) : withTiming(next, config, cb);
510
+ sharedValue.value = applyAnimation(next, animationConfig, cb);
253
511
  } else {
254
- runOnUI(() => {
512
+ runOnUI((targetValue, animationConfig2) => {
255
513
  "worklet";
256
514
 
257
- sharedValue.value = config.type === "spring" ? withSpring(next, config, cb) : withTiming(next, config, cb);
258
- })();
515
+ sharedValue.value = applyAnimation(targetValue, animationConfig2, cb);
516
+ })(next, animationConfig);
259
517
  }
260
518
  }
261
519
  },
@@ -294,20 +552,24 @@ function createAnimations(animationsConfig) {
294
552
 
295
553
  return getStyle(instance.value);
296
554
  });
297
- return useAnimatedStyle(() => {
555
+ const animatedStyle = useAnimatedStyle(() => {
298
556
  "worklet";
299
557
 
300
558
  return styleVal.value;
301
559
  });
560
+ silenceAnimatedComponentDevCheck(animatedStyle);
561
+ return animatedStyle;
302
562
  },
303
563
  useAnimatedNumbersStyle(vals, getStyle) {
304
564
  const instances = vals.map(v => v.getInstance());
305
- return useAnimatedStyle(() => {
565
+ const animatedStyle = useAnimatedStyle(() => {
306
566
  "worklet";
307
567
 
308
568
  const currentValues = instances.map(inst => inst.value);
309
569
  return getStyle(...currentValues);
310
570
  }, isWeb ? [getStyle, ...instances] : void 0);
571
+ silenceAnimatedComponentDevCheck(animatedStyle);
572
+ return animatedStyle;
311
573
  },
312
574
  // =========================================================================
313
575
  // useAnimations - Main animation hook for components
@@ -321,7 +583,8 @@ function createAnimations(animationsConfig) {
321
583
  useStyleEmitter,
322
584
  themeName,
323
585
  stateRef,
324
- styleState
586
+ styleState,
587
+ onDidAnimate
325
588
  } = animationProps;
326
589
  const isHydrating = componentState.unmounted === true;
327
590
  const isMounting = componentState.unmounted === "should-enter";
@@ -339,6 +602,25 @@ function createAnimations(animationsConfig) {
339
602
  const disableAnimation = isHydrating || !animationKey;
340
603
  const isDark = themeName?.startsWith("dark") || false;
341
604
  const sendExitComplete = presence?.[1];
605
+ const onDidAnimateRef = useRef(onDidAnimate);
606
+ useIsomorphicLayoutEffect(() => {
607
+ onDidAnimateRef.current = onDidAnimate;
608
+ }, [onDidAnimate]);
609
+ const didAnimateCycleIdRef = useRef(0);
610
+ const pendingDidAnimateKeysRef = useRef(/* @__PURE__ */new Set());
611
+ const didAnimateCompletedRef = useRef(false);
612
+ const isCompletingAnimationRef = useSharedValue(false);
613
+ const didAnimateCycleIdShared = useSharedValue(0);
614
+ const markDidAnimateKeyDone = useEvent((key, cycleId, finished) => {
615
+ if (!finished || cycleId !== didAnimateCycleIdRef.current) return;
616
+ if (didAnimateCompletedRef.current) return;
617
+ pendingDidAnimateKeysRef.current.delete(key);
618
+ if (pendingDidAnimateKeysRef.current.size === 0) {
619
+ didAnimateCompletedRef.current = true;
620
+ isCompletingAnimationRef.value = false;
621
+ onDidAnimateRef.current?.();
622
+ }
623
+ });
342
624
  const exitCycleIdRef = useRef(0);
343
625
  const pendingExitKeysRef = useRef(/* @__PURE__ */new Set());
344
626
  const exitCompletedRef = useRef(false);
@@ -360,6 +642,9 @@ function createAnimations(animationsConfig) {
360
642
  exitCycleIdRef.current++;
361
643
  exitCompletedRef.current = false;
362
644
  pendingExitKeysRef.current.clear();
645
+ didAnimateCycleIdRef.current++;
646
+ pendingDidAnimateKeysRef.current.clear();
647
+ didAnimateCompletedRef.current = true;
363
648
  }
364
649
  if (justStoppedExiting) {
365
650
  exitCycleIdRef.current++;
@@ -368,44 +653,85 @@ function createAnimations(animationsConfig) {
368
653
  useIsomorphicLayoutEffect(() => {
369
654
  isExitingRef.value = isExiting;
370
655
  exitCycleIdShared.value = exitCycleIdRef.current;
371
- }, [isExiting, exitCycleIdRef.current]);
656
+ if (justStartedExiting) {
657
+ isCompletingAnimationRef.value = false;
658
+ didAnimateCycleIdShared.value = didAnimateCycleIdRef.current;
659
+ }
660
+ }, [isExiting, exitCycleIdRef.current, justStartedExiting]);
372
661
  React.useEffect(() => {
373
662
  wasExitingRef.current = isExiting;
374
663
  });
375
- const animatedTargetsRef = useSharedValue(null);
376
- const staticTargetsRef = useSharedValue(null);
377
- const transformTargetsRef = useSharedValue(null);
664
+ const committedRenderKeysRef = useRef(/* @__PURE__ */new Set());
665
+ const lastPaintedRef = useRef({});
666
+ const carryRef = useRef({});
378
667
  const {
379
668
  animatedStyles,
380
- staticStyles
669
+ staticStyles,
670
+ nextCarry
381
671
  } = useMemo(() => {
382
- const animated = {};
383
- const staticStyles2 = {};
384
672
  const animateOnly = props.animateOnly;
385
- for (const key in style) {
386
- const rawValue = style[key];
387
- const value = resolveDynamicValue(rawValue, isDark);
388
- if (value === void 0) continue;
389
- if (disableAnimation) {
390
- staticStyles2[key] = value;
391
- continue;
392
- }
393
- if (canAnimateProperty(key, value, animateOnly)) {
394
- animated[key] = value;
395
- } else {
396
- staticStyles2[key] = value;
397
- }
673
+ const {
674
+ animated,
675
+ statics
676
+ } = splitAnimationStyles(style, isDark, disableAnimation, animateOnly);
677
+ const nextCarry2 = cloneStyleRecord(carryRef.current);
678
+ for (const key in nextCarry2) {
679
+ if (!(key in animated)) delete nextCarry2[key];
398
680
  }
399
- if (isMounting) {
400
- for (const key in animated) {
401
- staticStyles2[key] = animated[key];
681
+ for (const key in animated) {
682
+ if (!(key in nextCarry2)) {
683
+ nextCarry2[key] = cloneAnimationValue(animated[key]);
402
684
  }
685
+ statics[key] = nextCarry2[key];
403
686
  }
404
687
  return {
405
688
  animatedStyles: animated,
406
- staticStyles: staticStyles2
689
+ staticStyles: statics,
690
+ nextCarry: nextCarry2
407
691
  };
408
- }, [disableAnimation, style, isDark, isMounting, props.animateOnly]);
692
+ }, [disableAnimation, style, isDark, props.animateOnly]);
693
+ const renderSnapshot = useMemo(() => buildSnapshot(animatedStyles, staticStyles, getAnimatedTransforms(animatedStyles.transform), committedRenderKeysRef.current, lastPaintedRef.current), [animatedStyles, staticStyles]);
694
+ const renderSnapshotRef = useSharedValue({
695
+ animated: {},
696
+ statics: {},
697
+ transforms: [],
698
+ gatedKeys: {},
699
+ seeds: {},
700
+ removedKeys: {},
701
+ removeTransform: false,
702
+ clearValue: isWeb ? "" : null
703
+ });
704
+ const emitterSnapshotRef = useSharedValue(null);
705
+ const emitterKeysRef = useRef(null);
706
+ useIsomorphicLayoutEffect(() => {
707
+ carryRef.current = nextCarry;
708
+ committedRenderKeysRef.current = renderSnapshot.keys;
709
+ lastPaintedRef.current = renderSnapshot.painted;
710
+ }, [nextCarry, renderSnapshot]);
711
+ const pseudoActiveRef = useRef(false);
712
+ const isExitingJSRef = useRef(false);
713
+ isExitingJSRef.current = isExiting;
714
+ const publishedSnapshotRef = useRef(null);
715
+ useIsomorphicLayoutEffect(() => {
716
+ const droppingLatch = (isExiting || !pseudoActiveRef.current) && emitterSnapshotRef.value !== null;
717
+ const emitterKeys = droppingLatch ? emitterKeysRef.current : null;
718
+ if (droppingLatch || publishedSnapshotRef.current !== renderSnapshot.value) {
719
+ const removedKeys = emitterKeys ? {
720
+ ...renderSnapshot.value.removedKeys,
721
+ ...getRemovedAnimatedKeys(renderSnapshot.keys, emitterKeys)
722
+ } : renderSnapshot.value.removedKeys;
723
+ renderSnapshotRef.value = {
724
+ ...renderSnapshot.value,
725
+ removedKeys,
726
+ removeTransform: Object.keys(removedKeys).some(key => key.startsWith("transform:"))
727
+ };
728
+ publishedSnapshotRef.current = renderSnapshot.value;
729
+ }
730
+ if (droppingLatch) {
731
+ emitterSnapshotRef.value = null;
732
+ emitterKeysRef.current = null;
733
+ }
734
+ });
409
735
  const {
410
736
  baseConfig,
411
737
  propertyConfigs
@@ -419,8 +745,8 @@ function createAnimations(animationsConfig) {
419
745
  propertyConfigs: {}
420
746
  };
421
747
  }
422
- return buildTransitionConfig(props.transition, animations, animationState, getStyleKeys(animatedStyles));
423
- }, [isHydrating, props.transition, animatedStyles, animationState]);
748
+ return buildTransitionConfig(effectiveTransition, animations, animationState, getStyleKeys(animatedStyles));
749
+ }, [isHydrating, effectiveTransition, animatedStyles, animationState]);
424
750
  const configRef = useSharedValue({
425
751
  baseConfig,
426
752
  propertyConfigs,
@@ -428,92 +754,74 @@ function createAnimations(animationsConfig) {
428
754
  isHydrating
429
755
  });
430
756
  useIsomorphicLayoutEffect(() => {
431
- configRef.set({
757
+ configRef.value = {
432
758
  baseConfig,
433
759
  propertyConfigs,
434
760
  disableAnimation,
435
761
  isHydrating
436
- });
762
+ };
437
763
  }, [baseConfig, propertyConfigs, disableAnimation, isHydrating]);
438
- useStyleEmitter?.((nextStyle, effectiveTransition2) => {
764
+ useStyleEmitter?.((nextStyle, effectiveTransition2, pseudoActive) => {
765
+ if (isExitingJSRef.current) return;
766
+ pseudoActiveRef.current = pseudoActive === true;
439
767
  const animateOnly = props.animateOnly;
440
- const animated = {};
441
- const statics = {};
442
- const transforms = [];
443
768
  const transitionToUse = effectiveTransition2 ?? props.transition;
444
769
  const {
445
770
  baseConfig: newBase,
446
771
  propertyConfigs: newPropertyConfigs
447
772
  } = buildTransitionConfig(transitionToUse, animations, animationState, getStyleKeys(nextStyle));
448
- configRef.set({
773
+ configRef.value = {
449
774
  baseConfig: newBase,
450
775
  propertyConfigs: newPropertyConfigs,
451
- disableAnimation: configRef.get().disableAnimation,
452
- isHydrating: configRef.get().isHydrating
453
- });
454
- for (const key in nextStyle) {
455
- const rawValue = nextStyle[key];
456
- const value = resolveDynamicValue(rawValue, isDark);
457
- if (value == void 0) continue;
458
- if (configRef.get().disableAnimation) {
459
- statics[key] = value;
460
- continue;
461
- }
462
- if (key === "transform" && Array.isArray(value)) {
463
- for (const t of value) {
464
- if (t && typeof t === "object") {
465
- const tKey = Object.keys(t)[0];
466
- const tVal = t[tKey];
467
- if (typeof tVal === "number" || typeof tVal === "string") {
468
- transforms.push(t);
469
- }
470
- }
471
- }
472
- continue;
473
- }
474
- if (canAnimateProperty(key, value, animateOnly)) {
475
- animated[key] = value;
476
- } else {
477
- statics[key] = value;
478
- }
479
- }
480
- animatedTargetsRef.set(animated);
481
- staticTargetsRef.set(statics);
482
- transformTargetsRef.set(transforms);
776
+ disableAnimation: configRef.value.disableAnimation,
777
+ isHydrating: configRef.value.isHydrating
778
+ };
779
+ const previousKeys = emitterKeysRef.current ?? committedRenderKeysRef.current;
780
+ const {
781
+ animated,
782
+ statics
783
+ } = splitAnimationStyles(nextStyle, isDark, configRef.value.disableAnimation, animateOnly);
784
+ const snapshot = buildSnapshot(animated, statics, getAnimatedTransforms(animated.transform), previousKeys, lastPaintedRef.current);
785
+ emitterSnapshotRef.value = snapshot.value;
786
+ emitterKeysRef.current = snapshot.keys;
787
+ lastPaintedRef.current = snapshot.painted;
483
788
  if (process.env.NODE_ENV === "development" && props.debug && props.debug !== "profile") {
484
789
  console.info("[animations-reanimated] useStyleEmitter update", {
485
790
  animated,
486
791
  statics,
487
- transforms
792
+ transforms: snapshot.value.transforms
488
793
  });
489
794
  }
490
795
  });
491
796
  const exitKeysRegistered = useRef(false);
492
797
  if (justStartedExiting && sendExitComplete) {
493
- const exitKeys = [];
494
- const animateOnly = props.animateOnly;
495
- for (const key in animatedStyles) {
496
- if (key === "transform") continue;
497
- if (canAnimateProperty(key, animatedStyles[key], animateOnly)) {
498
- exitKeys.push(key);
499
- }
500
- }
501
- const transforms = animatedStyles.transform;
502
- if (transforms && Array.isArray(transforms)) {
503
- for (const t of transforms) {
504
- if (!t) continue;
505
- const tKey = Object.keys(t)[0];
506
- if (tKey) {
507
- if (animateOnly && !animateOnly.includes(tKey)) {
508
- continue;
509
- }
510
- exitKeys.push(`transform:${tKey}`);
511
- }
512
- }
513
- }
798
+ const exitKeys = getGatedKeys(renderSnapshot.value);
514
799
  pendingExitKeysRef.current = new Set(exitKeys);
515
800
  exitKeysRegistered.current = exitKeys.length > 0;
516
801
  }
802
+ const previousDidAnimateStyleRef = useRef(null);
803
+ const didAnimateStyle = onDidAnimate ? JSON.stringify(style) : null;
804
+ const previousDidAnimateStyle = previousDidAnimateStyleRef.current;
805
+ const shouldRegisterDidAnimate = didAnimateStyle !== null && previousDidAnimateStyle !== null && previousDidAnimateStyle !== didAnimateStyle && !isExiting && !isEntering && !justFinishedEntering;
806
+ const didAnimateKeys = shouldRegisterDidAnimate ? getGatedKeys(renderSnapshot.value) : [];
807
+ useIsomorphicLayoutEffect(() => {
808
+ if (didAnimateStyle === null) {
809
+ previousDidAnimateStyleRef.current = null;
810
+ isCompletingAnimationRef.value = false;
811
+ return;
812
+ }
813
+ const previousStyleStillCurrent = previousDidAnimateStyleRef.current === previousDidAnimateStyle;
814
+ previousDidAnimateStyleRef.current = didAnimateStyle;
815
+ if (!shouldRegisterDidAnimate || !previousStyleStillCurrent) return;
816
+ const cycleId = ++didAnimateCycleIdRef.current;
817
+ pendingDidAnimateKeysRef.current = new Set(didAnimateKeys);
818
+ didAnimateCompletedRef.current = didAnimateKeys.length === 0;
819
+ isCompletingAnimationRef.value = didAnimateKeys.length > 0;
820
+ didAnimateCycleIdShared.value = cycleId;
821
+ if (didAnimateKeys.length === 0) {
822
+ onDidAnimateRef.current?.();
823
+ }
824
+ }, [didAnimateStyle, shouldRegisterDidAnimate]);
517
825
  React.useEffect(() => {
518
826
  if (!justStartedExiting || !sendExitComplete) return;
519
827
  if (!exitKeysRegistered.current && pendingExitKeysRef.current.size === 0) {
@@ -523,76 +831,77 @@ function createAnimations(animationsConfig) {
523
831
  }
524
832
  }
525
833
  }, [justStartedExiting, sendExitComplete]);
834
+ const mapperStateRef = useSharedValue({
835
+ emitted: {}
836
+ });
526
837
  const animatedStyle = useAnimatedStyle(() => {
527
838
  "worklet";
528
839
 
529
- if (disableAnimation || isHydrating) {
840
+ const mapperState = mapperStateRef.value;
841
+ const config = configRef.value;
842
+ if (config.disableAnimation || config.isHydrating) {
843
+ updateMapperState(mapperStateRef, {});
530
844
  return {};
531
845
  }
846
+ const previouslyEmitted = mapperState.emitted;
847
+ const emitted = {};
532
848
  const result = {};
533
- const config = configRef.get();
534
- const emitterAnimated = animatedTargetsRef.value;
535
- const emitterStatic = staticTargetsRef.value;
536
- const emitterTransforms = transformTargetsRef.value;
537
- const hasEmitterUpdates = emitterAnimated !== null;
538
- const animatedValues = hasEmitterUpdates ? emitterAnimated : animatedStyles;
539
- const staticValues = hasEmitterUpdates ? emitterStatic : {};
849
+ const emitterSnapshot = emitterSnapshotRef.value;
850
+ const snapshot = emitterSnapshot ?? renderSnapshotRef.value;
851
+ const animatedValues = snapshot.animated;
852
+ const staticValues = snapshot.statics;
540
853
  const currentlyExiting = isExitingRef.value;
541
854
  const currentCycleId = exitCycleIdShared.value;
855
+ const currentlyCompletingAnimation = isCompletingAnimationRef.value;
856
+ const currentDidAnimateCycleId = didAnimateCycleIdShared.value;
542
857
  for (const key in staticValues) {
543
858
  result[key] = staticValues[key];
544
859
  }
860
+ for (const key in snapshot.removedKeys) {
861
+ if (!key.startsWith("transform:") && !(key in staticValues)) {
862
+ result[key] = snapshot.clearValue;
863
+ }
864
+ }
545
865
  for (const key in animatedValues) {
546
866
  if (key === "transform") continue;
547
867
  const targetValue = animatedValues[key];
548
- const propConfig = config.propertyConfigs[key] ?? config.baseConfig;
549
- let callback;
550
- if (currentlyExiting) {
551
- const capturedKey = key;
552
- const capturedCycleId = currentCycleId;
553
- callback = finished => {
554
- "worklet";
555
-
556
- runOnJS(markExitKeyDone)(capturedKey, capturedCycleId, finished ?? false);
557
- };
868
+ emitted[key] = true;
869
+ if (typeof targetValue !== "number" && typeof targetValue !== "string") {
870
+ result[key] = targetValue;
871
+ continue;
558
872
  }
559
- result[key] = applyAnimation(targetValue, propConfig, callback);
873
+ 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]);
560
874
  }
561
- const transforms = hasEmitterUpdates ? emitterTransforms : animatedStyles.transform;
562
- if (transforms && Array.isArray(transforms)) {
875
+ const transforms = snapshot.transforms;
876
+ if (transforms.length > 0 || snapshot.removeTransform) {
563
877
  const validTransforms = [];
564
878
  for (const t of transforms) {
565
879
  if (!t) continue;
566
880
  const keys = Object.keys(t);
567
881
  if (keys.length === 0) continue;
568
- const value = t[keys[0]];
882
+ const transformKey = keys[0];
883
+ const value = t[transformKey];
569
884
  if (typeof value === "number" || typeof value === "string") {
570
- const transformKey = Object.keys(t)[0];
571
- const targetValue = t[transformKey];
572
885
  const propConfig = config.propertyConfigs[transformKey] ?? config.baseConfig;
573
- let callback;
574
- if (currentlyExiting) {
575
- const capturedKey = `transform:${transformKey}`;
576
- const capturedCycleId = currentCycleId;
577
- callback = finished => {
578
- "worklet";
579
-
580
- runOnJS(markExitKeyDone)(capturedKey, capturedCycleId, finished ?? false);
581
- };
582
- }
886
+ const subKey = `transform:${transformKey}`;
887
+ emitted[subKey] = true;
583
888
  validTransforms.push({
584
- [transformKey]: applyAnimation(targetValue, propConfig, callback)
889
+ [transformKey]: animateSnapshotValue(subKey, transformKey, value, propConfig, !!previouslyEmitted[subKey], snapshot.seeds[subKey], !!snapshot.gatedKeys[subKey], currentlyExiting, currentCycleId, currentlyCompletingAnimation, currentDidAnimateCycleId, markExitKeyDone, markDidAnimateKeyDone)
585
890
  });
891
+ } else {
892
+ validTransforms.push(t);
586
893
  }
587
894
  }
588
- if (validTransforms.length > 0) {
895
+ if (validTransforms.length > 0 || !("transform" in staticValues)) {
589
896
  result.transform = validTransforms;
590
897
  }
591
898
  }
899
+ updateMapperState(mapperStateRef, emitted);
592
900
  return result;
593
- }, isWeb ? [animatedStyles, baseConfig, propertyConfigs, disableAnimation, isHydrating,
594
- // pass SharedValues so the mapper watches them on web (no babel plugin)
595
- animatedTargetsRef, staticTargetsRef, transformTargetsRef, isExitingRef, exitCycleIdShared, markExitKeyDone] : void 0);
901
+ }, isWeb ? [
902
+ // pass SharedValues so the stable mapper watches them on web (no babel plugin)
903
+ renderSnapshotRef, emitterSnapshotRef, configRef, isExitingRef, exitCycleIdShared, markExitKeyDone, isCompletingAnimationRef, didAnimateCycleIdShared, markDidAnimateKeyDone] : void 0);
904
+ silenceAnimatedComponentDevCheck(animatedStyle);
596
905
  if (process.env.NODE_ENV === "development" && props.debug && props.debug !== "profile") {
597
906
  console.info("[animations-reanimated] useAnimations", {
598
907
  animationKey,