-
Notifications
You must be signed in to change notification settings - Fork 665
Adding dedicated spring action
#189
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
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| --- | ||
| title: Spring | ||
| description: A UI spring simulation, based on Apple's CASpringAnimation. | ||
| category: action | ||
| --- | ||
|
|
||
| # Spring | ||
|
|
||
| A highly-accurate spring simulation based on Apple's `CASpringAnimation` implementation. | ||
|
|
||
| This simulation offers greater variety and accuracy than the basic spring equations in [`physics`](/api/physics). | ||
|
|
||
| `spring(props <Object>)` | ||
|
|
||
| ## Props | ||
| - `stiffness <Number>`: The spring stiffness (default: `100`) | ||
| - `damping <Number>`: The strength of the friction force used to dampen motion (default: `10`) | ||
| - `mass <Number>`: Mass of the moving object. (default: `1.0`) | ||
| - `velocity <Number>`: The initial velocity of the spring. (default: `0.0`) | ||
| - `from <Number>`: Start from this number. (default `0`) | ||
| - `to <Number>`: End at this number. (default `0`) | ||
| - `restDisplacement <Number>`: End the animation if the distance to target is below this value (and `restSpeed`) (default: `0.01`) | ||
| - `restSpeed <Number>`: End the animation if the speed drops below this value (and `restDisplacement`) (default: `0.01`) | ||
|
|
||
| ## Methods | ||
|
|
||
| [...Action](/api/action) | ||
|
|
||
| ## Playground | ||
|
|
||
| ```javascript | ||
| import { spring } from 'popmotion'; | ||
| ``` | ||
|
|
||
| ```marksy | ||
| <Example template="Ball">{` | ||
| const ball = document.querySelector('.ball'); | ||
| const ballRenderer = css(ball); | ||
|
|
||
| spring({ | ||
| mass: 2, | ||
| stiffness: 1000, | ||
| damping: 50, | ||
| to: 300 | ||
| }).start(); | ||
| `}</Example> | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import React from 'react'; | ||
| import { spring } from 'popmotion'; | ||
| import { MotionValue } from '../../src'; | ||
|
|
||
| const springProps = { | ||
| stiffness: 10000, | ||
| damping: 10, | ||
| mass: 10, | ||
| velocity: 1000 | ||
| }; | ||
|
|
||
| export default () => ( | ||
| <MotionValue | ||
| onStateChange={{ | ||
| on: ({value}) => spring({ | ||
| from: value.get(), | ||
| to: 300, | ||
| ...springProps, | ||
| onUpdate: value | ||
| }).start(), | ||
| off: ({value}) => spring({ | ||
| from: value.get(), | ||
| to: 0, | ||
| ...springProps, | ||
| onUpdate: value | ||
| }).start() | ||
| }} | ||
| > | ||
| {({ v, state, setStateTo }) => ( | ||
| <div | ||
| style={{ | ||
| background: 'red', | ||
| width: '100px', | ||
| height: '100px', | ||
| transform: 'translateX(' + v + 'px)' | ||
| }} | ||
| onClick={state === 'on' ? setStateTo.off : setStateTo.on} | ||
| /> | ||
| )} | ||
| </MotionValue> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| /* | ||
| The closed-form damped harmonic oscillating spring. | ||
| Or, spring. | ||
|
|
||
| This is a direct port of Adam Miskiewicz's (@skevy) React Animated | ||
| PR #15322 https://github.com/facebook/react-native/pull/15322/ | ||
|
|
||
| ``` | ||
| spring({ | ||
| mass: 2, | ||
| damping: 10, | ||
| stiffness: 100, | ||
| to: 100 | ||
| }).start(); | ||
| ``` | ||
|
|
||
| Adam Miskiewicz: | ||
| @skevy (twitter.com/skevy, github.com/skevy) | ||
| */ | ||
| import Action from './'; | ||
| import { timeSinceLastFrame } from '../framesync'; | ||
|
|
||
| class Spring extends Action { | ||
| static defaultProps = { | ||
| stiffness: 100, | ||
| damping: 10, | ||
| mass: 1.0, | ||
| velocity: 0.0, | ||
| from: 0.0, | ||
| to: 0.0, | ||
| restSpeed: 0.01, | ||
| restDisplacement: 0.01 | ||
| }; | ||
|
|
||
| onStart() { | ||
| const { velocity, to } = this.props; | ||
| this.t = 0; | ||
| this.initialVelocity = velocity / 1000; | ||
| this.isComplete = false; | ||
| this.delta = to - this.from; | ||
| } | ||
|
|
||
| update() { | ||
| const { stiffness, damping, mass, from, to, restSpeed, restDisplacement } = this.props; | ||
| const { delta, initialVelocity } = this; | ||
|
|
||
| const timeDelta = timeSinceLastFrame() / 1000; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @skevy We divide by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We want
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| const t = this.t = this.t + timeDelta; | ||
|
|
||
| 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))); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @skevy I added 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 commentThe 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)...
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep fixing this sorted it, thanks! |
||
|
|
||
| const x0 = 1; | ||
| let oscillation = 0.0; | ||
| let velocity = 0.0; | ||
|
|
||
| // Underdamped | ||
| if (dampingRatio < 1) { | ||
| const envelope = Math.exp(-dampingRatio * angularFreq * t); | ||
| oscillation = envelope * (((initialVelocity + dampingRatio * angularFreq * x0) / expoDecay) * Math.sin(expoDecay * t) + (x0 * Math.cos(expoDecay * t))); | ||
| velocity = (envelope * ((Math.cos(expoDecay * t) * (initialVelocity + dampingRatio * angularFreq * x0)) - (expoDecay * x0 * Math.sin(expoDecay * t))) - | ||
| ((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 commentThe reason will be displayed to describe this comment to others. Learn more. This should be |
||
| oscillation = envelope * (x0 + (initialVelocity + (expoDecay * x0)) * t); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| velocity = envelope * ((t * initialVelocity * angularFreq) - (t * x0 * (angularFreq * angularFreq)) + initialVelocity); | ||
| } | ||
|
|
||
| const fraction = 1 - oscillation; | ||
| let position = from + fraction * delta; | ||
|
|
||
| // Check if simulation is complete | ||
| // We do this here instead of `isActionComplete` as it allows us | ||
| // to clamp to end during update) | ||
| const isBelowVelocityThreshold = Math.abs(velocity) <= restSpeed; | ||
| const isBelowDisplacementThreshold = Math.abs(to - position) <= restDisplacement; | ||
| this.isComplete = isBelowVelocityThreshold && isBelowDisplacementThreshold; | ||
|
|
||
| if (this.isComplete) { | ||
| position = to; | ||
| } | ||
|
|
||
| return position; | ||
| } | ||
|
|
||
| isActionComplete() { | ||
| return this.isComplete; | ||
| } | ||
| } | ||
|
|
||
| export default (props) => new Spring(props); | ||
Uh oh!
There was an error while loading. Please reload this page.
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 by1000the spring would jump to itstovalue, afterwards it worked nicely. The1000is 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
Animatedto 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 (tis in seconds, thusdt(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
velocitycalculation 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.