Animating in React
React Rally 2016
Sarah Drasner
let's start from the beginning.
what happens when you visit a website?
Creating Spatial Awareness
Saccade
why is this important?
We already use this information
- Websites start to all look the same
- Buttons on a website as CTAs are the most powerful thing
- Growth-hacking
- User-testing
Animation gets a bad rap.
how?
context-shifting
Helps with spatial awareness
Subtlety and consistency.
From this CSS-Tricks Article
This pen.
This pen.
anticipatory cues
Custom Loaders:
Viget did an experiment and found that despite some individual variation, novel loaders as a whole had a higher wait time and lower abandon rate than generic ones
22 sec
14 sec
If animation just feels like the sugar on top, that's because you treated it that way.
Tough love
Do you hate animation?
Right tool for the right job
Tools!
Dom/Virtual DOM
- Great for UI/UX animation
- Great for SVG that is resolution independent
- Easier to debug
Canvas
- Dance, pixels, dance!
- Great for really impressive 3d or immersive stuff
- Movement of a tons of objects
Pros
Pros
Cons
Cons
- Harder to make accessible
- Not resolution independent out of the box
- Breaks to nothing
- Tanks with a lot of objects
- ^ Because of this you have to care about the way you animate
SVG!
This pen
This pen.
Small filesize + performance
Compare to using text with photos to illustrate an article.
8KB Gzipped.
That Whole Animation and SVG was
Accessibility
Title and associative aria tags:
<svg aria-labelledby="title"
id="svg"
role="presentation"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 765 587">
<title id="title"
lang="en">
Icons that illustrate Global Warming Solutions
</title>
Title for elements in the SVG DOM
Role to let the screen reader know whether to traverse
This resource, with support charts.
This article by Heather Migliorisi.
Simplest example
//Icon Office
function IconOffice(props) {
//props and default props
const width = props.width || '100px'
const height = props.height || '200px'
const bookside = props.bookside || '#353f49'
const bookfront = props.bookfront || '#474f59'
const bookfill = props.bookfill || '#474f59'
return (
<svg className="office" xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
viewBox="0 0 188.5 188.5"
aria-labelledby="Office Icon"
>
<title>Office Icon</title>
<g className="cls-2">
<circle id="background" className="cls-3" cx="94.2" cy="94.2" r="94.2"/>
<path className="cls-4" d="M50.3 69.8h10.4v72.51H50.3z"/>
<path fill={bookside} d="M50.3 77.5h10.4v57.18H50.3z"/>
<path fill={bookfront} d="M60.7 77.5h38.9v57.19H60.7z"/>
.switcher .office {
#bulb { animation: switch 3s 4 ease both; }
#background { animation: fillChange 3s 4 ease both; }
}
@keyframes switch {
50% {
opacity: 1;
}
}
@keyframes fillChange {
50% {
fill: #FFDB79;
}
}
// App
function App() {
return (
<div>
<div className="switcher">
<IconOffice />
</div>
<IconOffice bookfill={200} bookside="#39B39B" bookfront="#76CEBD"/>
<IconOffice width="200" height="200"/>
</div>
)
}
Cheng-Lou React Europe 2015
<ReactCSSTransitionGroup
transitionName={ {
enter: 'enter',
enterActive: 'enterActive',
leave: 'leave',
leaveActive: 'leaveActive',
appear: 'appear',
appearActive: 'appearActive'
} }>
{item}
</ReactCSSTransitionGroup>
<ReactCSSTransitionGroup
transitionName={ {
enter: 'enter',
leave: 'leave',
appear: 'appear'
} }>
{item2}
</ReactCSSTransitionGroup>
Native Vanilla JS
- animationstart
- animationiteration
- animationend
Then what?
correct tools for the job
my recommendations:
CSS/SCSS
- Small sequences and simple interactions
- Once you get more than 3... switch to:
GSAP (GreenSock)
- Great for sequencing and complex movement
- Cross-browser consistency
React-Motion
- Great for single movements that you'd like to look realistic
- Snap.svg is more like jQuery for SVG
- Web Animations API looks great, still waiting on support
- Velocity is similar to GSAP with less bells and whistles
- Mo.js looks great and is still in beta
- D3.js was built for data vis but you can do a lot more with it
Staggers
- Can help with performance of many objects
- React-Motion is interruptible
- Same with setTimeout- the only leg-up on RAF
- GSAP has cycleStagger
- SCSS has staggers that are very performant, but not as great for interaction
This pen.
<StaggeredMotion
defaultStyles = {arr}
styles={this.getStyles}>
{lines =>
<div className="demo">
{lines.map(({x, y, rotate}, i) =>
<div
key={i}
className={`playthings s${i}`}
// we have to subtract half the amount of $amt in the
//CSS panel so that the mouse stays in the center of
//the object we're creating
style={{
WebkitTransform: `translate3d(${x - amtHalf}px, ${y - amtHalf}px, 0) rotate(${rotate}deg)`,
transform: `translate3d(${x - amtHalf}px, ${y - amtHalf}px, 0) rotate(${rotate}deg)`
}} />
)}
</div>
}
</StaggeredMotion>
Not all are created equal
- Opacity
- Transforms
- Hardware Acceleration
@mixin accelerate($name) {
will-change: $name;
transform: translateZ(0);
backface-visibility: hidden;
perspective: 1000px;
}
.foo {
@include accelerate(transform);
}
Hardware Acceleration vs CONTROL
this pen.
src: Wealthfront
Case Study: Netflix
Let's make some cool stuff.
This pen.
How did we use SVG?
Three ways:
- Directly inline in the HTML, for loops
- Background images, for things like taco
- Inline in React
const App = React.createClass({
getInitialState: function() {
return {
score: 500,
startTime: 0,
finalScore: 0
};
},
_startGame: function(setGameToStart) {
this.setState({ isPlaying: setGameToStart,
score: 500,
startTime: Date.now()
});
},
_updateScore: function(scoreDelta) {
let score = Math.min(Math.max(0, this.state.score + scoreDelta), 1270);
this.setState({ score: score });
if (score === 0 || score === 1270) {
this.setState({ isPlaying: false,
finalScore: 10000 - Math.round((Date.now() - this.state.startTime) / 30) });
}
},
...
render() {
return (
<div>
...
<HeartMeter score={this.state.score}/>
...
</div>
)
}
});
const HeartMeter = React.createClass({
render() {
return (
<div>
<svg className="heartmeter" xmlns="http://www.w3.org/2000/svg" width="250" height="50" viewBox="0 0 1741.8 395.6">
<path d="M1741.8 197.7c0 109.3-89 197.8-198.8 197.8a198.6 198.6 0 0 1-158.5-78.4H11.2A11.2 11.2 0 0 1 0 305.9V89.5a11.2 11.2 0 0 1 11.2-11.1h1373.4A198.8 198.8 0 0 1 1543 0c109.8 0 198.8 88.5 198.8 197.7z" fill="#000"/>
<path d="M1591.8 127c-18.3 0-34.1 14.8-41.4 30.3-7.3-15.5-23.1-30.3-41.4-30.3a45.7 45.7 0 0 0-45.7 45.5c0 51.1 51.8 64.5 87.1 115.1 33.4-50.2 87.1-65.6 87.1-115.1a45.7 45.7 0 0 0-45.7-45.5z" fill="#b29968"/>
<rect x="68.2" y="140.8" width={this.props.score} height="101.55" fill="#9391aa"/>
</svg>
</div>
)
}
});
How did we animate?
FOUR ways:
- GSAP looping in functions outside React
- RAF to detect collisions
- GSAP for keypress events
- Repeating callbacks for tacos and margaritas
Request Animation Frame
(function getCoords() {
let tCoords = tC1.getBoundingClientRect(),
txCoords = txC1.getBoundingClientRect(),
mCoords = mC1.getBoundingClientRect(),
elCoords = ele1.getBoundingClientRect();
function intersectRect(a, b) {
return Math.max(a.left, b.left + 40) < Math.min(a.right, b.right - 40) &&
Math.max(a.top, b.top + 40) < Math.min(a.bottom, b.bottom - 40);
}
// can't do if/else because sometimes they both come out at once
// and one of them will be ignored
if (intersectRect(tCoords, elCoords)) {
getHitTestIncrease();
}
if (intersectRect(txCoords, elCoords)) {
getHitTestDecrease();
}
if (intersectRect(mCoords, elCoords)) {
getHitTestMargarita();
}
requestAnimationFrame(getCoords);
}());
request animation frame
- Use instead of setInterval/setTimeout
- polyfill on NPM
- declarative- you don't decide the time delta
- before drawing next frame, execute logic
- maximize perf for inactive tabs- relieves resources
- js-stroll - learn what's happening under the hood
Bubble.prototype = {
init: function (x, y, vx, vy) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
},
update: function (dt) {
// friction opposes the direction of velocity
var acceleration = -Math.sign(this.vx) * friction;
// distance = velocity * time + 0.5 * acceleration * (time ^ 2)
this.x += this.vx * dt + 0.5 * acceleration * (dt ^ 2);
this.y += this.vy * dt + 0.5 * gravity * (dt ^ 2);
// velocity + acceleration * time
this.vy += gravity * dt;
this.vx += acceleration * dt;
this.circ.setAttribute("cx", this.x);
this.circ.setAttribute("cy", this.y);
this.circ.setAttribute("stroke", "rgba(1,146,190," + this.opacity + ")");
}
};
BT Dubs, this is d3 under the hood
(function animate(currentTime) {
var dt;
requestAnimationFrame(animate);
...
for (var i = 0; i < particles.length; i++) {
particles[i].update(dt);
...
}
}());
GSAP for Animation
_flyBy: function(el, amt, name, delay) {
if (this.props._isGameOver()) return;
...
TweenMax.fromTo(el, amt, {
rotation:0,
y:randY,
x:window.innerWidth + 200
}, {
x: -200,
y:randY,
rotation: 360,
delay: delay,
onComplete:this._flyBy,
onCompleteParams:[el, amt, name, delay],
ease: Power1.easeInOut
});
},
syntax
Solves Cross-Browser Inconsistencies
Bad transform origin bug on rotation, soon to be solved in Firefox.
More in this CSS-Tricks article.
Chrome
IE
Firefox
Safari (zoomed)
Timeline
- stack tweens
- set them a little before and after one another
- change their placement in time
- group them into scenes
- add relative labels
- animate the scenes!
- make the whole thing faster, move the placement of the whole scene, nesting
All without recalculation
The issue with longer CSS animations:
This pen.
_hitTestIncrease: function() {
if (!this.state.tacoPassed && this.props.isPlaying) {
//animation for wowie
let inWow = this.refs.inWow,
tl = new TimelineLite();
audioIncrease.play();
tl.fromTo(inWow, 0.4, {
autoAlpha: 0,
scale: 0.5
}, {
autoAlpha: 1,
scale: 1,
ease: Power4.easeOut
});
tl.to(inWow, 0.2, {
autoAlpha: 0,
scale: 0.5,
ease: Power2.easeIn
}, "+=0.3");
this.setState({ tacoPassed: true });
this.props._updateScore(75);
}
},
Why didn't we use React-Motion?
- More verbose
- There is no way to write a loop without writing an infinite loop
One more gotcha:
- Staggers can't create arrays with new number of objects
This pen.
getStyles(prevStyles) {
// we're using the previous style to update the next ones placement
const endValue = prevStyles.map((_, i) => {
let staggerStiff = 100, staggerDamp = 19;
return i === 0
? { opacity: spring(this.state.open ? 0 : 1, {stiffness: staggerStiff, damping: staggerDamp}) }
: { opacity: spring(this.state.open ? 0 : 1, {stiffness: (staggerStiff - (i * 7)), damping: staggerDamp + (i * 0.2)}) }
});
return endValue;
},
const pathData = ["M48.8,0A48.8,48.8,0,1,0,97.6,48.8,48.8,48.8,0,0,0,48.8,0Zm0,88.6A39.8,39.8,0,1,1,88.6,48.8,39.8,39.8,0,0,1,48.8,88.6Z",
"M48.8,9.1A39.8,39.8,0,1,0,88.6,48.8,39.8,39.8,0,0,0,48.8,9.1Zm0,67.9A28.1,28.1,0,1,1,77,48.8,28.1,28.1,0,0,1,48.8,77Z",
"M48.8,20.7A28.1,28.1,0,1,0,77,48.8,28.1,28.1,0,0,0,48.8,20.7Zm0,50.1a22,22,0,1,1,22-22A22,22,0,0,1,48.8,70.8Z",
"M48.8,26.8a22,22,0,1,0,22,22A22,22,0,0,0,48.8,26.8Zm0,39.1A17.1,17.1,0,1,1,66,48.8,17.1,17.1,0,0,1,48.8,66Z",
"M48.8,31.7A17.1,17.1,0,1,0,66,48.8,17.1,17.1,0,0,0,48.8,31.7Zm0,29A11.9,11.9,0,1,1,60.7,48.8,11.9,11.9,0,0,1,48.8,60.7Z",
"M48.8,36.9A11.9,11.9,0,1,0,60.7,48.8,11.9,11.9,0,0,0,48.8,36.9Zm0,19.9a8,8,0,1,1,8-8A8,8,0,0,1,48.8,56.8Z",
"M48.8,40.8a8,8,0,1,0,8,8A8,8,0,0,0,48.8,40.8Zm0,12.6a4.6,4.6,0,1,1,4.6-4.6A4.6,4.6,0,0,1,48.8,53.5Z",
"M48.8,44.2a4.6,4.6,0,1,0,4.6,4.6A4.6,4.6,0,0,0,48.8,44.2Zm0,7.1a2.5,2.5,0,1,1,2.5-2.5A2.5,2.5,0,0,1,48.8,51.3Z",
"M51.3 48.8c0 1.38-1.12 2.5-2.5 2.5s-2.5-1.12-2.5-2.5 1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5z"]
<svg className="circled circle-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 97.6 97.6">
<StaggeredMotion
defaultStyles = {arr}
styles={this.getStyles}>
{circ =>
<g fill={pathColor} className="cPath">
{circ.map(({opacity}, i) =>
<path
key={i}
d={pathData[i]}
className={`things s${i}`}
style={{ opacity: opacity }} />
)}
</g>}
</StaggeredMotion>
</svg>
Drawn SVG
Done with stroke-dasharray and stroke-dashoffset
- Path or shape has a stroke
- The stroke is dashed
- Use JS to .getTotalLength()
- Dasharray the whole length of the shape
- Animate dashoffset
@keyframes dash {
50% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -274;
}
}
<Motion style={{
//designate all of the differences in interpolated values in these ternary operators
...
dash: spring(this.state.compact ? 0 : 200),
...
}}>
{/* make sure the values are passed below*/}
{({..., dash, ...}) =>
<svg viewBox="0 0 803.9 738.1" aria-labelledby="title">
<title>React-Motion</title>
...
<g style={{ strokeDashoffset: `${dash}` }}
className="react-letters" data-name="react motion letters">
<path className="cls-5" d="M178.4,247a2.2,2.2,0,1,1-3.5,2.6l-6.5-8.7h-8.6v7.4a2.2,2.2,0,0,1-4.4,0V220.1a2.2,2.2,0,0,1,2.2-2.2h10.8a11.5,11.5,0,0,1,4.8,22Zm-18.6-10.3h8.6a7.3,7.3,0,0,0,0-14.7h-8.6v14.7Z" transform="translate(3.1 1.5)"/>
...
</g>
</svg>
}
</Motion>
Canvas
React-canvas
- Created by Flipboard to get 60 fps animations
- Mostly UI/UX
React-konva
- Meant for creation of pixels
- Beautiful syntax
- API Animation is a little janky
Or write Canvas in React :)
React-Konva
This pen.
React + VR =
Aframe-React
This repo
This demo
class App extends React.Component {
render () {
...
for (let i = 0; i < 30; i++) {
...
items.push(<Entity geometry="primitive: box; depth: 1.5; height: 1.5; width: 6"
material={{color: updateColor}}
position={randoPos}
pivot="0 0.5 0"
key={i}>
<Animation attribute="rotation"
dur="12000"
to={updateRot}/>
<Animation attribute="scale"
from="0 0 0" to="1 1 1"
dur="12000"
fill="both"
easing="ease-out"/>
</Entity>);
}
return (
<Scene>
<Camera><Cursor/></Camera>
<Sky/>
<Entity light={{type: 'ambient', color: '#888'}}/>
<Entity light={{type: 'directional', intensity: 0.5}} position={[-1, 1, 0]}/>
<Entity light={{type: 'directional', intensity: 1}} position={[1, 1, 0]}/>
{items}
</Scene>
);
}
}
Social Coding Sites Help You Learn
Fun. Remember fun?
(I don't work for them and they don't pay me)
- Codepen
- JS Fiddle
- Dabblet
More Technical Information:
Frontend Masters Course
Advanced SVG Animation
O'Reilly Book
SVG Animation
Thank You!
@sarah_edo on twitter
@sdras on codepen
These Slides:
Animating in React
By sdrasner
Animating in React
Too much to cover in 40 minutes! But we go over some theory, some comparison of tools, as well as the strengths of each. We do some fun things like go over making a game in React, working with SVG animation in React, and some VR. We also look under the hood a bit so that people know why things work the way that they do.
- 25,138