Vue

Animating

Keynote

Sarah Drasner

@sarah_edo

Consultant

CSS-Tricks, IBM, Microsoft, Salesforce
Smashing Magazine, NetMag, Zillow, Workflo,
O’Reilly, Frontend Masters, & Mule Design

      Microsoft

Sr. Cloud
Developer Advocate

Why Animate?

Our story starts with performance.

The "so what" factor

User attention span is short.

2 seconds

until dropoff

Amazon has discovered that for every one second delay, conversions dropped by 7%. If you sell $100k per day, that’s an annual loss of $2.5m.

Walmart has found that it gains 1% revenue increase for every 100ms of improvement.

Over 4 seconds: HORROR

Perceived Performance

Humans over-estimate passive waits by 36% - Eli Fitch and Richard Larson, MIT

Your benchmarks aren't telling you the full story.

Custom Experience:

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

Creating Spatial Awareness

Saccade

“We’ve evolved to perform actions that flow more or less seamlessly.

 

"We aren’t wired to deal with the fits and starts of human-computer interaction.”

Sensory memory: Your occipital lobe (AKA “the memory store”) works in 100ms bursts.

-Tammy Everts

Gain understanding

Spatial or otherwise

Without Transitions

Paul Bakaus

Morphing

From this CSS-Tricks Article

this pen.

Interruption

Start with the end

If you know the end, you can figure out what comes in between

So Many Ways!

  • How to work with Vue
  • <transition /> component
  • Watchers/Reactivity
  • SVG!
  • Event driven animation with user input
  • Coordinating state with Vuex/ lifecycle methods
  • Animations via custom directives
  • Nuxt, server-side rendering, and page transitions

Vue Basics

Tiny Comparison

  • A Virtual DOM
  • Reactive components that offer the View layer only
  • Props and a Redux-like store similar to React.
  • Conditional rendering, and services, similar to Angular.
  • Inspired by Polymer for simplicity and performance, Vue offers a similar development style as HTML, styles, and JavaScript are composed in tandem.

see the comparison

Hello World!

Obligatory Example

<div id="app">{{ text }} Nice to meet Vue.</div>

Light Comparison:

Vanilla JS vs Vue for Conditional Rendering

Vanilla JS

const items = [
  'thingie',
  'another thingie',
  'lots of stuff',
  'yadda yadda'
];
function listOfStuff() {
  let full_list = '';
  for (let i = 0; i < items.length; i++) {
      full_list = full_list + `<li> ${items[i]} </li>`
  }
  const contain = document.querySelector('#container');
  contain.innerHTML = `<ul> ${full_list} </ul>`;     
}
listOfStuff();

HTML:

<div id="container"></div>

yields:

This pen

Vue

new Vue({
  el: '#app',
  data: {
    items: [
      'thingie',
      'another thingie',
      'lots of stuff',
      'yadda yadda'
    ]
  }
});
<div id="app">
  <ul>
    <li v-for="item in items">
      {{ item }}
    </li>
  </ul>
</div>

yields:

This pen

  • clean
  • semantic
  • declarative
  • legible
  • easy to maintain
  • reactive

V-Model

Creates a relationship between the data in the instance/component and a form input, so you can dynamically update values

Accepting user input and managing it in a responsible manner

new Vue({
  el: '#app',
  data() {
    return {
      message: 'This is a good place to type things.'  
    }
  }
});
<div id="app">
  <h3>Type here:</h3>
  <textarea v-model="message" class="message" rows="5" maxlength="72"/>
  <br>
  <p class="booktext">{{ message }} </p>
</div>

🏆

<Transition />

<div>
  <h2>Here I am!</h2>
  <slot></slot>
</div>
<div id="app">

  <h3>Let's trigger this here modal!</h3>
  <button @click="toggleShow">
    <span v-if="isShowing">Hide child</span>
    <span v-else>Show child</span>
  </button>

  <app-child v-if="isShowing" class="modal">
    <button @click="toggleShow">
      Close
    </button>
  </app-child>

</div>
const Child = {
  template: '#childarea'
};

new Vue({
  el: '#app',
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  },
  components: {
    appChild: Child
  }
});

This pen.

😳

<transition name="fade">
  <app-child v-if="isShowing" class="modal">
    <button @click="toggleShow">
      Close
    </button>
  </app-child>
</transition>

  <app-child v-if="isShowing" class="modal">
    <button @click="toggleShow">
      Close
    </button>
  </app-child>

Transition Component

Encapsulate what is changing declaratively

Vue Elegance

Default 'v-' prefix, otherwise name="foo"

Example:

.v-enter-active {
  transition: color 1s ease;
}
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.25s ease-out;
}

.fade-enter, .fade-leave-to {
  opacity: 0;
}

Reusable for other components

In this case, ease-out, but ease-out, ease-in for more complex effects

This is unnecessary, as it's default:

.fade-enter-to, .fade-leave {
  opacity: 1;
}

This pen.

Great!

But...

<div v-bind:class="[isShowing ? blurClass : '', bkClass]">
  <h3>Let's trigger this here modal!</h3>
  <button @click="toggleShow">
    <span v-if="isShowing">Hide child</span>
    <span v-else>Show child</span>
  </button>
</div>
new Vue({
  el: '#app',
  data() {
    return {
      isShowing: false,
      bkClass: 'bk',
      blurClass: 'blur'
    }
  },
  ...
});
.bk {
  transition: all 0.05s ease-out;
}

.blur {
  filter: blur(2px);
  opacity: 0.4;
}

This pen.

Transition Modes

🏆

🏆

This pen

Without transition modes

The current element waits until the new element is done transitioning in to fire

 

 

 

 

The current element transitions out and then the new element transitions in.

In-out

Out-in

<transition name="flip" mode="out-in">
  <slot v-if="!isShowing"></slot>
  <img v-else src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/cartoonvideo14.jpeg" />
</transition>

HTML

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

CSS Animation

enter-active-class="toasty"
leave-active-class="bounceOut"

Still <transition /> component, but

<div id="app">
  <h3>Bounce the Ball!</h3>
  <button @click="toggleShow">
    <span v-if="isShowing">Get it gone!</span>
    <span v-else>Here we go!</span>
  </button>

  <transition
    enter-active-class="bouncein"
    leave-active-class="rollout">
    <div v-if="isShowing">
      <app-child class="child"></app-child>
    </div>
  </transition>

</div>

Bounce a ball

@mixin ballb($yaxis: 0) {
  transform: translate3d(0, $yaxis, 0);
}

@keyframes bouncein { 
  1% { @include ballb(-400px); }
  20%, 40%, 60%, 80%, 95%, 99%, 100% { @include ballb() }
  30% { @include ballb(-80px); }
  50% { @include ballb(-40px); }
  70% { @include ballb(-30px); }
  90% { @include ballb(-15px); }
  97% { @include ballb(-10px); }
}

.bouncein { 
  animation: bouncein 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}

.ballmove-enter {
  @include ballb(-400px);
}

Keep it DRY

JavaScript Hooks

<transition 
  @before-enter="beforeEnter"
  @enter="enter"
  @after-enter="afterEnter"
  @enter-cancelled="enterCancelled"

  @before-leave="beforeLeave"
  @leave="leave"
  @after-leave="afterLeave"
  @leave-cancelled="leaveCancelled"
  :css="false">
 
 </transition>

Custom Naming

<transition 
  @enter="enterEl"
  @leave="leaveEl"
  :css="false">
  <!-- put element here-->
 </transition>

Most Basic Example

methods: {
   enterEl(el, done) {
     //entrance animation
     done();
  },
  leaveEl(el, done) {
    //exit animation
    done();
  },
}

Most Basic Example

This pen.

<textarea class="message" rows="5" v-model.lazy="message" maxlength="72" />
<br>
<button type="submit" class="submit" @click="load = !load">
  <span v-if="!load">
    Write Me
  </span>
  <span v-if="load">
    Erase
  </span>
</button>
<transition @before-enter="beforeEnter" @enter="enter" :css="false">
  <p class="booktext" v-if="load">
    {{ message }}
  </p>
</transition>
new Vue({
  el: '#app',
  data() {
    return {
      message: 'This is a good place to type things.',
      load: false
    }
  },
  methods: {
    beforeEnter(el) {
      TweenMax.set(el, {
        transformPerspective: 600,
        perspective: 300,
        transformStyle: "preserve-3d",
        autoAlpha: 1
      });
    },
    enter(el, done) {
      ...
      tl.add("drop");
      for (var i = 0; i < wordCount; i++) {
        tl.from(split.words[i], 1.5, {
          z: Math.floor(Math.random() * (1 + 150 - -150) + -150),
          ease: Bounce.easeOut
        }, "drop+=0." + (i/ 0.5));
       ...
    }
  }
});
let tl = new TimelineMax({ onComplete: done });

onComplete: done

done();

or

Leverage the Reactivity System for Transitions

Watchers

& Vue's Reactivity System

What is Reactive?

Reactive programming is programming with asynchronous data streams.

A stream is a sequence of ongoing events ordered in time that offer some hooks with which to observe it.

When we use reactive premises for building applications, this means it's very easy to update state in reaction to events.

What is Reactive?

 

  • Angular 1.x has dirty checking.
  • Cycle.js and Angular 2 use reactive streams like XStream and Rx.js.
  • Vue.js, MobX or Ractive.js all use a variation of getters/setters.

More Resources:

Despite the name, React is not Reactive- it uses a "pull" approach (rather than "push")

Vue takes the object, walks through its properties and converts them to getter/setters

new Vue({
  data: {
    text: 'msg'
  }
})

In Vue

The properties touched by the watcher during the render are registered as dependencies

When the setter is triggered, it lets the watcher know, and causes the component to re-render.

Each component has a watcher instance.

Watchers

Good for asynchronous updates,

and updates/transitions with data changes

We're going to 'watch' any data property declared on the Vue instance

State change can create the animation

SVG is good for this because it's built with MATH

<select v-model="selected">
  <option v-for="option in options" v-bind:value="option.value">
    {{ option.text }}
  </option>
</select>
new Vue({
  el: '#app',
  data() {
    return {
      selected: [25, 37, 15, 13, 25, 30, 11, 17, 35, 10, 25, 15, 5, 27, 15, 13, 25, 36, 15, 14, 35, 10, 14, 15, 35, 17, 12, 13, 25, 30, 14, 17, 35, 10, 25, 15],
      options: [
        { text: 'First Dataset', value: [25, 37, 15, 13, 25, 30, 11, 17, 35, 10, 25, 15, 5, 27, 15, 13, 25, 36, 15, 14, 35, 10, 14, 15, 35, 17, 12, 13, 25, 30, 14, 17, 35, 10, 25, 15] },
     ]
    }
  },
});
watch: {
    selected: function(newValue, oldValue) {

      var tweenedData = {}      

      var update = function() {
        let obj = Object.values(tweenedData);
        obj.pop();
        this.targetVal = obj;
      }

      var tweenSourceData = { onUpdate: update, onUpdateScope: this}

      for (let i = 0; i < oldValue.length; i++) {
        let key = i.toString()
        tweenedData[key] = oldValue[i]
        tweenSourceData[key] = newValue[i]
      }

      TweenMax.to(tweenedData, 1, tweenSourceData)
    }
  }

SVG!

Built with math

<!--xaxis -->
<g targetVal="targetVal"  class="xaxis">
  <line x1="0" y1="1" x2="350" y2="1"/>
  <g v-for="(select, index) in targetVal">
    <line y1="0" y2="7" v-bind="{ 'x1':index*10, 'x2':index*10 }"/>
    <text v-if="index % 5 === 0" v-bind="{ 'x':index*10, 'y':20 }">{{ index }}</text>
  </g>
</g>

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

Flexible

Loaders

great case for SVG

Entire filesize: 6KB!

What does the "scalable" mean?

You never have to worry about positioning in CSS

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

this pen.

SVG Animation

Vue.js

=

🔥

+

Personality

Emotions are tied to your limbic system and easier to remember

This pen.

<div id="app" @mousemove="coordinates">
coordinates(e) {
  const audio = new Audio('https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/Whoa.mp3'),
    walleBox = document.getElementById('walle').getBoundingClientRect(),
    walleCoords = walleBox.width / 2 + walleBox.left;
    ...

    TweenMax.set("#eyes", {
      scaleX: 1 + (1 - e.clientX / walleCoords) / 5
    });
    TweenMax.set("#walle", {
      x: ((e.clientX / walleCoords) * 50) - 40
    });

    this.startArms.progress(1 - (e.clientX / walleCoords)).pause();
  }
},

In <template>

In Vue Instance

clipPath- great support

Interpolation with style bindings- this pen

In the instance:

new Vue({
  el: '#app',
  data() {
    return {
      x: 0, 
      y: 0
    }
  },
  methods: {
    coords(e) {
      this.x = e.clientX / 10;
      this.y = e.clientY / 10;
    },
  }
})

In the template:

<div id="contain" :style="{ perspectiveOrigin: `${x}% ${y}%` }">
    <div class="square square2">
      <svg xmlns="http://www.w3.org/2000/svg" 
           viewBox="0 0 419.9 421.9" 
           preserveAspectRatio="none">

This pen.

createBigCircles() {
  const svgNS = this.$refs.figure.namespaceURI;
  this.$refs.patterngroup.innerHTML = '';
  
  for (let i = 0; i < this.numLines/2; i++) {
    let circ = document.createElementNS(svgNS, 'circle');
    this.append(this.$refs.patterngroup, circ);
    this.setAttributes(circ, {
      'cx': this.size/2,
      'cy': this.size/2,
      'r': this.totesRando(this.size/2, 0),
      'fill': 'none',
      'stroke': this.gradients2[this.totesRando(1, 0)],
      'stroke-width': 1
    });
  }
},
<div class="formarea">
  <h3>Create Circles:</h3>
  <button @click="createSmCircles">Make small circles</button>
  <button @click="createBigCircles">Make big circles</button>
</div>
animation() {
  let tl = new TimelineMax()
  
  tl.add('begin')
  tl.to('line', 2, {
    rotation: 360,
    repeat: -1,
    transformOrigin: '50% 50%',
    ease: Sine.easeOut
  }, 'begin')
  ...
  
  return tl;
},
pauseAnim() {
  var tl = TimelineLite.exportRoot();
  tl.pause(0);
},
<div class="formarea">
  <h3>Animation:</h3>
  <button @click="animation">Play Animation</button>
  <button @click="pauseAnim">Stop Animation</button>
</div>

This pen.

data() {
  return {
    total: 200,
    radius: 15,
  }
},
methods: {
  incrementHeight() {
    this.total += 100
  },
  incrementRadius() {
    this.radius += 1
  },
  bounceBall() {   
    this.vy += this.g; // gravity increases the vertical speed
    this.x1 += this.vx; // horizontal speed inccrements horizontal position 
    this.y1 += this.vy; // vertical speed increases vertical position

    if (this.y1 > this.total - this.radius) { // if ball hits the ground
      this.y1 = this.total - this.radius; // reposition it at the ground
      this.vy *= -0.8; // then reverse and reduce its speed
    }
  },
  ...
animateBall() {
  //use rAF to animate but put a boundary on it so it doesn't run forever
  let start,
      vueThis = this;
  this.running = true;
  
  function step(timestamp) {
    if (!start) start = timestamp;
    var progress = timestamp - start;
    if (progress < 13000) {
      vueThis.bounceBall();
      vueThis.req = window.requestAnimationFrame(step);
    } else {
      vueThis.x1 = this.radius;
      vueThis.y1 = this.radius;
      vueThis.running = false;
    }
  }
  this.req = window.requestAnimationFrame(step);
},
<button @click="animateBall" v-if="!running">Start</button>

Coordinating Transitions

End to end

Encapsulate what is changing - repo

State-driven animation

Encapsulate what is changing- Vuex

export const store = new Vuex.Store({
  state: {
    showWeather: false,
    template: 0
  },
  mutations: {
    toggle: state => state.showWeather = !state.showWeather,
    updateTemplate: (state) => {
      state.showWeather = !state.showWeather;
      state.template = (state.template + 1) % 4;
    }
  }
});
<transition @leave="leaveDialog" :css="false">
  <app-dialog v-if="showWeather"></app-dialog>
</transition>
<transition @leave="leaveDroparea" :css="false">
  <g v-if="showWeather">
    <app-droparea v-if="template == 1"></app-droparea>
    <app-windarea v-else-if="template == 2"></app-windarea>
    <app-rainbowarea v-else-if="template == 3"></app-rainbowarea>
    <app-tornadoarea v-else></app-tornadoarea>
  </g>
</transition>
export default {
  computed: {
    template() {
      return this.$store.state.template;
    }
  },
  methods: {
    toggle() {
      this.$store.commit('toggle');
    }
  },
  mounted() {
    //enter weather
    const tl = new TimelineMax();
    tl.add("enter");
    tl.fromTo("#dialog", 2, {
      opacity: 0
    }, {
      opacity: 1
    }, "enter");
    tl.fromTo("#dialog", 2, {
      rotation: -4
    }, {
      rotation: 0,
      transformOrigin: "50% 100%",
      ease: Elastic.easeOut
    }, "enter");
  }
}

Lifecycle hooks

This pen.

const Child = {
  beforeCreate() {
    console.log("beforeCreate!");
  }, 
  ...
};

Custom Directives

Vue.directive('tack', {
 bind(el, binding, vnode) {
    el.style.position = 'fixed'
  }
});
<p v-tack>I will now be tacked onto the page</p>

😳

Vue.directive('tack', {
  bind(el, binding, vnode) {
    el.style.position = 'fixed'
    el.style.top = binding.value + 'px'
  }
});
<div id="app">
  <p>Scroll down the page</p>
  <p v-tack="70">Stick me 70px from the top of the page</p>
</div>

🙂

Vue.directive('tack', {
  bind(el, binding, vnode) {
    el.style.position = 'fixed';
    const s = (binding.arg == 'left' ? 'left' : 'top');
    el.style[s] = binding.value + 'px';
  }
});
<p v-tack:left="70">I'll now be offset from the left instead of the top</p>

😊

Pass an argument

Vue.directive('tack', {
  bind(el, binding, vnode) {
    el.style.position = 'fixed';
    el.style.top = binding.value.top + 'px';
    el.style.left = binding.value.left + 'px';
  }
}); 
<p v-tack="{ top: '40', left: '100' }">Stick me 40px from the top of the
page and 100px from the left of the page</p>

😃

More than one value

Let's apply this to Animation

Vue.directive('scroll', {
  inserted: function(el, binding) {
    let f = function(evt) {
      if (binding.value(evt, el)) {
        window.removeEventListener('scroll', f);
      }
    };
    window.addEventListener('scroll', f);
  },
});

// main app
new Vue({
  el: '#app',
  methods: {
   handleScroll: function(evt, el) {
    if (window.scrollY > 50) {
      TweenMax.to(el, 1.5, {
        y: -10,
        opacity: 1,
        ease: Sine.easeOut
      })
    }
    return window.scrollY > 100;
    }
  }
});
<div class="box" v-scroll="handleScroll">
  <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. A atque amet harum aut ab veritatis earum porro praesentium ut corporis. Quasi provident dolorem officia iure fugiat, eius mollitia sequi quisquam.</p>
</div>

🔥

Custom Directives + D3

export default {
    methods: {
      totalImpact: function(evt, el) {
        if (window.scrollY > 1100) {
          TweenMax.to(el, 0.75, {
            opacity: 0
          })
          let circ =  d3.selectAll("circle")
                      .attr("cx", function(d) {
                        let lat = d["Longitude (Deg)"];
                        if (lat.includes("E")) {
                          return midX - parseInt(lat) * incByW;
                        } else {
                          return midX + (parseInt(lat) * incByW);
                        }
                      })
                      ...
                      .attr("r", 5)
                      .attr("fill", "url(#radgrad)")
        }
        return window.scrollY > 1300;
      },

Update the circle's coordinates

<div class="box accelerate impact" v-dscroll="totalImpact">
  <h3>Total Impact</h3>
  <p>Most alksdjflkjasd laksdjfl;kasjdf laksd falksdjf lsdj f</p>
</div>

Nuxt Routing & Page Transitions

Server Side Rendering

By rendering on the server, you can cache the final shape of your data

"

"

-Karl Seguin

Nuxt: Other Features

  • Automatic Code Splitting
  • Powerful Routing System
  • Great lighthouse scores out of the gate 🐎
  • Static File Serving
  • ES6/ES7 Transpilation
  • Hot reloading in Development
  • Pre-processors: SASS, LESS, Stylus, etc
  • Write Vue Files to create your pages and layouts!
  • My personal favorite: easily add transitions to your pages
npm install -g vue-cli

--------
 
vue init nuxt/starter my-project
cd my-project
yarn
 
npm run dev

Templates in the pages directory

<nuxt-link to="/product">Product</nuxt link>

Transition hook already available

name="page"
.page-enter-active, .page-leave-active {
  transition: all .25s ease-out;
}
.page-enter, .page-leave-active {
  opacity: 0;
  transform: scale(0.95);
  transform-origin: 50% 50%;
}

🏆

Animation as well

.page-enter-active {
  animation: acrossIn .45s ease-out both;
} 
 
.page-leave-active {
  animation: acrossOut .65s ease-in both;
} 

JS Hooks

export default {
  transition: {
    mode: 'out-in',
    css: false,
    enter (el, done) {

      let tl = new TimelineMax({ onComplete: done }),
          spt = new SplitText('h1', {type: 'chars' }), 
          chars = spt.chars;

      TweenMax.set(chars, {
        transformPerspective: 600,
        perspective: 300,
        transformStyle: 'preserve-3d'
      })

      tl.add('start')
      tl.from(el, 0.8, {
        scale: 0.9,
        transformOrigin: '50% 50%',
        ease: Sine.easeOut
      }, 'start')
      ...
      tl.timeScale(1.5)
    }
  ...

Vue & Nuxt

make it extraordinarily simple 

to create complex and beautiful interactions

that feel seamless for our users.

We can connect states and reduce cognitive load for things that are changing in our application with ease.

Avoid burnout.

Have fun.

Thank you!

@sarah_edo on twitter

These slides:

slides.com/sdrasner/animating-vue-keynote