Making Of : Text Effects and Animations
Published the
Hello there, as promised this post will introduce the making of this site.
The first topic I'll cover is the header and most precisely the effects and animations on the header's texts. To make it clear, there's no Flash, the whole header's animations and effects are done with a combination of HTML, JavaScript and CSS. I could have done it in Flash, yes, I could have, but I would not have learned by doing so.
Ok, no more blabla, if you want to see how I've done these effects, read the rest of this post.
The swing
The first thing that I placed as requirement is that I do not wanted to affect too much the HTML code to keep it clean and valid. In order to have a header that are legible even if Javascript and CSS are turned off.
Secondly, to animate text characters indivuadly it was necessary to split the HTML text to isolate each character and apply effects and animations for each. As I wanted to mix up two styles of text in the title, it was clear that I'll have to deal with nested nodes and probably most of the initial layout and styling of the title will be done with styles.
I quickly figured that I needed a really simple and generic solution : Taking a set of nodes (a jQuery object for example) and split all the text nodes without touching to neither the other nodes nor their index in their respective parent. Each char will be wrapped in a span with a custom class.
Here the code that do the split (this is an improved version compared to the one I use1) :
// take a set as argument, parent is here for recursions
function splitText( set, parent )
{
if( !set || !set.length || set.length == 0 )
throw "set was not valid, it must be an object with length != 0";
// to check wether we are in a recursion or not
var hasParent = parent != null;
var a = [];
var l = set.length;
for( var i=0; i<l;i++)
{
// for root nodes, a placeholder is created
if( !hasParent )
var newContent = $("<div></div>");
var n = set[i];
// we're looking for text node to split
if( n.nodeType == 3 )
{
n = $(n);
var txt = n.text();
var m = txt.length;
for(var j=0;j<m;j++)
{
var t = txt.substr(j,1);
var c = $("<span class='char'>" + t + "</span>");
// place the new element either in its parent
// or in the placeholder
if( hasParent )
parent.append(c);
else
newContent.append(c);
a.push(c[0]);
}
}
// recursively split nodes
else
{
var nodeClone = $(n).clone();
nodeClone.html("");
// place the new element either in its parent
// or in the placeholder
if( hasParent )
parent.append( nodeClone );
else
newContent.append( nodeClone );
var chars = splitText( n.childNodes, nodeClone );
a = a.concat( chars );
}
// once splitted, a root node is replaced in the DOM
// with the generated content
if(!hasParent )
{
n = $(n)
var previous = n.prev();
// according to the parent structure we use append
// or insert
if( previous.length > 0 )
previous.after( newContent.children() );
else
{
var next = n.next();
if( next.length > 0 )
next.before( newContent.children() );
else
{
var p = n.parent();
p.append( newContent.children() );
}
}
n.remove();
}
}
return a;
}
Now to use it I'll just have to add a call to splitText in the document.ready callback :
var chars = splitText( $("h1") );
And add a bit of styling to highlight chars :
.char {
border:1px solid #E4D8CE;
}
And this the result and the corresponding unit test. If you have Firebug, you'll see that each chars in the <h1> are wrapped in a <span> node. This is the basic setup that will allow further manipulation such as animation.
The approach
Now that the text is splitted into chars, the next step is to animate them, which is not simple as it sounds. Once again, the big issue lies in styling, Ideally, animating the text shouldn't alter too much the page layout. In the previous example, that means that the paragraphs surrounding the titles must remain at the same place, whatever animations I use on the titles.
The goal here is to end with my titles splitted, with their size set to the one before the split (their original size), and each char as a position:absolute element positionned as the same position as before the split (their original position).
To acheive that the splitText function will have to register the size and positions of the elements during the split. Which make the splitText function look like this :
// take a set as argument, parent is here for recursions
function splitText( set, parent )
{
if( !set || !set.length || set.length == 0 )
throw "set was not valid, it must be an object with length != 0";
// to check wether we are in a recursion or not
var hasParent = parent != null;
var a = [];
var l = set.length;
for( var i=0; i<l;i++)
{
// for root nodes, a placeholder is created
if( !hasParent )
var newContent = $("<div></div>");
var n = set[i];
// we're looking for text node to split
if( n.nodeType == 3 )
{
n = $(n);
var txt = n.text();
var m = txt.length;
for(var j=0;j<m;j++)
{
var t = txt.substr(j,1);
var c = $("<span>" + t + "</span>");
// place the new element either in its parent
// or in the placeholder
if( hasParent )
parent.append(c);
else
newContent.append(c);
a.push(c[0]);
}
}
// recursively split nodes
else
{
var nodeClone = $(n).clone();
nodeClone.html("");
// place the new element either in its parent
// or in the placeholder
if( hasParent )
parent.append( nodeClone );
else
newContent.append( nodeClone );
var chars = splitText( n.childNodes, nodeClone );
a = a.concat( chars );
}
// once splitted, a root node is replaced in the DOM
// with the generated content
if(!hasParent )
{
n = $(n);
// fix the size of root node since its child will
// have an absolute position
newContent.children().width( n.width() )
.height( n.height() );
var previous = n.prev();
// according to the parent structure we use append
// or insert
if( previous.length > 0 )
previous.after( newContent.children() );
else
{
var next = n.next();
if( next.length > 0 )
next.before( newContent.children() );
else
{
var p = n.parent();
p.append( newContent.children() );
}
}
n.remove();
}
}
// at the end of the main call we register
// positions and affect class to chars.
if( !hasParent )
{
a.forEach( function(o,i){
o = $(o);
var offset = o.offset();
o.css( "left", offset.left ).css( "top", offset.top );
} );
// adding the class is done in a second pass
// otherwise they will all have the first char position
a.forEach( function(o,i){
o = $(o);
o.addClass("char");
} );
}
return a;
}
As the chars position have to be absolute the CSS is changed to :
.char {
border:1px solid #E4D8CE;
position:absolute;
}
And this the result and the corresponding unit test. You can notice that the layout is not broken, and even the borders on chars doesn't affect their placement.
The putt
Now that the text to animate is properly setup the last thing to do is to put it in motion. For example with a simple tremble effect (as in the header).
The simpler the better, I'll just have to randomly set the margin-left and margin-right property of each char. At this point there's no need to link each char to a particle (as done in the header when it explode).
function tremble ( chars )
{
chars.forEach( function(o,i){
o = $(o);
o.css( "margin-left", 2 - Math.floor( Math.random() * 4 ) )
.css( "margin-top", 2 - Math.floor( Math.random() * 4 ) );
});
}
$(document).ready(function(){
var chars = splitText( $("h1") );
// yeah that's only 10 frames per second
// but I don't want to kill your computer
setInterval( tremble, 100, chars );
});
Out of bounds
A few last words to say that this technique, while great to quickly produce some nice effects on text elements, can be quite CPU consuming. For instance, on my computer, animations in Firefox 6 are three to four times slower than in Chromium. And I encourage to use the HTML5 requestAnimationFrame instead of setInterval or setTimeout to deal with animation, I could observe huge improvement in firefox (like two times faster) when using it, but for the sake of keeping this example simple I used setInterval.
If you want to know more about the requestAnimationFrame function you should read the excellent post by Paul Irish about it.
Notes:
- In fact, the version in my header is a quick and dirty proof of concept, and this post allow me to clean up this stuff.
This post is part of the "Making Of" series.
Wants to leave a word ?