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

Subtle tweaks to spring animations #2627

Merged
merged 1 commit into from
May 4, 2019
Merged

Subtle tweaks to spring animations #2627

merged 1 commit into from
May 4, 2019

Conversation

lorenbrichter
Copy link
Contributor

@lorenbrichter lorenbrichter commented Apr 30, 2019

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.

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.
@lorenbrichter
Copy link
Contributor Author

Demo here:
(top red box uses Verlet with soft & hard sets, bottom is existing)
https://svelte.dev/repl?version=3.1.0&gist=d4978c4d519cca6b2d63d033158d30e9

@Rich-Harris Rich-Harris merged commit 26d736f into sveltejs:master May 4, 2019
@Rich-Harris
Copy link
Member

This is fantastic, thank you so much! I don't pretend to fully understand the maths, but the results (and the diff) speak for themselves.

@205g0
Copy link

205g0 commented Apr 11, 2023

@lorenbrichter big fan of spring and its API (it's Svelte's killer feature). Just wondering if I can get something (a Promise?) once the spring settled.

@Rich-Harris Rich-Harris mentioned this pull request May 9, 2024
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants