Let's Make a Framework: Animations Part 5
Welcome to part 19 of Let’s Make a Framework, the ongoing series about building a JavaScript framework. This part continues looking at JavaScript animations.
If you haven’t been following along, these articles are tagged with lmaf. The project we’re creating is called Turing and is available on GitHub: turing.js.
Colour Support
Last week I mentioned that I wanted to build in a helper for highlighting an element (the Yellow Fade Technique). The problem with this is the library doesn’t currently support colour parsing.
The reason colour parsing is required is we need to convert values to rgb(0, 0, 0) notation: most people will expect to be able to pass hexadecimal colours.
I based our colour parser on Stoyan Stefanov’s parser here: RGB color parser in JavaScript. The coding style is particularly suitable for this tutorial series, because it presents the concepts in easy to read code.
The core of the colour parser is an array of regular expressions and functions that can extract numbers for a given format and turn it into RGB values:
Colour.matchers = [
{
re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'],
process: function (bits){
return [
parseInt(bits[1]),
parseInt(bits[2]),
parseInt(bits[3])
];
}
},
Notice the example property documents how the matcher works which is a great touch that I lifted directly from Stoyan’s code (which I think he got from Simon Willison’s date library).
Now the parser just needs to loop through each matcher. There are only 3, and colour parsing will only be used outside of the main animation loop:
Colour.prototype.parse = function() {
var channels = [], i;
// Loop through each matcher
for (i = 0; i < Colour.matchers.length; i++) {
channels = this.value.match(Colour.matchers[i].re);
// An array of numbers will be returned if the value was matched by the regex
// else null
if (channels) {
// Set each value to r, g, b properties in this object
channels = Colour.matchers[i].process(channels);
this.r = channels[0];
this.g = channels[1];
this.b = channels[2];
break;
}
}
this.validate();
}
I could have used an array for each property here, but I thought I’d be explicit and use r, g, b properties for each value. This means there is some code repetition later, but at least you can easily see what’s going on.
Something else I took from Stoyan’s library is the validation code:
Colour.prototype.validate = function() {
this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r);
this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g);
this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b);
}
This just makes sure values are between 0 and 255.
Channel Surfing
Something I realised early on when developing this was channels should be able to move independently. Imagine if you wanted to animate this:
// From:
rgb(55, 255, 0);
// To:
rgb(255, 0, 10);
The red, green and blue properties are moving in different directions. Our easing-based transforms so far just multiply values by position, so we need something that can:
- Figure out the “direction” of each colour
- Correctly multiply each colour by the current position in the animation
Direction can be represented by -1 and 1. We don’t need to conditionally check for direction though, the values can just be multiplied by the direction:
nextRed = startRed + (redDirection * (Math.abs(startRed - endRed) * easingFunction(position)))
Math.abs is used here to make sure the value isn’t negative. Technically the start value might be less than the end value — we just need to know the magnitude:
155 - 55
// 100
55 - 155
// -100
Math.abs(55 - 155)
100
Transformations
We might find CSS values are numbers or colours. To handle this, I’ve made Turing normalise properties we want to animate into a format that includes a “transformation” function. These functions know how to correctly manipulate colours or numbers. This was already hinted at because we needed the basis to write out units (px, em, etc.)
The core animation loop now includes a reference to a transform property that the CSS value parser sets.
The colour transform looks like this:
function colourTransform(v, position, easingFunction) {
var colours = [];
colours[0] = Math.round(v.base.r + (v.direction[0] * (Math.abs(v.base.r - v.value.r) * easingFunction(position))));
colours[1] = Math.round(v.base.g + (v.direction[1] * (Math.abs(v.base.g - v.value.g) * easingFunction(position))));
colours[2] = Math.round(v.base.b + (v.direction[2] * (Math.abs(v.base.b - v.value.b) * easingFunction(position))));
return 'rgb(' + colours.join(', ') + ')';
}
It doesn’t bother using new Colour(...) to set up a new colour because we just care about the RGB values here.
I got the idea for normalised CSS values into objects that contain unit and value presentation functions from Emile.
Highlight Helper
The highlight helper function can now be written:
anim.highlight = function(element, duration, options) {
var style = element.currentStyle ? element.currentStyle : getComputedStyle(element, null);
options = options || {};
options.from = options.from || '#ff9';
options.to = options.to || style.backgroundColor;
options.easing = options.easing || easing.sine;
duration = duration || 500;
element.style.backgroundColor = options.from;
return setTimeout(function() {
anim.animate(element, duration, { 'backgroundColor': options.to, 'easing': options.easing })
}, 200);
};
// Usage:
turing.anim.highlight(element);
It sets a bunch of defaults then sets a bright yellow background colour. After that it animates fading the background colour to the original colour. getComputedStyle has to be used to get the current colour. There might be instances where passing options.from is a better idea.
Conclusion
Animating CSS values is difficult because we need to detect what type of value we’re dealing with, parse it, change it, then correctly update the DOM with the new values. By keeping the parsed results non-specific the animation loop can be kept decoupled from the code that does the real work.
Eventually the CSS-related code should be moved to turing.css, because it could be useful outside of animations.
To play with the code and make some experiments of your own, the easiest way is to check out the source, cd into test and open up anim_test.html in a browser and anim_test.js in an editor.
If you find bugs or seriously awesome improvements, why not write a guest post for the series?
