Build your own animation... engine?

With a little bit of Vue.js

The base

function animate (duration, delta, step) {
  // set start time
  const start = performance.now() // 3606016.2050000005
  // create function that accepts current time
  const animation = function (t) {
    // currentTime - startTime gives us
    // the time that already passed from start
    // dividing by the duration gives us 0 < progress < 1
    let progress = (t - start) / duration

    if (progress > 1) progress = 1
    // run the stepFunction
    // modify the progress with delta fun
    step(delta(progress))
    // if progress !== 1 repeat the whole thing
    if (progress !== 1) {
      window.requestAnimationFrame(animation)
    }
  }
  // start the animation
  // requestAnimationFrame calls the callback with
  // the new time
  window.requestAnimationFrame(animation)
}

Duration

The time the animation lasts (in ms)

 

Delta

A function progress -> value multipler

 

Step

A function that modifies a value based on the results from the delta function

createStep

function createStep (
  obj, // source object
  property, // property to modify
  from, // initial value when progress == 0
  to // end value when progress == 1
) {
  // calculate the absolute difference
  const absTo = Math.abs(to - from) 
  // return function that accepts progress (0-1)
  return progress => { 
    // depending on which value is greater
    obj[property] = to > from 
      // increase the property’s value
      ? from + (absTo * progress) 
      // decrease the property’s value
      : from - (absTo * progress) 
  }
}

Vue.js part - data model

// Create the data model that we can work on
data () {
  return {
    logoDegrees: 180,
    logoY: -100,
    logoOpacity: 0,
    sunRadius: 0,
    typo: {
      v: 25,
      u: 25,
      e: 25,
      c: 25,
      o: 25,
      n: 25,
      f: 25
    }
  }
}

Vue.js part - SVG template

<template>
  <!-- an SVG file that can have dynamic properties like :r here -->
  <g transform="matrix(0.25,0,0,0.25,577.25,193.34)">
    <circle 
      cx="363" 
      cy="715" 
      <!-- value from the model -->
      :r="sunRadius" 
      style="fill:none;stroke:rgb(58,185,130);stroke-width:15.22px;"/>
  </g>
</template>

delta

function circ (progress) {
  return 1 - Math.sin(Math.acos(progress))
}
function circ (progress) {
  return Math.pow(progress, 2)
}

delta

function back (x) {
  // x - defines the pushback distance
  return progress => Math.pow(progress, 2) * ((x + 1) * progress - x)
}

Back – also called bow function

delta

function bounce (progress) {
  for(var a = 0, b = 1, result; 1; a += b, b /= 2) {
    if (progress >= (7 - 4 * a) / 11) {
      return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2);
    }
  }
}

And there is bounce...

reverse functions

function makeEaseOut (delta) {
  return function (progress) {
    return 1 - delta(1 - progress)
  }
}

By default all the functions are so called EaseIn

 

Sometimes we want to reverse a function. This is then called EaseOut

You might remember this from CSS animations.

Red – EaseIn

Green – EaseOut

mixed functions

function makeEaseInOut (delta) {
  return function (progress) {
    if (progress < .5) {
      return delta(2*progress) / 2
    }
    else {
      return (2 - delta(2*(1-progress))) / 2
    }
  }
}
// usage
const circEaseInOut = makeEaseInOut(circ)

You can merge EaseIn and EaseOut.

This is then called EaseInOut.

Red – EaseIn

Green – EaseOut

Blue - EaseInOut

Usage

animate(
  600, 
  circEaseInOut, 
  createStep(this.element, 'posY', 0, 100)
)

Cool but what if we want to chain the animations?

Chaining

animate(
  600, 
  circEaseInOut, 
  createStep(this.element, 'posY', 0, 100)
)

// so we can do something like
animate(
  600, 
  circEaseInOut, 
  createStep(this.element, 'posY', 0, 100)
).then(
  animate(400, circ, createStep(this.element, 'posY', 100, 300))
)

Chaining would require to wait for the previous animation to complete.

So my first thought was – Promises!

Chaining

function animate (duration, delta, step) {
  return new Promise( // return a Promise
    function (resolve, reject) {
      const start = performance.now()
      const animation = function (t) {
        let progress = (t - start) / duration
        if (progress > 1) progress = 1
        step(delta(progress))
        if (progress !== 1) {
          window.requestAnimationFrame(animation)
        } else {
          // Resolve the Promise when 
          // animation is complete
          resolve()
        }
      }
      window.requestAnimationFrame(animation)
    }
  )
}

So lets change our animate function to a Promise

Some helpers

function wait (duration) {
  return new Promise(resolve => {
    setTimeout(() => resolve(), duration))
  }
}

One so we can wait for a given time

function combineSteps (...steps) {
  return function (progress) {
    steps.forEach(step => step(progress))
  }
}

One to combine steps

Some helpers

function createAnimation (duration, delta, step) {
  return () => animate(duration, delta, step)
}

One to defer the animation execution

Some helpers

function* toGenerator (iterable) {
  yield* iterable // hue hue
}
function stagger (animations, delay) {
  // transform the animations array to generator
  const animationsGenerator = toGenerator(animations)
  async function animateNext () {
    // get the new animation from the generator
    const { value: animation, done } = animationsGenerator.next()
    // if generator is not yet complete
    if (!done) {
      // run the animation
      animation()
      // wait for the delay
      await wait(delay)
      // repeat
      animateNext()
    }
  }
  // start!
  animateNext()
}

Two to create the stagger effect

Example animations

async animateLogo () {
  await animate(
    2200,
    backEaseOut(2),
    combineSteps(
      createStep(this, 'logoY', -100, 0),
      createStep(this, 'logoOpacity', 0, 1)
    )
  )
  await wait(400) // pause for 400ms
  await animate(600, circEaseInOut, createStep(this, 'logoDegrees', 180, 360))
  return
}

Yup. Since our animations are promises we can await them

async animateTypo () {
  const duration = 500
  stagger([
    createAnimation(duration, backEaseOut(1), combineSteps(
      createStep(this.typo, 'vO', 0, 1),
      createStep(this.typo, 'v', 25, 0)
    )),
    createAnimation(duration, backEaseOut(1), combineSteps(
      createStep(this.typo, 'uO', 0, 1),
      createStep(this.typo, 'u', 25, 0)
    )),
    createAnimation(duration, backEaseOut(1), combineSteps(
      createStep(this.typo, 'eO', 0, 1),
      createStep(this.typo, 'e', 25, 0)
    )),
    createAnimation(duration, backEaseOut(2), combineSteps(
      createStep(this.typo, 'cO', 0, 1),
      createStep(this.typo, 'c', 25, 0)
    )),
    createAnimation(duration, backEaseOut(2), combineSteps(
      createStep(this.typo, 'oO', 0, 1),
      createStep(this.typo, 'o', 25, 0)
    )),
    createAnimation(duration, backEaseOut(2), combineSteps(
      createStep(this.typo, 'nO', 0, 1),
      createStep(this.typo, 'n', 25, 0)
    )),
    createAnimation(duration, backEaseOut(2), combineSteps(
      createStep(this.typo, 'fO', 0, 1),
      createStep(this.typo, 'f', 25, 0
    )))
  ], 100)
  await wait(500)
  animate(duration, backEaseOut(3), createStep(this, 'dotRadius', 0, 1.5))
}

Result

http://jsfiddle.net/q5gwf3ba/7/

Thank you! :)

Build a simple animation engine with Vue.js

By Damian Dulisz

Build a simple animation engine with Vue.js

Build a simple animation engine with a little bit of Vue.js

  • 5,126