Skip to content

Commit 5bab467

Browse files
Adam Miskiewiczide
authored andcommitted
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 facebook#15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
1 parent ac4a214 commit 5bab467

File tree

11 files changed

+491
-310
lines changed

11 files changed

+491
-310
lines changed

Libraries/Animated/src/AnimatedImplementation.js

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,8 @@ module.exports = {
698698
*
699699
* - `velocity`: Initial velocity. Required.
700700
* - `deceleration`: Rate of decay. Default 0.997.
701+
* - `isInteraction`: Whether or not this animation creates an "interaction handle" on the
702+
* `InteractionManager`. Default true.
701703
* - `useNativeDriver`: Uses the native driver when true. Default false.
702704
*/
703705
decay,
@@ -712,21 +714,56 @@ module.exports = {
712714
* - `easing`: Easing function to define curve.
713715
* Default is `Easing.inOut(Easing.ease)`.
714716
* - `delay`: Start the animation after delay (milliseconds). Default 0.
717+
* - `isInteraction`: Whether or not this animation creates an "interaction handle" on the
718+
* `InteractionManager`. Default true.
715719
* - `useNativeDriver`: Uses the native driver when true. Default false.
716720
*/
717721
timing,
718722
/**
719-
* Spring animation based on Rebound and
720-
* [Origami](https://facebook.github.io/origami/). Tracks velocity state to
721-
* create fluid motions as the `toValue` updates, and can be chained together.
723+
* Animates a value according to an analytical spring model based on
724+
* [damped harmonic oscillation](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator).
725+
* Tracks velocity state to create fluid motions as the `toValue` updates, and
726+
* can be chained together.
722727
*
723-
* Config is an object that may have the following options. Note that you can
724-
* only define bounciness/speed or tension/friction but not both:
728+
* Config is an object that may have the following options.
729+
*
730+
* Note that you can only define one of bounciness/speed, tension/friction, or
731+
* stiffness/damping/mass, but not more than one:
732+
*
733+
* The friction/tension or bounciness/speed options match the spring model in
734+
* [Facebook Pop](https://github.com/facebook/pop), [Rebound](http://facebook.github.io/rebound/),
735+
* and [Origami](http://origami.design/).
725736
*
726737
* - `friction`: Controls "bounciness"/overshoot. Default 7.
727738
* - `tension`: Controls speed. Default 40.
728739
* - `speed`: Controls speed of the animation. Default 12.
729740
* - `bounciness`: Controls bounciness. Default 8.
741+
*
742+
* Specifying stiffness/damping/mass as parameters makes `Animated.spring` use an
743+
* analytical spring model based on the motion equations of a [damped harmonic
744+
* oscillator](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator).
745+
* This behavior is slightly more precise and faithful to the physics behind
746+
* spring dynamics, and closely mimics the implementation in iOS's
747+
* CASpringAnimation primitive.
748+
*
749+
* - `stiffness`: The spring stiffness coefficient. Default 100.
750+
* - `damping`: Defines how the spring’s motion should be damped due to the forces of friction.
751+
* Default 10.
752+
* - `mass`: The mass of the object attached to the end of the spring. Default 1.
753+
*
754+
* Other configuration options are as follows:
755+
*
756+
* - `velocity`: The initial velocity of the object attached to the spring. Default 0 (object
757+
* is at rest).
758+
* - `overshootClamping`: Boolean indiciating whether the spring should be clamped and not
759+
* bounce. Default false.
760+
* - `restDisplacementThreshold`: The threshold of displacement from rest below which the
761+
* spring should be considered at rest. Default 0.001.
762+
* - `restSpeedThreshold`: The speed at which the spring should be considered at rest in pixels
763+
* per second. Default 0.001.
764+
* - `delay`: Start the animation after delay (milliseconds). Default 0.
765+
* - `isInteraction`: Whether or not this animation creates an "interaction handle" on the
766+
* `InteractionManager`. Default true.
730767
* - `useNativeDriver`: Uses the native driver when true. Default false.
731768
*/
732769
spring,

Libraries/Animated/src/SpringConfig.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
'use strict';
1414

1515
type SpringConfigType = {
16-
tension: number,
17-
friction: number,
16+
stiffness: number,
17+
damping: number,
1818
};
1919

20-
function tensionFromOrigamiValue(oValue) {
20+
function stiffnessFromOrigamiValue(oValue) {
2121
return (oValue - 30) * 3.62 + 194;
2222
}
2323

24-
function frictionFromOrigamiValue(oValue) {
24+
function dampingFromOrigamiValue(oValue) {
2525
return (oValue - 8) * 3 + 25;
2626
}
2727

@@ -30,8 +30,8 @@ function fromOrigamiTensionAndFriction(
3030
friction: number,
3131
): SpringConfigType {
3232
return {
33-
tension: tensionFromOrigamiValue(tension),
34-
friction: frictionFromOrigamiValue(friction)
33+
stiffness: stiffnessFromOrigamiValue(tension),
34+
damping: dampingFromOrigamiValue(friction),
3535
};
3636
}
3737

@@ -91,8 +91,8 @@ function fromBouncinessAndSpeed(
9191
);
9292

9393
return {
94-
tension: tensionFromOrigamiValue(bouncyTension),
95-
friction: frictionFromOrigamiValue(bouncyFriction)
94+
stiffness: stiffnessFromOrigamiValue(bouncyTension),
95+
damping: dampingFromOrigamiValue(bouncyFriction),
9696
};
9797
}
9898

Libraries/Animated/src/__tests__/Animated-test.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ describe('Animated tests', () => {
135135
expect(callback).toBeCalled();
136136
});
137137

138-
it('send toValue when a spring stops', () => {
138+
it('send toValue when an underdamped spring stops', () => {
139139
var anim = new Animated.Value(0);
140140
var listener = jest.fn();
141141
anim.addListener(listener);
@@ -147,6 +147,18 @@ describe('Animated tests', () => {
147147
expect(anim.__getValue()).toBe(15);
148148
});
149149

150+
it('send toValue when a critically damped spring stops', () => {
151+
var anim = new Animated.Value(0);
152+
var listener = jest.fn();
153+
anim.addListener(listener);
154+
Animated.spring(anim, {stiffness: 8000, damping: 2000, toValue: 15}).start();
155+
jest.runAllTimers();
156+
var lastValue = listener.mock.calls[listener.mock.calls.length - 2][0].value;
157+
expect(lastValue).not.toBe(15);
158+
expect(lastValue).toBeCloseTo(15);
159+
expect(anim.__getValue()).toBe(15);
160+
});
161+
150162
it('convert to JSON', () => {
151163
expect(JSON.stringify(new Animated.Value(10))).toBe('10');
152164
});

Libraries/Animated/src/__tests__/AnimatedNative-test.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -594,12 +594,38 @@ describe('Native Animated', () => {
594594
jasmine.any(Number),
595595
{
596596
type: 'spring',
597-
friction: 16,
597+
stiffness: 679.08,
598+
damping: 16,
599+
mass: 1,
600+
initialVelocity: 0,
601+
overshootClamping: false,
602+
restDisplacementThreshold: 0.001,
603+
restSpeedThreshold: 0.001,
604+
toValue: 10,
605+
iterations: 1,
606+
},
607+
jasmine.any(Function)
608+
);
609+
610+
Animated.spring(anim, {
611+
toValue: 10,
612+
stiffness: 1000,
613+
damping: 500,
614+
mass: 3,
615+
useNativeDriver: true
616+
}).start();
617+
expect(nativeAnimatedModule.startAnimatingNode).toBeCalledWith(
618+
jasmine.any(Number),
619+
jasmine.any(Number),
620+
{
621+
type: 'spring',
622+
stiffness: 1000,
623+
damping: 500,
624+
mass: 3,
598625
initialVelocity: 0,
599626
overshootClamping: false,
600627
restDisplacementThreshold: 0.001,
601628
restSpeedThreshold: 0.001,
602-
tension: 679.08,
603629
toValue: 10,
604630
iterations: 1,
605631
},
@@ -612,12 +638,13 @@ describe('Native Animated', () => {
612638
jasmine.any(Number),
613639
{
614640
type: 'spring',
615-
friction: 23.05223140901191,
641+
damping: 23.05223140901191,
616642
initialVelocity: 0,
617643
overshootClamping: false,
618644
restDisplacementThreshold: 0.001,
619645
restSpeedThreshold: 0.001,
620-
tension: 299.61882352941177,
646+
stiffness: 299.61882352941177,
647+
mass: 1,
621648
toValue: 10,
622649
iterations: 1,
623650
},

0 commit comments

Comments
 (0)