Skip to content

Adding dedicated spring action#189

Merged
mattgperry merged 4 commits into
masterfrom
spring
Aug 7, 2017
Merged

Adding dedicated spring action#189
mattgperry merged 4 commits into
masterfrom
spring

Conversation

@mattgperry

Copy link
Copy Markdown
Collaborator

This is a port of @skevy's React Animated PR react/react-native#15322

A "closed-form damped harmonic oscillator algorithm" simulates spring motion using stiffness, mass and damping.

This allows the creation of a great range of springs, with a smoother motion than our current (fast) physics approximation.

@mattgperry mattgperry left a comment

Copy link
Copy Markdown
Collaborator Author

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!

Comment thread src/actions/spring.js Outdated
onStart() {
const { velocity, to } = this.props;
this.t = 0;
this.initialVelocity = velocity / 1000;

@mattgperry mattgperry Aug 5, 2017

Copy link
Copy Markdown
Collaborator Author

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?

Copy link
Copy Markdown

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).

Copy link
Copy Markdown
Collaborator Author

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.

Comment thread src/actions/spring.js
const { stiffness, damping, mass, from, to, restSpeed, restDisplacement } = this.props;
const { delta, initialVelocity } = this;

const timeDelta = timeSinceLastFrame() / 1000;

@mattgperry mattgperry Aug 5, 2017

Copy link
Copy Markdown
Collaborator Author

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.

Copy link
Copy Markdown

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.

Copy link
Copy Markdown
Collaborator Author

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.

Comment thread 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)));

@mattgperry mattgperry Aug 5, 2017

Copy link
Copy Markdown
Collaborator Author

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?

Copy link
Copy Markdown

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).

Copy link
Copy Markdown
Collaborator Author

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!

@skevy skevy left a comment

Copy link
Copy Markdown

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!

Comment thread 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);

Copy link
Copy Markdown

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)

Comment thread src/actions/spring.js Outdated
// Critically damped
} else {
const envelope = Math.exp(-expoDecay * t);
oscillation = envelope * (x0 + (initialVelocity + (expoDecay * x0)) * t);

Copy link
Copy Markdown

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);

Copy link
Copy Markdown
Collaborator Author

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.

Comment thread 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)));

Copy link
Copy Markdown

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).

Comment thread src/actions/spring.js
const { stiffness, damping, mass, from, to, restSpeed, restDisplacement } = this.props;
const { delta, initialVelocity } = this;

const timeDelta = timeSinceLastFrame() / 1000;

Copy link
Copy Markdown

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.

Comment thread src/actions/spring.js Outdated
onStart() {
const { velocity, to } = this.props;
this.t = 0;
this.initialVelocity = velocity / 1000;

Copy link
Copy Markdown

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).

@mattgperry mattgperry merged commit 77269a7 into master Aug 7, 2017
@appsforartists

Copy link
Copy Markdown

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.

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