-
Notifications
You must be signed in to change notification settings - Fork 663
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
Adding dedicated spring
action
#189
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @skevy, I've added some questions on the algo in here. My maths is really rudimentary so apologises for any daft questions!
src/actions/spring.js
Outdated
onStart() { | ||
const { velocity, to } = this.props; | ||
this.t = 0; | ||
this.initialVelocity = velocity / 1000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@skevy Popmotion uses per-second measurements for velocity
. Before I divided by 1000
the spring would jump to its to
value, afterwards it worked nicely. The 1000
is an assumption that React Animated must be using per-millisecond velocities - is this correct, or is there a better way for me to convert per-second to the velocity expected by the simulation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's an example set of values you were using (before you divided v0 by 1000? I coded the velocity in Animated
to also be per second...for example I usually get values for velocity in the 0-10 (+/-) px/sec range. This whole function is calculated in per second values (t
is in seconds, thus dt
(velocity) is also per second).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been using your example of { stiffness: 1000, damping: 500, mass: 3 }
as a control, as I know that's a spring that's meant to look normal.
The velocity
calculation in this update loop does seem to output values +/-0-10 and it works fine if I feed this value back into the next spring (like React Animated).
However we use a standard velocity calculation across every action for interoperability: speedPerSecond(current - prev, timeDelta)
The numbers I've historically received from that are closer to +/-500-5000 unit/sec. This kind of magnitude makes more sense to me because if we're making a spring that moves from 0 - 800 and it takes ~ half a second to do so, then at it's fastest you're expecting a velocity of at least ~ 1600 px/sec rather than something in the 10s.
const { stiffness, damping, mass, from, to, restSpeed, restDisplacement } = this.props; | ||
const { delta, initialVelocity } = this; | ||
|
||
const timeDelta = timeSinceLastFrame() / 1000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@skevy We divide by 1000
here, this originally confused me as at first glance I thought the original PR was dividing by 1000
to get a ms
value. Then I realised it looks like you're dividing milliseconds by 1000? When I applied this, the spring worked! Is there a reason for this? I'm confused about the units here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want t
to be in units per sec. If were to just let this loop run with no animation inside of it and log t
, you'd see it just counts up by fractions of a second.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see - I naturally think about this value in milliseconds, this makes sense if t
is in seconds.
src/actions/spring.js
Outdated
|
||
const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass)); | ||
const angularFreq = Math.sqrt(stiffness / mass); | ||
const expoDecay = angularFreq * Math.sqrt(Math.abs(1.0 - (dampingRatio * dampingRatio))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@skevy I added Math.abs
here as with certain combinations of damping
and stiffness
(inc the 500/1000 in your original example) dampingRatio * dampingRatio
came out as more than 1.0
, which then lead to a negative number (throwing a NaN
with sqrt
)
This makes me think there's an error in my implementation, but I can't find it? Is this expected?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error is in your "critically damped" branch (I pointed it out below)...expoDecay
is only used when the spring is underdamped (in which case the math will work out correctly, because dampingRatio^2 will be less than one).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep fixing this sorted it, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some thoughts! Awesome to see this coming over to another lib!
src/actions/spring.js
Outdated
((dampingRatio * angularFreq * envelope) * ((((Math.sin(expoDecay * t) * (initialVelocity + dampingRatio * angularFreq * x0)) ) / expoDecay) + (x0 * Math.cos(expoDecay * t))))); | ||
// Critically damped | ||
} else { | ||
const envelope = Math.exp(-expoDecay * t); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be Math.exp(-angularFreq * t)
src/actions/spring.js
Outdated
// Critically damped | ||
} else { | ||
const envelope = Math.exp(-expoDecay * t); | ||
oscillation = envelope * (x0 + (initialVelocity + (expoDecay * x0)) * t); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be oscillation = envelope * (x0 + (initialVelocity + (angularFreq * x0)) * t);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doh! I was so concerned with finding an error up till that abs line that I didn't check later on. Thanks for your clear explanations, I'll take a look and make them amendments.
src/actions/spring.js
Outdated
|
||
const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass)); | ||
const angularFreq = Math.sqrt(stiffness / mass); | ||
const expoDecay = angularFreq * Math.sqrt(Math.abs(1.0 - (dampingRatio * dampingRatio))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error is in your "critically damped" branch (I pointed it out below)...expoDecay
is only used when the spring is underdamped (in which case the math will work out correctly, because dampingRatio^2 will be less than one).
const { stiffness, damping, mass, from, to, restSpeed, restDisplacement } = this.props; | ||
const { delta, initialVelocity } = this; | ||
|
||
const timeDelta = timeSinceLastFrame() / 1000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want t
to be in units per sec. If were to just let this loop run with no animation inside of it and log t
, you'd see it just counts up by fractions of a second.
src/actions/spring.js
Outdated
onStart() { | ||
const { velocity, to } = this.props; | ||
this.t = 0; | ||
this.initialVelocity = velocity / 1000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's an example set of values you were using (before you divided v0 by 1000? I coded the velocity in Animated
to also be per second...for example I usually get values for velocity in the 0-10 (+/-) px/sec range. This whole function is calculated in per second values (t
is in seconds, thus dt
(velocity) is also per second).
FWIW, this also exists as its own independent library: https://npmjs.com/package/wobble in case you're interested in sharing a dependency (and any improvements) with other motion libs vs. inlining it yourselves. |
This is a port of @skevy's React Animated PR facebook/react-native#15322
A "closed-form damped harmonic oscillator algorithm" simulates spring motion using
stiffness
,mass
anddamping
.This allows the creation of a great range of springs, with a smoother motion than our current (fast)
physics
approximation.