addimated
Version:
An always interruptable, declarative animation library for React
257 lines (223 loc) • 7.82 kB
JavaScript
// @flow
import invariant from "invariant";
import { type AnimatedValue } from "./AnimatedValue";
import { Animation, type AnimationConfig, type EndCallback } from "./Animation";
import {
fromBouncinessAndSpeed,
fromOrigamiTensionAndFriction
} from "./SpringConfig";
import { withDefault } from "./WithDefault";
type SpringAnimationConfigSingle = AnimationConfig & {
toValue: number,
overshootClamping?: boolean,
restDisplacementThreshold?: number,
restSpeedThreshold?: number,
velocity?: number,
bounciness?: number,
speed?: number,
tension?: number,
friction?: number,
stiffness?: number,
damping?: number,
mass?: number,
delay?: number
};
class SpringAnimation extends Animation {
currentValue: number;
overshootClamping: boolean;
restDisplacementThreshold: number;
restSpeedThreshold: number;
lastVelocity: number;
startPosition: number;
lastPosition: number;
fromValue: number;
toValue: any;
stiffness: number;
damping: number;
mass: number;
initialVelocity: number;
delay: number;
timeout: any;
startTime: number;
lastTime: number;
frameTime: number;
constructor(config: SpringAnimationConfigSingle) {
super();
this.overshootClamping = withDefault(config.overshootClamping, false);
this.restDisplacementThreshold = withDefault(
config.restDisplacementThreshold,
0.001
);
this.restSpeedThreshold = withDefault(config.restSpeedThreshold, 0.001);
this.initialVelocity = withDefault(config.velocity, NaN);
this.lastVelocity = withDefault(config.velocity, NaN);
this.toValue = config.toValue;
this.delay = withDefault(config.delay, 0);
if (
config.stiffness !== undefined ||
config.damping !== undefined ||
config.mass !== undefined
) {
invariant(
config.bounciness === undefined &&
config.speed === undefined &&
config.tension === undefined &&
config.friction === undefined,
"You can define one of bounciness/speed, tension/friction, or stiffness/damping/mass, but not more than one"
);
this.stiffness = withDefault(config.stiffness, 100);
this.damping = withDefault(config.damping, 10);
this.mass = withDefault(config.mass, 1);
} else if (config.bounciness !== undefined || config.speed !== undefined) {
// Convert the origami bounciness/speed values to stiffness/damping
// We assume mass is 1.
invariant(
config.tension === undefined &&
config.friction === undefined &&
config.stiffness === undefined &&
config.damping === undefined &&
config.mass === undefined,
"You can define one of bounciness/speed, tension/friction, or stiffness/damping/mass, but not more than one"
);
const springConfig = fromBouncinessAndSpeed(
withDefault(config.bounciness, 8),
withDefault(config.speed, 12)
);
this.stiffness = springConfig.stiffness;
this.damping = springConfig.damping;
this.mass = 1;
} else {
// Convert the origami tension/friction values to stiffness/damping
// We assume mass is 1.
const springConfig = fromOrigamiTensionAndFriction(
withDefault(config.tension, 40),
withDefault(config.friction, 7)
);
this.stiffness = springConfig.stiffness;
this.damping = springConfig.damping;
this.mass = 1;
}
invariant(this.stiffness > 0, "Stiffness value must be greater than 0");
invariant(this.damping > 0, "Damping value must be greater than 0");
invariant(this.mass > 0, "Mass value must be greater than 0");
}
getInternalState(): Object {
return {
lastPosition: this.lastPosition,
lastVelocity: this.lastVelocity,
lastTime: this.lastTime
};
}
nextFrame(now: number): [number, boolean] {
// TODO: Rethink delay handling here
if (now <= this.lastTime) return [this.startPosition, false];
const deltaTime = (now - this.lastTime) / 1000;
this.frameTime += deltaTime;
const c: number = this.damping;
const m: number = this.mass;
const k: number = this.stiffness;
const v0: number = -this.initialVelocity;
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.0 - zeta * zeta); // exponential decay
const x0 = this.toValue - this.startPosition; // calculate the oscillation from x0 = 1 to x = 0
let position = 0.0;
let velocity = 0.0;
const t = this.frameTime;
if (zeta < 1) {
// Under damped
const envelope = Math.exp(-zeta * omega0 * t);
position =
this.toValue -
envelope *
(((v0 + zeta * omega0 * x0) / omega1) * Math.sin(omega1 * t) +
x0 * Math.cos(omega1 * t));
// This looks crazy -- it's actually just the derivative of the
// oscillation function
velocity =
zeta *
omega0 *
envelope *
((Math.sin(omega1 * t) * (v0 + zeta * omega0 * x0)) / omega1 +
x0 * Math.cos(omega1 * t)) -
envelope *
(Math.cos(omega1 * t) * (v0 + zeta * omega0 * x0) -
omega1 * x0 * Math.sin(omega1 * t));
} else {
// Critically damped
const envelope = Math.exp(-omega0 * t);
position = this.toValue - envelope * (x0 + (v0 + omega0 * x0) * t);
velocity =
envelope * (v0 * (t * omega0 - 1) + t * x0 * (omega0 * omega0));
}
this.lastTime = now;
this.lastPosition = position;
this.lastVelocity = velocity;
// Conditions for stopping the spring animation
let finished = false;
let isOvershooting = false;
if (this.overshootClamping && this.stiffness !== 0) {
if (this.startPosition < this.toValue) {
isOvershooting = position > this.toValue;
} else {
isOvershooting = position < this.toValue;
}
}
const isVelocity = Math.abs(velocity) <= this.restSpeedThreshold;
let isDisplacement = true;
if (this.stiffness !== 0) {
isDisplacement =
Math.abs(this.toValue - position) <= this.restDisplacementThreshold;
}
if (isOvershooting || (isVelocity && isDisplacement)) {
if (this.stiffness !== 0) {
// Ensure that we end up with a round value
this.lastPosition = this.toValue;
this.lastVelocity = 0;
position = this.toValue;
}
finished = true;
}
return [position, finished];
}
start(
animatedVal: AnimatedValue,
_fromValue: number,
onEnd: ?EndCallback
): Animation[] {
const currentVal = animatedVal.__getValue();
animatedVal.model = this.toValue;
this.active = true;
this.fromValue = currentVal - this.toValue;
this.toValue = 0;
this.endCallback = onEnd;
if (isNaN(this.initialVelocity) || isNaN(this.lastVelocity)) {
const velocity =
animatedVal.velocity != null ? animatedVal.velocity * 1000 : 0;
this.initialVelocity = velocity;
this.lastVelocity = velocity;
}
this.startPosition = this.fromValue;
this.lastPosition = this.startPosition;
this.currentValue = currentVal;
this.lastTime = performance.now() + this.delay;
this.frameTime = 0;
animatedVal.animations.forEach(anim => anim.stop(false));
return [this];
}
step(timestamp: number): void {
const [currentValue, finished] = this.nextFrame(timestamp);
this.currentValue = currentValue;
if (finished) {
this.stop(true);
}
}
getValue(): number {
return this.currentValue;
}
stop(finished?: boolean = false): void {
this.ended = true;
this.endCallback && this.endCallback({ finished });
}
}
export { SpringAnimation };