Add closed-form damped harmonic oscillator algorithm to Animated.spring#15322
Add closed-form damped harmonic oscillator algorithm to Animated.spring#15322skevy wants to merge 6 commits into
Conversation
@facebook-github-bot label Core Team @facebook-github-bot large-pr Attention: @janicduplessis Generated by 🚫 dangerJS |
|
Nice work! I'll have a closer look tomorrow, could you also add a test for the native implementations? Here are the tests for the other spring implementation: https://github.com/facebook/react-native/blob/master/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java#L265 and https://github.com/facebook/react-native/blob/master/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m#L269. |
|
@janicduplessis yah for sure. Sorry I missed them! |
3a852a1 to
0573ae8
Compare
|
Thanks @kmagiera for the thoughts. On analytic solution for velocity: I'll take a look at this further today. Just have to do the math. I think it's a reasonable ask. On replacing the current RK4 solution with DHO: I can play around with this. You're right that it theoretically should work, although, I also thought that I would be able to get the RK4 method to take mass into account by doing as you say -- treating tension as k, friction as c, and dividing On frame drops: I'm not sure how CASpringAnimation handles frame drops (probably not very common for it to ever encounter any, given that core animation runs out of process in a higher priority thread than the app). Re this particular implementation however, I've already added it to the JS and Android implementations: https://github.com/facebook/react-native/pull/15322/files#diff-f048d92ca0be679bc38d38147b311100R682 (JS) and https://github.com/facebook/react-native/pull/15322/files#diff-3205235300ac0a6c2dbcf7d2a7264117R161 (Android). This matches the behavior in the RK4 implementation. I didn't add it the iOS implementation, as I didn't see that we were accounting for frame drops anywhere else in the native Animated implementations, but maybe I should add the same logic here. |
|
|
@janicduplessis all sounds good. Worked on this a bit today. Hope to have a final version tomorrow |
|
This looks great. Quick note, I removed the "Core Team" label as we're currently using this label to track PRs that have already been assigned internally to a Facebook employee for further review. I wouldn't want to set the expectation that adding the label will automatically flag it for internal review - that process is still done manually. That said, if you want to get this reviewed by a FB employee, let me know! |
|
@janicduplessis @kmagiera this is ready for re-review. I've taken all of @kmagiera's comments and implemented them.
|
|
Awesome detective work :) @willbailey will likely be interested as he implemented rebound and the RK4 algorithm, I just copy pasted his implementation in RN |
|
As the others have said, fantastic work and sleuthing, @skevy! Would you be interested in breaking this out into its own library? Rebound is great, but it's encumbered by the PATENTS clause (for better or worse, many companies refuse to use any dependencies with a PATENTS clause). It's also 12K minified. If there was an accurate and unencumbered springs microlibrary, the JavaScript ecosystem would be better for it. Animated could wrap it for React Native, and we'd wrap it for material-motion. Please consider it. 😃 |
|
Nice work! (such math!) Out of curiosity, what is the use case for |
|
I think it is if you want your animation to not block InteractionManager.runAfterInteractions, could be useful for a looping animation. |
|
@appsforartists yah, I could take a crack at this. I'll DM you on twitter and we can chat about it. |
|
Not sure what your policy is on introducing external dependencies, but this algorithm now exists in its own repo: https://github.com/skevy/wobble/ Would be nice to have it in one canonical place, rather than two. |
957c77d to
67d47ed
Compare
|
@janicduplessis this is rebased now and ready for final review. Thanks for your patience! cc @kmagiera |
|
@appsforartists fwiw I decided to keep the implementation implemented separately here for now, given the fact that RN also contains Obj-C/Java implementations of the same algorithm, and I think it's reasonable to keep them colocated. Maybe we can change this in the future, but I think it's easier for now. |
|
|
||
| @Test | ||
| public void testSpringAnimation() { | ||
| public void performSpringAnimationTestWithConfig(JavaOnlyMap config, Boolean testForCriticallyDamped) { |
There was a problem hiding this comment.
Nit: boolean instead of Boolean
janicduplessis
left a comment
There was a problem hiding this comment.
Awesome work, just some small implementation details comments.
| position += dxdt * step; | ||
| velocity += dvdt * step; | ||
| let deltaTime = 0.0; | ||
| if (now > this._lastTime) { |
There was a problem hiding this comment.
Is this necesary? I don't see any case where now could be smaller than _lastTime.
| _toValue: any; | ||
| _tension: number; | ||
| _friction: number; | ||
| _stiffness: ?number; |
There was a problem hiding this comment.
Can we make _stiffness, _damping, _mass and _initialVelocity non-nullable now? Looks like they are initialized in every case.
| } | ||
| this._frameTime += deltaTime; | ||
|
|
||
| const c: number = this._damping || 0; |
There was a problem hiding this comment.
If we make those non-nullable we can remove the ||
|
|
||
| @end | ||
|
|
||
| const CGFloat MAX_DELTA_TIME = 0.064; |
There was a problem hiding this comment.
Let's use NSTimeInterval for time values
| NSInteger _iterations; | ||
| NSInteger _currentLoop; | ||
|
|
||
| CGFloat _t; // Current time (startTime + dt) |
| _animationStartTime = _animationCurrentTime = currentTime; | ||
|
|
||
| // calculate delta time | ||
| CFTimeInterval deltaTime; |
| const k: number = this._stiffness || 0; | ||
| const v0: number = -(this._initialVelocity || 0); | ||
|
|
||
| invariant(m > 0, 'Mass value must be greater than 0'); |
There was a problem hiding this comment.
Can we assert this in the ctor instead? If we do that we could also remove those asserts from the native implementations.
| <Animated.Image | ||
| {...handlers} | ||
| key={i} | ||
| source={{uri: CHAIN_IMGS[j]}} |
There was a problem hiding this comment.
Are these changes on purpose?
There was a problem hiding this comment.
Yah, but I can remove them if you want. It just puts some images in there so you can see the effect. The images that are currently in this list are broken links.
There was a problem hiding this comment.
Sure we can keep it, I assume the images were already there?
67d47ed to
02e6356
Compare
|
Thanks for working on this. I'll import it and let some internal tests run, and if all goes well, I'll land it on Monday. |
|
@hramos has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator. |
|
This is almost ready to land. I need to perform some manual steps before doing so. Stay tuned. |
| * | ||
| * We use the closed form of the second order differential equation: | ||
| * | ||
| * x'' + (2ζ⍵_0)x' + ⍵^2x = 0 |
There was a problem hiding this comment.
Our linter is complaining about these unicode characters (⍵, ζ, √). It may not be a big deal to import, as this is a comment and not executable code, but the check is probably there for a reason and may prevent issues with our current tooling.
Do you have any preference on how to format this instead? I could replace these with html entities, but that affects readability when looking at the code.
No need to edit your PR, I already have a copy of this PR checked out internally and I can apply any patch prior to landing.
There was a problem hiding this comment.
Hmm.
x'' + (2[zeta][omega]_0)x' + [omega]^2x = 0?
|
@hramos has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator. |

Motivation
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
CASpringAnimationwith the parameters: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.springimplementation, 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
CASpringAnimationdoes 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, and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is here).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
CASpringAnimationbehavior exactly, I needed to implement the analytical model (as is implemented inCASpringAnimation) inAnimated.Implementation
We add three new optional parameters to
Animated.spring(to both the JS and native implementations):stiffness, a value describing the spring's stiffness coefficientdamping, 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 springJust like if a developer were to specify
bounciness/speedandtension/frictionin the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown.Defaults forAnimated.springacross all three implementations (JS/iOS/Android) stay the same, so this is intended to be a non-breaking change.Ifstiffness,damping, ormassare provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (DHOas 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).Test Plan
Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester.