UNPKG

17.1 kBPlain TextView Raw
1/* eslint-disable @typescript-eslint/no-shadow */
2'use strict';
3import type { HigherOrderAnimation, StyleLayoutAnimation } from './commonTypes';
4import type { ParsedColorArray } from '../Colors';
5import {
6 isColor,
7 convertToRGBA,
8 rgbaArrayToRGBAColor,
9 toGammaSpace,
10 toLinearSpace,
11} from '../Colors';
12import { ReduceMotion, isWorkletFunction } from '../commonTypes';
13import type {
14 SharedValue,
15 AnimatableValue,
16 Animation,
17 AnimationObject,
18 Timestamp,
19 AnimatableValueObject,
20} from '../commonTypes';
21import type {
22 AffineMatrixFlat,
23 AffineMatrix,
24} from './transformationMatrix/matrixUtils';
25import {
26 flatten,
27 multiplyMatrices,
28 scaleMatrix,
29 addMatrices,
30 decomposeMatrixIntoMatricesAndAngles,
31 isAffineMatrixFlat,
32 subtractMatrices,
33 getRotationMatrix,
34} from './transformationMatrix/matrixUtils';
35import { isReducedMotion, shouldBeUseWeb } from '../PlatformChecker';
36import type { EasingFunction, EasingFunctionFactory } from '../Easing';
37
38let IN_STYLE_UPDATER = false;
39const IS_REDUCED_MOTION = isReducedMotion();
40const SHOULD_BE_USE_WEB = shouldBeUseWeb();
41
42if (__DEV__ && IS_REDUCED_MOTION) {
43 console.warn(
44 `[Reanimated] Reduced motion setting is enabled on this device. This warning is visible only in the development mode. Some animations will be disabled by default. You can override the behavior for individual animations, see https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#reduced-motion-setting-is-enabled-on-this-device.`
45 );
46}
47
48export function assertEasingIsWorklet(
49 easing: EasingFunction | EasingFunctionFactory
50): void {
51 'worklet';
52 if (_WORKLET) {
53 // If this is called on UI (for example from gesture handler with worklets), we don't get easing,
54 // but its bound copy, which is not a worklet. We don't want to throw any error then.
55 return;
56 }
57 if (SHOULD_BE_USE_WEB) {
58 // It is possible to run reanimated on web without plugin, so let's skip this check on web
59 return;
60 }
61 // @ts-ignore typescript wants us to use `in` instead, which doesn't work with host objects
62 if (easing?.factory) {
63 return;
64 }
65
66 if (!isWorkletFunction(easing)) {
67 throw new Error(
68 '[Reanimated] The easing function is not a worklet. Please make sure you import `Easing` from react-native-reanimated.'
69 );
70 }
71}
72
73export function initialUpdaterRun<T>(updater: () => T) {
74 IN_STYLE_UPDATER = true;
75 const result = updater();
76 IN_STYLE_UPDATER = false;
77 return result;
78}
79
80interface RecognizedPrefixSuffix {
81 prefix?: string;
82 suffix?: string;
83 strippedValue: number;
84}
85
86export function recognizePrefixSuffix(
87 value: string | number
88): RecognizedPrefixSuffix {
89 'worklet';
90 if (typeof value === 'string') {
91 const match = value.match(
92 /([A-Za-z]*)(-?\d*\.?\d*)([eE][-+]?[0-9]+)?([A-Za-z%]*)/
93 );
94 if (!match) {
95 throw new Error("[Reanimated] Couldn't parse animation value.");
96 }
97 const prefix = match[1];
98 const suffix = match[4];
99 // number with scientific notation
100 const number = match[2] + (match[3] ?? '');
101 return { prefix, suffix, strippedValue: parseFloat(number) };
102 } else {
103 return { strippedValue: value };
104 }
105}
106
107/**
108 * Returns whether the motion should be reduced for a specified config.
109 * By default returns the system setting.
110 */
111export function getReduceMotionFromConfig(config?: ReduceMotion) {
112 'worklet';
113 return !config || config === ReduceMotion.System
114 ? IS_REDUCED_MOTION
115 : config === ReduceMotion.Always;
116}
117
118/**
119 * Returns the value that should be assigned to `animation.reduceMotion`
120 * for a given config. If the config is not defined, `undefined` is returned.
121 */
122export function getReduceMotionForAnimation(config?: ReduceMotion) {
123 'worklet';
124 // if the config is not defined, we want `reduceMotion` to be undefined,
125 // so the parent animation knows if it should overwrite it
126 if (!config) {
127 return undefined;
128 }
129
130 return getReduceMotionFromConfig(config);
131}
132
133function applyProgressToMatrix(
134 progress: number,
135 a: AffineMatrix,
136 b: AffineMatrix
137) {
138 'worklet';
139 return addMatrices(a, scaleMatrix(subtractMatrices(b, a), progress));
140}
141
142function applyProgressToNumber(progress: number, a: number, b: number) {
143 'worklet';
144 return a + progress * (b - a);
145}
146
147function decorateAnimation<T extends AnimationObject | StyleLayoutAnimation>(
148 animation: T
149): void {
150 'worklet';
151 const baseOnStart = (animation as Animation<AnimationObject>).onStart;
152 const baseOnFrame = (animation as Animation<AnimationObject>).onFrame;
153
154 if ((animation as HigherOrderAnimation).isHigherOrder) {
155 animation.onStart = (
156 animation: Animation<AnimationObject>,
157 value: number,
158 timestamp: Timestamp,
159 previousAnimation: Animation<AnimationObject>
160 ) => {
161 if (animation.reduceMotion === undefined) {
162 animation.reduceMotion = getReduceMotionFromConfig();
163 }
164 return baseOnStart(animation, value, timestamp, previousAnimation);
165 };
166 return;
167 }
168
169 const animationCopy = Object.assign({}, animation);
170 delete animationCopy.callback;
171
172 const prefNumberSuffOnStart = (
173 animation: Animation<AnimationObject>,
174 value: string | number,
175 timestamp: number,
176 previousAnimation: Animation<AnimationObject>
177 ) => {
178 // recognize prefix, suffix, and updates stripped value on animation start
179 const { prefix, suffix, strippedValue } = recognizePrefixSuffix(value);
180 animation.__prefix = prefix;
181 animation.__suffix = suffix;
182 animation.strippedCurrent = strippedValue;
183 const { strippedValue: strippedToValue } = recognizePrefixSuffix(
184 animation.toValue as string | number
185 );
186 animation.current = strippedValue;
187 animation.startValue = strippedValue;
188 animation.toValue = strippedToValue;
189 if (previousAnimation && previousAnimation !== animation) {
190 const {
191 prefix: paPrefix,
192 suffix: paSuffix,
193 strippedValue: paStrippedValue,
194 } = recognizePrefixSuffix(previousAnimation.current as string | number);
195 previousAnimation.current = paStrippedValue;
196 previousAnimation.__prefix = paPrefix;
197 previousAnimation.__suffix = paSuffix;
198 }
199
200 baseOnStart(animation, strippedValue, timestamp, previousAnimation);
201
202 animation.current =
203 (animation.__prefix ?? '') +
204 animation.current +
205 (animation.__suffix ?? '');
206
207 if (previousAnimation && previousAnimation !== animation) {
208 previousAnimation.current =
209 (previousAnimation.__prefix ?? '') +
210 // FIXME
211 // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
212 previousAnimation.current +
213 (previousAnimation.__suffix ?? '');
214 }
215 };
216 const prefNumberSuffOnFrame = (
217 animation: Animation<AnimationObject>,
218 timestamp: number
219 ) => {
220 animation.current = animation.strippedCurrent;
221 const res = baseOnFrame(animation, timestamp);
222 animation.strippedCurrent = animation.current;
223 animation.current =
224 (animation.__prefix ?? '') +
225 animation.current +
226 (animation.__suffix ?? '');
227 return res;
228 };
229
230 const tab = ['R', 'G', 'B', 'A'];
231 const colorOnStart = (
232 animation: Animation<AnimationObject>,
233 value: string | number,
234 timestamp: Timestamp,
235 previousAnimation: Animation<AnimationObject>
236 ): void => {
237 let RGBAValue: ParsedColorArray;
238 let RGBACurrent: ParsedColorArray;
239 let RGBAToValue: ParsedColorArray;
240 const res: Array<number> = [];
241 if (isColor(value)) {
242 RGBACurrent = toLinearSpace(convertToRGBA(animation.current));
243 RGBAValue = toLinearSpace(convertToRGBA(value));
244 if (animation.toValue) {
245 RGBAToValue = toLinearSpace(convertToRGBA(animation.toValue));
246 }
247 }
248 tab.forEach((i, index) => {
249 animation[i] = Object.assign({}, animationCopy);
250 animation[i].current = RGBACurrent[index];
251 animation[i].toValue = RGBAToValue ? RGBAToValue[index] : undefined;
252 animation[i].onStart(
253 animation[i],
254 RGBAValue[index],
255 timestamp,
256 previousAnimation ? previousAnimation[i] : undefined
257 );
258 res.push(animation[i].current);
259 });
260
261 animation.current = rgbaArrayToRGBAColor(
262 toGammaSpace(res as ParsedColorArray)
263 );
264 };
265
266 const colorOnFrame = (
267 animation: Animation<AnimationObject>,
268 timestamp: Timestamp
269 ): boolean => {
270 const RGBACurrent = toLinearSpace(convertToRGBA(animation.current));
271 const res: Array<number> = [];
272 let finished = true;
273 tab.forEach((i, index) => {
274 animation[i].current = RGBACurrent[index];
275 const result = animation[i].onFrame(animation[i], timestamp);
276 // We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
277 finished = finished && result;
278 res.push(animation[i].current);
279 });
280
281 animation.current = rgbaArrayToRGBAColor(
282 toGammaSpace(res as ParsedColorArray)
283 );
284 return finished;
285 };
286
287 const transformationMatrixOnStart = (
288 animation: Animation<AnimationObject>,
289 value: AffineMatrixFlat,
290 timestamp: Timestamp,
291 previousAnimation: Animation<AnimationObject>
292 ): void => {
293 const toValue = animation.toValue as AffineMatrixFlat;
294
295 animation.startMatrices = decomposeMatrixIntoMatricesAndAngles(value);
296 animation.stopMatrices = decomposeMatrixIntoMatricesAndAngles(toValue);
297
298 // We create an animation copy to animate single value between 0 and 100
299 // We set limits from 0 to 100 (instead of 0-1) to make spring look good
300 // with default thresholds.
301
302 animation[0] = Object.assign({}, animationCopy);
303 animation[0].current = 0;
304 animation[0].toValue = 100;
305 animation[0].onStart(
306 animation[0],
307 0,
308 timestamp,
309 previousAnimation ? previousAnimation[0] : undefined
310 );
311
312 animation.current = value;
313 };
314
315 const transformationMatrixOnFrame = (
316 animation: Animation<AnimationObject>,
317 timestamp: Timestamp
318 ): boolean => {
319 let finished = true;
320 const result = animation[0].onFrame(animation[0], timestamp);
321 // We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
322 finished = finished && result;
323
324 const progress = animation[0].current / 100;
325
326 const transforms = ['translationMatrix', 'scaleMatrix', 'skewMatrix'];
327 const mappedTransforms: Array<AffineMatrix> = [];
328
329 transforms.forEach((key, _) =>
330 mappedTransforms.push(
331 applyProgressToMatrix(
332 progress,
333 animation.startMatrices[key],
334 animation.stopMatrices[key]
335 )
336 )
337 );
338
339 const [currentTranslation, currentScale, skewMatrix] = mappedTransforms;
340
341 const rotations: Array<'x' | 'y' | 'z'> = ['x', 'y', 'z'];
342 const mappedRotations: Array<AffineMatrix> = [];
343
344 rotations.forEach((key, _) => {
345 const angle = applyProgressToNumber(
346 progress,
347 animation.startMatrices['r' + key],
348 animation.stopMatrices['r' + key]
349 );
350 mappedRotations.push(getRotationMatrix(angle, key));
351 });
352
353 const [rotationMatrixX, rotationMatrixY, rotationMatrixZ] = mappedRotations;
354
355 const rotationMatrix = multiplyMatrices(
356 rotationMatrixX,
357 multiplyMatrices(rotationMatrixY, rotationMatrixZ)
358 );
359
360 const updated = flatten(
361 multiplyMatrices(
362 multiplyMatrices(
363 currentScale,
364 multiplyMatrices(skewMatrix, rotationMatrix)
365 ),
366 currentTranslation
367 )
368 );
369
370 animation.current = updated;
371
372 return finished;
373 };
374
375 const arrayOnStart = (
376 animation: Animation<AnimationObject>,
377 value: Array<number>,
378 timestamp: Timestamp,
379 previousAnimation: Animation<AnimationObject>
380 ): void => {
381 value.forEach((v, i) => {
382 animation[i] = Object.assign({}, animationCopy);
383 animation[i].current = v;
384 animation[i].toValue = (animation.toValue as Array<number>)[i];
385 animation[i].onStart(
386 animation[i],
387 v,
388 timestamp,
389 previousAnimation ? previousAnimation[i] : undefined
390 );
391 });
392
393 animation.current = value;
394 };
395
396 const arrayOnFrame = (
397 animation: Animation<AnimationObject>,
398 timestamp: Timestamp
399 ): boolean => {
400 let finished = true;
401 (animation.current as Array<number>).forEach((_, i) => {
402 const result = animation[i].onFrame(animation[i], timestamp);
403 // We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
404 finished = finished && result;
405 (animation.current as Array<number>)[i] = animation[i].current;
406 });
407
408 return finished;
409 };
410
411 const objectOnStart = (
412 animation: Animation<AnimationObject>,
413 value: AnimatableValueObject,
414 timestamp: Timestamp,
415 previousAnimation: Animation<AnimationObject>
416 ): void => {
417 for (const key in value) {
418 animation[key] = Object.assign({}, animationCopy);
419 animation[key].onStart = animation.onStart;
420
421 animation[key].current = value[key];
422 animation[key].toValue = (animation.toValue as AnimatableValueObject)[
423 key
424 ];
425 animation[key].onStart(
426 animation[key],
427 value[key],
428 timestamp,
429 previousAnimation ? previousAnimation[key] : undefined
430 );
431 }
432 animation.current = value;
433 };
434
435 const objectOnFrame = (
436 animation: Animation<AnimationObject>,
437 timestamp: Timestamp
438 ): boolean => {
439 let finished = true;
440 const newObject: AnimatableValueObject = {};
441 for (const key in animation.current as AnimatableValueObject) {
442 const result = animation[key].onFrame(animation[key], timestamp);
443 // We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
444 finished = finished && result;
445 newObject[key] = animation[key].current;
446 }
447 animation.current = newObject;
448 return finished;
449 };
450
451 animation.onStart = (
452 animation: Animation<AnimationObject>,
453 value: number,
454 timestamp: Timestamp,
455 previousAnimation: Animation<AnimationObject>
456 ) => {
457 if (animation.reduceMotion === undefined) {
458 animation.reduceMotion = getReduceMotionFromConfig();
459 }
460 if (animation.reduceMotion) {
461 if (animation.toValue !== undefined) {
462 animation.current = animation.toValue;
463 } else {
464 // if there is no `toValue`, then the base function is responsible for setting the current value
465 baseOnStart(animation, value, timestamp, previousAnimation);
466 }
467 animation.startTime = 0;
468 animation.onFrame = () => true;
469 return;
470 }
471 if (isColor(value)) {
472 colorOnStart(animation, value, timestamp, previousAnimation);
473 animation.onFrame = colorOnFrame;
474 return;
475 } else if (isAffineMatrixFlat(value)) {
476 transformationMatrixOnStart(
477 animation,
478 value,
479 timestamp,
480 previousAnimation
481 );
482 animation.onFrame = transformationMatrixOnFrame;
483 return;
484 } else if (Array.isArray(value)) {
485 arrayOnStart(animation, value, timestamp, previousAnimation);
486 animation.onFrame = arrayOnFrame;
487 return;
488 } else if (typeof value === 'string') {
489 prefNumberSuffOnStart(animation, value, timestamp, previousAnimation);
490 animation.onFrame = prefNumberSuffOnFrame;
491 return;
492 } else if (typeof value === 'object' && value !== null) {
493 objectOnStart(animation, value, timestamp, previousAnimation);
494 animation.onFrame = objectOnFrame;
495 return;
496 }
497 baseOnStart(animation, value, timestamp, previousAnimation);
498 };
499}
500
501type AnimationToDecoration<
502 T extends AnimationObject | StyleLayoutAnimation,
503 U extends AnimationObject | StyleLayoutAnimation
504> = T extends StyleLayoutAnimation
505 ? Record<string, unknown>
506 : U | (() => U) | AnimatableValue;
507
508export function defineAnimation<
509 T extends AnimationObject | StyleLayoutAnimation, // type that's supposed to be returned
510 U extends AnimationObject | StyleLayoutAnimation = T // type that's received
511>(starting: AnimationToDecoration<T, U>, factory: () => T): T {
512 'worklet';
513 if (IN_STYLE_UPDATER) {
514 return starting as unknown as T;
515 }
516 const create = () => {
517 'worklet';
518 const animation = factory();
519 decorateAnimation<U>(animation as unknown as U);
520 return animation;
521 };
522
523 if (_WORKLET || SHOULD_BE_USE_WEB) {
524 return create();
525 }
526 // @ts-ignore: eslint-disable-line
527 return create;
528}
529
530/**
531 * Lets you cancel a running animation paired to a shared value.
532 *
533 * @param sharedValue - The shared value of a running animation that you want to cancel.
534 * @see https://docs.swmansion.com/react-native-reanimated/docs/core/cancelAnimation
535 */
536export function cancelAnimation<T>(sharedValue: SharedValue<T>): void {
537 'worklet';
538 // setting the current value cancels the animation if one is currently running
539 sharedValue.value = sharedValue.value; // eslint-disable-line no-self-assign
540}