diff --git a/README.md b/README.md index 254f19dca..ed650d7bf 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Create unique animations and interactions with tweens, physics and input tracking. Popmotion is: -- **Tiny:** At ~9kb, it's 75% smaller than GreenSock. +- **Tiny:** At ~10kb, it's 75% smaller than GreenSock. - **Fast:** Stands up to popular alternatives in [performance tests](http://codepen.io/popmotion/pen/zNYXmR). - **Compatible:** Full browser support and preloaded with CSS, SVG and SVG path renderers. - **Composable:** Actions and output functions can be composed to [create complex motion systems](http://codepen.io/popmotion/pen/EZaPxZ). diff --git a/docs/api/action/spring.md b/docs/api/action/spring.md new file mode 100644 index 000000000..3294d59ae --- /dev/null +++ b/docs/api/action/spring.md @@ -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 )` + +## Props +- `stiffness `: The spring stiffness (default: `100`) +- `damping `: The strength of the friction force used to dampen motion (default: `10`) +- `mass `: Mass of the moving object. (default: `1.0`) +- `velocity `: The initial velocity of the spring. (default: `0.0`) +- `from `: Start from this number. (default `0`) +- `to `: End at this number. (default `0`) +- `restDisplacement `: End the animation if the distance to target is below this value (and `restSpeed`) (default: `0.01`) +- `restSpeed `: 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 +{` +const ball = document.querySelector('.ball'); +const ballRenderer = css(ball); + +spring({ + mass: 2, + stiffness: 1000, + damping: 50, + to: 300 +}).start(); +`} +``` diff --git a/packages/popmotion-react/stories/index.js b/packages/popmotion-react/stories/index.js index 280290caa..32d53a8bf 100644 --- a/packages/popmotion-react/stories/index.js +++ b/packages/popmotion-react/stories/index.js @@ -7,6 +7,7 @@ import { linkTo } from '@storybook/addon-links'; import DragSingleChild from './react/DragSingleChild'; import TransitionGroup from './react/TransitionGroup'; import Toggle from './popmotion/Toggle'; +import Spring from './popmotion/Spring'; import SpinnableDom from './spinnable/SpinnableDOM'; import SpinnableSvg from './spinnable/SpinnableSVG'; import Timeline from './timeline/Timeline'; @@ -37,5 +38,9 @@ storiesOf('timeline').add('Timeline stagger', () => ); // Draggable storiesOf('draggable').add('Drag XY', () => ); +// Spring +storiesOf('spring').add('Spring', () => ); + + // Inertia //storiesOf('inertia').add('Throw to inertia', () => ); diff --git a/packages/popmotion-react/stories/popmotion/Spring.js b/packages/popmotion-react/stories/popmotion/Spring.js new file mode 100644 index 000000000..180d21615 --- /dev/null +++ b/packages/popmotion-react/stories/popmotion/Spring.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { spring } from '../../../../lib/popmotion'; +import { MotionValue } from '../../src'; + +const makeSpring = (value, to) => spring({ + stiffness: 1000, + damping: 500, + mass: 3, + from: value.get(), + to, + velocity: value.getVelocity(), + onUpdate: value +}); + +export default () => ( + { + console.log(value.getVelocity()) + makeSpring(value, 800).start(); + }, + off: ({value}) => { + console.log(value.getVelocity()) + makeSpring(value, 0).start(); + } + }} + > + {({ v, state, setStateTo }) => ( +
+
+
+ )} + +); diff --git a/site/components/examples/Example.js b/site/components/examples/Example.js index 4555292eb..112ef3fa7 100644 --- a/site/components/examples/Example.js +++ b/site/components/examples/Example.js @@ -28,6 +28,7 @@ import { trackOffset, tween, stagger, + spring, value, Renderer, css, @@ -102,6 +103,7 @@ export default ({ children, template, id, isReactComponent=false }) => { pointer, trackOffset, tween, + spring, stagger, value, Renderer, diff --git a/site/templates/homepage/USPs.js b/site/templates/homepage/USPs.js index b8198a2e8..2d6177b23 100644 --- a/site/templates/homepage/USPs.js +++ b/site/templates/homepage/USPs.js @@ -70,7 +70,7 @@ export default () => (
Tiny
- At ~9kb, it's 75% smaller than Greensock TweenMax. Great for users on slower connections. + At ~10kb, it's 75% smaller than Greensock TweenMax. Great for users on slower connections.
Blazing Fast
diff --git a/src/actions/spring.js b/src/actions/spring.js new file mode 100644 index 000000000..d385634a6 --- /dev/null +++ b/src/actions/spring.js @@ -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, from } = this.props; + this.t = 0; + this.initialVelocity = velocity ? velocity / 1000 : 0.0; + this.isComplete = false; + this.delta = to - from; + } + + update() { + const { stiffness, damping, mass, from, to, restSpeed, restDisplacement } = this.props; + const { delta, initialVelocity } = this; + + const timeDelta = timeSinceLastFrame() / 1000; + 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(1.0 - (dampingRatio * dampingRatio)); + + const x0 = 1; + let oscillation = 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))); + this.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(-angularFreq * t); + oscillation = envelope * (x0 + (initialVelocity + (angularFreq * x0)) * t); + this.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(this.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); diff --git a/src/popmotion.js b/src/popmotion.js index 467a17574..9e828ca5f 100644 --- a/src/popmotion.js +++ b/src/popmotion.js @@ -31,6 +31,7 @@ export pointer from './actions/pointer'; export trackOffset from './actions/track-offset'; export tween from './actions/tween'; export stagger from './actions/stagger'; +export spring from './actions/spring'; export value from './actions/value'; // Renderers