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