react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
198 lines (175 loc) • 5.31 kB
text/typescript
import { defineAnimation } from './util';
import type {
Animation,
AnimationCallback,
AnimationObject,
AnimatableValue,
Timestamp,
} from '../commonTypes';
import { isWeb } from '../PlatformChecker';
interface DecayConfig {
deceleration?: number;
velocityFactor?: number;
clamp?: number[];
velocity?: number;
}
interface DefaultDecayConfig {
deceleration: number;
velocityFactor: number;
clamp?: number[];
velocity: number;
rubberBandEffect?: boolean;
rubberBandFactor: number;
}
export interface DecayAnimation extends Animation<DecayAnimation> {
lastTimestamp: Timestamp;
startTimestamp: Timestamp;
initialVelocity: number;
velocity: number;
current: AnimatableValue;
}
export interface InnerDecayAnimation
extends Omit<DecayAnimation, 'current'>,
AnimationObject {
current: number;
}
const IS_WEB = isWeb();
// TODO TYPESCRIPT This is a temporary type to get rid of .d.ts file.
type withDecayType = (
userConfig: DecayConfig,
callback?: AnimationCallback
) => number;
export const withDecay = function (
userConfig: DecayConfig,
callback?: AnimationCallback
): Animation<DecayAnimation> {
'worklet';
return defineAnimation<DecayAnimation>(0, () => {
'worklet';
const config: DefaultDecayConfig = {
deceleration: 0.998,
velocityFactor: 1,
velocity: 0,
rubberBandFactor: 0.6,
};
if (userConfig) {
Object.keys(userConfig).forEach(
(key) =>
((config as any)[key] = userConfig[key as keyof typeof userConfig])
);
}
const VELOCITY_EPS = IS_WEB ? 1 / 20 : 1;
const SLOPE_FACTOR = 0.1;
let decay: (animation: InnerDecayAnimation, now: number) => boolean;
if (config.rubberBandEffect) {
decay = (animation: InnerDecayAnimation, now: number): boolean => {
const { lastTimestamp, startTimestamp, current, velocity } = animation;
const deltaTime = Math.min(now - lastTimestamp, 64);
const clampIndex =
Math.abs(current - config.clamp![0]) <
Math.abs(current - config.clamp![1])
? 0
: 1;
let derivative = 0;
if (current < config.clamp![0] || current > config.clamp![1]) {
derivative = current - config.clamp![clampIndex];
}
if (derivative !== 0) {
animation.springActive = true;
} else if (derivative === 0 && animation.springActive) {
animation.current = config.clamp![clampIndex];
return true;
}
const v =
velocity *
Math.exp(
-(1 - config.deceleration) * (now - startTimestamp) * SLOPE_FACTOR
) -
derivative * config.rubberBandFactor;
animation.current =
current + (v * config.velocityFactor * deltaTime) / 1000;
animation.velocity = v;
animation.lastTimestamp = now;
return false;
};
} else {
decay = (animation: InnerDecayAnimation, now: number): boolean => {
const {
lastTimestamp,
startTimestamp,
initialVelocity,
current,
velocity,
} = animation;
const deltaTime = Math.min(now - lastTimestamp, 64);
const v =
velocity *
Math.exp(
-(1 - config.deceleration) * (now - startTimestamp) * SLOPE_FACTOR
);
animation.current =
current + (v * config.velocityFactor * deltaTime) / 1000;
animation.velocity = v;
animation.lastTimestamp = now;
if (config.clamp) {
if (initialVelocity < 0 && animation.current <= config.clamp[0]) {
animation.current = config.clamp[0];
return true;
} else if (
initialVelocity > 0 &&
animation.current >= config.clamp[1]
) {
animation.current = config.clamp[1];
return true;
}
}
return Math.abs(v) < VELOCITY_EPS;
};
}
function validateConfig(): void {
if (config.clamp) {
if (!Array.isArray(config.clamp)) {
throw Error(
`config.clamp must be an array but is ${typeof config.clamp}`
);
}
if (config.clamp.length !== 2) {
throw Error(
`clamp array must contain 2 items but is given ${config.clamp.length}`
);
}
}
if (config.velocityFactor <= 0) {
throw Error(
`config.velocityFactor must be greather then 0 but is ${config.velocityFactor}`
);
}
if (config.rubberBandEffect && !config.clamp) {
throw Error(
'You need to set `clamp` property when using `rubberBandEffect`.'
);
}
}
function onStart(
animation: DecayAnimation,
value: number,
now: Timestamp
): void {
animation.current = value;
animation.lastTimestamp = now;
animation.startTimestamp = now;
animation.initialVelocity = config.velocity;
validateConfig();
}
return {
onFrame: decay,
onStart,
callback,
velocity: config.velocity ?? 0,
initialVelocity: 0,
current: 0,
lastTimestamp: 0,
startTimestamp: 0,
} as DecayAnimation;
});
} as unknown as withDecayType;