Add closed-form damped harmonic oscillator algorithm to Animated.spring
Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
This commit is contained in:
parent
4d54b48167
commit
26133beda9
|
@ -698,6 +698,8 @@ module.exports = {
|
|||
*
|
||||
* - `velocity`: Initial velocity. Required.
|
||||
* - `deceleration`: Rate of decay. Default 0.997.
|
||||
* - `isInteraction`: Whether or not this animation creates an "interaction handle" on the
|
||||
* `InteractionManager`. Default true.
|
||||
* - `useNativeDriver`: Uses the native driver when true. Default false.
|
||||
*/
|
||||
decay,
|
||||
|
@ -712,21 +714,56 @@ module.exports = {
|
|||
* - `easing`: Easing function to define curve.
|
||||
* Default is `Easing.inOut(Easing.ease)`.
|
||||
* - `delay`: Start the animation after delay (milliseconds). Default 0.
|
||||
* - `isInteraction`: Whether or not this animation creates an "interaction handle" on the
|
||||
* `InteractionManager`. Default true.
|
||||
* - `useNativeDriver`: Uses the native driver when true. Default false.
|
||||
*/
|
||||
timing,
|
||||
/**
|
||||
* Spring animation based on Rebound and
|
||||
* [Origami](https://facebook.github.io/origami/). Tracks velocity state to
|
||||
* create fluid motions as the `toValue` updates, and can be chained together.
|
||||
* Animates a value according to an analytical spring model based on
|
||||
* [damped harmonic oscillation](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator).
|
||||
* Tracks velocity state to create fluid motions as the `toValue` updates, and
|
||||
* can be chained together.
|
||||
*
|
||||
* Config is an object that may have the following options. Note that you can
|
||||
* only define bounciness/speed or tension/friction but not both:
|
||||
* Config is an object that may have the following options.
|
||||
*
|
||||
* Note that you can only define one of bounciness/speed, tension/friction, or
|
||||
* stiffness/damping/mass, but not more than one:
|
||||
*
|
||||
* The friction/tension or bounciness/speed options match the spring model in
|
||||
* [Facebook Pop](https://github.com/facebook/pop), [Rebound](http://facebook.github.io/rebound/),
|
||||
* and [Origami](http://origami.design/).
|
||||
*
|
||||
* - `friction`: Controls "bounciness"/overshoot. Default 7.
|
||||
* - `tension`: Controls speed. Default 40.
|
||||
* - `speed`: Controls speed of the animation. Default 12.
|
||||
* - `bounciness`: Controls bounciness. Default 8.
|
||||
*
|
||||
* Specifying stiffness/damping/mass as parameters makes `Animated.spring` use an
|
||||
* analytical spring model based on the motion equations of a [damped harmonic
|
||||
* oscillator](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator).
|
||||
* This behavior is slightly more precise and faithful to the physics behind
|
||||
* spring dynamics, and closely mimics the implementation in iOS's
|
||||
* CASpringAnimation primitive.
|
||||
*
|
||||
* - `stiffness`: The spring stiffness coefficient. Default 100.
|
||||
* - `damping`: Defines how the spring’s motion should be damped due to the forces of friction.
|
||||
* Default 10.
|
||||
* - `mass`: The mass of the object attached to the end of the spring. Default 1.
|
||||
*
|
||||
* Other configuration options are as follows:
|
||||
*
|
||||
* - `velocity`: The initial velocity of the object attached to the spring. Default 0 (object
|
||||
* is at rest).
|
||||
* - `overshootClamping`: Boolean indiciating whether the spring should be clamped and not
|
||||
* bounce. Default false.
|
||||
* - `restDisplacementThreshold`: The threshold of displacement from rest below which the
|
||||
* spring should be considered at rest. Default 0.001.
|
||||
* - `restSpeedThreshold`: The speed at which the spring should be considered at rest in pixels
|
||||
* per second. Default 0.001.
|
||||
* - `delay`: Start the animation after delay (milliseconds). Default 0.
|
||||
* - `isInteraction`: Whether or not this animation creates an "interaction handle" on the
|
||||
* `InteractionManager`. Default true.
|
||||
* - `useNativeDriver`: Uses the native driver when true. Default false.
|
||||
*/
|
||||
spring,
|
||||
|
|
|
@ -13,15 +13,15 @@
|
|||
'use strict';
|
||||
|
||||
type SpringConfigType = {
|
||||
tension: number,
|
||||
friction: number,
|
||||
stiffness: number,
|
||||
damping: number,
|
||||
};
|
||||
|
||||
function tensionFromOrigamiValue(oValue) {
|
||||
function stiffnessFromOrigamiValue(oValue) {
|
||||
return (oValue - 30) * 3.62 + 194;
|
||||
}
|
||||
|
||||
function frictionFromOrigamiValue(oValue) {
|
||||
function dampingFromOrigamiValue(oValue) {
|
||||
return (oValue - 8) * 3 + 25;
|
||||
}
|
||||
|
||||
|
@ -30,8 +30,8 @@ function fromOrigamiTensionAndFriction(
|
|||
friction: number,
|
||||
): SpringConfigType {
|
||||
return {
|
||||
tension: tensionFromOrigamiValue(tension),
|
||||
friction: frictionFromOrigamiValue(friction)
|
||||
stiffness: stiffnessFromOrigamiValue(tension),
|
||||
damping: dampingFromOrigamiValue(friction),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -91,8 +91,8 @@ function fromBouncinessAndSpeed(
|
|||
);
|
||||
|
||||
return {
|
||||
tension: tensionFromOrigamiValue(bouncyTension),
|
||||
friction: frictionFromOrigamiValue(bouncyFriction)
|
||||
stiffness: stiffnessFromOrigamiValue(bouncyTension),
|
||||
damping: dampingFromOrigamiValue(bouncyFriction),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -135,7 +135,7 @@ describe('Animated tests', () => {
|
|||
expect(callback).toBeCalled();
|
||||
});
|
||||
|
||||
it('send toValue when a spring stops', () => {
|
||||
it('send toValue when an underdamped spring stops', () => {
|
||||
var anim = new Animated.Value(0);
|
||||
var listener = jest.fn();
|
||||
anim.addListener(listener);
|
||||
|
@ -147,6 +147,18 @@ describe('Animated tests', () => {
|
|||
expect(anim.__getValue()).toBe(15);
|
||||
});
|
||||
|
||||
it('send toValue when a critically damped spring stops', () => {
|
||||
var anim = new Animated.Value(0);
|
||||
var listener = jest.fn();
|
||||
anim.addListener(listener);
|
||||
Animated.spring(anim, {stiffness: 8000, damping: 2000, toValue: 15}).start();
|
||||
jest.runAllTimers();
|
||||
var lastValue = listener.mock.calls[listener.mock.calls.length - 2][0].value;
|
||||
expect(lastValue).not.toBe(15);
|
||||
expect(lastValue).toBeCloseTo(15);
|
||||
expect(anim.__getValue()).toBe(15);
|
||||
});
|
||||
|
||||
it('convert to JSON', () => {
|
||||
expect(JSON.stringify(new Animated.Value(10))).toBe('10');
|
||||
});
|
||||
|
|
|
@ -595,12 +595,38 @@ describe('Native Animated', () => {
|
|||
jasmine.any(Number),
|
||||
{
|
||||
type: 'spring',
|
||||
friction: 16,
|
||||
stiffness: 679.08,
|
||||
damping: 16,
|
||||
mass: 1,
|
||||
initialVelocity: 0,
|
||||
overshootClamping: false,
|
||||
restDisplacementThreshold: 0.001,
|
||||
restSpeedThreshold: 0.001,
|
||||
toValue: 10,
|
||||
iterations: 1,
|
||||
},
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
Animated.spring(anim, {
|
||||
toValue: 10,
|
||||
stiffness: 1000,
|
||||
damping: 500,
|
||||
mass: 3,
|
||||
useNativeDriver: true
|
||||
}).start();
|
||||
expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith(
|
||||
jasmine.any(Number),
|
||||
jasmine.any(Number),
|
||||
{
|
||||
type: 'spring',
|
||||
stiffness: 1000,
|
||||
damping: 500,
|
||||
mass: 3,
|
||||
initialVelocity: 0,
|
||||
overshootClamping: false,
|
||||
restDisplacementThreshold: 0.001,
|
||||
restSpeedThreshold: 0.001,
|
||||
tension: 679.08,
|
||||
toValue: 10,
|
||||
iterations: 1,
|
||||
},
|
||||
|
@ -613,12 +639,13 @@ describe('Native Animated', () => {
|
|||
jasmine.any(Number),
|
||||
{
|
||||
type: 'spring',
|
||||
friction: 23.05223140901191,
|
||||
damping: 23.05223140901191,
|
||||
initialVelocity: 0,
|
||||
overshootClamping: false,
|
||||
restDisplacementThreshold: 0.001,
|
||||
restSpeedThreshold: 0.001,
|
||||
tension: 299.61882352941177,
|
||||
stiffness: 299.61882352941177,
|
||||
mass: 1,
|
||||
toValue: 10,
|
||||
iterations: 1,
|
||||
},
|
||||
|
|
|
@ -32,6 +32,9 @@ export type SpringAnimationConfig = AnimationConfig & {
|
|||
speed?: number,
|
||||
tension?: number,
|
||||
friction?: number,
|
||||
stiffness?: number,
|
||||
damping?: number,
|
||||
mass?: number,
|
||||
delay?: number,
|
||||
};
|
||||
|
||||
|
@ -45,6 +48,9 @@ export type SpringAnimationConfigSingle = AnimationConfig & {
|
|||
speed?: number,
|
||||
tension?: number,
|
||||
friction?: number,
|
||||
stiffness?: number,
|
||||
damping?: number,
|
||||
mass?: number,
|
||||
delay?: number,
|
||||
};
|
||||
|
||||
|
@ -59,17 +65,20 @@ class SpringAnimation extends Animation {
|
|||
_overshootClamping: boolean;
|
||||
_restDisplacementThreshold: number;
|
||||
_restSpeedThreshold: number;
|
||||
_initialVelocity: ?number;
|
||||
_lastVelocity: number;
|
||||
_startPosition: number;
|
||||
_lastPosition: number;
|
||||
_fromValue: number;
|
||||
_toValue: any;
|
||||
_tension: number;
|
||||
_friction: number;
|
||||
_stiffness: number;
|
||||
_damping: number;
|
||||
_mass: number;
|
||||
_initialVelocity: number;
|
||||
_delay: number;
|
||||
_timeout: any;
|
||||
_startTime: number;
|
||||
_lastTime: number;
|
||||
_frameTime: number;
|
||||
_onUpdate: (value: number) => void;
|
||||
_animationFrame: any;
|
||||
_useNativeDriver: boolean;
|
||||
|
@ -83,7 +92,7 @@ class SpringAnimation extends Animation {
|
|||
0.001,
|
||||
);
|
||||
this._restSpeedThreshold = withDefault(config.restSpeedThreshold, 0.001);
|
||||
this._initialVelocity = config.velocity;
|
||||
this._initialVelocity = withDefault(config.velocity, 0);
|
||||
this._lastVelocity = withDefault(config.velocity, 0);
|
||||
this._toValue = config.toValue;
|
||||
this._delay = withDefault(config.delay, 0);
|
||||
|
@ -92,24 +101,54 @@ class SpringAnimation extends Animation {
|
|||
config.isInteraction !== undefined ? config.isInteraction : true;
|
||||
this.__iterations = config.iterations !== undefined ? config.iterations : 1;
|
||||
|
||||
let springConfig;
|
||||
if (config.bounciness !== undefined || config.speed !== undefined) {
|
||||
if (
|
||||
config.stiffness !== undefined ||
|
||||
config.damping !== undefined ||
|
||||
config.mass !== undefined
|
||||
) {
|
||||
invariant(
|
||||
config.tension === undefined && config.friction === undefined,
|
||||
'You can only define bounciness/speed or tension/friction but not both',
|
||||
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',
|
||||
);
|
||||
springConfig = SpringConfig.fromBouncinessAndSpeed(
|
||||
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 = SpringConfig.fromBouncinessAndSpeed(
|
||||
withDefault(config.bounciness, 8),
|
||||
withDefault(config.speed, 12),
|
||||
);
|
||||
this._stiffness = springConfig.stiffness;
|
||||
this._damping = springConfig.damping;
|
||||
this._mass = 1;
|
||||
} else {
|
||||
springConfig = SpringConfig.fromOrigamiTensionAndFriction(
|
||||
// Convert the origami tension/friction values to stiffness/damping
|
||||
// We assume mass is 1.
|
||||
const springConfig = SpringConfig.fromOrigamiTensionAndFriction(
|
||||
withDefault(config.tension, 40),
|
||||
withDefault(config.friction, 7),
|
||||
);
|
||||
this._stiffness = springConfig.stiffness;
|
||||
this._damping = springConfig.damping;
|
||||
this._mass = 1;
|
||||
}
|
||||
this._tension = springConfig.tension;
|
||||
this._friction = springConfig.friction;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
__getNativeAnimationConfig() {
|
||||
|
@ -118,8 +157,9 @@ class SpringAnimation extends Animation {
|
|||
overshootClamping: this._overshootClamping,
|
||||
restDisplacementThreshold: this._restDisplacementThreshold,
|
||||
restSpeedThreshold: this._restSpeedThreshold,
|
||||
tension: this._tension,
|
||||
friction: this._friction,
|
||||
stiffness: this._stiffness,
|
||||
damping: this._damping,
|
||||
mass: this._mass,
|
||||
initialVelocity: withDefault(this._initialVelocity, this._lastVelocity),
|
||||
toValue: this._toValue,
|
||||
iterations: this.__iterations,
|
||||
|
@ -140,16 +180,16 @@ class SpringAnimation extends Animation {
|
|||
this._onUpdate = onUpdate;
|
||||
this.__onEnd = onEnd;
|
||||
this._lastTime = Date.now();
|
||||
this._frameTime = 0.0;
|
||||
|
||||
if (previousAnimation instanceof SpringAnimation) {
|
||||
const internalState = previousAnimation.getInternalState();
|
||||
this._lastPosition = internalState.lastPosition;
|
||||
this._lastVelocity = internalState.lastVelocity;
|
||||
// Set the initial velocity to the last velocity
|
||||
this._initialVelocity = this._lastVelocity;
|
||||
this._lastTime = internalState.lastTime;
|
||||
}
|
||||
if (this._initialVelocity !== undefined && this._initialVelocity !== null) {
|
||||
this._lastVelocity = this._initialVelocity;
|
||||
}
|
||||
|
||||
const start = () => {
|
||||
if (this._useNativeDriver) {
|
||||
|
@ -175,13 +215,28 @@ class SpringAnimation extends Animation {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This spring model is based off of a damped harmonic oscillator
|
||||
* (https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator).
|
||||
*
|
||||
* We use the closed form of the second order differential equation:
|
||||
*
|
||||
* x'' + (2ζ⍵_0)x' + ⍵^2x = 0
|
||||
*
|
||||
* where
|
||||
* ⍵_0 = √(k / m) (undamped angular frequency of the oscillator),
|
||||
* ζ = c / 2√mk (damping ratio),
|
||||
* c = damping constant
|
||||
* k = stiffness
|
||||
* m = mass
|
||||
*
|
||||
* The derivation of the closed form is described in detail here:
|
||||
* http://planetmath.org/sites/default/files/texpdf/39745.pdf
|
||||
*
|
||||
* This algorithm happens to match the algorithm used by CASpringAnimation,
|
||||
* a QuartzCore (iOS) API that creates spring animations.
|
||||
*/
|
||||
onUpdate(): void {
|
||||
let position = this._lastPosition;
|
||||
let velocity = this._lastVelocity;
|
||||
|
||||
let tempPosition = this._lastPosition;
|
||||
let tempVelocity = this._lastVelocity;
|
||||
|
||||
// If for some reason we lost a lot of frames (e.g. process large payload or
|
||||
// stopped in the debugger), we only advance by 4 frames worth of
|
||||
// computation and will continue on the next frame. It's better to have it
|
||||
|
@ -192,56 +247,47 @@ class SpringAnimation extends Animation {
|
|||
now = this._lastTime + MAX_STEPS;
|
||||
}
|
||||
|
||||
// We are using a fixed time step and a maximum number of iterations.
|
||||
// The following post provides a lot of thoughts into how to build this
|
||||
// loop: http://gafferongames.com/game-physics/fix-your-timestep/
|
||||
const TIMESTEP_MSEC = 1;
|
||||
const numSteps = Math.max(
|
||||
1, // Always take at least one step to make progress.
|
||||
Math.floor((now - this._lastTime) / TIMESTEP_MSEC),
|
||||
);
|
||||
const deltaTime = (now - this._lastTime) / 1000;
|
||||
this._frameTime += deltaTime;
|
||||
|
||||
for (let i = 0; i < numSteps; ++i) {
|
||||
// Velocity is based on seconds instead of milliseconds
|
||||
const step = TIMESTEP_MSEC / 1000;
|
||||
const c: number = this._damping;
|
||||
const m: number = this._mass;
|
||||
const k: number = this._stiffness;
|
||||
const v0: number = -this._initialVelocity;
|
||||
|
||||
// This is using RK4. A good blog post to understand how it works:
|
||||
// http://gafferongames.com/game-physics/integration-basics/
|
||||
const aVelocity = velocity;
|
||||
const aAcceleration =
|
||||
this._tension * (this._toValue - tempPosition) -
|
||||
this._friction * tempVelocity;
|
||||
tempPosition = position + aVelocity * step / 2;
|
||||
tempVelocity = velocity + aAcceleration * step / 2;
|
||||
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
|
||||
|
||||
const bVelocity = tempVelocity;
|
||||
const bAcceleration =
|
||||
this._tension * (this._toValue - tempPosition) -
|
||||
this._friction * tempVelocity;
|
||||
tempPosition = position + bVelocity * step / 2;
|
||||
tempVelocity = velocity + bAcceleration * step / 2;
|
||||
|
||||
const cVelocity = tempVelocity;
|
||||
const cAcceleration =
|
||||
this._tension * (this._toValue - tempPosition) -
|
||||
this._friction * tempVelocity;
|
||||
tempPosition = position + cVelocity * step / 2;
|
||||
tempVelocity = velocity + cAcceleration * step / 2;
|
||||
|
||||
const dVelocity = tempVelocity;
|
||||
const dAcceleration =
|
||||
this._tension * (this._toValue - tempPosition) -
|
||||
this._friction * tempVelocity;
|
||||
tempPosition = position + cVelocity * step / 2;
|
||||
tempVelocity = velocity + cAcceleration * step / 2;
|
||||
|
||||
const dxdt = (aVelocity + 2 * (bVelocity + cVelocity) + dVelocity) / 6;
|
||||
const dvdt =
|
||||
(aAcceleration + 2 * (bAcceleration + cAcceleration) + dAcceleration) /
|
||||
6;
|
||||
|
||||
position += dxdt * step;
|
||||
velocity += dvdt * step;
|
||||
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;
|
||||
|
@ -256,7 +302,7 @@ class SpringAnimation extends Animation {
|
|||
|
||||
// Conditions for stopping the spring animation
|
||||
let isOvershooting = false;
|
||||
if (this._overshootClamping && this._tension !== 0) {
|
||||
if (this._overshootClamping && this._stiffness !== 0) {
|
||||
if (this._startPosition < this._toValue) {
|
||||
isOvershooting = position > this._toValue;
|
||||
} else {
|
||||
|
@ -265,14 +311,16 @@ class SpringAnimation extends Animation {
|
|||
}
|
||||
const isVelocity = Math.abs(velocity) <= this._restSpeedThreshold;
|
||||
let isDisplacement = true;
|
||||
if (this._tension !== 0) {
|
||||
if (this._stiffness !== 0) {
|
||||
isDisplacement =
|
||||
Math.abs(this._toValue - position) <= this._restDisplacementThreshold;
|
||||
}
|
||||
|
||||
if (isOvershooting || (isVelocity && isDisplacement)) {
|
||||
if (this._tension !== 0) {
|
||||
if (this._stiffness !== 0) {
|
||||
// Ensure that we end up with a round value
|
||||
this._lastPosition = this._toValue;
|
||||
this._lastVelocity = 0;
|
||||
this._onUpdate(this._toValue);
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
|
||||
@end
|
||||
|
||||
const NSTimeInterval MAX_DELTA_TIME = 0.064;
|
||||
|
||||
@implementation RCTSpringAnimation
|
||||
{
|
||||
CGFloat _toValue;
|
||||
|
@ -32,8 +34,9 @@
|
|||
BOOL _overshootClamping;
|
||||
CGFloat _restDisplacementThreshold;
|
||||
CGFloat _restSpeedThreshold;
|
||||
CGFloat _tension;
|
||||
CGFloat _friction;
|
||||
CGFloat _stiffness;
|
||||
CGFloat _damping;
|
||||
CGFloat _mass;
|
||||
CGFloat _initialVelocity;
|
||||
NSTimeInterval _animationStartTime;
|
||||
NSTimeInterval _animationCurrentTime;
|
||||
|
@ -44,6 +47,8 @@
|
|||
|
||||
NSInteger _iterations;
|
||||
NSInteger _currentLoop;
|
||||
|
||||
NSTimeInterval _t; // Current time (startTime + dt)
|
||||
}
|
||||
|
||||
- (instancetype)initWithId:(NSNumber *)animationId
|
||||
|
@ -57,13 +62,16 @@
|
|||
_animationId = animationId;
|
||||
_toValue = [RCTConvert CGFloat:config[@"toValue"]];
|
||||
_fromValue = valueNode.value;
|
||||
_lastPosition = 0;
|
||||
_valueNode = valueNode;
|
||||
_overshootClamping = [RCTConvert BOOL:config[@"overshootClamping"]];
|
||||
_restDisplacementThreshold = [RCTConvert CGFloat:config[@"restDisplacementThreshold"]];
|
||||
_restSpeedThreshold = [RCTConvert CGFloat:config[@"restSpeedThreshold"]];
|
||||
_tension = [RCTConvert CGFloat:config[@"tension"]];
|
||||
_friction = [RCTConvert CGFloat:config[@"friction"]];
|
||||
_stiffness = [RCTConvert CGFloat:config[@"stiffness"]];
|
||||
_damping = [RCTConvert CGFloat:config[@"damping"]];
|
||||
_mass = [RCTConvert CGFloat:config[@"mass"]];
|
||||
_initialVelocity = [RCTConvert CGFloat:config[@"initialVelocity"]];
|
||||
|
||||
_callback = [callback copy];
|
||||
|
||||
_lastPosition = _fromValue;
|
||||
|
@ -101,57 +109,57 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||
return;
|
||||
}
|
||||
|
||||
if (_animationStartTime == -1) {
|
||||
_animationStartTime = _animationCurrentTime = currentTime;
|
||||
// calculate delta time
|
||||
NSTimeInterval deltaTime;
|
||||
if(_animationStartTime == -1) {
|
||||
_t = 0.0;
|
||||
_animationStartTime = currentTime;
|
||||
deltaTime = 0.0;
|
||||
} else {
|
||||
// Handle frame drops, and only advance dt by a max of MAX_DELTA_TIME
|
||||
deltaTime = MIN(MAX_DELTA_TIME, currentTime - _animationCurrentTime);
|
||||
_t = _t + deltaTime;
|
||||
}
|
||||
|
||||
// We are using a fixed time step and a maximum number of iterations.
|
||||
// The following post provides a lot of thoughts into how to build this
|
||||
// loop: http://gafferongames.com/game-physics/fix-your-timestep/
|
||||
CGFloat TIMESTEP_MSEC = 1;
|
||||
// Velocity is based on seconds instead of milliseconds
|
||||
CGFloat step = TIMESTEP_MSEC / 1000;
|
||||
|
||||
NSInteger numSteps = floorf((currentTime - _animationCurrentTime) / step);
|
||||
// store the timestamp
|
||||
_animationCurrentTime = currentTime;
|
||||
if (numSteps == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGFloat position = _lastPosition;
|
||||
CGFloat velocity = _lastVelocity;
|
||||
CGFloat c = _damping;
|
||||
CGFloat m = _mass;
|
||||
CGFloat k = _stiffness;
|
||||
CGFloat v0 = -_initialVelocity;
|
||||
|
||||
CGFloat tempPosition = _lastPosition;
|
||||
CGFloat tempVelocity = _lastVelocity;
|
||||
CGFloat zeta = c / (2 * sqrtf(k * m));
|
||||
CGFloat omega0 = sqrtf(k / m);
|
||||
CGFloat omega1 = omega0 * sqrtf(1.0 - (zeta * zeta));
|
||||
CGFloat x0 = _toValue - _fromValue;
|
||||
|
||||
for (NSInteger i = 0; i < numSteps; ++i) {
|
||||
// This is using RK4. A good blog post to understand how it works:
|
||||
// http://gafferongames.com/game-physics/integration-basics/
|
||||
CGFloat aVelocity = velocity;
|
||||
CGFloat aAcceleration = _tension * (_toValue - tempPosition) - _friction * tempVelocity;
|
||||
tempPosition = position + aVelocity * step / 2;
|
||||
tempVelocity = velocity + aAcceleration * step / 2;
|
||||
|
||||
CGFloat bVelocity = tempVelocity;
|
||||
CGFloat bAcceleration = _tension * (_toValue - tempPosition) - _friction * tempVelocity;
|
||||
tempPosition = position + bVelocity * step / 2;
|
||||
tempVelocity = velocity + bAcceleration * step / 2;
|
||||
|
||||
CGFloat cVelocity = tempVelocity;
|
||||
CGFloat cAcceleration = _tension * (_toValue - tempPosition) - _friction * tempVelocity;
|
||||
tempPosition = position + cVelocity * step / 2;
|
||||
tempVelocity = velocity + cAcceleration * step / 2;
|
||||
|
||||
CGFloat dVelocity = tempVelocity;
|
||||
CGFloat dAcceleration = _tension * (_toValue - tempPosition) - _friction * tempVelocity;
|
||||
tempPosition = position + cVelocity * step / 2;
|
||||
tempVelocity = velocity + cAcceleration * step / 2;
|
||||
|
||||
CGFloat dxdt = (aVelocity + 2 * (bVelocity + cVelocity) + dVelocity) / 6;
|
||||
CGFloat dvdt = (aAcceleration + 2 * (bAcceleration + cAcceleration) + dAcceleration) / 6;
|
||||
|
||||
position += dxdt * step;
|
||||
velocity += dvdt * step;
|
||||
CGFloat position;
|
||||
CGFloat velocity;
|
||||
if (zeta < 1) {
|
||||
// Under damped
|
||||
CGFloat envelope = expf(-zeta * omega0 * _t);
|
||||
position =
|
||||
_toValue -
|
||||
envelope *
|
||||
((v0 + zeta * omega0 * x0) / omega1 * sinf(omega1 * _t) +
|
||||
x0 * cosf(omega1 * _t));
|
||||
// This looks crazy -- it's actually just the derivative of the
|
||||
// oscillation function
|
||||
velocity =
|
||||
zeta *
|
||||
omega0 *
|
||||
envelope *
|
||||
(sinf(omega1 * _t) * (v0 + zeta * omega0 * x0) / omega1 +
|
||||
x0 * cosf(omega1 * _t)) -
|
||||
envelope *
|
||||
(cosf(omega1 * _t) * (v0 + zeta * omega0 * x0) -
|
||||
omega1 * x0 * sinf(omega1 * _t));
|
||||
} else {
|
||||
CGFloat envelope = expf(-omega0 * _t);
|
||||
position = _toValue - envelope * (x0 + (v0 + omega0 * x0) * _t);
|
||||
velocity =
|
||||
envelope * (v0 * (_t * omega0 - 1) + _t * x0 * (omega0 * omega0));
|
||||
}
|
||||
|
||||
_lastPosition = position;
|
||||
|
@ -159,13 +167,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||
|
||||
[self onUpdate:position];
|
||||
|
||||
if (_animationHasFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Conditions for stopping the spring animation
|
||||
BOOL isOvershooting = NO;
|
||||
if (_overshootClamping && _tension != 0) {
|
||||
if (_overshootClamping && _stiffness != 0) {
|
||||
if (_fromValue < _toValue) {
|
||||
isOvershooting = position > _toValue;
|
||||
} else {
|
||||
|
@ -174,12 +178,12 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||
}
|
||||
BOOL isVelocity = ABS(velocity) <= _restSpeedThreshold;
|
||||
BOOL isDisplacement = YES;
|
||||
if (_tension != 0) {
|
||||
if (_stiffness != 0) {
|
||||
isDisplacement = ABS(_toValue - position) <= _restDisplacementThreshold;
|
||||
}
|
||||
|
||||
if (isOvershooting || (isVelocity && isDisplacement)) {
|
||||
if (_tension != 0) {
|
||||
if (_stiffness != 0) {
|
||||
// Ensure that we end up with a round value
|
||||
if (_animationHasFinished) {
|
||||
return;
|
||||
|
@ -190,6 +194,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||
if (_iterations == -1 || _currentLoop < _iterations) {
|
||||
_lastPosition = _fromValue;
|
||||
_lastVelocity = _initialVelocity;
|
||||
// Set _animationStartTime to -1 to reset instance variables on the next animation step.
|
||||
_animationStartTime = -1;
|
||||
_currentLoop++;
|
||||
[self onUpdate:_fromValue];
|
||||
} else {
|
||||
|
|
|
@ -266,19 +266,12 @@ static id RCTPropChecker(NSString *prop, NSNumber *value)
|
|||
XCTAssertEqual(observer.calls.count, 7UL);
|
||||
}
|
||||
|
||||
- (void)testSpringAnimation
|
||||
- (void)performSpringAnimationTestWithConfig:(NSDictionary*)config isCriticallyDamped:(BOOL)testForCriticallyDamped
|
||||
{
|
||||
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
||||
[_nodesManager startAnimatingNode:@1
|
||||
nodeTag:@1
|
||||
config:@{@"type": @"spring",
|
||||
@"friction": @7,
|
||||
@"tension": @40,
|
||||
@"initialVelocity": @0,
|
||||
@"toValue": @1,
|
||||
@"restSpeedThreshold": @0.001,
|
||||
@"restDisplacementThreshold": @0.001,
|
||||
@"overshootClamping": @NO}
|
||||
config:config
|
||||
endCallback:nil];
|
||||
|
||||
BOOL wasGreaterThanOne = NO;
|
||||
|
@ -299,7 +292,7 @@ static id RCTPropChecker(NSString *prop, NSNumber *value)
|
|||
}
|
||||
|
||||
// Verify that animation step is relatively small.
|
||||
XCTAssertLessThan(fabs(currentValue - previousValue), 0.1);
|
||||
XCTAssertLessThan(fabs(currentValue - previousValue), 0.12);
|
||||
|
||||
previousValue = currentValue;
|
||||
}
|
||||
|
@ -308,13 +301,45 @@ static id RCTPropChecker(NSString *prop, NSNumber *value)
|
|||
XCTAssertEqual(previousValue, 1.0);
|
||||
|
||||
// Verify that value has reached some maximum value that is greater than the final value (bounce).
|
||||
XCTAssertTrue(wasGreaterThanOne);
|
||||
if (testForCriticallyDamped) {
|
||||
XCTAssertFalse(wasGreaterThanOne);
|
||||
} else {
|
||||
XCTAssertTrue(wasGreaterThanOne);
|
||||
}
|
||||
|
||||
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
||||
[_nodesManager stepAnimations:_displayLink];
|
||||
[_uiManager verify];
|
||||
}
|
||||
|
||||
- (void)testUnderdampedSpringAnimation
|
||||
{
|
||||
[self performSpringAnimationTestWithConfig:@{@"type": @"spring",
|
||||
@"stiffness": @230.3,
|
||||
@"damping": @22,
|
||||
@"mass": @1,
|
||||
@"initialVelocity": @0,
|
||||
@"toValue": @1,
|
||||
@"restSpeedThreshold": @0.001,
|
||||
@"restDisplacementThreshold": @0.001,
|
||||
@"overshootClamping": @NO}
|
||||
isCriticallyDamped:NO];
|
||||
}
|
||||
|
||||
- (void)testCritcallyDampedSpringAnimation
|
||||
{
|
||||
[self performSpringAnimationTestWithConfig:@{@"type": @"spring",
|
||||
@"stiffness": @1000,
|
||||
@"damping": @500,
|
||||
@"mass": @3,
|
||||
@"initialVelocity": @0,
|
||||
@"toValue": @1,
|
||||
@"restSpeedThreshold": @0.001,
|
||||
@"restDisplacementThreshold": @0.001,
|
||||
@"overshootClamping": @NO}
|
||||
isCriticallyDamped:YES];
|
||||
}
|
||||
|
||||
- (void)testDecayAnimation
|
||||
{
|
||||
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
||||
|
@ -415,8 +440,9 @@ static id RCTPropChecker(NSString *prop, NSNumber *value)
|
|||
nodeTag:@1
|
||||
config:@{@"type": @"spring",
|
||||
@"iterations": @5,
|
||||
@"friction": @7,
|
||||
@"tension": @40,
|
||||
@"stiffness": @230.2,
|
||||
@"damping": @22,
|
||||
@"mass": @1,
|
||||
@"initialVelocity": @0,
|
||||
@"toValue": @1,
|
||||
@"restSpeedThreshold": @0.001,
|
||||
|
@ -440,7 +466,7 @@ static id RCTPropChecker(NSString *prop, NSNumber *value)
|
|||
|
||||
if (!didComeToRest) {
|
||||
// Verify that animation step is relatively small.
|
||||
XCTAssertLessThan(fabs(currentValue - previousValue), 0.1);
|
||||
XCTAssertLessThan(fabs(currentValue - previousValue), 0.12);
|
||||
}
|
||||
|
||||
// Test to see if it reset after coming to rest
|
||||
|
|
|
@ -73,7 +73,7 @@ class AnExChained extends React.Component<Object, any> {
|
|||
<Animated.Image
|
||||
{...handlers}
|
||||
key={i}
|
||||
source={{uri: CHAIN_IMGS[j]}}
|
||||
source={CHAIN_IMGS[j]}
|
||||
style={[styles.sticker, {
|
||||
transform: this.state.stickers[j].getTranslateTransform(), // simple conversion
|
||||
}]}
|
||||
|
@ -100,11 +100,11 @@ var styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
var CHAIN_IMGS = [
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpf1/t39.1997-6/p160x160/10574705_1529175770666007_724328156_n.png',
|
||||
'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xfa1/t39.1997-6/p160x160/851575_392309884199657_1917957497_n.png',
|
||||
'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xfa1/t39.1997-6/p160x160/851567_555288911225630_1628791128_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xfa1/t39.1997-6/p160x160/851583_531111513625557_903469595_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpa1/t39.1997-6/p160x160/851577_510515972354399_2147096990_n.png',
|
||||
require('../hawk.png'),
|
||||
require('../bunny.png'),
|
||||
require('../relay.png'),
|
||||
require('../hawk.png'),
|
||||
require('../bunny.png')
|
||||
];
|
||||
|
||||
module.exports = AnExChained;
|
||||
|
|
|
@ -429,7 +429,7 @@ exports.examples = [
|
|||
},
|
||||
},
|
||||
{
|
||||
title: 'translateX => Animated.spring',
|
||||
title: 'translateX => Animated.spring (bounciness/speed)',
|
||||
render: function() {
|
||||
return (
|
||||
<Tester type="spring" config={{bounciness: 0}}>
|
||||
|
@ -454,6 +454,32 @@ exports.examples = [
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'translateX => Animated.spring (stiffness/damping/mass)',
|
||||
render: function() {
|
||||
return (
|
||||
<Tester type="spring" config={{stiffness: 1000, damping: 500, mass: 3 }}>
|
||||
{anim => (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.block,
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
translateX: anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 100],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Tester>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'translateX => Animated.decay',
|
||||
render: function() {
|
||||
|
|
|
@ -24,14 +24,14 @@ import com.facebook.react.bridge.ReadableMap;
|
|||
private boolean mSpringStarted;
|
||||
|
||||
// configuration
|
||||
private double mSpringFriction;
|
||||
private double mSpringTension;
|
||||
private double mSpringStiffness;
|
||||
private double mSpringDamping;
|
||||
private double mSpringMass;
|
||||
private double mInitialVelocity;
|
||||
private boolean mOvershootClampingEnabled;
|
||||
|
||||
// all physics simulation objects are final and reused in each processing pass
|
||||
private final PhysicsState mCurrentState = new PhysicsState();
|
||||
private final PhysicsState mPreviousState = new PhysicsState();
|
||||
private final PhysicsState mTempState = new PhysicsState();
|
||||
private double mStartValue;
|
||||
private double mEndValue;
|
||||
// thresholds for determining when the spring is at rest
|
||||
|
@ -44,9 +44,11 @@ import com.facebook.react.bridge.ReadableMap;
|
|||
private double mOriginalValue;
|
||||
|
||||
SpringAnimation(ReadableMap config) {
|
||||
mSpringFriction = config.getDouble("friction");
|
||||
mSpringTension = config.getDouble("tension");
|
||||
mCurrentState.velocity = config.getDouble("initialVelocity");
|
||||
mSpringStiffness = config.getDouble("stiffness");
|
||||
mSpringDamping = config.getDouble("damping");
|
||||
mSpringMass = config.getDouble("mass");
|
||||
mInitialVelocity = config.getDouble("initialVelocity");
|
||||
mCurrentState.velocity = mInitialVelocity;
|
||||
mEndValue = config.getDouble("toValue");
|
||||
mRestSpeedThreshold = config.getDouble("restSpeedThreshold");
|
||||
mDisplacementFromRestThreshold = config.getDouble("restDisplacementThreshold");
|
||||
|
@ -65,6 +67,7 @@ import com.facebook.react.bridge.ReadableMap;
|
|||
}
|
||||
mStartValue = mCurrentState.position = mAnimatedValue.mValue;
|
||||
mLastTime = frameTimeMillis;
|
||||
mTimeAccumulator = 0.0;
|
||||
mSpringStarted = true;
|
||||
}
|
||||
advance((frameTimeMillis - mLastTime) / 1000.0);
|
||||
|
@ -97,7 +100,7 @@ import com.facebook.react.bridge.ReadableMap;
|
|||
private boolean isAtRest() {
|
||||
return Math.abs(mCurrentState.velocity) <= mRestSpeedThreshold &&
|
||||
(getDisplacementDistanceForState(mCurrentState) <= mDisplacementFromRestThreshold ||
|
||||
mSpringTension == 0);
|
||||
mSpringStiffness == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,31 +108,12 @@ import com.facebook.react.bridge.ReadableMap;
|
|||
* @return true if the spring is overshooting its target
|
||||
*/
|
||||
private boolean isOvershooting() {
|
||||
return mSpringTension > 0 &&
|
||||
return mSpringStiffness > 0 &&
|
||||
((mStartValue < mEndValue && mCurrentState.position > mEndValue) ||
|
||||
(mStartValue > mEndValue && mCurrentState.position < mEndValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* linear interpolation between the previous and current physics state based on the amount of
|
||||
* timestep remaining after processing the rendering delta time in timestep sized chunks.
|
||||
* @param alpha from 0 to 1, where 0 is the previous state, 1 is the current state
|
||||
*/
|
||||
private void interpolate(double alpha) {
|
||||
mCurrentState.position = mCurrentState.position * alpha + mPreviousState.position *(1-alpha);
|
||||
mCurrentState.velocity = mCurrentState.velocity * alpha + mPreviousState.velocity *(1-alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* advance the physics simulation in SOLVER_TIMESTEP_SEC sized chunks to fulfill the required
|
||||
* realTimeDelta.
|
||||
* The math is inlined inside the loop since it made a huge performance impact when there are
|
||||
* several springs being advanced.
|
||||
* @param time clock time
|
||||
* @param realDeltaTime clock drift
|
||||
*/
|
||||
private void advance(double realDeltaTime) {
|
||||
|
||||
if (isAtRest()) {
|
||||
return;
|
||||
}
|
||||
|
@ -143,87 +127,55 @@ import com.facebook.react.bridge.ReadableMap;
|
|||
|
||||
mTimeAccumulator += adjustedDeltaTime;
|
||||
|
||||
double tension = mSpringTension;
|
||||
double friction = mSpringFriction;
|
||||
double c = mSpringDamping;
|
||||
double m = mSpringMass;
|
||||
double k = mSpringStiffness;
|
||||
double v0 = -mInitialVelocity;
|
||||
|
||||
double position = mCurrentState.position;
|
||||
double velocity = mCurrentState.velocity;
|
||||
double tempPosition = mTempState.position;
|
||||
double tempVelocity = mTempState.velocity;
|
||||
double zeta = c / (2 * Math.sqrt(k * m ));
|
||||
double omega0 = Math.sqrt(k / m);
|
||||
double omega1 = omega0 * Math.sqrt(1.0 - (zeta * zeta));
|
||||
double x0 = mEndValue - mStartValue;
|
||||
|
||||
double aVelocity, aAcceleration;
|
||||
double bVelocity, bAcceleration;
|
||||
double cVelocity, cAcceleration;
|
||||
double dVelocity, dAcceleration;
|
||||
|
||||
double dxdt, dvdt;
|
||||
|
||||
// iterate over the true time
|
||||
while (mTimeAccumulator >= SOLVER_TIMESTEP_SEC) {
|
||||
/* begin debug
|
||||
iterations++;
|
||||
end debug */
|
||||
mTimeAccumulator -= SOLVER_TIMESTEP_SEC;
|
||||
|
||||
if (mTimeAccumulator < SOLVER_TIMESTEP_SEC) {
|
||||
// This will be the last iteration. Remember the previous state in case we need to
|
||||
// interpolate
|
||||
mPreviousState.position = position;
|
||||
mPreviousState.velocity = velocity;
|
||||
}
|
||||
|
||||
// Perform an RK4 integration to provide better detection of the acceleration curve via
|
||||
// sampling of Euler integrations at 4 intervals feeding each derivative into the calculation
|
||||
// of the next and taking a weighted sum of the 4 derivatives as the final output.
|
||||
|
||||
// This math was inlined since it made for big performance improvements when advancing several
|
||||
// springs in one pass of the BaseSpringSystem.
|
||||
|
||||
// The initial derivative is based on the current velocity and the calculated acceleration
|
||||
aVelocity = velocity;
|
||||
aAcceleration = (tension * (mEndValue - tempPosition)) - friction * velocity;
|
||||
|
||||
// Calculate the next derivatives starting with the last derivative and integrating over the
|
||||
// timestep
|
||||
tempPosition = position + aVelocity * SOLVER_TIMESTEP_SEC * 0.5;
|
||||
tempVelocity = velocity + aAcceleration * SOLVER_TIMESTEP_SEC * 0.5;
|
||||
bVelocity = tempVelocity;
|
||||
bAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity;
|
||||
|
||||
tempPosition = position + bVelocity * SOLVER_TIMESTEP_SEC * 0.5;
|
||||
tempVelocity = velocity + bAcceleration * SOLVER_TIMESTEP_SEC * 0.5;
|
||||
cVelocity = tempVelocity;
|
||||
cAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity;
|
||||
|
||||
tempPosition = position + cVelocity * SOLVER_TIMESTEP_SEC;
|
||||
tempVelocity = velocity + cAcceleration * SOLVER_TIMESTEP_SEC;
|
||||
dVelocity = tempVelocity;
|
||||
dAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity;
|
||||
|
||||
// Take the weighted sum of the 4 derivatives as the final output.
|
||||
dxdt = 1.0/6.0 * (aVelocity + 2.0 * (bVelocity + cVelocity) + dVelocity);
|
||||
dvdt = 1.0/6.0 * (aAcceleration + 2.0 * (bAcceleration + cAcceleration) + dAcceleration);
|
||||
|
||||
position += dxdt * SOLVER_TIMESTEP_SEC;
|
||||
velocity += dvdt * SOLVER_TIMESTEP_SEC;
|
||||
double velocity;
|
||||
double position;
|
||||
double t = mTimeAccumulator;
|
||||
if (zeta < 1) {
|
||||
// Under damped
|
||||
double envelope = Math.exp(-zeta * omega0 * t);
|
||||
position =
|
||||
mEndValue -
|
||||
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 spring
|
||||
double envelope = Math.exp(-omega0 * t);
|
||||
position = mEndValue - envelope * (x0 + (v0 + omega0 * x0) * t);
|
||||
velocity =
|
||||
envelope * (v0 * (t * omega0 - 1) + t * x0 * (omega0 * omega0));
|
||||
}
|
||||
|
||||
mTempState.position = tempPosition;
|
||||
mTempState.velocity = tempVelocity;
|
||||
|
||||
mCurrentState.position = position;
|
||||
mCurrentState.velocity = velocity;
|
||||
|
||||
if (mTimeAccumulator > 0) {
|
||||
interpolate(mTimeAccumulator / SOLVER_TIMESTEP_SEC);
|
||||
}
|
||||
|
||||
// End the spring immediately if it is overshooting and overshoot clamping is enabled.
|
||||
// Also make sure that if the spring was considered within a resting threshold that it's now
|
||||
// snapped to its end value.
|
||||
if (isAtRest() || (mOvershootClampingEnabled && isOvershooting())) {
|
||||
// Don't call setCurrentValue because that forces a call to onSpringUpdate
|
||||
if (tension > 0) {
|
||||
if (mSpringStiffness > 0) {
|
||||
mStartValue = mEndValue;
|
||||
mCurrentState.position = mEndValue;
|
||||
} else {
|
||||
|
|
|
@ -284,31 +284,14 @@ public class NativeAnimatedNodeTraversalTest {
|
|||
verifyNoMoreInteractions(valueListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpringAnimation() {
|
||||
public void performSpringAnimationTestWithConfig(JavaOnlyMap config, boolean testForCriticallyDamped) {
|
||||
createSimpleAnimatedViewWithOpacity(1000, 0d);
|
||||
|
||||
Callback animationCallback = mock(Callback.class);
|
||||
mNativeAnimatedNodesManager.startAnimatingNode(
|
||||
1,
|
||||
1,
|
||||
JavaOnlyMap.of(
|
||||
"type",
|
||||
"spring",
|
||||
"friction",
|
||||
7d,
|
||||
"tension",
|
||||
40.0d,
|
||||
"initialVelocity",
|
||||
0d,
|
||||
"toValue",
|
||||
1d,
|
||||
"restSpeedThreshold",
|
||||
0.001d,
|
||||
"restDisplacementThreshold",
|
||||
0.001d,
|
||||
"overshootClamping",
|
||||
false),
|
||||
config,
|
||||
animationCallback);
|
||||
|
||||
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
|
||||
|
@ -332,18 +315,76 @@ public class NativeAnimatedNodeTraversalTest {
|
|||
wasGreaterThanOne = true;
|
||||
}
|
||||
// verify that animation step is relatively small
|
||||
assertThat(Math.abs(currentValue - previousValue)).isLessThan(0.1d);
|
||||
assertThat(Math.abs(currentValue - previousValue)).isLessThan(0.12d);
|
||||
previousValue = currentValue;
|
||||
}
|
||||
// verify that we've reach the final value at the end of animation
|
||||
assertThat(previousValue).isEqualTo(1d);
|
||||
// verify that value has reached some maximum value that is greater than the final value (bounce)
|
||||
assertThat(wasGreaterThanOne);
|
||||
if (testForCriticallyDamped) {
|
||||
assertThat(!wasGreaterThanOne);
|
||||
} else {
|
||||
assertThat(wasGreaterThanOne);
|
||||
}
|
||||
reset(mUIImplementationMock);
|
||||
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||
verifyNoMoreInteractions(mUIImplementationMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnderdampedSpringAnimation() {
|
||||
performSpringAnimationTestWithConfig(
|
||||
JavaOnlyMap.of(
|
||||
"type",
|
||||
"spring",
|
||||
"stiffness",
|
||||
230.2d,
|
||||
"damping",
|
||||
22d,
|
||||
"mass",
|
||||
1d,
|
||||
"initialVelocity",
|
||||
0d,
|
||||
"toValue",
|
||||
1d,
|
||||
"restSpeedThreshold",
|
||||
0.001d,
|
||||
"restDisplacementThreshold",
|
||||
0.001d,
|
||||
"overshootClamping",
|
||||
false
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCriticallyDampedSpringAnimation() {
|
||||
performSpringAnimationTestWithConfig(
|
||||
JavaOnlyMap.of(
|
||||
"type",
|
||||
"spring",
|
||||
"stiffness",
|
||||
1000d,
|
||||
"damping",
|
||||
500d,
|
||||
"mass",
|
||||
3.0d,
|
||||
"initialVelocity",
|
||||
0d,
|
||||
"toValue",
|
||||
1d,
|
||||
"restSpeedThreshold",
|
||||
0.001d,
|
||||
"restDisplacementThreshold",
|
||||
0.001d,
|
||||
"overshootClamping",
|
||||
false
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpringAnimationLoopsFiveTimes() {
|
||||
createSimpleAnimatedViewWithOpacity(1000, 0d);
|
||||
|
@ -355,10 +396,12 @@ public class NativeAnimatedNodeTraversalTest {
|
|||
JavaOnlyMap.of(
|
||||
"type",
|
||||
"spring",
|
||||
"friction",
|
||||
7d,
|
||||
"tension",
|
||||
40.0d,
|
||||
"stiffness",
|
||||
230.2d,
|
||||
"damping",
|
||||
22d,
|
||||
"mass",
|
||||
1d,
|
||||
"initialVelocity",
|
||||
0d,
|
||||
"toValue",
|
||||
|
@ -370,7 +413,8 @@ public class NativeAnimatedNodeTraversalTest {
|
|||
"overshootClamping",
|
||||
false,
|
||||
"iterations",
|
||||
5),
|
||||
5
|
||||
),
|
||||
animationCallback);
|
||||
|
||||
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
|
||||
|
@ -403,7 +447,7 @@ public class NativeAnimatedNodeTraversalTest {
|
|||
}
|
||||
|
||||
// verify that an animation step is relatively small, unless it has come to rest and reset
|
||||
if (!didComeToRest) assertThat(Math.abs(currentValue - previousValue)).isLessThan(0.1d);
|
||||
if (!didComeToRest) assertThat(Math.abs(currentValue - previousValue)).isLessThan(0.12d);
|
||||
|
||||
|
||||
// record that the animation did come to rest when it rests on toValue
|
||||
|
|
Loading…
Reference in New Issue