Subtle tweaks to spring animations #2627
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Use verlet-style integration for spring animations. Rather than keeping track of "value" and velocity over time, keep track of value and previous-value, and derive velocity from the delta every tick. This has a few benefits, including greater stability (position and velocity can't drift) and simplifying signature of tick_spring (no need to pass velocity back up).
Pulling "settled" flag out of the return signature as well means return value is just "next value", simplifying code mapping over objects and arrays, and eliminating duplicated code across get_initial_velocity, get_threshold and tick_spring.
Refactored "threshold" calcs, extremely inexpensive to do inline in tick_spring rather than create a parallel structure. Also fixes a rare pathological case where springs will never settle (reading through the code, could happen if velocity was non-zero during a set() where target==current, threshold will be calculated to be zero and settled will always be set to false, leading to infinite animations).
Functional changes:
In my experience dealing with spring animations, there are a handful of edge-cases where it is nice to have library support. 99% of the time, the only times you'd want to fudge 'stiffness' and 'damping' is during a live interaction (e.g. dragging something around). By providing an idiomatic mechanism hopefully the code around dealing with that could be simpler.
I propose an additional "options" parameter to 'set()' and 'update()'.
If passed {hard:true} the set will be considered a "hard" set, where you want the value to be set to the target value immediately. This could be extremely useful when implementing dragging for instance.
If passed {soft:true} or {soft:[duration]}, the set will be considered a "soft" set, where momentum will be preserved for some duration before settling. This could be useful when implementing "throwing", e.g. after a drag, on mouseup, 'soft set' to some position and the user's previous momentum will be honored before settling down.
Technically momentum preservation happens to a degree now, but aggressive stiffness and/or damping values make it nearly unapparent. This handles the case where you may want more aggressive or heavily underdamped springs but without the apparent velocity discontinuity that happens on throw. (As a real example, in FaceTime, note behavior when tossing around the picture-in-picture, or the iPhone X gestural behavior when tossing apps back to the home screen).
Internally this is implemented by temporarily setting mass to infinity and ramping back to normal over some duration.
"Hard sets" are also special-cased to trigger a same-frame set and fulfilment, leading to more responsive dragging. Best case is a one frame improvement in drag latency (noticed in Safari). This also handles the "old way" method of munging 'stiffness' and 'damping' to 1, so the improvement applies to existing code.