UNPKG

7.42 kBPlain TextView Raw
1'use strict';
2import { defineAnimation, getReduceMotionForAnimation } from './util';
3import type {
4 Animation,
5 AnimationCallback,
6 AnimatableValue,
7 Timestamp,
8} from '../commonTypes';
9import type {
10 SpringConfig,
11 SpringAnimation,
12 InnerSpringAnimation,
13 SpringConfigInner,
14 DefaultSpringConfig,
15} from './springUtils';
16import {
17 initialCalculations,
18 calculateNewMassToMatchDuration,
19 underDampedSpringCalculations,
20 criticallyDampedSpringCalculations,
21 isAnimationTerminatingCalculation,
22 scaleZetaToMatchClamps,
23 checkIfConfigIsValid,
24} from './springUtils';
25
26// TODO TYPESCRIPT This is a temporary type to get rid of .d.ts file.
27type withSpringType = <T extends AnimatableValue>(
28 toValue: T,
29 userConfig?: SpringConfig,
30 callback?: AnimationCallback
31) => T;
32
33/**
34 * Lets you create spring-based animations.
35 *
36 * @param toValue - the value at which the animation will come to rest - {@link AnimatableValue}
37 * @param config - the spring animation configuration - {@link SpringConfig}
38 * @param callback - a function called on animation complete - {@link AnimationCallback}
39 * @returns an [animation object](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#animation-object) which holds the current state of the animation
40 * @see https://docs.swmansion.com/react-native-reanimated/docs/animations/withSpring
41 */
42export const withSpring = ((
43 toValue: AnimatableValue,
44 userConfig?: SpringConfig,
45 callback?: AnimationCallback
46): Animation<SpringAnimation> => {
47 'worklet';
48
49 return defineAnimation<SpringAnimation>(toValue, () => {
50 'worklet';
51 const defaultConfig: DefaultSpringConfig = {
52 damping: 10,
53 mass: 1,
54 stiffness: 100,
55 overshootClamping: false,
56 restDisplacementThreshold: 0.01,
57 restSpeedThreshold: 2,
58 velocity: 0,
59 duration: 2000,
60 dampingRatio: 0.5,
61 reduceMotion: undefined,
62 clamp: undefined,
63 } as const;
64
65 const config: DefaultSpringConfig & SpringConfigInner = {
66 ...defaultConfig,
67 ...userConfig,
68 useDuration: !!(userConfig?.duration || userConfig?.dampingRatio),
69 skipAnimation: false,
70 };
71
72 config.skipAnimation = !checkIfConfigIsValid(config);
73
74 if (config.duration === 0) {
75 config.skipAnimation = true;
76 }
77
78 function springOnFrame(
79 animation: InnerSpringAnimation,
80 now: Timestamp
81 ): boolean {
82 // eslint-disable-next-line @typescript-eslint/no-shadow
83 const { toValue, startTimestamp, current } = animation;
84
85 const timeFromStart = now - startTimestamp;
86
87 if (config.useDuration && timeFromStart >= config.duration) {
88 animation.current = toValue;
89 // clear lastTimestamp to avoid using stale value by the next spring animation that starts after this one
90 animation.lastTimestamp = 0;
91 return true;
92 }
93
94 if (config.skipAnimation) {
95 animation.current = toValue;
96 animation.lastTimestamp = 0;
97 return true;
98 }
99 const { lastTimestamp, velocity } = animation;
100
101 const deltaTime = Math.min(now - lastTimestamp, 64);
102 animation.lastTimestamp = now;
103
104 const t = deltaTime / 1000;
105 const v0 = -velocity;
106 const x0 = toValue - current;
107
108 const { zeta, omega0, omega1 } = animation;
109
110 const { position: newPosition, velocity: newVelocity } =
111 zeta < 1
112 ? underDampedSpringCalculations(animation, {
113 zeta,
114 v0,
115 x0,
116 omega0,
117 omega1,
118 t,
119 })
120 : criticallyDampedSpringCalculations(animation, {
121 v0,
122 x0,
123 omega0,
124 t,
125 });
126
127 animation.current = newPosition;
128 animation.velocity = newVelocity;
129
130 const { isOvershooting, isVelocity, isDisplacement } =
131 isAnimationTerminatingCalculation(animation, config);
132
133 const springIsNotInMove =
134 isOvershooting || (isVelocity && isDisplacement);
135
136 if (!config.useDuration && springIsNotInMove) {
137 animation.velocity = 0;
138 animation.current = toValue;
139 // clear lastTimestamp to avoid using stale value by the next spring animation that starts after this one
140 animation.lastTimestamp = 0;
141 return true;
142 }
143
144 return false;
145 }
146
147 function isTriggeredTwice(
148 previousAnimation: SpringAnimation | undefined,
149 animation: SpringAnimation
150 ) {
151 return (
152 previousAnimation?.lastTimestamp &&
153 previousAnimation?.startTimestamp &&
154 previousAnimation?.toValue === animation.toValue &&
155 previousAnimation?.duration === animation.duration &&
156 previousAnimation?.dampingRatio === animation.dampingRatio
157 );
158 }
159
160 function onStart(
161 animation: SpringAnimation,
162 value: number,
163 now: Timestamp,
164 previousAnimation: SpringAnimation | undefined
165 ): void {
166 animation.current = value;
167 animation.startValue = value;
168
169 let mass = config.mass;
170 const triggeredTwice = isTriggeredTwice(previousAnimation, animation);
171
172 const duration = config.duration;
173
174 const x0 = triggeredTwice
175 ? // If animation is triggered twice we want to continue the previous animation
176 // form the previous starting point
177 previousAnimation?.startValue
178 : Number(animation.toValue) - value;
179
180 if (previousAnimation) {
181 animation.velocity =
182 (triggeredTwice
183 ? previousAnimation?.velocity
184 : previousAnimation?.velocity + config.velocity) || 0;
185 } else {
186 animation.velocity = config.velocity || 0;
187 }
188
189 if (triggeredTwice) {
190 animation.zeta = previousAnimation?.zeta || 0;
191 animation.omega0 = previousAnimation?.omega0 || 0;
192 animation.omega1 = previousAnimation?.omega1 || 0;
193 } else {
194 if (config.useDuration) {
195 const actualDuration = triggeredTwice
196 ? // If animation is triggered twice we want to continue the previous animation
197 // so we need to include the time that already elapsed
198 duration -
199 ((previousAnimation?.lastTimestamp || 0) -
200 (previousAnimation?.startTimestamp || 0))
201 : duration;
202
203 config.duration = actualDuration;
204 mass = calculateNewMassToMatchDuration(
205 x0 as number,
206 config,
207 animation.velocity
208 );
209 }
210
211 const { zeta, omega0, omega1 } = initialCalculations(mass, config);
212 animation.zeta = zeta;
213 animation.omega0 = omega0;
214 animation.omega1 = omega1;
215
216 if (config.clamp !== undefined) {
217 animation.zeta = scaleZetaToMatchClamps(animation, config.clamp);
218 }
219 }
220
221 animation.lastTimestamp = previousAnimation?.lastTimestamp || now;
222
223 animation.startTimestamp = triggeredTwice
224 ? previousAnimation?.startTimestamp || now
225 : now;
226 }
227
228 return {
229 onFrame: springOnFrame,
230 onStart,
231 toValue,
232 velocity: config.velocity || 0,
233 current: toValue,
234 startValue: 0,
235 callback,
236 lastTimestamp: 0,
237 startTimestamp: 0,
238 zeta: 0,
239 omega0: 0,
240 omega1: 0,
241 reduceMotion: getReduceMotionForAnimation(config.reduceMotion),
242 } as SpringAnimation;
243 });
244}) as withSpringType;
245
\No newline at end of file