react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
499 lines (448 loc) • 15.4 kB
text/typescript
import type { HigherOrderAnimation, StyleLayoutAnimation } from './commonTypes';
import type { ParsedColorArray } from '../Colors';
import {
isColor,
convertToRGBA,
rgbaArrayToRGBAColor,
toGammaSpace,
toLinearSpace,
} from '../Colors';
import { ReduceMotion } from '../commonTypes';
import type {
SharedValue,
AnimatableValue,
Animation,
AnimationObject,
Timestamp,
AnimatableValueObject,
} from '../commonTypes';
import NativeReanimatedModule from '../NativeReanimated';
import type {
AffineMatrixFlat,
AffineMatrix,
} from './transformationMatrix/matrixUtils';
import {
flatten,
multiplyMatrices,
scaleMatrix,
addMatrices,
decomposeMatrixIntoMatricesAndAngles,
isAffineMatrixFlat,
subtractMatrices,
getRotationMatrix,
} from './transformationMatrix/matrixUtils';
import { isReducedMotion } from '../PlatformChecker';
let IN_STYLE_UPDATER = false;
const IS_REDUCED_MOTION = isReducedMotion();
export function initialUpdaterRun<T>(updater: () => T) {
IN_STYLE_UPDATER = true;
const result = updater();
IN_STYLE_UPDATER = false;
return result;
}
interface RecognizedPrefixSuffix {
prefix?: string;
suffix?: string;
strippedValue: number;
}
function recognizePrefixSuffix(value: string | number): RecognizedPrefixSuffix {
'worklet';
if (typeof value === 'string') {
const match = value.match(
/([A-Za-z]*)(-?\d*\.?\d*)([eE][-+]?[0-9]+)?([A-Za-z%]*)/
);
if (!match) {
throw new Error("[Reanimated] Couldn't parse animation value.");
}
const prefix = match[1];
const suffix = match[4];
// number with scientific notation
const number = match[2] + (match[3] ?? '');
return { prefix, suffix, strippedValue: parseFloat(number) };
} else {
return { strippedValue: value };
}
}
/**
* Returns whether the motion should be reduced for a specified config.
* By default returns the system setting.
*/
export function getReduceMotionFromConfig(config?: ReduceMotion) {
'worklet';
return !config || config === ReduceMotion.System
? IS_REDUCED_MOTION
: config === ReduceMotion.Always;
}
/**
* Returns the value that should be assigned to `animation.reduceMotion`
* for a given config. If the config is not defined, `undefined` is returned.
*/
export function getReduceMotionForAnimation(config?: ReduceMotion) {
'worklet';
// if the config is not defined, we want `reduceMotion` to be undefined,
// so the parent animation knows if it should overwrite it
if (!config) {
return undefined;
}
return getReduceMotionFromConfig(config);
}
function applyProgressToMatrix(
progress: number,
a: AffineMatrix,
b: AffineMatrix
) {
'worklet';
return addMatrices(a, scaleMatrix(subtractMatrices(b, a), progress));
}
function applyProgressToNumber(progress: number, a: number, b: number) {
'worklet';
return a + progress * (b - a);
}
function decorateAnimation<T extends AnimationObject | StyleLayoutAnimation>(
animation: T
): void {
'worklet';
const baseOnStart = (animation as Animation<AnimationObject>).onStart;
const baseOnFrame = (animation as Animation<AnimationObject>).onFrame;
if ((animation as HigherOrderAnimation).isHigherOrder) {
animation.onStart = (
animation: Animation<AnimationObject>,
value: number,
timestamp: Timestamp,
previousAnimation: Animation<AnimationObject>
) => {
if (animation.reduceMotion === undefined) {
animation.reduceMotion = getReduceMotionFromConfig();
}
return baseOnStart(animation, value, timestamp, previousAnimation);
};
return;
}
const animationCopy = Object.assign({}, animation);
delete animationCopy.callback;
const prefNumberSuffOnStart = (
animation: Animation<AnimationObject>,
value: string | number,
timestamp: number,
previousAnimation: Animation<AnimationObject>
) => {
// recognize prefix, suffix, and updates stripped value on animation start
const { prefix, suffix, strippedValue } = recognizePrefixSuffix(value);
animation.__prefix = prefix;
animation.__suffix = suffix;
animation.strippedCurrent = strippedValue;
const { strippedValue: strippedToValue } = recognizePrefixSuffix(
animation.toValue as string | number
);
animation.current = strippedValue;
animation.startValue = strippedValue;
animation.toValue = strippedToValue;
if (previousAnimation && previousAnimation !== animation) {
const {
prefix: paPrefix,
suffix: paSuffix,
strippedValue: paStrippedValue,
} = recognizePrefixSuffix(previousAnimation.current as string | number);
previousAnimation.current = paStrippedValue;
previousAnimation.__prefix = paPrefix;
previousAnimation.__suffix = paSuffix;
}
baseOnStart(animation, strippedValue, timestamp, previousAnimation);
animation.current =
(animation.__prefix ?? '') +
animation.current +
(animation.__suffix ?? '');
if (previousAnimation && previousAnimation !== animation) {
previousAnimation.current =
(previousAnimation.__prefix ?? '') +
previousAnimation.current +
(previousAnimation.__suffix ?? '');
}
};
const prefNumberSuffOnFrame = (
animation: Animation<AnimationObject>,
timestamp: number
) => {
animation.current = animation.strippedCurrent;
const res = baseOnFrame(animation, timestamp);
animation.strippedCurrent = animation.current;
animation.current =
(animation.__prefix ?? '') +
animation.current +
(animation.__suffix ?? '');
return res;
};
const tab = ['R', 'G', 'B', 'A'];
const colorOnStart = (
animation: Animation<AnimationObject>,
value: string | number,
timestamp: Timestamp,
previousAnimation: Animation<AnimationObject>
): void => {
let RGBAValue: ParsedColorArray;
let RGBACurrent: ParsedColorArray;
let RGBAToValue: ParsedColorArray;
const res: Array<number> = [];
if (isColor(value)) {
RGBACurrent = toLinearSpace(convertToRGBA(animation.current));
RGBAValue = toLinearSpace(convertToRGBA(value));
if (animation.toValue) {
RGBAToValue = toLinearSpace(convertToRGBA(animation.toValue));
}
}
tab.forEach((i, index) => {
animation[i] = Object.assign({}, animationCopy);
animation[i].current = RGBACurrent[index];
animation[i].toValue = RGBAToValue ? RGBAToValue[index] : undefined;
animation[i].onStart(
animation[i],
RGBAValue[index],
timestamp,
previousAnimation ? previousAnimation[i] : undefined
);
res.push(animation[i].current);
});
animation.current = rgbaArrayToRGBAColor(
toGammaSpace(res as ParsedColorArray)
);
};
const colorOnFrame = (
animation: Animation<AnimationObject>,
timestamp: Timestamp
): boolean => {
const RGBACurrent = toLinearSpace(convertToRGBA(animation.current));
const res: Array<number> = [];
let finished = true;
tab.forEach((i, index) => {
animation[i].current = RGBACurrent[index];
const result = animation[i].onFrame(animation[i], timestamp);
// We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
finished = finished && result;
res.push(animation[i].current);
});
animation.current = rgbaArrayToRGBAColor(
toGammaSpace(res as ParsedColorArray)
);
return finished;
};
const transformationMatrixOnStart = (
animation: Animation<AnimationObject>,
value: AffineMatrixFlat,
timestamp: Timestamp,
previousAnimation: Animation<AnimationObject>
): void => {
const toValue = animation.toValue as AffineMatrixFlat;
animation.startMatrices = decomposeMatrixIntoMatricesAndAngles(value);
animation.stopMatrices = decomposeMatrixIntoMatricesAndAngles(toValue);
// We create an animation copy to animate single value between 0 and 100
// We set limits from 0 to 100 (instead of 0-1) to make spring look good
// with default thresholds.
animation[0] = Object.assign({}, animationCopy);
animation[0].current = 0;
animation[0].toValue = 100;
animation[0].onStart(
animation[0],
0,
timestamp,
previousAnimation ? previousAnimation[0] : undefined
);
animation.current = value;
};
const transformationMatrixOnFrame = (
animation: Animation<AnimationObject>,
timestamp: Timestamp
): boolean => {
let finished = true;
const result = animation[0].onFrame(animation[0], timestamp);
// We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
finished = finished && result;
const progress = animation[0].current / 100;
const transforms = ['translationMatrix', 'scaleMatrix', 'skewMatrix'];
const mappedTransforms: Array<AffineMatrix> = [];
transforms.forEach((key, _) =>
mappedTransforms.push(
applyProgressToMatrix(
progress,
animation.startMatrices[key],
animation.stopMatrices[key]
)
)
);
const [currentTranslation, currentScale, skewMatrix] = mappedTransforms;
const rotations: Array<'x' | 'y' | 'z'> = ['x', 'y', 'z'];
const mappedRotations: Array<AffineMatrix> = [];
rotations.forEach((key, _) => {
const angle = applyProgressToNumber(
progress,
animation.startMatrices['r' + key],
animation.stopMatrices['r' + key]
);
mappedRotations.push(getRotationMatrix(angle, key));
});
const [rotationMatrixX, rotationMatrixY, rotationMatrixZ] = mappedRotations;
const rotationMatrix = multiplyMatrices(
rotationMatrixX,
multiplyMatrices(rotationMatrixY, rotationMatrixZ)
);
const updated = flatten(
multiplyMatrices(
multiplyMatrices(
currentScale,
multiplyMatrices(skewMatrix, rotationMatrix)
),
currentTranslation
)
);
animation.current = updated;
return finished;
};
const arrayOnStart = (
animation: Animation<AnimationObject>,
value: Array<number>,
timestamp: Timestamp,
previousAnimation: Animation<AnimationObject>
): void => {
value.forEach((v, i) => {
animation[i] = Object.assign({}, animationCopy);
animation[i].current = v;
animation[i].toValue = (animation.toValue as Array<number>)[i];
animation[i].onStart(
animation[i],
v,
timestamp,
previousAnimation ? previousAnimation[i] : undefined
);
});
animation.current = value;
};
const arrayOnFrame = (
animation: Animation<AnimationObject>,
timestamp: Timestamp
): boolean => {
let finished = true;
(animation.current as Array<number>).forEach((_, i) => {
const result = animation[i].onFrame(animation[i], timestamp);
// We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
finished = finished && result;
(animation.current as Array<number>)[i] = animation[i].current;
});
return finished;
};
const objectOnStart = (
animation: Animation<AnimationObject>,
value: AnimatableValueObject,
timestamp: Timestamp,
previousAnimation: Animation<AnimationObject>
): void => {
for (const key in value) {
animation[key] = Object.assign({}, animationCopy);
animation[key].onStart = animation.onStart;
animation[key].current = value[key];
animation[key].toValue = (animation.toValue as AnimatableValueObject)[
key
];
animation[key].onStart(
animation[key],
value[key],
timestamp,
previousAnimation ? previousAnimation[key] : undefined
);
}
animation.current = value;
};
const objectOnFrame = (
animation: Animation<AnimationObject>,
timestamp: Timestamp
): boolean => {
let finished = true;
const newObject: AnimatableValueObject = {};
for (const key in animation.current as AnimatableValueObject) {
const result = animation[key].onFrame(animation[key], timestamp);
// We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
finished = finished && result;
newObject[key] = animation[key].current;
}
animation.current = newObject;
return finished;
};
animation.onStart = (
animation: Animation<AnimationObject>,
value: number,
timestamp: Timestamp,
previousAnimation: Animation<AnimationObject>
) => {
if (animation.reduceMotion === undefined) {
animation.reduceMotion = getReduceMotionFromConfig();
}
if (animation.reduceMotion) {
if (animation.toValue !== undefined) {
animation.current = animation.toValue;
} else {
// if there is no `toValue`, then the base function is responsible for setting the current value
baseOnStart(animation, value, timestamp, previousAnimation);
}
animation.startTime = 0;
animation.onFrame = () => true;
return;
}
if (isColor(value)) {
colorOnStart(animation, value, timestamp, previousAnimation);
animation.onFrame = colorOnFrame;
return;
} else if (isAffineMatrixFlat(value)) {
transformationMatrixOnStart(
animation,
value,
timestamp,
previousAnimation
);
animation.onFrame = transformationMatrixOnFrame;
return;
} else if (Array.isArray(value)) {
arrayOnStart(animation, value, timestamp, previousAnimation);
animation.onFrame = arrayOnFrame;
return;
} else if (typeof value === 'string') {
prefNumberSuffOnStart(animation, value, timestamp, previousAnimation);
animation.onFrame = prefNumberSuffOnFrame;
return;
} else if (typeof value === 'object' && value !== null) {
objectOnStart(animation, value, timestamp, previousAnimation);
animation.onFrame = objectOnFrame;
return;
}
baseOnStart(animation, value, timestamp, previousAnimation);
};
}
type AnimationToDecoration<
T extends AnimationObject | StyleLayoutAnimation,
U extends AnimationObject | StyleLayoutAnimation
> = T extends StyleLayoutAnimation
? Record<string, unknown>
: U | (() => U) | AnimatableValue;
const IS_NATIVE = NativeReanimatedModule.native;
export function defineAnimation<
T extends AnimationObject | StyleLayoutAnimation, // type that's supposed to be returned
U extends AnimationObject | StyleLayoutAnimation = T // type that's received
>(starting: AnimationToDecoration<T, U>, factory: () => T): T {
'worklet';
if (IN_STYLE_UPDATER) {
return starting as unknown as T;
}
const create = () => {
'worklet';
const animation = factory();
decorateAnimation<U>(animation as unknown as U);
return animation;
};
if (_WORKLET || !IS_NATIVE) {
return create();
}
// @ts-ignore: eslint-disable-line
return create;
}
export function cancelAnimation<T>(sharedValue: SharedValue<T>): void {
'worklet';
// setting the current value cancels the animation if one is currently running
sharedValue.value = sharedValue.value; // eslint-disable-line no-self-assign
}