9.27.06 Flash “wait” animation - i.e. browser throbber

So it’s key in any application to keep the user informed and it can be a huge disconnect if the user doesn’t know if they should interact or wait. Such is the case with many applications - where the user doesn’t know if the program is “thinking” and if they should wait. Hence the simple elegance of the animated “busy” icon present in every popular operating system. Hence the need to add it to my growing list of custom components.

With custom ecommerce flash RIA’s I see an immediate need for a generic and non-instrusive “please wait” animation that essentially keeps the user in sync with the application. This is especially necessary considering the amount of remoting involved with every action made in an ecommerce application. This standard was literally set since the first “throbber” in the NCSA Mosiac program with the ugly “N” graphic moving in and out. Very non-exciting - but it got it’s job done and rather well for that matter.

I tried to put a couple test animations together using purely AS2.0. I’ve found that lately the FireFox “throbber” animation has a good adoption for numerous flash RIAs and plain animations as well as fancy pre-loaders. I essentially wanted a lightweight piece of AS2.0 code that could render such an animation… The code below draws either a circle or a bar in a circular manner and uses the Tween class change the opacity of each leaf node in a circular fashion - just like the FireFox throbber.

New way of doing it

I tried to manifest this idea through tween running for 1000millisecond which were started on a 100millisecond interval if there were 10 leaf nodes and relying on Tween::onMontionFinished() to precisely loop the animation. This is impossible. now… on to purely using a setInterval without any Tween madness so that the only affect a few millisecond slippage will have is if the viewer’s eyes are quick enough to notice it (highly unlikely considering we can’t notice the difference at 33.3milliseconds (NTSC 30 frames per second format)).

Here’s the much better manifestation - that doesn’t “bog” down or get out of sync:
placeholder for flash movie

and for the new code:

class Throbber extends MovieClip {

    private var _leafSize:Number = 4;
    private var _cycleSpeed:Number = 1000;
    private var _color:Number = 0x808080;
    private var _leafCount:Number = 12;
    private var _trailCount:Number = 5;
    private var _circleRadius:Number = 10;
    private var _startLeaf:Number = 9;
    private var _padding:Number = 10;
    private var throbber:MovieClip;
    private var leafNodeItr:Number = 1;
    private var interval;

    // no constructor
   
    function init(width:Number, height:Number, Txt:String) {

        this.throbber = this.createEmptyMovieClip("throbber", this.getNextHighestDepth());
        this.throbber._x = this._circleRadius + this._padding;
        this.throbber._y = 0;
        var offsetMilliseconds = new Date().getMilliseconds();
        var offsetLeafCnt:Number;
        for (var i=this._startLeaf; i < (this._leafCount + this._startLeaf); i++) {
            offsetLeafCnt = i - this._startLeaf + 1;
            this.throbber["leaf" + offsetLeafCnt] = this.throbber.createEmptyMovieClip("leaf" + offsetLeafCnt, this.throbber.getNextHighestDepth());
            //this.drawLeafCircle(this.throbber["leaf" + offsetLeafCnt], (Math.cos((i * (360 / this._leafCount)) * (3.14/180)) * this._circleRadius), (Math.sin((i * (360 / this._leafCount)) * (3.14/180)) * this._circleRadius));
            this.drawLeafStub(this.throbber["leaf" + offsetLeafCnt], (Math.cos((i * (360 / this._leafCount)) * (3.14/180)) * this._circleRadius), (Math.sin((i * (360 / this._leafCount)) * (3.14/180)) * this._circleRadius));
            this.throbber["leaf" + offsetLeafCnt]._alpha = 25;
        }
       
        this.interval = setInterval(
            mx.utils.Delegate.create(this,
                function () {
                    if (this.leafNodeItr > this._leafCount)
                        this.leafNodeItr = 1;
                    var preLo = (this._trailCount > this.leafNodeItr) ? 0 : (this.leafNodeItr - this._trailCount);
                    var preHi = this.leafNodeItr;
                    var apreLo = (this._trailCount > this.leafNodeItr) ? (this._leafCount - (this._trailCount - this.leafNodeItr)) : this._leafCount;
                    var apreHi = this._leafCount;
                    var fadeCount:Number = 25;
                    for (var i:Number = 1; i <= this._leafCount; i++) {
                        if (i == this.leafNodeItr) {
                            this.throbber["leaf" + i]._alpha = 100;
                        } else if ( i > preLo && i <= preHi ) {
                            this.throbber["leaf" + i]._alpha = 25 + (Math.round(75 / this._trailCount) * (i - preLo - (apreLo - this._leafCount)));
                        } else if ( i > apreLo && i <= apreHi ) {
                            this.throbber["leaf" + i]._alpha = 25 + (Math.round(75 / this._trailCount) * (i - (this._leafCount - (preLo - (apreLo - this._leafCount)))));
                        } else {
                            this.throbber["leaf" + i]._alpha = 25;
                        }
                    }
                    this.leafNodeItr++;
                }
            ),
            Math.round(this._cycleSpeed / this._leafCount));
       
    }
   
    function drawLeafStub (target:MovieClip, x:Number, y:Number) {
        target.lineStyle(this._leafSize, this._color, 100);
        target.moveTo(x, y);
        target.lineTo(x*1.5, y*1.5);
    }

    function drawLeafCircle (target:MovieClip, x:Number, y:Number) {
        var w = this._leafSize;
        var h = this._leafSize;
        // center the circle
        w /= 2; h /= 2;
        x += w; y += h;
        var xc1 = w*(Math.SQRT2-1), xc2 = w*(Math.SQRT2/2);
        var yc1 = h*(Math.SQRT2-1), yc2 = h*(Math.SQRT2/2);
        target.lineStyle(0, this._color, 100);
        target.beginFill(this._color);
        target.moveTo(x+w, y);
        target.curveTo(x+w, y+yc1, x+xc2, y+yc2);
        target.curveTo(x+xc1, y+h, x, y+h);
        target.curveTo(x-xc1, y+h, x-xc2, y+yc2);
        target.curveTo(x-w, y+yc1, x-w, y);
        target.curveTo(x-w, y-yc1, x-xc2, y-yc2);
        target.curveTo(x-xc1, y-h, x, y-h);
        target.curveTo(x+xc1, y-h, x+xc2, y-yc2);
        target.curveTo(x+w, y-yc1, x+w, y);
        target.endFill();
    }

    function _close () {
        clearInterval(this.interval);
        this.removeMovieClip();
    }

}

Download this code: throbber-alpha1-snippet

This is the wrong way to do it (this is the old flawed code I first posted)
I found quickly that this throbber doesn’t work well outside of the flash IDE seeing as there’s no way to rely on internal tween setinterval timing. I quickly found that the setInterval is anything but precise in terms of timing events in Milliseconds. This is especially true in the browser flash plugin.

I’m keeping this code here for the sake of pointing out the trial-by-fire learning curve. Such event timing is always an issue hence relying on it for precise animations on the Milliseconds level is just a no-go.

Bar (aka “stub”) leaf node example:
placeholder for flash movie

Circle leaf node example:
placeholder for flash movie

Here’s some uncommented code:

import mx.transitions.Tween;
import mx.transitions.easing.Regular;
import mx.utils.Delegate;

class Throbber extends MovieClip {

    private var _leafSize:Number = 8;
    private var _color:Number = 0x808080;
    private var _leafCount:Number = 8;
    private var _circleRadius:Number = 14;
    private var _padding:Number = 10;
    private var throbber:MovieClip;
    private var intervalArray:Array;
    private var tweenArray:Array;
   
    // no constructor
   
    function init(width:Number, height:Number) {

        this.throbber = this.createEmptyMovieClip("throbber", this.getNextHighestDepth());
        this.throbber._x = this._circleRadius + this._padding;
        this.throbber._y = 0;
        this.intervalArray = new Array();
        this.tweenArray = new Array();
        var offsetMilliseconds = new Date().getMilliseconds();
        for (var i=1; i <= this._leafCount; i++) {
            this.throbber["leaf" + i] = this.throbber.createEmptyMovieClip("leaf" + i, this.throbber.getNextHighestDepth());
            this.drawLeafCircle(this.throbber["leaf" + i], (Math.cos((i * (360 / this._leafCount)) * (3.14/180)) * this._circleRadius), (Math.sin((i * (360 / this._leafCount)) * (3.14/180)) * this._circleRadius));
            //this.drawLeafStub(this.throbber["leaf" + i], (Math.cos((i * (360 / this._leafCount)) * (3.14/180)) * this._circleRadius), (Math.sin((i * (360 / this._leafCount)) * (3.14/180)) * this._circleRadius));
            this.throbber["leaf" + i]._alpha = 25;
            this.intervalArray[i] = setInterval(
                Delegate.create(this,
                    function (i) {
                            this.tweenArray[i] = new Tween(this.throbber["leaf" + i], "_alpha", Regular.easeOut, 100, 25, 1, true);
                            this.tweenArray[i].onMotionFinished = function () {
                                this.rewind();
                                this.start();
                            };
                        clearInterval(this.intervalArray[i]);
                    }
                )
            , (((1000 / this._leafCount) * i) - (new Date().getMilliseconds() - offsetMilliseconds)), i);
        }
       
    }
   
    function drawLeafStub (target:MovieClip, x:Number, y:Number) {
        target.lineStyle(this._leafSize, this._color, 100);
        target.moveTo(x, y);
        target.lineTo(x*1.5, y*1.5);
    }

    function drawLeafCircle (target:MovieClip, x:Number, y:Number) {
        var w = this._leafSize;
        var h = this._leafSize;
        // center the circle
        w /= 2; h /= 2;
        x += w; y += h;
        var xc1 = w*(Math.SQRT2-1), xc2 = w*(Math.SQRT2/2);
        var yc1 = h*(Math.SQRT2-1), yc2 = h*(Math.SQRT2/2);
        target.lineStyle(0, this._color, 100);
        target.beginFill(this._color);
        target.moveTo(x+w, y);
        target.curveTo(x+w, y+yc1, x+xc2, y+yc2);
        target.curveTo(x+xc1, y+h, x, y+h);
        target.curveTo(x-xc1, y+h, x-xc2, y+yc2);
        target.curveTo(x-w, y+yc1, x-w, y);
        target.curveTo(x-w, y-yc1, x-xc2, y-yc2);
        target.curveTo(x-xc1, y-h, x, y-h);
        target.curveTo(x+xc1, y-h, x+xc2, y-yc2);
        target.curveTo(x+w, y-yc1, x+w, y);
        target.endFill();
    }

    /* It is important to stop previous Tween object instantiation if this class
     * is to be created/removed more than once in a specific animation */

    function _close () {
        for (var tCnt in this.tweenArray) {
            this.tweenArray[tCnt].stop();
            delete(this.tweenArray[tCnt]);
        }
        for (var iCnt in this.intervalArray) {
            clearInterval(this.intervalArray[iCnt]);
            delete(this.intervalArray[iCnt]);
        }
        this.removeMovieClip();
    }
   
}

Download this code: throbber-alpha-snippet


11 Comments

Nikolaj on 11.30.06 at 11am

This r0xORz!!!111111111

j00 have some 31337 flash sk1lzz

Jim Palmer on 11.30.06 at 11am

Me thinks Nikolaj is afraid of the defunct tweenArray container for this object. Either that or the horribly amazing circle drawing function.

Art on 3.6.07 at 2am

Hi, I’ve been looking for a tutorial that had this kind of visual for a flash loader. Then I found it here, but when I copied and pasted the code into Flash 8, nothing happened.

Would it be possible to provide a tutorial or flash file on how to make this a flash preloader? Thanks.

Also how effective is the Anti-spam verify code, and would you recommend it on all websites with a contact form? thanks again.

Jim Palmer on 3.6.07 at 11am

That’s a really good question. I built this as a class so the above code is not ideal for being a pre-loader animation. It could easily be changed to be so though. I can whip a new version together to be a simple and straight-forward pre-loader that can be just cut-n-pasted into Frame 1 and will work. To do this, we’ll just need to not make it a class or a function - but just take the guts of the _init() function and paste them directly onto Frame 1 (with the private variables and draw function in the code as well).

The anti-spam verify code works rather well, simple yet effective. It’s AuthImage thanks to Sandy Lee.

Dermot Williams on 10.3.07 at 3am

I’d love to use this throbber for a personal project I’m working on but I’m a bit of an AS newb. How do I actually get my script to display one?

Jim Palmer on 10.3.07 at 9am

Dermot,
- Copy the first block of code as Throbber.as in the same directory you’ll be creating and saving the flash document in.
- Create a new Flash Document
- Create a new “Symbol” in the Library
- Name the Symbol “Throbber”
- Check the “Export for ActionScript” checkbox in the middle of the Create New Symbol window next to “Linkage:”
- Change the “Identifier” to “Throbber”
- Change the “AS 2.0 class:” to “Throbber”
- Drag the Throbber from the library onto your stage.
- Click on the white dot with a black border created on the stage and view the properties panel.
- put in an instance name to replace the “” in the properties panel. Replace it with “throbber”.
- Edit the action script on the first frame by clicking on a blank spot on the stage making sure the throbber dot is not selected.
- add the following code to the actionscript panel for Frame1: “this.throbber.init();”
- run a test and all should work.
- Ensure the “AS 2.0 class” value matches the class declaration as well as the class declaration’s filename, i.e. they all need to be “Throbber”.
- The newly created Symbol in the library MUST be a MovieClip (that’s in the Create New Symbol window or the properties of a newly created library asset)

Dermot Williams on 10.4.07 at 5am

Worked like a charm - thanks!

Victor on 12.8.07 at 10pm

Thank you very much great code. Here is the code in ActionScript 3.
import fl.transitions.Tween;
import fl.transitions.easing.*;
import fl.transitions.TweenEvent;

var _leafSize:Number = 5;
var _color:Number = 0×808080;
var _leafCount:Number = 8;
var _circleRadius:Number = 10;
var _padding:Number = 10;
var throbber:MovieClip;
var intervalArray:Array;
var tweenArray:Array;
var offsetMilliseconds = new Date().getMilliseconds();

// create movie clip
throbber = new MovieClip();
throbber.x = this.bg_mc.x + this.bg_mc.width/2 + _circleRadius + _padding;
throbber.y = this.bg_mc.y + this.bg_mc.height/2;
throbber.alpha = 1;

// create instance of arrays
intervalArray = new Array();
tweenArray = new Array();

// loop and create circles
for (var i=1; i <= _leafCount; i++) {
this.throbber["leaf" + i] = new MovieClip();
//this.drawLeafCircle(this.throbber["leaf" + i], (Math.cos((i * (360 / _leafCount)) * (3.14/180)) * _circleRadius), (Math.sin((i * (360 / _leafCount)) * (3.14/180)) * _circleRadius));
this.drawLeafStub(this.throbber["leaf" + i], (Math.cos((i * (360 / _leafCount)) * (3.14/180)) * _circleRadius), (Math.sin((i * (360 / _leafCount)) * (3.14/180)) * _circleRadius));
this.throbber["leaf" + i].alpha = 0.25;
this.intervalArray[i] = flash.utils.setInterval(aniTween,(((1000 /_leafCount) * i) - (new Date().getMilliseconds() - offsetMilliseconds)), i);
// Add child
this.throbber.addChild(this.throbber["leaf" + i]);
}
addChild(throbber);

function aniTween(i:Number) {
this.tweenArray[i] = new Tween(this.throbber["leaf" + i],”alpha”,Regular.easeOut, 1, 0.25,1, true);
this.tweenArray[i].addEventListener(TweenEvent.MOTION_FINISH,restartTween);
clearInterval(this.intervalArray[i]);
//this.tweenArray[i].onMotionFinished = function () {
// this.rewind();
// this.start();
// };
//clearInterval((this.intervalArray[i].id);
}
function restartTween(event:TweenEvent) {
Tween(event.currentTarget).rewind();
Tween(event.currentTarget).start();
}
function drawLeafStub (target:MovieClip, x:Number, y:Number) {
target.graphics.lineStyle(this._leafSize, this._color, 100);
target.graphics.moveTo(x, y);
target.graphics.lineTo(x*1.5, y*1.5);
}
function drawLeafCircle(target:MovieClip, x:Number, y:Number) {
var w = this._leafSize;
var h = this._leafSize;
// center the circle
w /= 2;
h /= 2;
x += w;
y += h;
var xc1 = w*(Math.SQRT2-1), xc2 = w*(Math.SQRT2/2);
var yc1 = h*(Math.SQRT2-1), yc2 = h*(Math.SQRT2/2);

target.graphics.lineStyle(0, _color, 100);
target.graphics.beginFill(_color);
target.graphics.moveTo(x+w, y);
target.graphics.curveTo(x+w, y+yc1, x+xc2, y+yc2);
target.graphics.curveTo(x+xc1, y+h, x, y+h);
target.graphics.curveTo(x-xc1, y+h, x-xc2, y+yc2);
target.graphics.curveTo(x-w, y+yc1, x-w, y);
target.graphics.curveTo(x-w, y-yc1, x-xc2, y-yc2);
target.graphics.curveTo(x-xc1, y-h, x, y-h);
target.graphics.curveTo(x+xc1, y-h, x+xc2, y-yc2);
target.graphics.curveTo(x+w, y-yc1, x+w, y);
target.graphics.endFill();
}

Victor on 12.8.07 at 10pm

My Bad the code above is a translation of the old version.
Here is the new one in action script 3.
import fl.transitions.Tween;
import fl.transitions.easing.*;
import fl.transitions.TweenEvent;

var _leafSize:Number = 4;
var _cycleSpeed:Number = 1000;
var _color:Number = 0×808080;
var _leafCount:Number = 12;
var _trailCount:Number = 5;
var _circleRadius:Number = 10;
var _startLeaf:Number = 9;
var _padding:Number = 10;
var throbber:MovieClip;
var leafNodeItr:Number = 1;
var interval;

// no constructor
//function init(width:Number, height:Number, Txt:String) {

this.throbber = new MovieClip();
this.throbber.x = this.bg_mc.x + this.bg_mc.width/2 + _circleRadius + _padding;
throbber.y = this.bg_mc.y + this.bg_mc.height/2;
var offsetMilliseconds = new Date().getMilliseconds();
var offsetLeafCnt:Number;
// run through all leave
for (var i=this._startLeaf; i this._leafCount) {
this.leafNodeItr = 1;
}
var preLo = (this._trailCount > this.leafNodeItr) ? 0 : (this.leafNodeItr - this._trailCount);
var preHi = this.leafNodeItr;
var apreLo = (this._trailCount > this.leafNodeItr) ? (this._leafCount - (this._trailCount - this.leafNodeItr)) : this._leafCount;
var apreHi = this._leafCount;
var fadeCount:Number = 25;
for (var i:Number = 1; i preLo && i apreLo && i <= apreHi ) {
this.throbber["leaf" + i]._alpha = .25 + (Math.round(75 / this._trailCount) * (i - (this._leafCount - (preLo - (apreLo - this._leafCount)))))/100;
}
else {
this.throbber["leaf" + i]._alpha = .25;
}
}
this.leafNodeItr++;
}
//
// draw leaf
//
function drawLeafStub (target:MovieClip, x:Number, y:Number) {
target.graphics.lineStyle(this._leafSize, this._color, 100);
target.graphics.moveTo(x, y);
target.graphics.lineTo(x*1.5, y*1.5);
}

function _close () {
clearInterval(this.interval);
this.removeChild(this.throbber);
}

Neal on 1.31.08 at 5pm

What does ‘this["message"]‘ refer to in the init() function?

Jim Palmer on 2.13.08 at 4pm

I’ll take that out - it’s useless. this["message"] was another TextField that was added to sit beside the throbber - for instance “Please Wait…”. It’s useless in the context of this post.




1.139s