I want to understand how spring animation works so I can replicate it on Android as a developer. I have already read documentations on Figma and this blog post How Figma put the bounce in spring animations
But the animation duration does not match my understanding of mass, spring, damper physics Mass-spring-damper model - Wikipedia. The expected animation duration should be much longer given the parameters stiffness, damping, mass.
For example, in the following screenshot, the duration is 400ms using stiffness = 80 damping = 30 and mass = 1.
But when the differential equations for mass, spring, damper is solved (see screenshot below using matlab and laplace transform method), it gives an approximate animation duration of 1.3615s, a much longer time than what is designed on figma.
So what is going on within figma? What mathematically model is used to calculate the animation duration? How can I implement the exact animation on android (or other platforms) provided by design?
And it sounds like (based on the blog post) they did additional tweaking so it may be tough to exactly map what you see in Figma to an implementation in Android.
But maybe you can ping Ricky or Willy on Twitter or Discord and they could give you a definite answer. I’m just guessing here.
From what I can tell, the model in WebKit SpringSolver is a classic model of mass, spring, damper, with the same definition of w0 (w_n in wikipedia) and zeta
How can I reach the devs on Discord? I think it’s also good to have a solution posted here for the benefit of the community.
Also it would be very important in general to be able to reliably translate design on figma to code, whether it’s android or other platforms, otherwise, what’s the point of designing on figma.
From there, you want to make sure to pick up the “Developers” interest role which will give you access to the dev-related channels. I’m sure one of the design/developer advocates could help point you in the right direction, especially since this relates to ensuring good handoff from design to development.
And thanks for looking into that WebKit code. It was hard for me to see how different/same it was to the wiki math page.
I’m waiting for a reply from discord. In the meanwhile, I noticed that the WebKit code doesn’t implement the all the possible spring mass damper configurations. It’s specifically commented in that code
// Critically damped (ignoring over-damped case for now).
So, if figma is taking that code directly, it would also not implement some configurations correctly.
Building spring animations from scratch can be pretty tricky. That’s why Android has a built-in spring library to help you with this!
You can configure it with two values:
Stiffness - This can be grabbed from Figma directly. In your case this is 80. Damping Ratio - This can be calculated based on the values in Figma with the formula: damping / (2 * Math.sqrt(stiffness * mass)). We don’t show this to you currently, but maybe we should in the inspect panel. In your case, this comes out to 1.677, meaning it’s an overdamped spring. @Jiahao_Zhang is correct that Figma doesn’t handle overdamped springs, and instead limits the damping ratio to 1. To get the Android animation to look like Figma, you should use 1.
Note that it does NOT require duration! Why is this?
The long answer:
Duration is a tricky subject when it comes to springs. Unlike traditional bezier or keyframed animations, springs theoretically approach but never reach their final value.
The step time you see in matlab is based on an approach of 2% to the final value, in this case the time it takes to reach amplitude 0.98. If you were making an animation that travels 1000px and cut the animation off at 1.3615 seconds, it would stop 20px short of the final position, visibly snapping to the user. Not good.
Internally at Figma, we use a much tighter tolerance of 0.1%, meaning that an 1000px animation would stop within 1px of the final position. Most of this animation time isn’t actually “meaningful” animation as the object moves fairly little at the end of the long tail, but it’s necessary to ensure smoothness and avoid little janks and hiccups.
We don’t expose this duration directly because it’s an implementation detail. Other spring libraries such as React Spring and Framer Motion use a more dynamic system that looks at the object’s position and velocity to determine if the animation should end. In effect, objects that move shorter distances can stop animating earlier, saving CPU but breaking the notion of specifying a set duration. For this reason, most spring libraries don’t actually take in duration, but instead other, more fundamental constants such as Mass, Stiffness, Damping, and Damping Ratio.
But what is the duration you do see in Figma? In early prototypes, we surfaced the 0.1% duration to designers. However, it wasn’t very helpful because durations calculated this way tended to be seemingly random numbers (like 1361ms) and really really large, much longer than the animation “appeared” to take. This made it harder to share intuition between spring and bezier animations; for context, a 1000ms bezier animation would be considered really long. To address this, the duration you see today is carefully tuned constant to produce more “meaningful” and round numbers within the ballpark of bezier animations, even though the animation doesn’t technically end there.
Thanks @William_Wu for the info. I largely understand what you are trying to explain.
I see that the animations for underdamped and critically damped springs appear correct on figma. But as you confirmed Figma doesn’t handle overdamped springs, and instead limits the damping ratio to 1. Is that a design choice to not support it? Or is it a bug on figma that should be corrected in the future? I wonder if there should be a big red warning box when designers enter parameters that result in an overdamped configuration or something that prevents it from being configured.
Other spring libraries such as React Spring and Framer Motion use a more dynamic system that looks at the object’s position and velocity to determine if the animation should end.
right, to add to that, android also uses distance relative to final position and velocity to determine the end of animation.
The duration is definitely a bit wonky in the overdamped situation. I think we mainly focused on underdamped and critically damped due to scoping - we thought that overdamped wouldn’t really get used that often, also because the Webkit library didn’t support it. It’d add more math and complexity to the product.
No promises on future support for overdamped springs, but fingers crossed that for the design you’re working on, you can get a similar looking animation with a critically damped spring with a longer duration.