Innovative SVG Animations

 

Sarah Drasner

 

@sarah_edo : twitter || @sdras : codepen

Smashing Conf Freiburg

 

Sarah Drasner

This pen.

SVG!

  • Crisp on any display
  • Less HTTP requests to handle
  • Easily scalable for responsive
  • Small filesize if you design for performance
  • Easy to animate
  • Easy to make accessible
  • Using the web to its full potential
  • Fun! 

SVG!

This pen.

Flash is dead! Long live flash!

  • Interactive, immersive
  • Narrative
  • UI/UX Animations
  • Prototyping
  • Data Visualizations

 

SVG ANIMATION:

What is it good for?

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.

Before we get started:

Optimize!

illustrator: export as

(not save as)

Not all are created equal

  • Opacity
  • Transforms

This pen.

Narrative

From this CSS-Tricks Article

This pen.

Main Principle:

Design everything first, slowly unveil things.

Ugly storyboards save you time!

Tools!

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

This pen.

React-Motion

<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>

Let's make some cool stuff.

GreenSock/GSAP

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
    });
  },

This pen.

syntax

Solves Cross-Browser Inconsistencies

Bad transform origin bug on rotation, now 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

Other Cool Things

for Complex Animation

  • Motion along a path (widest support)
  • Draggable
  • CSS Properties
  • Draw SVG- make an SVG look like it draws itself.
  • MorphSVG
  • Relative Color Tweening
  • CycleStagger

DrawSVG

DrawSVG

Plugin for GSAP, very simple code.

TweenMax.staggerFromTo($draw, 4,{ drawSVG:'0' }, { drawSVG: true }, 0.1);

Under the Hood

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;
  }
}

This pen.

Motion Along a Path

Motion Along a Path for Realism

This pen.

TweenMax.to($firefly1, 6, {
bezier: {
  type:"soft", 
  values:[{x:10, y:30}, {x:-30, y:20}, {x:-40, y:10}, 
          {x:30, y:20}, {x:10, y:30}],
  autoRotate:true
},
ease:Linear.easeNone, repeat:-1}, "start+=3");
function displayVals() {
    //get the value from the dropdown
    var singleValues = $("#single").val();
    //the timeline for the changing animation
    tl.to($('.s2'), 1.75, {
      rotation: 360,
      bezier: {
        type: 'thru',
        values: bezier,
        curviness: singleValues
      },
      ease: Power1.easeInOut
    });
  }
  displayVals();

Cycle Stagger

This pen.

.container
  -(1..3).each do |i|
    %div{:class => ['unit', "unit#{i}"]}
      .shape
        .face.bm
        .face.tp
        -(1..14).each do |i|
          %div{:class => ['face', 'side', "s#{i}"]}

HAML

@mixin colors($max-count, $color-frequency){
  $color: 360/$max-count;
  
  @for $i from 1 through $max-count {
    .s#{$i} {
      background: hsl(90%, ($i - 1)*($color / $color-frequency), 40%);
    }
  }
}

SASS

var bP = $(".face"),
    unit = $(".unit"),
    tl = new TimelineLite();

tl.add("start");
tl.staggerFrom(bP, 3, {
  cycle:{
    backgroundColor:["#00a99d", "#0072bc", "#2e3192"],
    y:[1000, -500],
    x:[500, -500],
    opacity:[0.9, 0.7, 0.5],
    rotation: function(i) {
      return i * 25
    }
  }, 
  rotation: 360,
  ease:Circ.easeInOut
}, 0.006, "start");

JS

Relative Color Tweening

This pen.

Incrementing

//button hue
function hued() {
  var ch1 = "hsl(+=110%, +=0%, +=0%)", 
  tl = new TimelineMax({
    paused: true
  });

  tl.add("hu");
  tl.to(mult, 1.25, {
      fill: ch1
    }, "hu");
  ...
  tl.to(body, 1.25, {
      backgroundColor: ch1
    }, "hu");

  return tl;
}

var hue = hued();

Relative Color Tweening

This pen.

Draggable

Draggable to Control a Timeline Interaction

Draggable.create($gear, {
  type: "rotation",
  bounds: {
    minRotation: 0,
    maxRotation: 360
  },
  onDrag: function() {
    master.progress((this.rotation)/360 );
  }
});
Draggable.create(features, {
  edgeResistance: 0.65,
  type: "x,y",
  throwProps: true,
  autoScroll: true
});

This pen.

This pen.

Responsive

SVG Sprites

Start with this technique from Joe Harrison

Make it a Responsive SVG Animation Sprite

This pen.

Compare to using text with photos to illustrate an article.

8KB Gzipped.

That Whole Animation and SVG was

We can do stuff like this, All fully responsive in every direction

It doesn't neccessarily have to be fully fluid, either. Let's Implement some Responsive Design.

Like legos.

This pen.

function paintPanda() {
  var tl = new TimelineMax();

  tl.to(lh, 1, {
    scaleY: 1.2,
    rotation: -5,
    transformOrigin: "50% 0",
    ease: Circ.easeOut
  }, "paintIt+=1");
  ...

  return tl;
}

//create a timeline but initially pause it so that we can control it via click
var triggerPaint = new TimelineMax({
  paused: true
});
...

//this button kicks off the panda painting timeline
$("#button").on("click", function(e) {
  e.preventDefault();
  triggerPaint.restart();
});

...

Atmosphere

Elemental Motion

  • Further away is less contrast, blurry
  • Does the air or environment effect movement
  • Reducing precision allows for understanding

Putting Techniques Together

MorphSVG

  • Tween paths to paths
  • Tween shapes to paths
  • Make animation magic

Point from one id to another

TweenMax.to("#start", 1, {morphSVG:{shape:"#end"}, 
   ease:Linear.easeNone});

Use shapeIndex

TweenMax.to("#start", 1, {morphSVG:{shape:"#end", 
   shapeIndex:"1"}});
  • Default is shapeIndex: "auto"
  • Load the extra plugin, and a GUI will come up
  • Usually auto will be correct, but you can pick
  • Use findShapeIndex(#start, #end)

This pen.

How was this done?

MorphSVGPlugin.convertToPath("ellipse");

function flame() {
  var tl = new TimelineMax();

  tl.add("begin");
  tl.fromTo(blurNode, 2.5, {
    attr: {
      stdDeviation: 9
    }
  }, {
    attr: {
      stdDeviation: 3
    }
  }, "begin");
  var num = 9;
  for (var i = 1; i <= num; i++) {
    tl.to(fStable, 1, {
      morphSVG: {
        shape: "#f" + i
      },
      opacity: ((Math.random() * 0.7) + 0.7),
      ease: Linear.easeNone
    }, "begin+=" + i);
  }
 

More than one way of working

function solve(data) {

  var size = data.length;
  var last = size - 4;    

  var path = "M" + [data[0], data[1]];

  for (var i = 0; i < size - 2; i +=2) {

    var x0 = i ? data[i - 2] : data[0];
    var y0 = i ? data[i - 1] : data[1];

    var x1 = data[i + 0];
    var y1 = data[i + 1];

    var x2 = data[i + 2];
    var y2 = data[i + 3];

    var x3 = i !== last ? data[i + 4] : x2;
    var y3 = i !== last ? data[i + 5] : y2;

    var cp1x = (-x0 + 6 * x1 + x2) / 6;
    var cp1y = (-y0 + 6 * y1 + y2) / 6;

    var cp2x = (x1 + 6 * x2 - x3) / 6;
    var cp2y = (y1 + 6 * y2 - y3) / 6;
   
    path += "C" + [cp1x, cp1y, cp2x, cp2y, x2, y2];
  } 

  return path;
}

Catmull-Rom Spline

Article about history in computer science.

var poly = document.querySelector("polyline");
var path = document.querySelector("path");

var points = [
  100,350,  
  200,100,
  300,350,
  400,150,
  500,350,
  600,200,
  700,350
];

poly.setAttribute("points", points);
path.setAttribute("d", solve(points));
var points = [
  100,350,  
  200,150,
  300,350,
  400,120,
  500,350,
  600,180,
  700,350
];
var points = [
  100,350,  
  200,100,
  300,350,
  400,150,
  500,350,
  600,200,
  700,350
];

This pen.

viewbox as a camera

get bbox();

This pen.

Data Visualization

This pen.

FLOW CHART

var graphic = document.getElementById("graphic");

function moveChart(origin, end) {
  $(origin).on("click", function() {
    var moveTo = document.getElementById(end);
    var s = moveTo.getBBox();
    var amt = 75;
    newView = "" + (s.x - amt) + " " + (s.y - amt) + " " + (s.width + amt*2) + " " + (s.height + amt*2);
    TweenMax.to(graphic, 1.5, {
        attr: { viewBox: newView },
        ease:Circ.easeOut
      });
  });
}


moveChart(".start-yes", "variation");
moveChart(".start-no", "delorean");
//balloon
function balloonGrow(){
  var balloon = $("#balloon")[0];
  var radius = balloon.getAttribute("r");
  var cySet = balloon.getAttribute("cy");
  balloon.setAttribute('r', parseInt(radius) + 10);
  balloon.setAttribute('cy', parseInt(cySet) - 10);
  if (parseInt(radius) > 125) {
    ion.sound.play("balloon-pop3");
    var balloonTl = new TimelineMax();
    balloonTl.add("pop");
    balloonTl.to("#balloon", 0.05, {
      scale: 5,
      opacity: 0,
      transformOrigin: "50% 50%",
      ease: Circ.easeOut
    }, "pop");
    ...
    setTimeout(function(){
      balloon.setAttribute("r", "45");
      balloon.setAttribute("cy", "233.5");
    }, 200);
  }
}
//desktop keyboard
$(document).keydown(function(e) {
  switch(e.which) {
        
    case 70: // f
    //balloon
      balloonGrow();
      ion.sound.play("hit4");
    break;
        
    ...
          
    default: return; // exit this handler for other keys
  }
  e.preventDefault(); // prevent scrolling with arrows
});

Touch on Mobile

This pen.

People You Should Know About

  • Val Head
  • Sara Soueidan
  • Rachel Nabors
  • Tiffany Rayside
  • Chris Gannon
  • CJ Gammon
  • LegoMushroom
  • Ana Tudor
  • David Walsh Blog
  • Gregor Adams
  • Diaco ML
  • Amelia Bellamy-Royds
  • Taylor Hunt
  • Dudley Storey
  • GreenSock
  • Blake Bowen
  • I Hate Tomatoes (Petr Tichy)
  • Lucas Bebber
  • Rachel Smith
  • Joni Trythall
  • Jake Albaugh
  • Louis Hoegbregts

More!

SVG Immersion Podcast and Web Animation Weekly

More Technical Information:

Val head and I

Web animation workshops!

Launching this week!

🎉

O'Reilly Book

SVG Animation

Thank You!

Sarah Drasner

@sarah_edo on twitter

@sdras on codepen

These Slides:

slides.com/sdrasner/innovative

Innovative SVG Animations

By sdrasner

Innovative SVG Animations

Exploring many different techniques to create engaging and fun SVG animations, ranging from elemental motion to interaction.

  • 5,943