create-expo-cljs-app
Version:
Create a react native application with Expo and Shadow-CLJS!
167 lines (143 loc) • 4.94 kB
text/typescript
import { defineAnimation } from './util';
import {
Animation,
AnimationCallback,
AnimatableValue,
Timestamp,
} from './commonTypes';
interface SpringConfig {
mass?: number;
stiffness?: number;
overshootClamping?: boolean;
restDisplacementThreshold?: number;
restSpeedThreshold?: number;
velocity?: number;
damping?: number;
}
export interface SpringAnimation extends Animation<SpringAnimation> {
current: AnimatableValue;
toValue: AnimatableValue;
velocity: number;
lastTimestamp: Timestamp;
}
export interface InnerSpringAnimation
extends Omit<SpringAnimation, 'toValue' | 'current'> {
toValue: number;
current: number;
}
export function withSpring(
toValue: AnimatableValue,
userConfig?: SpringConfig,
callback?: AnimationCallback
): Animation<SpringAnimation> {
'worklet';
return defineAnimation<SpringAnimation>(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: Required<SpringConfig> = {
damping: 10,
mass: 1,
stiffness: 100,
overshootClamping: false,
restDisplacementThreshold: 0.01,
restSpeedThreshold: 2,
velocity: 0,
};
if (userConfig) {
Object.keys(userConfig).forEach(
(key) =>
((config as any)[key] = userConfig[key as keyof typeof userConfig])
);
}
function spring(animation: InnerSpringAnimation, now: Timestamp): boolean {
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;
}
// 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: SpringAnimation,
value: number,
now: Timestamp,
previousAnimation: SpringAnimation
): void {
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,
} as SpringAnimation;
});
}