Animating in React

 

Sarah Drasner

@sarah_edo : twitter || @sdras : codepen

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

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

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.

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

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.

  • 24,403