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

Adding dedicated spring action #189

Merged
merged 4 commits into from
Aug 7, 2017
Merged

Adding dedicated spring action #189

merged 4 commits into from
Aug 7, 2017

Conversation

mattgperry
Copy link
Collaborator

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 and damping.

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

Copy link
Collaborator Author

@mattgperry mattgperry left a 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!

onStart() {
const { velocity, to } = this.props;
this.t = 0;
this.initialVelocity = velocity / 1000;
Copy link
Collaborator Author

@mattgperry mattgperry Aug 5, 2017

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

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

const { stiffness, damping, mass, from, to, restSpeed, restDisplacement } = this.props;
const { delta, initialVelocity } = this;

const timeDelta = timeSinceLastFrame() / 1000;
Copy link
Collaborator Author

@mattgperry mattgperry Aug 5, 2017

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

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


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
Collaborator Author

@mattgperry mattgperry Aug 5, 2017

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

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
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!

Copy link

@skevy skevy left a 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!

((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

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)

// Critically damped
} else {
const envelope = Math.exp(-expoDecay * t);
oscillation = envelope * (x0 + (initialVelocity + (expoDecay * x0)) * t);
Copy link

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


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

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;
Copy link

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.

onStart() {
const { velocity, to } = this.props;
this.t = 0;
this.initialVelocity = velocity / 1000;
Copy link

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

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