Animate Plus: Behind the Scenes

Benjamin De Cock
6 min readJun 10, 2015

--

As I was implementing the landing page for Stripe’s iOS dashboard app, I started to feel the need for using an animation library. I needed something lightweight and performant with built-in spring physics, CSS, and full SVG support. I couldn’t find a library fulfilling my requirements so I built my own. I had a lot of fun developing it and wanted to share some (hopefully useful!) technical details.

ECMAScript 2015

Animate Plus is written in ES 2015 (ECMAScript’s upcoming specification) and transpiled with Babel. If you haven’t played with ES 2015 yet, I strongly encourage you to take the plunge — it’ll likely make you enjoy writing JavaScript again. Here are some examples taken from the source code illustrating some of my favorite features in ES 2015.

Arrow functions

Arrow functions may seem like unnecessary syntactic sugar at first, but they genuinely improve your code’s legibility. The implicit return expressions and optional single-argument parentheses make higher-order functions short and expressive:

const isFilled = params =>
required.every(param => params.has(param));

As a comparison, here’s the same function transpiled to ES 5 :

var isFilled = function(params) {
return required.every(function(param) {
return params.has(param);
});
};

Destructuring assignment

Just like arrow functions, destructuring assignments are powerful expressions making your code much more clear and concise:

const [from, to] = params.get(prop).map(splitDigits);

Prior to ES 2015, the example above would have been written like this:

var arr = params.get(prop).map(splitDigits);
var from = arr[0];
var to = arr[1];

Destructuring assignments come with other interesting benefits such as the ability for a function to return multiple values, something that was simply not possible before.

Spread operator

The spread operator expands an expression into multiple separate values. The use cases are varied and spread (pun unintentend), one of them being converting a NodeList object into a plain array:

if (obj instanceof NodeList || obj instanceof HTMLCollection)
return [...obj];

Rest parameter

The rest parameter looks like the spread operator but serves as a vastly superior replacement for the arguments object:

const difference = (arr, ...others) => {
const combined = flatten(others);
return arr.filter(el => not(contains)(combined, el));
};

Unlike the arguments object, rest parameters are real arrays, giving you instant access to Array.prototype methods and other niceties.

Map

Objects have historically been used as dictionaries because JavaScript didn’t have a proper object for storing simple key/value pairs. That’s no longer the case as Maps handle this task brilliantly:

const defaults = new Map();
defaults.set("duration", 1000);
defaults.set("easing", "easeOutElastic");

Animate Plus relies on maps internally so it’ll convert parameter objects when needed:

const toMap = (() => {
const convert = obj => {
const map = new Map();
Object.keys(obj).forEach(key => map.set(key, obj[key]));
return map;
};
return obj => obj instanceof Map ? obj : convert(obj);
})();

The support for Map in modern browsers is good but incomplete. Most browsers won’t accept arguments passed to a Map constructor, which makes Map cloning less intuitive. This little helper checks once for the support of constructor arguments and defines a custom clone method if needed:

const cloneMap = (() => {
try {
new Map(new Map());
}
catch (e) {
return map => {
const clone = new Map();
map.forEach((value, key) => clone.set(key, value));
return clone;
};
}
return map => new Map(map);
})();

Template strings

Last but not least, template strings finally make (amongst other benefits) string concatenations human-readable:

const [r, g, b] = compose(expand, convert)(hex);
return `rgb(${r}, ${g}, ${b})`;

Functional Programming

Animate Plus strictly follows functional programming’s core principles. In case you’re not familiar with it, functional programming (“FP”) is a coding style essentially based on two best practices: immutability and statelessness. Let’s pick again a few examples from the source code illustrating some typical FP concepts.

Compose

Composing functions is at the heart of any FP system. In FP, it’s common to find lots of small, generic and highly reusable functions. Thus, most of the functions you create will presumably rely on successive calls of other, previously defined functions. In order to make the creation of these “composed” functions easier, most FP programs introduce a generic compose function. Animate Plus’ implementation looks like this:

const compose = (...funcs) =>
value => funcs.reduce((a, b) => b(a), value);

Typically, compose functions read from right to left, which makes sense mathematically speaking. I personally find it unnatural and harder to read, so my version of compose reads from left to right (which is sometimes called “pipelining”).

Animate Plus’ arguments validation process uses a bunch of operations to check if some parameters are missing, to convert potential hexadecimal colors into RGB, etc. Thus, the validateParams function is a good candidate for being “composed” :

const validateParams = compose(
toMap,
fillBlankParams,
buildMissingArrays,
ensureRGB,
setElements,
setInitialDirection
);

Notice how the composed function doesn’t mention the parameter that’ll eventually be used, it simply defines a sequence of function calls (more on that later). This newly created function can then be called with the parameters provided by the developer:

validateParams(params);

Immutability

Many complicated issues can be prevented by simply never allowing data to change. The validateParams function explained above passes some paramaters to a series of validity check and, if needed, returns a sane version of these parameters. Now, each of these checks might alter the parameters, which goes against immutability rules. So, instead of editing the parameters object, every function will duplicate the original object and return a modified copy.

const setElements = params => {
const map = cloneMap(params);
map.set("el", getElements(params.get("el")));
return map;
};

setElements is said to be “pure”. Pure functions don’t mutate data and don’t produce side effects, making them predictable, reliable and easily testable. Ideally, every function should be pure.

As with many FP idioms, constantly creating and throwing away data structures might seem suboptimal but it’s important to understand the trade-offs. I can’t stress enough this advice:

Favor readability, correctness and expressiveness over performance. JavaScript will basically never be your performance bottleneck. Optimize things like image compression, network access and DOM reflows instead.

Reusability

As mentioned earlier, most functions in FP rely on other generic functions, from the most basic helpers:

const first = arr => arr[0];
const last = arr => first(arr.slice(-1));

To most advanced functions:

const not = fn => (...args) => !fn(...args);const flatten = arr => arr.reduce((a, b) => a.concat(b));const contains = (() =>
Array.prototype.includes
? (arr, value) => arr.includes(value)
: (arr, value) => arr.some(el => el === value)
)();
const difference = (arr, ...others) => {
const combined = flatten(others);
return arr.filter(el => not(contains)(combined, el));
};

The function that filters SVG attributes from all parameters will, for example, simply rely on the difference between all animatable properties and CSS properties:

const getSVGprops = params =>
difference(getAnimatedProps(params), getCSSprops(params));

Point-free

Point-free is a coding style where you avoid defining temporary variables by using functions relying on implicit data, resulting in more succinct and robust functions:

split.set("digits", toStr(value).match(re).map(Number));

Reduce

In FP, you constantly deal with the Array object and its 3 central prototype methods: Map, Filter and Reduce. Amongst them, many believe Reduce is the most important one as the possibilities provided by its accumulator are endless (in fact, Map and Filter could easily be implemented with Reduce). For example, this function “reduces” an array of arrays to a single flat array:

const flatten = arr => arr.reduce((a, b) => a.concat(b));

Currying

A curried function is a function that returns another function if all the parameters it expects haven’t been provided. It’s a powerful and flexible paradigm that can help you write cleaner code, notably with callbacks:

const propIsArray = params =>
prop => Array.isArray(params.get(prop));
const isValid = params =>
getCSSprops(params).every(propIsArray(params));

In this example, propIsArray is initially called with params as its argument. These parameters are “cached” in the closure, making them accessible for further function calls. This initial function call returns another function expecting an argument (prop) provided by every() for each element in the array. Another example of this concept applied to a callback can be found inside the requestAnimationFrame loop where both getProgress and getFinalValues are curried:

const progress = animatedProps.map(
running
? getProgress(validatedParams, time.get("elapsed"))
: getFinalValues(validatedParams)
);

Now, currying functions by hand is tedious and the legibility of curried functions often suffer from these multiple nested functions. In order to fix that, I’m using a custom curry function:

const curry = fn => {
const arity = fn.length;
const curried = (...args) =>
args.length < arity
? (...more) => curried(...args, ...more)
: fn(...args);
return curried;
};

Thanks to this decorator, functions like the propIsArray example mentioned earlier become cleaner and more flexible as they can now accept all their arguments at once or single arguments recursively:

const propIsArray = curry((params, prop) =>
Array.isArray(params.get(prop)));

Conclusion

ES 2015 and FP principles make writing JavaScript a much more enjoyable experience. The code is shorter, more robust and easier to read. Modern browsers optimize so many things you wouldn’t suspect (ask Egorov!) that writing expressive code without worrying about algorithm micro-optimizations is now a reality.

If you care about animations, I recommend you to give Animate Plus a try. It’s making my work better by allowing me to think of possibilites I couldn’t even consider before (yay morphing animations!) and I suspect/hope other people might also benefit from it. Enjoy!

--

--