Skip to content

Commit

Permalink
Add closed-form damped harmonic oscillator algorithm to Animated.spring
Browse files Browse the repository at this point in the history
  • Loading branch information
Adam Miskiewicz committed Sep 3, 2017
1 parent e964a7f commit 67d47ed
Show file tree
Hide file tree
Showing 11 changed files with 483 additions and 309 deletions.
39 changes: 34 additions & 5 deletions Libraries/Animated/src/AnimatedImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ 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 +713,49 @@ 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
29 changes: 25 additions & 4 deletions Libraries/Animated/src/__tests__/AnimatedNative-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -594,12 +594,32 @@ 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 @@ -612,12 +632,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

0 comments on commit 67d47ed

Please sign in to comment.