react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
653 lines (565 loc) • 18.6 kB
text/typescript
/* global _WORKLET */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { Easing } from './Easing';
import { isColor, convertToHSVA, toRGBA } from './Colors';
import NativeReanimated from './NativeReanimated';
let IN_STYLE_UPDATER = false;
export function initialUpdaterRun(updater) {
IN_STYLE_UPDATER = true;
const result = updater();
IN_STYLE_UPDATER = false;
return result;
}
export function transform(value, handler) {
'worklet';
if (value === undefined) {
return undefined;
}
if (typeof value === 'string') {
// toInt
// TODO handle color
const match = value.match(/([A-Za-z]*)(-?\d*\.?\d*)([A-Za-z%]*)/);
const prefix = match[1];
const suffix = match[3];
const number = match[2];
handler.__prefix = prefix;
handler.__suffix = suffix;
return parseFloat(number);
}
// toString if __prefix is available and number otherwise
if (handler.__prefix === undefined) {
return value;
}
return handler.__prefix + value + handler.__suffix;
}
export function transformAnimation(animation) {
'worklet';
if (!animation) {
return;
}
animation.toValue = transform(animation.toValue, animation);
animation.current = transform(animation.current, animation);
animation.startValue = transform(animation.startValue, animation);
}
export function decorateAnimation(animation) {
'worklet';
if (animation.isHigherOrder) {
return;
}
const baseOnStart = animation.onStart;
const baseOnFrame = animation.onFrame;
const animationCopy = Object.assign({}, animation);
delete animationCopy.callback;
const prefNumberSuffOnStart = (
animation,
value,
timestamp,
previousAnimation
) => {
const val = transform(value, animation);
transformAnimation(animation);
if (previousAnimation !== animation) transformAnimation(previousAnimation);
baseOnStart(animation, val, timestamp, previousAnimation);
transformAnimation(animation);
if (previousAnimation !== animation) transformAnimation(previousAnimation);
};
const prefNumberSuffOnFrame = (animation, timestamp) => {
transformAnimation(animation);
const res = baseOnFrame(animation, timestamp);
transformAnimation(animation);
return res;
};
const tab = ['H', 'S', 'V', 'A'];
const colorOnStart = (animation, value, timestamp, previousAnimation) => {
let HSVAValue;
let HSVACurrent;
let HSVAToValue;
const res = [];
if (isColor(value)) {
HSVACurrent = convertToHSVA(animation.current);
HSVAValue = convertToHSVA(value);
if (animation.toValue) {
HSVAToValue = convertToHSVA(animation.toValue);
}
}
tab.forEach((i, index) => {
animation[i] = Object.assign({}, animationCopy);
animation[i].current = HSVACurrent[index];
animation[i].toValue = HSVAToValue ? HSVAToValue[index] : undefined;
animation[i].onStart(
animation[i],
HSVAValue[index],
timestamp,
previousAnimation ? previousAnimation[i] : undefined
);
res.push(animation[i].current);
});
animation.current = toRGBA(res);
};
const colorOnFrame = (animation, timestamp) => {
const HSVACurrent = convertToHSVA(animation.current);
const res = [];
let finished = true;
tab.forEach((i, index) => {
animation[i].current = HSVACurrent[index];
finished &= animation[i].onFrame(animation[i], timestamp);
res.push(animation[i].current);
});
animation.current = toRGBA(res);
return finished;
};
animation.onStart = (animation, value, timestamp, previousAnimation) => {
if (isColor(value)) {
colorOnStart(animation, value, timestamp, previousAnimation);
animation.onFrame = colorOnFrame;
return;
} else if (typeof value === 'string') {
prefNumberSuffOnStart(animation, value, timestamp, previousAnimation);
animation.onFrame = prefNumberSuffOnFrame;
return;
}
baseOnStart(animation, value, timestamp, previousAnimation);
};
}
export function defineAnimation(starting, factory) {
'worklet';
if (IN_STYLE_UPDATER) {
return starting;
}
const create = () => {
'worklet';
const animation = factory();
decorateAnimation(animation);
return animation;
};
if (_WORKLET || !NativeReanimated.native) {
return create();
}
return create;
}
export function cancelAnimation(sharedValue) {
'worklet';
// setting the current value cancels the animation if one is currently running
sharedValue.value = sharedValue.value; // eslint-disable-line no-self-assign
}
export function withTiming(toValue, userConfig, callback) {
'worklet';
return defineAnimation(toValue, () => {
'worklet';
const config = {
duration: 300,
easing: Easing.inOut(Easing.quad),
};
if (userConfig) {
Object.keys(userConfig).forEach((key) => (config[key] = userConfig[key]));
}
function timing(animation, now) {
const { toValue, progress, startTime, current } = animation;
const runtime = now - startTime;
if (runtime >= config.duration) {
// reset startTime to avoid reusing finished animation config in `start` method
animation.startTime = 0;
animation.current = toValue;
return true;
}
const newProgress = config.easing(runtime / config.duration);
const dist =
((toValue - current) * (newProgress - progress)) / (1 - progress);
animation.current += dist;
animation.progress = newProgress;
return false;
}
function onStart(animation, value, now, previousAnimation) {
if (
previousAnimation &&
previousAnimation.type === 'timing' &&
previousAnimation.toValue === toValue &&
previousAnimation.startTime
) {
// to maintain continuity of timing animations we check if we are starting
// new timing over the old one with the same parameters. If so, we want
// to copy animation timeline properties
animation.startTime = previousAnimation.startTime;
animation.progress = previousAnimation.progress;
} else {
animation.startTime = now;
animation.progress = 0;
}
animation.current = value;
}
return {
type: 'timing',
onFrame: timing,
onStart,
progress: 0,
toValue,
current: toValue,
callback,
};
});
}
export function withSpring(toValue, userConfig, callback) {
'worklet';
return defineAnimation(toValue, () => {
'worklet';
// TODO: figure out why we can't use spread or Object.assign here
// when user config is "frozen object" we can't enumerate it (perhaps
// something is wrong with the object prototype).
const config = {
damping: 10,
mass: 1,
stiffness: 100,
overshootClamping: false,
restDisplacementThreshold: 0.01,
restSpeedThreshold: 2,
};
if (userConfig) {
Object.keys(userConfig).forEach((key) => (config[key] = userConfig[key]));
}
function spring(animation, now) {
const { toValue, lastTimestamp, current, velocity } = animation;
const deltaTime = Math.min(now - lastTimestamp, 64);
animation.lastTimestamp = now;
const c = config.damping;
const m = config.mass;
const k = config.stiffness;
const v0 = -velocity;
const x0 = toValue - current;
const zeta = c / (2 * Math.sqrt(k * m)); // damping ratio
const omega0 = Math.sqrt(k / m); // undamped angular frequency of the oscillator (rad/ms)
const omega1 = omega0 * Math.sqrt(1 - zeta ** 2); // exponential decay
const t = deltaTime / 1000;
const sin1 = Math.sin(omega1 * t);
const cos1 = Math.cos(omega1 * t);
// under damped
const underDampedEnvelope = Math.exp(-zeta * omega0 * t);
const underDampedFrag1 =
underDampedEnvelope *
(sin1 * ((v0 + zeta * omega0 * x0) / omega1) + x0 * cos1);
const underDampedPosition = toValue - underDampedFrag1;
// This looks crazy -- it's actually just the derivative of the oscillation function
const underDampedVelocity =
zeta * omega0 * underDampedFrag1 -
underDampedEnvelope *
(cos1 * (v0 + zeta * omega0 * x0) - omega1 * x0 * sin1);
// critically damped
const criticallyDampedEnvelope = Math.exp(-omega0 * t);
const criticallyDampedPosition =
toValue - criticallyDampedEnvelope * (x0 + (v0 + omega0 * x0) * t);
const criticallyDampedVelocity =
criticallyDampedEnvelope *
(v0 * (t * omega0 - 1) + t * x0 * omega0 * omega0);
const isOvershooting = () => {
if (config.overshootClamping && config.stiffness !== 0) {
return current < toValue
? animation.current > toValue
: animation.current < toValue;
} else {
return false;
}
};
const isVelocity = Math.abs(velocity) < config.restSpeedThreshold;
const isDisplacement =
config.stiffness === 0 ||
Math.abs(toValue - current) < config.restDisplacementThreshold;
if (zeta < 1) {
animation.current = underDampedPosition;
animation.velocity = underDampedVelocity;
} else {
animation.current = criticallyDampedPosition;
animation.velocity = criticallyDampedVelocity;
}
if (isOvershooting() || (isVelocity && isDisplacement)) {
if (config.stiffness !== 0) {
animation.velocity = 0;
animation.current = toValue;
}
return true;
}
}
function onStart(animation, value, now, previousAnimation) {
animation.current = value;
if (previousAnimation) {
animation.velocity =
previousAnimation.velocity || animation.velocity || 0;
animation.lastTimestamp = previousAnimation.lastTimestamp || now;
} else {
animation.lastTimestamp = now;
}
}
return {
onFrame: spring,
onStart,
toValue,
velocity: config.velocity || 0,
current: toValue,
callback,
};
});
}
export function withDecay(userConfig, callback) {
'worklet';
return defineAnimation(0, () => {
'worklet';
const config = {
deceleration: 0.998,
};
if (userConfig) {
Object.keys(userConfig).forEach((key) => (config[key] = userConfig[key]));
}
const VELOCITY_EPS = 5;
function decay(animation, now) {
const { lastTimestamp, initialVelocity, current, velocity } = animation;
const deltaTime = Math.min(now - lastTimestamp, 64);
animation.lastTimestamp = now;
const kv = Math.pow(config.deceleration, deltaTime);
const kx = (config.deceleration * (1 - kv)) / (1 - config.deceleration);
const v0 = velocity / 1000;
const v = v0 * kv * 1000;
const x = current + v0 * kx;
animation.current = x;
animation.velocity = v;
let toValueIsReached = null;
if (Array.isArray(config.clamp)) {
if (initialVelocity < 0 && animation.current <= config.clamp[0]) {
toValueIsReached = config.clamp[0];
} else if (
initialVelocity > 0 &&
animation.current >= config.clamp[1]
) {
toValueIsReached = config.clamp[1];
}
}
if (Math.abs(v) < VELOCITY_EPS || toValueIsReached !== null) {
if (toValueIsReached !== null) {
animation.current = toValueIsReached;
}
return true;
}
}
function onStart(animation, value, now) {
animation.current = value;
animation.lastTimestamp = now;
animation.initialVelocity = config.velocity;
}
return {
onFrame: decay,
onStart,
velocity: config.velocity || 0,
callback,
};
});
}
export function withDelay(delayMs, _nextAnimation) {
'worklet';
return defineAnimation(_nextAnimation, () => {
'worklet';
const nextAnimation =
typeof _nextAnimation === 'function' ? _nextAnimation() : _nextAnimation;
function delay(animation, now) {
const { startTime, started, previousAnimation } = animation;
if (now - startTime > delayMs) {
if (!started) {
nextAnimation.onStart(
nextAnimation,
animation.current,
now,
previousAnimation
);
animation.previousAnimation = null;
animation.started = true;
}
const finished = nextAnimation.onFrame(nextAnimation, now);
animation.current = nextAnimation.current;
return finished;
} else if (previousAnimation) {
const finished = previousAnimation.onFrame(previousAnimation, now);
animation.current = previousAnimation.current;
if (finished) {
animation.previousAnimation = null;
}
}
return false;
}
function onStart(animation, value, now, previousAnimation) {
animation.startTime = now;
animation.started = false;
animation.current = value;
animation.previousAnimation = previousAnimation;
}
const callback = (finished) => {
if (nextAnimation.callback) {
nextAnimation.callback(finished);
}
};
return {
isHigherOrder: true,
onFrame: delay,
onStart,
current: nextAnimation.current,
callback,
};
});
}
export function withSequence(..._animations) {
'worklet';
return defineAnimation(_animations[0], () => {
'worklet';
const animations = _animations.map((a) => {
const result = typeof a === 'function' ? a() : a;
result.finished = false;
return result;
});
const firstAnimation = animations[0];
const callback = (finished) => {
if (finished) {
// we want to call the callback after every single animation
// not after all of them
return;
}
// this is going to be called only if sequence has been cancelled
animations.forEach((animation) => {
if (typeof animation.callback === 'function' && !animation.finished) {
animation.callback(finished);
}
});
};
function sequence(animation, now) {
const currentAnim = animations[animation.animationIndex];
const finished = currentAnim.onFrame(currentAnim, now);
animation.current = currentAnim.current;
if (finished) {
// we want to call the callback after every single animation
if (currentAnim.callback) {
currentAnim.callback(true /* finished */);
}
currentAnim.finished = true;
animation.animationIndex += 1;
if (animation.animationIndex < animations.length) {
const nextAnim = animations[animation.animationIndex];
nextAnim.onStart(nextAnim, currentAnim.current, now, currentAnim);
return false;
}
return true;
}
return false;
}
function onStart(animation, value, now, previousAnimation) {
if (animations.length === 1) {
throw Error(
'withSequence() animation require more than one animation as argument'
);
}
animation.animationIndex = 0;
if (previousAnimation === undefined) {
previousAnimation = animations[animations.length - 1];
}
firstAnimation.onStart(firstAnimation, value, now, previousAnimation);
}
return {
isHigherOrder: true,
onFrame: sequence,
onStart,
animationIndex: 0,
current: firstAnimation.current,
callback,
};
});
}
export function withRepeat(
_nextAnimation,
numberOfReps = 2,
reverse = false,
callback
) {
'worklet';
return defineAnimation(_nextAnimation, () => {
'worklet';
const nextAnimation =
typeof _nextAnimation === 'function' ? _nextAnimation() : _nextAnimation;
function repeat(animation, now) {
const finished = nextAnimation.onFrame(nextAnimation, now);
animation.current = nextAnimation.current;
if (finished) {
animation.reps += 1;
// call inner animation's callback on every repetition
// as the second argument the animation's current value is passed
if (nextAnimation.callback) {
nextAnimation.callback(true /* finished */, animation.current);
}
if (numberOfReps > 0 && animation.reps >= numberOfReps) {
return true;
}
const startValue = reverse
? nextAnimation.current
: animation.startValue;
if (reverse) {
nextAnimation.toValue = animation.startValue;
animation.startValue = startValue;
}
nextAnimation.onStart(
nextAnimation,
startValue,
now,
nextAnimation.previousAnimation
);
return false;
}
return false;
}
const repCallback = (finished) => {
if (callback) {
callback(finished);
}
// when cancelled call inner animation's callback
if (!finished && nextAnimation.callback) {
nextAnimation.callback(false /* finished */);
}
};
function onStart(animation, value, now, previousAnimation) {
animation.startValue = value;
animation.reps = 0;
nextAnimation.onStart(nextAnimation, value, now, previousAnimation);
}
return {
isHigherOrder: true,
onFrame: repeat,
onStart,
reps: 0,
current: nextAnimation.current,
callback: repCallback,
};
});
}
/* Deprecated section, kept for backward compatibility. Will be removed soon */
export function delay(delayMs, _nextAnimation) {
'worklet';
console.warn('Method `delay` is deprecated. Please use `withDelay` instead');
return withDelay(delayMs, _nextAnimation);
}
export function repeat(
_nextAnimation,
numberOfReps = 2,
reverse = false,
callback
) {
'worklet';
console.warn(
'Method `repeat` is deprecated. Please use `withRepeat` instead'
);
return withRepeat(_nextAnimation, numberOfReps, reverse, callback);
}
export function loop(nextAnimation, numberOfLoops = 1) {
'worklet';
console.warn('Method `loop` is deprecated. Please use `withRepeat` instead');
return repeat(nextAnimation, Math.round(numberOfLoops * 2), true);
}
export function sequence(..._animations) {
'worklet';
console.warn(
'Method `sequence` is deprecated. Please use `withSequence` instead'
);
return withSequence(..._animations);
}
/* Deprecated section end */