views:

513

answers:

1

How would I animate individual characters of text on a page in HTML5. Like this example in flash. http://www.greensock.com/splittextfield/

+3  A: 

You'd have to wrap each character in a <span> then move that span using CSS position/top/left.

You couldn't completely reproduce what the Flash example did, because that example uses a blur effect. CSS can't do that; SVG could, and IE's non-standard filter could, but that would mean a total code branch.

You could change the size of each letter by setting the font-size, but to so the kind of shear/rotate effects the example does you'd have to use a CSS transform. This isn't standardised yet and there are holes in browser support.

Here's a proof-of-concept I just hacked up mainly for Firefox (sorry for the code length):

<p id="effect">I've got a lovely bunch of coconuts.</p>
<button id="animate">Animate</button>


// Add ECMA262-5 method binding if not supported natively
//
if (!('bind' in Function.prototype)) {
    Function.prototype.bind= function(owner) {
        var that= this;
        if (arguments.length<=1) {
            return function() {
                return that.apply(owner, arguments);
            };
        } else {
            var args= Array.prototype.slice.call(arguments, 1);
            return function() {
                return that.apply(owner, arguments.length===0? args : args.concat(Array.prototype.slice.call(arguments)));
            };
        }
    };
}

// Lightweight class/instance system
//
Function.prototype.makeSubclass= function() {
    function Class() {
        if (!(this instanceof Class))
            throw 'Constructor function requires new operator';
        if ('_init' in this)
            this._init.apply(this, arguments);
    }
    if (this!==Object) {
        Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
        Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
    }
    return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};

// Abstract base for animated linear sliding switch between 0 and 1
//
var Animation= Object.makeSubclass();
Animation.prototype._init= function(period, initial) {
    this.period= period;
    this.interval= null;
    this.aim= initial || 0;
    this.t= 0;
};
Animation.prototype.set= function(aim) {
    if (aim===this.aim)
        return;
    this.aim= aim;
    var now= new Date().getTime();
    if (this.interval===null) {
        this.t= now;
        this.interval= window.setInterval(this.update.bind(this), 32);
    } else {
        this.t= now-this.t-this.period+now
        this.update();
    }
};
Animation.prototype.toggle= function() {
    this.set(this.aim===0? 1 : 0);
};
Animation.prototype.update= function() {
    var now= new Date().getTime();
    var x= Math.min((now-this.t)/this.period, 1);
    this.show(this.aim===0? 1-x : x);
    if (x===1) {
        window.clearInterval(this.interval);
        this.interval= null;
    }
};
Animation.prototype.show= function(d) {};

// Animation that spins each character in a text node apart
//
var ExplodeAnimation= Animation.makeSubclass();
ExplodeAnimation.prototype._init= function(node, period) {
    Animation.prototype._init.call(this, period, 0);
    this.spans= [];

    // Wrap each character in a <span>
    //
    for (var i= 0; i<node.data.length; i++) {
        var span= document.createElement('span');
        span.style.position= 'relative';
        span.appendChild(document.createTextNode(node.data.charAt(i)));
        node.parentNode.insertBefore(span, node);
        this.spans.push(span);
    }

    // Make up random positions and speeds for each character.
    // Possibly this should be re-randomised on each toggle?
    //
    this.randomness= [];
    for (var i= this.spans.length; i-->0;)
        this.randomness.push({
            dx: Math.random()*200-100, dy: Math.random()*200-150,
            sx: Math.random()*1.5, sy: Math.random()*1.5,
            dr: Math.random()*240-120, og: Math.random()+0.5
        });

    node.parentNode.removeChild(node);
};
ExplodeAnimation.prototype.show= function(d) {
    for (var i= this.spans.length; i-->0;) {
        var style= this.spans[i].style;
        var r= this.randomness[i];

        style.left= d*r.dx+'px';
        style.top= d*r.dy+'px';
        var transform= 'rotate('+Math.floor(d*r.dr)+'deg) scale('+(d*r.sx+1-d)+', '+(d*r.sy+1-d)+')';
        if ('transform' in style)
            style.transform= transform;
        else if ('MozTransform' in style)
            style.MozTransform= transform;

        var o= 1-Math.pow(d, r.og);
        if ('opacity' in style)
            style.opacity= o+'';
        else if ('filter' in style)
            style.filter= 'opacity(alpha='+Math.ceil(o*100)+')';
    }
};


var animation= new ExplodeAnimation(document.getElementById('effect').firstChild, 1000);
document.getElementById('animate').onclick= animation.toggle.bind(animation);

This could be improved by adding gravity and better 3D-space modelling of the currently-completely-random transforms, plus better browser support for the scale/rotation in browsers other than Firefox.

(IE has its own non-standard CSS transform filters it might be possible to support; Webkit and Opera have webkitTransform and oTransform styles, but they refuse to transform relative-positioning inline spans, so you'd have to absolute-position each character, which would mean reading all their normal positions to use as baseline. I got bored at this point.)

bobince
would love to see this in action! PS: you could fake the blur with a `text-shadow: 0 0 5px samecolorasfont`
meo
...combined with `color: rgba(0,0,0,0)` to stop the text itself appearing. Clever! But I'm off to bed.
bobince
yeah nice idea i have tried it with opacity but it would look much better with rgba!! http://jsfiddle.net/GqTam/1/
meo
cool i think if you tune the the values a little it could look like a real blur: http://jsfiddle.net/GqTam/4/
meo
Thanks, this is extremely helpful, the code I was struggling with was the wrapping in span. AS3 has a function for getcharboundaries(); and this seems pretty painless. Very much appreciated.
mconnors