How We Chained Animation In jQuery
By Hisa Ishibashi
Lead Backend Developer at Amedia Creative
While working on a project a few weeks ago, a client of ours placed an exciting new challenge on our laps. This challenge involved a few requirements, which needed to be met and animated via JavaScript. At Amedia Creative, we use jQuery as our preferred JavaScript development framework for our web design and development services. Even though jQuery made things easier, this piece of animation proved to be a small challenge.
Solution = Team+Effort+Skills+Experience+Knowledge... coffee?
There were four requirements for this interactive piece.
1: The client submitted a design that had an image spanning across multiple squares. This design created a "loft" window like affect.
Loft Windows

2: Each image needed to rotate. The total images in the sequence were three. These images needed to appear in a consistent order--not appear at random.
3: Each square needed to animate the new image in separately at random. What does this mean? If we have a total of twelve squares, each square would need be replaced with a piece of the new image. This needed to occur at different times, additionally, in a ramdom order.
4: These images needed to be able to be managed via a CMS.
Last Thursday, one of my co-workers & Lead Frontend Developer here at Amedia Creative, Fito, was working on this issue. Fito did a fantastic job on the design. He created the required animation with jQuery. This was the code he provided in his proof of concept.
/*---------- Landing Animation ----------*/
var curni = 1;
var ni = 2;
window.setTimeout('xoxo()', 1600);
xoxo=function(){
$('.anim_blk6').animate({opacity:'0'},400, function(){
$('.anim_blk6').removeClass('back0'+curni).addClass('back0'+ni).animate({opacity:'1'},600);
$('.anim_blk4').animate({opacity:'0'},400, function(){
$('.anim_blk4').removeClass('back0'+curni).addClass('back0'+ni).animate({opacity:'1'},600);
$('.anim_blk11').animate({opacity:'0'},400, function(){
$('.anim_blk11').removeClass('back0'+curni).addClass('back0'+ni).animate({opacity:'1'},600);
$('.anim_blk5').animate({opacity:'0'},400, function(){
$('.anim_blk5').removeClass('back0'+curni).addClass('back0'+ni).animate({opacity:'1'},600);
$('.anim_blk1').animate({opacity:'0'},400, function(){
$('.anim_blk1').removeClass('back0'+curni).addClass('back0'+ni).animate({opacity:'1'},600);
$('.anim_blk8').animate({opacity:'0'},400, function(){
$('.anim_blk8').removeClass('back0'+curni).addClass('back0'+ni).animate({opacity:'1'},600);
$('.anim_blk9').animate({opacity:'0'},400, function(){
$('.anim_blk9').removeClass('back0'+curni).addClass('back0'+ni).animate({opacity:'1'},600);
$('.anim_blk3').animate({opacity:'0'},400, function(){
$('.anim_blk3').removeClass('back0'+curni).addClass('back0'+ni).animate({opacity:'1'},600);
$('.anim_blk7').animate({opacity:'0'},400, function(){
$('.anim_blk7').removeClass('back0'+curni).addClass('back0'+ni).animate({opacity:'1'},600);
$('.anim_blk10').animate({opacity:'0'},400, function(){
$('.anim_blk10').removeClass('back0'+curni).addClass('back0'+ni).animate({opacity:'1'},600);
$('.anim_blk2').animate({opacity:'0'},400, function(){
$('.anim_blk2').removeClass('back0'+curni).addClass('back0'+ni).animate({opacity:'1'},600);
$('.anim_blk12').animate({opacity:'0'},400, function(){
$('.anim_blk12').removeClass('back0'+curni).addClass('back0'+ni).animate({opacity:'1'},600, function(){
curni++;
ni++;
if(curni > 4){ curni = 1 };
if(ni > 4){ ni = 1 };
window.setTimeout('xoxo()', 5000);
});
});
});
});
});
});
});
});
});
});
});
});
});
};
//end here
At first, I thought that the recursion was unnecessary. I felt, this type of development was a maintenance nightmare. However, the client's business requirements were fulfilled. That's a win in itself. Fito managed to create the look of a random sequence that was not actually random.
Later, Fito asked me how he could dynamically create a random sequence and keep track of blocks already changed. He explored using JavaScript's Math.random() coupled with a series of arrays.
The next day, Fito revised his proof of concept using Math.random(). At that time, the only missing piece was to keep track of random numbers using arrays. I was thinking, "how hard can it be?" Famous last words, because after an hour I could not figure out what was wrong. At that time I took a copy of Fito's code to my computer to address it further. I couldn't continue on it because I had to go back to the issue I was currently working on. After a day's worth of work and a night worth of fun I came back to the office at 1am and decided to put in some extra time to figure this out.
Remember when we thought the recursion was unnecessary? Well, I was wrong. Some of the assumptions I made were:
- I thought I could achieve the animation wrapping repeating the code inside a while(true) loop.
- I thought I could run the animation sequentially or in a loop.
Yep, those didn't work out. Let's explore why.
While(true) = death of a browser.
While(true) kills the browser. That's not good, at all. Additionally, You can't wrap the .animate() inside a loop, or make it sequential. Finding this out the hard way, I tried the following:
$('.anim_blk'+rand).animate({opacity:'0'},400, function(){
$('.anim_blk'+rand).removeClass('back0'+cur).addClass('back0'+next).animate({opacity:'1'},600);
});
This was placed inside a loop with random changing values. I also tried repeating those lines one after another several times. It did not produce the results we needed. Everything ended up changing at the same time. I realized then, that the loop was running faster than the .animate() function. I tried to delay the animation in all possible ways but still no results. The reason for this behavior? JavaScript is asynchronous by nature. I had to find out a way to chain the animation.
Overloading my brain with Google research, I had no idea that the solution was under my nose. Going back to the original code, Fito was doing exactly what he should have done; chaining the .animate() calls in a queue, thus creating an animation sequence. I had to shift my focus. Before passing out at 4:30 am I created the following code.
/**
* Amedia Creative Puzzle Slider
*/
var cur = 1;
var next = 2;
function acPuzzleSlider(){
var divList = Array(12);
var total = divList.length;
var countMarked = 0;
//Initialize array to false
for(var i=0; i<divList.length; i++){
divList[i] = false;
}
//Enqueue the elements for animation
while(countMarked<total){
var rand = Math.floor(Math.random()*total);
if(divList[rand]== false){
divList[rand] = $('.anim_blk'+countMarked);
countMarked++;
}
}
//Animate doesn't work well inside loops. That's why we have to make a tail recursion
animateDivs(divList);
//
cur++;
next++;
if(cur>4) cur = 1;
if(next>4) next = 1;
t = setTimeout("acPuzzleSlider()", 9600);
}//end acPuzzleSlider()
function animateDivs(divList){
if(divList.length>0){
value = divList.shift();
value.animate({opacity:'0.5'}, 400, function(){
value.removeClass('back0'+cur).addClass('back0'+next).animate({opacity:'1'}, 400, function(){
animateDivs(divList);
});
});
}
}
I called this using the document ready function.
/*---------- Landing Animation ----------*/
window.setTimeout('acPuzzleSlider()', 900);
Now my next issue, this code could not be used within a CMS fashion. The background image displayed depended on the class attribute. Although I achieved a dynamic sequence, that solution was not satisfactory. I needed to design another solution. As you may imagine, yes, I thought I could write a jQuery plugin. And that is exactly what I did on Sunday after a Mother's day party with mother.
/**
* jquery.blocksCycle.js
*
* This plugin is used to randomly change the background of the inner blocks
* of a container. I don't know anything about licenses.
*
* @author: Hisakazu Ishibashi
* @email: h.ishibashi@amediacreative.com
*/
(function($) {
/**
* the actual plugin
*/
$.fn.blocksCycle = function(options) {
// build main options before element iteration
var opts = $.extend({}, $.fn.blocksCycle.defaults, options);
// Iterate through the matching elements and start the animation on each of their children
return this.each(function() {
$this = $(this);
// build element specific options
var o = $.meta ? $.extend({}, opts, $this.data('blocksCycle')) : opts;
// the blocks
var blocks = $this.children();
blocks = $.makeArray(blocks);
if(o.shuffle==true)
$.fn.blocksCycle.shuffle(blocks);
//Animate doesn't work well inside loops, that's why we have to make a tail recursive call
cycle(blocks, o.infinite, 0);
});
};
/**
* Starts the animation sequence
*/
function cycle(blocks, infinite, bgIndex){
var curBlocks = blocks.slice();
var url = $.fn.blocksCycle.defaults.urls[bgIndex];
var delay = $.fn.blocksCycle.defaults.delay;
if(url){
changeBlocks(curBlocks, url);
t = setTimeout(function(){cycle(blocks, infinite, ++bgIndex)}, delay);
}else{
if(infinite)
cycle(blocks, infinite, 0)
//t = setTimeout(function(){cycle(blocks, infinite, 0)}, delay);
}
return;
}
/**
* Used to change to the next image by changing each of the blocks sequentially
*/
function changeBlocks(blocks, url){
if(blocks.length>0){
var value = $(blocks.shift());
var rule = {
'background': 'url('+url+')',
'background-repeat': '',
'background-position':''
};
var duration = $.fn.blocksCycle.defaults.duration;
var opacity = $.fn.blocksCycle.defaults.opacity;
value.animate({opacity:opacity}, duration, function(){
value.css(rule).animate({opacity:1}, duration, function(){
changeBlocks(blocks, url);
});
});
}
}
/**
* This functions creates the order in which blocks will be changing to the
* next image.
*/
$.fn.blocksCycle.shuffle = function(list) {
var len = list.length;
var i = len;
while (i--) {
var p = parseInt(Math.random()*len);
var t = list[i];
list[i] = list[p];
list[p] = t;
}
};
//
// plugin defaults
//
$.fn.blocksCycle.defaults = {
shuffle: true, //Indicates if the animation should be random.
infinite: true, //Indicates if the cycle should be infinite, it starts looping again from the first background.
urls: null, //List of URLs from where to get the images for animation
first: 0, //Indicates the first element in the backgrounds list
/** CAUTION WHEN SETTING THE FOLLOWING VALUES**/
delay: 16400, //Indicates the amount of time to wait in order to show the next background
duration: 400, //This value is passed to jquery.anime() called inside. Indicates the duration of a single block change animation.
opacity: 0.5 //From 0 to 1 indicates the intensity of a single block change animation.
};
//
// end of closure
//
})(jQuery);
Alas! A plugin is born!
With this plugin you can configure and start the animation in the following way:
var images = [];
images.push('images/back02.jpg');
images.push('images/back03.jpg');
images.push('images/back04.jpg');
images.push('images/back01.jpg');
$.fn.blocksCycle.defaults.urls = images;
$('.slider').blocksCycle();
The only requirement remaining was to have the following HTML/CSS structure, and to format the background position and other attributes like background-repeat, of each block. For example:
<div class="slider">
<div class="block blk0"></div>
<div class="block blk1"></div>
<div class="block blk2"></div>
<div class="block blk3"></div>
<div class="block blk4"></div>
<div class="block blk5"></div>
</div>
The amount of inner blocks and format is totally arbitrary. Also, the div tags could be replaced by another type of container that supports the background css property. By the way, you can see in the plugin, that I made some optimizations of the first approach and in the changeBlocks() function you can see that I replaced the add/remove Class methods for css('background').
Thanks for reading!
-Hisa
Resources:
http://www.learningjquery.com/2007/10/a-plugin-development-pattern
http://docs.jquery.com/Plugins/Authoring
--
Got questions?
Call us. 702 / 944 / 8744
Email us: info@amediacreative.com
Visit us: http://www.amediacreative.com
Amedia Creative is headquartered in Las Vegas, NV
We deliver highly scalable feature rich web portals & applications. We are web strategists, not just designers & developers. We’d love to create your web solution.



Comments (2)