Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add closed-form damped harmonic oscillator algorithm to Animated.spring #15322

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions Libraries/Animated/src/AnimatedImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
16 changes: 8 additions & 8 deletions Libraries/Animated/src/SpringConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -30,8 +30,8 @@ function fromOrigamiTensionAndFriction(
friction: number,
): SpringConfigType {
return {
tension: tensionFromOrigamiValue(tension),
friction: frictionFromOrigamiValue(friction)
stiffness: stiffnessFromOrigamiValue(tension),
damping: dampingFromOrigamiValue(friction),
};
}

Expand Down Expand Up @@ -91,8 +91,8 @@ function fromBouncinessAndSpeed(
);

return {
tension: tensionFromOrigamiValue(bouncyTension),
friction: frictionFromOrigamiValue(bouncyFriction)
stiffness: stiffnessFromOrigamiValue(bouncyTension),
damping: dampingFromOrigamiValue(bouncyFriction),
};
}

Expand Down
14 changes: 13 additions & 1 deletion Libraries/Animated/src/__tests__/Animated-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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');
});
Expand Down
35 changes: 31 additions & 4 deletions Libraries/Animated/src/__tests__/AnimatedNative-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -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,
},
Expand Down
Loading