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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
47 changes: 47 additions & 0 deletions docs/api/action/spring.md
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>
```
5 changes: 5 additions & 0 deletions packages/popmotion-react/stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -37,5 +38,9 @@ storiesOf('timeline').add('Timeline stagger', () => <TimelineStagger />);
// Draggable
storiesOf('draggable').add('Drag XY', () => <DragXY />);

// Spring
storiesOf('spring').add('Spring', () => <Spring />);


// Inertia
//storiesOf('inertia').add('Throw to inertia', () => <Inertia />);
41 changes: 41 additions & 0 deletions packages/popmotion-react/stories/popmotion/Spring.js
Original file line number Diff line number Diff line change
@@ -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 () => (
<MotionValue
onStateChange={{
on: ({value}) => {
console.log(value.getVelocity())
makeSpring(value, 800).start();
},
off: ({value}) => {
console.log(value.getVelocity())
makeSpring(value, 0).start();
}
}}
>
{({ v, state, setStateTo }) => (
<div style={{ width: '100vw', height: '100vh' }} onClick={state === 'on' ? setStateTo.off : setStateTo.on}>
<div
style={{
background: 'red',
width: '100px',
height: '100px',
transform: 'translateX(' + v + 'px)'
}}
/>
</div>
)}
</MotionValue>
);
2 changes: 2 additions & 0 deletions site/components/examples/Example.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
trackOffset,
tween,
stagger,
spring,
value,
Renderer,
css,
Expand Down Expand Up @@ -102,6 +103,7 @@ export default ({ children, template, id, isReactComponent=false }) => {
pointer,
trackOffset,
tween,
spring,
stagger,
value,
Renderer,
Expand Down
2 changes: 1 addition & 1 deletion site/templates/homepage/USPs.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default () => (
</USP>
<USP>
<Header>Tiny</Header>
<Description>At ~9kb, it's <Strong>75% smaller</Strong> than Greensock TweenMax. Great for users on slower connections.</Description>
<Description>At ~10kb, it's <Strong>75% smaller</Strong> than Greensock TweenMax. Great for users on slower connections.</Description>
</USP>
<USP>
<Header>Blazing Fast</Header>
Expand Down
93 changes: 93 additions & 0 deletions src/actions/spring.js
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, 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;
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 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);
1 change: 1 addition & 0 deletions src/popmotion.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down