react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
104 lines (103 loc) • 4.58 kB
JavaScript
import { defineAnimation } from './util';
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,
velocity: 0,
};
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 - Math.pow(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;
}
// clear lastTimestamp to avoid using stale value by the next spring animation that starts after this one
animation.lastTimestamp = 0;
return true;
}
return false;
}
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,
lastTimestamp: 0,
};
});
}