Skip to content

Add a "dampingRatio" config prop #678

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

Closed
aleclarson opened this issue May 27, 2019 · 7 comments
Closed

Add a "dampingRatio" config prop #678

aleclarson opened this issue May 27, 2019 · 7 comments
Labels
kind: request New feature or request that should be actioned

Comments

@aleclarson
Copy link
Contributor

aleclarson commented May 27, 2019

🚀 Feature Proposal

While at React Europe, I talked to @mtiller about how "damping ratio" is not the same as "friction" and, in fact, the concept of a damping ratio can be easier for users to understand. When the damping ratio is less than 1, it guarantees that your spring animation will never overshoot its target value. Likewise, when above 1, it guarantees overshooting will occur.

Note: I'm not suggesting we should remove friction, but you wouldn't be able to specify both dampingRatio and friction for the same animation.

Motivation

Make it easier to control whether or not overshooting occurs, without having to resort to using the clamp config prop.

Related: https://twitter.com/mtiller/status/1131676374840815617

@aleclarson aleclarson added the kind: request New feature or request that should be actioned label May 27, 2019
@mtiller
Copy link

mtiller commented May 27, 2019

Note that while you are at it, you can also tune things based on natural frequency rather than mass and stiffness. In this way, I suspect you’d have much more intuitive props (natural frequency and damping ratio).

A few more comments. You are probably too far along to deal with these, but:

  1. Friction involves rubbing of solids whereas damping is generally a fluid effect of some kind. Friction is generally non-linear while damping can be linear. Damping is really what you have here. I would avoid the term friction.
  2. The term tension was used during the presentation. Sorry to be pedantic, but tension is the term for forces that stretch things (making them longer than their neutral, unstretched length). What you really should say is force (which can be both tension or compression).

The library is very cool and I don’t really see any fundamental issues. These comments are really just to avoid confusion in terminology and suggestions for properties that might be more intuitive for users.

@aleclarson
Copy link
Contributor Author

@mtiller Is there any prior art that implements these concepts? What algorithm would you recommend? Let's say we want freq and damp config props. How can we process those on a frame-by-frame basis?

This is the current JavaScript implementation: https://github.com/react-spring/react-spring/blob/85d5fea07e3877a8873c14d305df2b246d705596/src/animated/FrameLoop.ts#L76-L109

Someone wrote a Rust version (except it uses RK4 physics): https://gist.github.com/aleclarson/dc2064b80d1615e19a75dcbbc0ed5f31

@mtiller
Copy link

mtiller commented Jun 2, 2019

@aleclarson So the basic skeleton here is reasonably sound. Let me point out a few things though.

Being pedantic, I'd write it more like this:

 const dt = 1; /** unit: milliseconds */
 const numSteps = Math.floor(time - lastTime)/dt;
 for (let i = 0; i < numSteps; ++i) { 
   let springForce = -config.stiffness * (position - to); 
   let dampingForce = -config.damping * velocity;
   let acceleration = (springForce + dampingForce) / config.mass 
   velocity = velocity + (acceleration * 1) *dt /1000;
   position = position + (velocity * 1) *dt/1000;

The key points are that you are implicitly using a "time step" of 1 millisecond. This isn't very clear from the way it is written, so I tried to make that more explicit. The nice thing here is that you could actually change it (by changing the value of dt). I also changed the names of force and damping to springForce and dampingForce because they are both really forces.

Now the important point is that you can keep the code the way it is if you want. Even if you adopt the changes I suggest, I'm really just renaming things and making stuff more explicit. The calculations should yield precisely the same results.

So now let me get to your essential question...how to represent this damping ratio and natural frequency stuff? How do you need to change those lines of code to handle that? The answers is you don't need to change that code. Instead, what you need is to have a (potentially optional) way of computing config.tension and config.friction. You see, the damping ratio is just 0.5*config.friction/sqrt(config.tensions*config.mass) and the natural frequency is just sqrt(config.tension/config.mass). So right now your users specify tension, friction and mass. But they could also specify tension, dampingRatio and naturalFrequency and you could then compute friction and mass from dampingRatio and naturalFrequency by using algebra to rearrange the terms. The key point is the "frame by frame" basis doesn't need to change if it is working well for you.

Regarding solver:

It isn't correct to say "RK4 physics". RK4 Runge-Kutta 4-order is a solution method. It is a way of taking the continuous mathematical relations (which are derived from the physics) and producing a solution that is represented as a sequence of numbers. What you are doing is a simple forward Euler solution method which is intuitive but has serious stability issue for "real" systems. In your case, this is a simple second order system so I don't think you'll run into any problems assuming your time steps are sufficiently small.

On that note, I should point out that you are currently using 1 millisecond time steps. But I suspect that is much finer than necessary. According to this (page 23, equation 23) you could probably get by with setting dt to 2/naturalFrequency. That will be much larger than 1 millisecond. You may get into trouble here with your units. Just keep in mind that all equations I'm using are apply for SI units (i.e., seconds). So since dt is in units of milliseconds, in that system dt could be evaluated as 2000/naturalFrequency. Now, this is the stability limit, and I don't think you want to be right at the limit. So I'd try something more like 500/naturalFrequency. However, there could be other unit conversion issues here so I'd be a bit careful about taking that as gospel. But I think you'd be ok there. This could save a lot of CPU time and yield pretty much the same results, FYI.

Clamping:

Hopefully, using a dampingRatio of 1 will help with your "clamping" concerns here. I presume these exist in cases where parameters have a limited domain (e.g., opacity being 0-1). In that case, I would suggest you get rid of the concept of clamping and actually implement what we would call a "hard stop" or "mechanical limit". It is actually quite easy to do, physical and fun. Let me explain.

Let's say you go past the min or max value. What you can do is simulate the behavior of a bouncing ball (shameless plug). What that means is that whenever you hit the limit, you set the position to the limit value (min or max, depending on which way you are going) but you reverse the value of the velocity and multiple it by what is called the "coefficient of restitution". This will trigger a "bounce" away from the mechanical limit. In this way, you never get a position value that is outside the domain you want.

But what if you want the "clamp" behavior? Easy...make the coefficient of restitution zero. Then you get a perfectly plastic collision which means the object just stops where it is. As long as your to value isn't outside your domain, you should be just fine.

@drcmda
Copy link
Member

drcmda commented Aug 10, 2019

react-spring initially had animated's approach, rk4 and the same configs, but i decided to go for react-motions configs and also exchanged rk4 for what seemed to me could be a little simpler impl (semi explicit euler i believe). as for the configs, i figured, react-motion is the biggest animation library in react, so i wanted users to get a familiar feel.

@mtiller going forward im not sure if changing everything would be a good way. i would suggest if we do it we add it incrementally. this is also how animated rolls, they always offered both ways, see: https://github.com/animatedjs/animated/blob/master/src/SpringConfig.js

some links that were of importance back then:

https://web.archive.org/web/20170606145659/http://gafferongames.com/game-physics/integration-basics/

https://web.archive.org/web/20160101081158/http://gafferongames.com/game-physics/fix-your-timestep/

i'm not versed in this stuff at all, i just care about ergonomics and most importantly speed. super high physics accuracy isn't that important, but even if an implementation is a split second faster, i'd take it.

it would be awesome if you could take a look and see if we can improve especially performance.

@dbismut
Copy link
Contributor

dbismut commented Aug 27, 2019

Hey, I've tried to implement the CASpringAnimation inspired algorithm from the wobble library in this PR.

https://github.com/react-spring/react-spring/blob/bd2e9600843192c34507e0f060f948446387d081/packages/core/src/FrameLoop.ts#L185-L239

I'm not sure about the impact, maybe @mtiller has an opinion on it?

@dbismut
Copy link
Contributor

dbismut commented Aug 29, 2019

Adding these slides to the conversation which make a good job at explaining the different methods for beginners like me!

http://box2d.org/files/GDC2015/ErinCatto_NumericalMethods.pdf

aleclarson pushed a commit to aleclarson/react-spring that referenced this issue Sep 1, 2019
- Calculate velocity for "decay" and "easing" animations
- When "config.clamp" is a number, it becomes the coefficient of restitution
- Account for dropped frames in "decay" and "easing" animations
- Fixed "config.velocity" to be per ms instead of per sec
- Implement variable timesteps as described in pmndrs#678
- Bounce animations can be done with "config.clamp" > 0
aleclarson pushed a commit to aleclarson/react-spring that referenced this issue Sep 2, 2019
- Calculate velocity for "decay" and "easing" animations
- When "config.clamp" is a number, it becomes the coefficient of restitution
- Account for dropped frames in "decay" and "easing" animations
- Fixed "config.velocity" to be per ms instead of per sec
- Implement variable timesteps as described in pmndrs#678
- Bounce animations can be done with "config.clamp" > 0
aleclarson pushed a commit to aleclarson/react-spring that referenced this issue Sep 2, 2019
- Calculate velocity for "decay" and "easing" animations
- When "config.clamp" is a number, it becomes the coefficient of restitution
- Account for dropped frames in "decay" and "easing" animations
- Fixed "config.velocity" to be per ms instead of per sec
- Implement variable timesteps as described in pmndrs#678
- Bounce animations can be done with "config.clamp" > 0
aleclarson pushed a commit to aleclarson/react-spring that referenced this issue Sep 2, 2019
- Calculate velocity for "decay" and "easing" animations
- When "config.clamp" is a number, it becomes the coefficient of restitution
- Account for dropped frames in "decay" and "easing" animations
- Fixed "config.velocity" to be per ms instead of per sec
- Implement variable timesteps as described in pmndrs#678
- Bounce animations can be done with "config.clamp" > 0
aleclarson pushed a commit to aleclarson/react-spring that referenced this issue Sep 6, 2019
- Calculate velocity for "decay" and "easing" animations
- When "config.clamp" is a number, it becomes the coefficient of restitution
- Account for dropped frames in "decay" and "easing" animations
- Fixed "config.velocity" to be per ms instead of per sec
- Implement variable timesteps as described in pmndrs#678
- Bounce animations can be done with "config.clamp" > 0
- The default value of "config.precision" is now derived from the distance between "to" and "from"
aleclarson pushed a commit that referenced this issue Sep 27, 2019
- Calculate velocity for "decay" and "easing" animations
- When "config.clamp" is a number, it becomes the coefficient of restitution
- Account for dropped frames in "decay" and "easing" animations
- Fixed "config.velocity" to be per ms instead of per sec
- Implement variable timesteps as described in #678
- Bounce animations can be done with "config.clamp" > 0
- The default value of "config.precision" is now derived from the distance between "to" and "from"
aleclarson pushed a commit that referenced this issue Oct 2, 2019
- Calculate velocity for "decay" and "easing" animations
- When "config.clamp" is a number, it becomes the coefficient of restitution
- Account for dropped frames in "decay" and "easing" animations
- Fixed "config.velocity" to be per ms instead of per sec
- Implement variable timesteps as described in #678
- Bounce animations can be done with "config.clamp" > 0
- The default value of "config.precision" is now derived from the distance between "to" and "from"
@aleclarson
Copy link
Contributor Author

Added in #808

  • feat: config.frequency and config.damping props
  • feat: config.bounce prop

aleclarson pushed a commit that referenced this issue May 3, 2020
- Calculate velocity for "decay" and "easing" animations
- When "config.clamp" is a number, it becomes the coefficient of restitution
- Account for dropped frames in "decay" and "easing" animations
- Fixed "config.velocity" to be per ms instead of per sec
- Implement variable timesteps as described in #678
- Bounce animations can be done with "config.clamp" > 0
- The default value of "config.precision" is now derived from the distance between "to" and "from"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind: request New feature or request that should be actioned
Projects
None yet
Development

No branches or pull requests

4 participants