The Browser Whisperer RSS Feed


JavaScript-based Animation

Enlivening a Page With JavaScript

As we discussed in our last blog post, we're going to look at how to implement animation using JavaScript. Back during the dark times of the browser wars, implementing animation was a challenge due to the differences between how Microsoft and Netscape implemented DHTML. In fact, DHTML had a bad rep. Both developers and end users, wanted to avoid it. Although the idea behind DHTML was great, there was no standard way to implement it. In fact, it was mostly impossible to create the same DHTML that worked identically across browsers. As such, DHTML was a niche that gradually fading into oblivion. That is, until the dawn of modern DOM based JavaScript. Since all major browser vendors started supporting standards-based DOM APIs, developers have responded by creating many different JavaScript libraries to make the task of creating complex and sophisticated Web interfaces easier.

Technically, you don't need a library to create animation. All you need are five things: an element, at least one of its properties, the value of said property, the duration of the animation, and the number of frames per second for the animation. A sixth value could be some type of easing. It is fairly easy to create one-offs of this type of animation. In deed the Web is littered with such animation. I myself am guilty of contribution to this litter. I have even blogged about such animations in the past. Now I'm going to present a better way. This is a module I've been working on to break down the tasks of animation into logical parts. This allows for a lot of flexibility. You may be thinking, "Oh no, not another animation library!" Well, this is not another library and that's the point. Script.acul.us, Dojo, jQuery, YUI, Mootools are all awesome libraries that include support for animation. I wanted something that did only animation with no dependencies on any other library so that I could add it to whatever project needed animation, regardless of what the development environment was employing. I looked long and hard at Tween from the Flash/ActionScript side. This is a single purpose kit which has also been ported to JavaScript and Silverlight/C#. However, Tween had one significant shortcoming. It was designed to handle a single attribute at a time. I wanted something that could animate multiple values simultaneously.

In the end I created an animation module comprised of the features of the aforementioned libraries in a manner that fulfilled my requirements for size, independence and capabilities. Since I already have my own JavaScript library called Vx, I named this Vx-anim. When the module loads, it checks to see if Vx is present. If it is not present, Vx-anim creates a namespace of Vx to operate in, along with a few utility methods. This means that Vx-anim can run fine by itself or with any other JavaScript library. It has no dependencies.

Vx-anim supports two methods that allow you to fire other events: onStart and onEnd. This means that if you need to, you can fire event at the start or end or start and end of your animation. Vx-anim also has a method for delays. Combining delay with onStart and onEnd allows you to create complex interactions. Vx-anim can animate color, background-color and border-color. It also accepts hex or RGB values. Optionally you can use the sixteen common color names, with two alternative spellings for gray: aqua, black, blue, gray, grey, green, lime, maroon, navy, olive, purple, red, silver, teal, white, yellow. The module has the intelligence to handle multi-value padding and margin. Rather than defining a huge library to handle animating all possible CSS properties, I use a factory pattern that manufactures methods to handle animating properties based on the values passed to it. This method will take hyphenated CSS properties and turn them into camel case JavaScript properties, so you could supply either borderWidth or "border-width" and the method will create the correct function to animate that property.

This module has full support for easing, using the easing equations I presented in my previous post. The original equations are the work of the Flash/ActionScript virtuoso, Robert Penner. Since he published and gifted these to the development community, they have been ported and incorporated into practically every animation framework out there. I gave it a few tweaks and incorporated into mine. Originally I toyed with my own home-grown method to create a very limited type of easing for acceleration and deceleration. However, when I compared my implementation with what his equations could do, I decided to use his.

Vx-anim takes an object literal to define the properties to be animated. Below is an example of an animation. Notice that the properties in  opts1 contain hyphenated values in quotes, whereas in opts2 the corresponding values are in camel case. Notice that font-size has a units value of "pt" for points instead of pixels, which is the default. Also notice that I've mixed color names with hex values for animating the background color.

I use setInterval to create a timer for the animation. Because of the way browsers implement both setInterval and setTimeout, it is impossible to have precise animations all the time. There will be circumstances where it will undershoot or overshoot the effect that you are trying to implement. However, this is an exception, not the rule. This means, especially if your target is IE, you may need to tweak your animations to get them perfect.

Setting up an animation is fairly straightforward. Here is an example:

   1: function Morph(param) {
   2:     this.onEnd = function() {
   3:         alert("This is the beginning!");
   4:     };
   5:     this.onEnd = vx.delay(1000, function() {
   6:         endMsg();
   7:     });
   8:     var elem = document.getElementById("morph");
   9:     var opts1 = {
  10:        "background-color" : { start : "black", end : "#00FF00" }, 
  11:        color : { start : "#FFFFFF", end : "#0000FF" }, 
  12:        "border-width" : { start : 4, end : 2 }, 
  13:        "border-color" : { start : "#FF0800", end : "#0000FF" }, 
  14:        width : { start : 350, end : 250 }, 
  15:        padding : { start : "20 20 20", end : "20 200 100" }
  16:        top : { start : 0, end : 30 }, 
  17:        left : { start : 0, end : 50 }, 
  18:        "font-size" : { start : 14, end : 32, units: "pt" }, 
  19:        "line-height" : { start : 18, end : 40 }
  20:     };
  21:     var opts2 = {
  22:        backgroundColor : {  start : "#00FF00", end : "red" }, 
  23:        color : { start : "#0000FF", end : "#FFFFFF" }, 
  24:        borderWidth : { start : 1, end : 4 }, 
  25:        borderColor : { start : "#FF08FF", end : "#FF0800" }, 
  26:        width : { start : 250, end : 350 }, 
  27:        padding : {start : "20 200 100", end : "20 20 20" }
  28:        top : { start : 30, end : 0 }, 
  29:        left : { start : 50, end : 0 }, 
  30:        fontSize : { start : 32, end : 14 }, 
  31:        lineHeight : { start : 40, end : 18 }
  32:    };
  33:    if (param == 1) {
  34:         vx.animate(elem, opts1, 1500, 20, vx.easing.bounceEaseIn));
  35:    } else {
  36:         vx.animate(elem, opts2, 1500, 20, vx.easing.bounceEaseOut));
  37:    }
  38: }
  39: function endMsg() {
  40:     alert("This is the end of Morph!");
  41: }

 

Vx-anim has a number of utility methods which it uses to create an animation. Here is the run down:

vx.loadEvent: This method allows you to chain multiple events together on page load.

vx.delay: This method allows you to create delays in events being fired at the start or end of an animation.

vx.animate: This is the main method. It initializes base values for the animation and invokes the vx.utils.manufacture method to invoke or create appropriate methods to animate an element's properties.

vx.utils.manufacture: This is a factory pattern for invoking already existing methods, or, in their absence, creating new methods to animate an element's properties.

vx.utils.calculate: This method is used by vx.utils.manufacture to determined the amount of each frame of an animation.

vx.utils.style: This method is used by calculate to set the newly calculated value on an element's property.

determineMultiValue: This method is used by vx.util.padding and vx.utils.margin to check for multi-part values. If they are present, this method calculates the necessary values for each of the four sides of the element.

opacity: Animates and element's opacity in a cross-browser manner.

color: If the property to be animated contains the word "color," this method is invoked. It can animate hex values or color name strings.

backgroundColor: If the property being animated is a background color, then the vx.utils.color method invokes this method.

vx.utils.borderColor If the property being animated is a border color, then the vx.utils.color method invokes this method.

vx.utils.bezier: This method creates a bezier value for animating either an element value or an animation path.

vx.utils.hexToDec: This method turns a two digit hex value into an RBG value.

vx.utils.hexToRgb: This method uses vx.util.hextToDec to calculate the RGB values of the hex color.

vx.utils.hexToArray: This method uses vx.utils.hexToRgb to return an array of RBG values.

vx.utils.camelize: Method to turn hyphenated words into camel case.

vx.color This is an object literal of color names to be used with the color animation methods.

easing: This is an object literal of easing functions to be used with animation.

And now, without further ado, I present Vx-anim:

   1: if (typeof vx === "undefined") {
   2:     vx = {};
   3:     vx.loadEvent = function ( F ) {
   4:         var oldonload = window.onload;
   5:         if (typeof window.onload !== 'function') {
   6:             window.onload = F;
   7:         } else {
   8:             window.onload = function ( ) {
   9:                 oldonload();
  10:                 F();
  11:            };
  12:         }
  13:     };
  14: };
  15:  
  16: vx.delay = function( amount, F ) {
  17:    if (!F) { 
  18:        return false;
  19:     } else {
  20:        setTimeout(function() { F(); }, amount);
  21:     }
  22: };
  23: vx.animate = function( elem, opts, duration, fps, easing ) {
  24:     var duration = duration ? parseFloat(duration) : 1000;
  25:     var fps = fps ? parseFloat(fps) : 20;
  26:     var startTime  = new Date().getTime();
  27:     var endTime = startTime + duration;
  28:        var frame = 1;
  29:       var timer = undefined;
  30:     var easing = easing ? easing: vx.easing.linear;
  31:     var interval = Math.ceil(1000 / fps);
  32:     var totalframes = Math.ceil(duration / interval);
  33:       
  34:     var setAnimation = function() {
  35:            var time = new Date().getTime();
  36:         frame = parseInt((time - startTime) / interval);
  37:         var onEnd = undefined;
  38:         var onStart = undefined;
  39:         
  40:         for (var prop in opts){
  41:             if (prop !== "onEnd" || prop !== "onStart") {
  42:                    vx.animate.utils.manufacture(elem, totalframes, frame, prop, opts, easing);
  43:                }
  44:                if (prop === "onEnd") {
  45:                     onEnd = opts[prop];
  46:                }
  47:                if (prop === "onStart") {
  48:                    onStart = opts[prop];
  49:                }
  50:         }
  51:         if (onStart) {
  52:             onStart();
  53:         }
  54:         if (time >= startTime + duration) {
  55:            if (onEnd) {
  56:                onEnd();
  57:            }
  58:            //console.log("endTime is: " + endTime + "; present time is: " + new Date().getTime());
  59:            clearInterval(timer);
  60:         }
  61:    };
  62:    var timer = setInterval(setAnimation, interval);
  63: };
  64:  
  65: vx.animate.utils = {
  66:     manufacture : function ( elem, totalframes, frame, prop, opts, easing ) {
  67:         var start = opts[prop].start;
  68:         if (/[0-9]+/.test(parseInt(start)) && !/\s/.test(start)) {
  69:             this.calculate(elem, totalframes, frame, prop, opts, easing);
  70:             return true;
  71:         }
  72:         var method = this.camelize(prop);
  73:         if (this[method]) {
  74:             this[method](elem, totalframes, frame, prop, opts, easing);
  75:             return true;
  76:         }
  77:         return false;
  78:     },
  79:     calculate : function ( elem, totalframes, frame, prop, opts, easing ) {
  80:         var start = opts[prop].start * 100;
  81:         var end = opts[prop].end * 100;
  82:         var units = undefined;
  83:         if (opts[prop].units) {
  84:             units = opts[prop].units;
  85:         }
  86:         var bezier = opts[prop].bezier * 100;
  87:         var tween = easing(frame, start, end - start, totalframes);
  88:         if (bezier) {
  89:             tween = this.bezier(frame, tween, end, bezier, totalframes);
  90:         }
  91:         this.style(elem, prop, tween / 100, units);
  92:         elem.style.zoom = 1;
  93:     },
  94:     style : function ( elem, prop, val, units ) {
  95:         if (units) {
  96:             var units = units;
  97:         } else {
  98:             var units = "";
  99:         }
 100:         if (prop === "opacity") {
 101:             return this.opacity(elem, parseFloat(val));
 102:         }
 103:         if (prop === "float") {
 104:             prop = (window.attachEvent) ? 'styleFloat': 'cssFloat';
 105:         }
 106:         prop = this.camelize(prop);
 107:         if (!units) {
 108:             units = (prop === 'zIndex' || prop === 'zoom') ? '': 'px';
 109:         }
 110:         try {
 111:             elem.style[prop] = (typeof val === "string") ? val : val + units;
 112:             return elem;
 113:         } catch(e){}
 114:     },
 115:     determineMultiValue : function ( elem, totalframes, frame, prop, opts, easing ) {
 116:     
 117:         var suffix = ["Top", "Right", "Bottom", "Left"];
 118:           var prop = prop;
 119:         var start = opts[prop].start.split(/\s/) || [];
 120:         var end = opts[prop].end.split(/\s/) || [];
 121:         var units = (opts[prop].units) ? opts[prop].units : "px";
 122:         
 123:         var t = 0; r = 0; b = 0; l = 0;
 124:         if (start.length === 2) {
 125:             t = 0; r = 1; b = 0; l = 1;
 126:         }
 127:         if (start.length === 3) {
 128:             t = 0; r = 1; b = 2; l = 1;
 129:         }
 130:         if (start.length === 4) {
 131:             t = 0; r = 1; b = 2; l = 3;
 132:         }
 133:         var tempProp = undefined;
 134:         var a = [t, r, b, l];        
 135:         var len = a.length;
 136:         for (var i = 0; i < len; i++) {
 137:             tempProp = prop + suffix[i];
 138:             
 139:             var z = eval("opts4 = { " + tempProp + ": { start : " + start[a[i]] + ", end : " + end[a[i]] + " }}");
 140:             this.calculate(elem, totalframes, frame, tempProp, z, easing);
 141:         }
 142:     },
 143:     opacity : function ( elem, val ) {
 144:         if (elem.style.filter) {
 145:             elem.style.zoom = 1;
 146:         }
 147:         elem.style.filter = "alpha(opacity=" + parseFloat(val * 100) + ")";
 148:         elem.style.opacity = parseFloat(val);
 149:         return elem;
 150:     },
 151:     color : function ( elem, totalframes, frame, prop, opts, easing ) {
 152:         
 153:         var c1 = opts[prop].start.toLowerCase();
 154:         var c2 = opts[prop].end.toLowerCase();
 155:         
 156:         function returnColorValue ( v ) {
 157:             for (color in vx.color) {
 158:                 if (v === color) {
 159:                     return vx.color[color];
 160:                 }
 161:             }
 162:         }
 163:         if (!/#/.test(c1)) {
 164:             c1 = returnColorValue(c1);
 165:         }
 166:         if (!/#/.test(c2)) {
 167:             c2 = returnColorValue(c2);
 168:         }
 169:         var b = this.hexToArray(c1);
 170:         var e = this.hexToArray(c2);
 171:         var rgb = [];
 172:         for (j = 0; j < 3; j++) {
 173:             rgb.push(parseInt(easing(frame, b[j], e[j] - b[j], totalframes)));
 174:         }
 175:         this.style(elem, prop, 'rgb(' + rgb.join(',') + ')');
 176:     },
 177:     backgroundColor : function ( elem, totalframes, frame, prop, opts, easing ) {
 178:         this.color(elem, totalframes, frame, prop, opts, easing);
 179:     },
 180:     borderColor : function ( elem, totalframes, frame, prop, opts, easing ) {
 181:         this.color(elem, totalframes, frame, prop, opts, easing);
 182:     },
 183:     padding : function ( elem, totalframes, frame, prop, opts, easing ) {
 184:         this.determineMultiValue(elem, totalframes, frame, prop, opts, easing);
 185:     },
 186:     margin : function ( elem, totalframes, frame, prop, opts, easing ) {
 187:         this.determineMultiValue(elem, totalframes, frame, prop, opts, easing);
 188:     },
 189:     bezier : function ( frame, begin, end, deviator, totalframes ) {
 190:         var t = frame / totalframes;
 191:         return (1 - t) * (1 - t) * begin + 2 * t * (1 - t) * deviator + t * t * end;
 192:     },
 193:     hexToDec : function ( hex ) {
 194:         return parseInt(hex, 16);
 195:     },
 196:     hexToRgb : function ( h, e, x ) {
 197:         return [this.hexToDec(h), this.hexToDec(e), this.hexToDec(x)];
 198:     },
 199:     hexToArray : function ( color ) {
 200:         return this.hexToRgb(color.substring(1, 3), color.substring(3, 5), color.substring(5, 7));
 201:     },
 202:     camelize : function ( value ) {
 203:         return value.replace(/-(.)/g,
 204:         function(m, l) {