Creating animated effects using particles

One of the most common uses of particle systems is to create animated visual effects such as smoke and fire. In this section, we show you simple ways of producing some of these effects using physics-based animation. We start with a very simple example and then create a particle emitter that will allow us to create some more elaborate effects. The approach in this section is that we wish to produce animations that look realistic, but that don't have to be completely accurate.

A simple example: splash effect with particles

In this example, we will drop objects in water and then create a splash effect using particles. The objects will be balls of different sizes and will be dropped one at a time from the same height. The splash will be made from a group of Ball objects of smaller size. Let's dive straight into the code that creates these objects. The file is called Splash.as. Here is a full listing of the code:

package{
        import flash.display.Sprite;
        import com.physicscodes.objects.Ball;
        import com.physicscodes.math.Vector2D;

        public class Splash extends Sprite{
                public function Splash():void{
                        init();
                }
                private function init():void{

                        var water:Sprite = new Sprite();
                        with (water.graphics) {
                                beginFill(0x00ffff,0.5);
                                drawRect(0,500,stage.stageWidth,stage.stageHeight-500);
                                endFill();
                        }
                        addChild(water);

                        var drop:Ball = new Ball(8,0x399ff);
                        drop.pos2D = new Vector2D(400,100);
                        drop.velo2D = new Vector2D(0,100);
                        addChild(drop);

                        var numSplashDrops:uint = 20;
                        var splash:Array = new Array();
                        for (var i:uint=0; i<numSplashDrops; i++){
                                var radius:Number = Math.random()*2+1;
                                var splashDrop:Ball = new Ball(radius,0x3399ff);
                                splashDrop.visible = false;

                                addChild(splashDrop);
                                splash.push(splashDrop);
                        }

                        var mover:SplashMover=new SplashMover(drop,splash);
                        mover.startTime();
                        drop.move();
                }
        }
}

As you can see, we first create a filled rectangle with a transparency of 0.5 to represent water and position it at the bottom of the stage. We then create a Ball object named drop (think raindrop) with a radius of 8 pixels and position it above the water surface, at y = 100. The drop is then given a velocity of 100 px/s vertically downward. Next, 20 Ball objects are created with random radiuses between 1 and 3 pixels. These will be the splash droplets, and their visibility is initially set to false. They are put into an array called splash.

The drop and the splash array are then passed to a SplashMover instance (to be described shortly) to start the animation. The drop is animated independently by invoking its move() method. This makes it move at the constant downward velocity of 100 px/s specified earlier in the code. You may be wondering why we make the object fall at a constant velocity instead of making it accelerate under gravity. The answer is that if it is a raindrop, it would probably have reached terminal velocity anyway.

We'll now take a look at the SplashMover class. The full code is not too long, so here it is:

package {
        import flash.display.Sprite;
        import com.physicscodes.motion.MultiForcer;
        import com.physicscodes.motion.Forces;
        import com.physicscodes.objects.Particle;
        import com.physicscodes.objects.Ball;
        import com.physicscodes.math.Vector2D;

        public class SplashMover extends MultiForcer{

                private var _drop:Ball;
                private var _particles:Array = new Array();
                private var _g:Number = 20;
                private var _vx:Number = 20;
                private var _vy:Number = -15;
                private var _wlevel:Number = 510;
                private var _fac:Number = 1;

                public function SplashMover(pdrop:Ball,psplash:Array):void{
                        _drop = pdrop;
                        _particles = psplash;
                        super(_particles);
                }

                override protected function moveObject():void{
                        super.moveObject();
                        checkDrop();
                }

                private function checkDrop():void{
                        if (_drop.ypos > _wlevel){
                                for (var i:uint=0; i<_particles.length; i++){
                                        var splashDrop:Ball = _particles[i];
                                        var posx:Number = _drop.xpos+Image
(Math.random()-0.5)*_drop.width;
                                        var posy:Number = _wlevel-10+Image
Math.random()*_drop.height;
                                        var velx:Number = (Math.random()-0.5)*_vx*_fac;
                                        var vely:Number = (Math.random()+0.5)*_vy*_fac;
                                        splashDrop.pos2D = new Vector2D(posx,posy);
                                        splashDrop.velo2D = new Vector2D(velx,vely);
                                }
                                _drop.xpos = Math.random()*600+100;
                                _drop.ypos = 100;
                                _drop.radius = 4 + 8*Math.random();
                                _fac = Math.pow(_drop.radius/8,1.5);;
                        }
                }

                override protected function calcForce(pparticle:Particle):void{
                        force = Forces.constantGravity(pparticle.mass,_g);
                        if (pparticle.ypos > _wlevel){
                                pparticle.visible = false;
                        }else{
                                pparticle.visible = true;
                        }
                }

        }
}

The first thing to note is that SplashMover extends MultiForcer. That's because we have a whole bunch of particles to move (the splash droplets) and they will be moving under a force (gravity). The splash array is assigned to a private variable called _particles.

The main action takes place in the checkDrop() method, which is executed at every timestep, being called from an overriding moveObject() method. This code checks if the drop has fallen below the water level (set at 10px below the top edge of the blue rectangle representing the body of water to give a fake 3D effect). If that happens, the splash droplets are repositioned around the region of impact and given random velocities with an upward vertical component. The magnitude of the velocities depends on parameters _vx and _vy that are set initially in the code as well as on a factor _fac, which is updated every time a splash occurs, as will be described shortly. The droplets are then made to fall under gravity by using the Forces.constantGravity() method in calcForce(). There is an if-else condition that makes them invisible if they are below the water level; otherwise they are made visible.

Back in the checkDrop() method, the falling drop is repositioned at the initial height and at a new random horizontal location as soon as it hits the water level. Its radius is then changed to a random value between 4 and 12 pixels.

Then the velocity factor _fac is updated according to the following formula, in which rm is the mean radius of the drop (8 pixels):

Image

What is happening here? The idea is that the larger the drop is, the heavier its mass will be (its mass is proportional to its volume, or the cube of its radius, r3). Because the drop always has the same velocity, its kinetic energy (Ek = ½ mv2) will be proportional to its mass, and therefore to r3. A fraction of that energy will be imparted to the splash droplets, and one can therefore expect the average kinetic energy of those droplets to be proportional to r3. Because the masses of the droplets are constant, their kinetic energy is proportional to the square of their velocity. So we have the following:

Image

Therefore, we can write the following:

Image

This shows that we expect the droplets to have a mean velocity proportional to the radius of the falling drop raised to the power of 1.5. The previous formula for _fac implements this fact, with the value of _fac being set to 1 when the radius is equal to the mean radius of 8 px.

If you run the simulation, you will find that the larger the drop is, the higher the droplets will splash because of their larger initial velocities. A screenshot is shown in Figure 12-1. As usual, feel free to experiment with different parameters. Of course, the visual aspects of the animation can be enhanced in many ways according to your imagination and artistic skills. We have focused on the physics and coding aspects; feel free to season the appearance to taste.

Image

Figure 12-1. Producing a splash!

Creating a particle emitter

Many applications of particle systems require a continuous source of particles. This can be achieved using particle emitters. Creating a particle emitter is not difficult; essentially what you need is to create new particles over time. But it is also important to somehow limit the total number of particles to avoid crashing your computer! This may mean fixing the total number of particles or recycling/removing them.

There are several approaches you could take to do these things. We can only give an example here, without attempting to cover all the different approaches.

An example is given in the ParticleEmitterExample.as and ParticleEmitter.as files. The ParticleEmitterExample class is very simple, and looks like this:

package{
        import flash.display.Sprite;

        public class ParticleEmitterExample extends Sprite{
                public function ParticleEmitterExample():void{
                        init();
                }
                private function init():void{
                        var sprite:Sprite = new Sprite();
                        addChild(sprite);

                        var mover:ParticleEmitter = new ParticleEmitter(sprite);
                        mover.startTime();
                }
        }
}

As you can see, there is not much to it. ParticleEmitterExample simply creates a sprite and passes it to a ParticleEmitter instance. Everything is happening within the ParticleEmitter class, which looks like this:

package {
        import flash.display.Sprite;
        import com.physicscodes.motion.MultiForcer;
        import com.physicscodes.motion.Forces;
        import com.physicscodes.objects.Particle;
        import com.physicscodes.objects.Ball;
        import com.physicscodes.math.Vector2D;

        public class ParticleEmitter extends MultiForcer{
                private var _sprite:Sprite;
                private var _particles:Array = new Array();
                private var _maxParticles:uint = 120;
                private var _g:Number = 20;
                private var _k:Number = 0.003;
                private var _vx:Number = 60;
                private var _vy:Number = -80;

                public function ParticleEmitter(psprite:Sprite):void{
                        _sprite = psprite;
                        super(_particles);
                }

                override protected function calcForce(pparticle:Particle):void{
                        var gravity:Vector2D = Forces.constantGravity(pparticle.mass,_g);
                        var drag:Vector2D = Forces.drag(_k,pparticle.velo2D);
                        force = Forces.add([gravity, drag]);
                }

                override protected function moveObject():void{
                        super.moveObject();
                        createNewParticles(new Vector2D(_sprite.mouseX,_sprite.mouseY));
                        limitParticles();
                }

                private function createNewParticles(ppos:Vector2D):void{
                        var newBall:Ball = new Ball(2);
                        newBall.pos2D = ppos;
                        newBall.velo2D = new Vector2D((Math.random()-0.5)*_vx,Image
(Math.random()+0.5)*_vy);
                        _sprite.addChild(newBall);
                        _particles.push(newBall);
                }

                private function limitParticles():void{
                        if (_particles.length > _maxParticles){
                                _sprite.removeChild(_particles[0]);
                                _particles.shift();
                        }
                }

        }
}

As in the previous example, ParticleEmitter extends MultiForcer. The particles are stored in an array named _particles. In this example, the calcForce() method includes gravity and drag force. The really new stuff appears in the two methods createNewParticles() and limitParticles(), which are called from within the moveObject() method.

The createNewParticles() method does exactly what its name suggests, creating new particles as Ball objects at a location specified as a Vector2D input parameter. In this case, the particles are given a random velocity, added to the stage of the sprite object, and then pushed into the _particles array. This happens once per timestep, so that a new particle is created at each timestep. You might alternatively specify a rate at which new particles are created per second; we leave this as an exercise.

Without the limitParticles() method your animation would quickly fill up with particles until your computer couldn't cope any more. In this example, limitParticles() checks to see if the number of particles exceeds the maximum allowed number _maxParticles. If it does, the first (and oldest) particle is removed. You don't want to set _maxParticles too high. In the example file, a value of 120 is chosen.

Run the simulation and you'll have a particle emitter! Little balls projected upward and falling back under gravity and drag, looking a bit like a spray. We have specified the position of the emitter as the location of the mouse cursor, so that you can move the mouse around and watch the fun.

You can produce some interesting effects by changing the parameters and initial conditions. For example, instead of giving the particles a random velocity, you can introduce the following lines of code into createNewParticles():

var n:int;
n = _i%7;
var angle:Number = -(60+10*n)*Math.PI/180;
var mag:Number = 100;
newBall.velo2D = Vector2D.vector2D(mag,angle);
_i++;

Here, _i is a private int variable initially given the value of 0. This produces a fountain-like pattern, as shown in Figure 12-2.

Image

Figure 12-2. Creating a particle emitter

Creating a smoke effect

Let us now use our particle emitter to create a smoke effect. This is easier than you might think; in fact, you've already done most of the work. Let us start with a setup file, Smoke.as, that is essentially the same as that in the previous example, except for the relevant class names:

package{
        import flash.display.Sprite;

        public class Smoke extends Sprite{
                public function Smoke():void{
                        init();
                }
                private function init():void{
                        var sprite:Sprite = new Sprite();
                        addChild(sprite);

                        var mover:SmokeMover=new SmokeMover(sprite);
                        mover.startTime();
                }
        }
}

The corresponding mover class SmokeMover is built on top of the ParticleEmitter class of the previous example, and looks like this, with the new or modified bits highlighted in bold:

package {
        import flash.display.Sprite;
        import com.physicscodes.motion.MultiForcer;
        import com.physicscodes.motion.Forces;
        import com.physicscodes.objects.Particle;
        import com.physicscodes.objects.Ball;
        import com.physicscodes.math.Vector2D;

        public class SmokeMover extends MultiForcer{
                private var _sprite:Sprite;
                private var _particles:Array = new Array();
                private var _maxParticles:uint = 120;
                private var _g:Number = -5;
                private var _k:Number = 0.005;
                private var _vx:Number = 60;
                private var _vy:Number = -80;

                public function SmokeMover(psprite:Sprite):void{
                        _sprite = psprite;
                        super(_particles);
                }

                override protected function calcForce(pparticle:Particle):void{
                        var gravity:Vector2D = Forces.constantGravity(pparticle.mass,_g);
                        var drag:Vector2D = Forces.drag(_k,pparticle.velo2D);
                        force = Forces.add([gravity, drag]);
                }

                override protected function moveObject():void{
                        super.moveObject();
                        createNewParticles(new Vector2D(_sprite.mouseX,_sprite.mouseY));
                        limitParticles();
                        modifyParticles();
                }

                private function createNewParticles(ppos:Vector2D):void{
                        var radius:Number = 1 + 3*Math.random();
                        var newBall:Ball = new Ball(radius,0xffffff);
                        newBall.pos2D = ppos;
                        newBall.velo2D = new Vector2D((Math.random()-0.5)*_vx,Image
(Math.random()+0.5)*_vy);
                        _sprite.addChild(newBall);
                        _particles.push(newBall);
                }

                private function limitParticles():void{
                        if (_particles.length > _maxParticles){
                                _sprite.removeChild(_particles[0]);
                                _particles.shift();
                        }
                }

                private function modifyParticles():void{
                        for (var i:uint=0; i<_particles.length; i++){
                                var particle:Ball = _particles[i];
                                particle.scaleX *= 1.01;
                                particle.scaleY *= 1.01;
                                particle.alpha += -0.01;
                        }
                }

        }
}

Compared with ParticleEmitter there are two modifications plus the addition of a new method: modifyParticles(). The first modification is that we are randomizing the radiuses of the particles between the values of 1 and 4 pixels in createNewParticles(). The second modification might appear a bit weird: we are giving the gravity constant g a negative value (–5)! What's going on here? The thing is that smoke will tend to rise rather than fall because forces such as buoyancy (upthrust) will overcome the force of gravity. But instead of modeling buoyancy, it makes more sense here to simply modify gravity so that it acts upward because we are interested only in reproducing the visual effect of smoke approximately.

The new method, modifyParticles(), is called from moveObject() and is therefore executed at each timestep. All we are doing here is to increase the size of the particles by a constant factor and reduce their transparency by a fixed amount at each timestep.

The result of the previous additions and modifications is that the particles grow and fade as they rise, before disappearing altogether and being removed and replaced by new particles. If you were to run the code as shown previously, you would get an interesting effect, but you couldn't really call it smoke–see Figure 12-3.

Image

Figure 12-3. An interesting effect, but not quite smoke yet!

Time to introduce filters! If you are not familiar with ActionScript filters, we encourage you to spend some time reading about them and playing around with them. Check out Adobe's online AS3.0 documentation or pick up a copy of Foundation ActionScript 3.0 Image Effects by Todd Yard.

We'll use one of the simplest filters, the BlurFilter, applying it to the Smoke class, which now looks like this:

package{
        import flash.display.Sprite;
        import flash.filters.BlurFilter;

        public class Smoke extends Sprite{
                public function Smoke():void{
                        init();
                }
                private function init():void{
                        var sprite:Sprite = new Sprite();
                        addChild(sprite);

                        var mover:SmokeMover=new SmokeMover(sprite);
                        mover.startTime();

                        filters = [new BlurFilter(8,8,2)];
                }
        }
}

As you can see, the changes (highlighted in bold) are minimal: just one line of code plus the appropriate import statement. The BlurFilter() function takes three optional arguments:

BlurFilter(blurX:Number = 4.0, blurY:Number = 4.0, quality:int = 1)

The first two arguments specify the amount of blur to apply in the x and y directions. The third argument specifies the number of times to apply the filter and therefore indicates the quality of the blur. A value of 1 is a low quality blur, 2 is medium quality, and 3 is high quality. We have specified a medium quality blur with blur amounts of eight units in each direction; feel free to experiment with different values of these parameters.

If you now run the code, you will see something that looks much more like smoke, as depicted in the screenshot in Figure 12-4.

Image

Figure 12-4. The smoke effect at last

This is a basic smoke effect that can be tweaked and enhanced in endless ways. Think of it as a starting point for further experimentation rather than as a final result. Moreover, there are lots of other ways to produce a smoke effect like this. The preceding method has been adapted from a tutorial by Seb Lee-Delisle that appeared in Computer Arts magazine (March 2008). It's nice because of its simplicity and the fact that it can be done with pure code alone.

Creating a fire effect

It is extremely easy to modify the smoke animation to produce something that looks like fire. In fact, we changed only one line of code in the SmokeMover class, just to make the particles slightly bigger:

var radius:Number = 3 + 3*Math.random();

This makes particles with radiuses between 3 and 6 pixels. We saved the new file as FireMover.as. We then modify the Smoke class by adding a new filter GradientGlowFilter and after appropriate class and constructor name changes, save the new file as Fire.as:

package{
        import flash.display.Sprite;
        import flash.filters.BlurFilter;
        import flash.filters.GradientGlowFilter;

        public class Fire extends Sprite{
                public function Fire():void{
                        init();
                }
                private function init():void{
                        var sprite:Sprite = new Sprite();
                        addChild(sprite);

                        var mover:FireMover=new FireMover(sprite);
                        mover.startTime();

                        var blurFilter:BlurFilter = new BlurFilter(8,8,2);
                        var gradientGlowFilter:GradientGlowFilter = new GradientGlowFilterImage
(0,45,[0xff0000,0xffff00],[1,1],[50,255],15,15);
                        filters = [blurFilter, gradientGlowFilter];
                }
        }
}

The relevant changes are highlighted in bold. The GradientGlowFilter is a more complicated filter than the BlurFilter, with no fewer than 11 optional parameters, of which we've specified 7. We refer you to Adobe's online AS3.0 reference for what these parameters mean and leave you to play with their values to see their effects. If you run the code with the values shown in the previous listing, you'll see something like Figure 12-5. Of course, you need to see the actual animation to appreciate the effect and colors.

Image

Figure 12-5. Creating a fire effect

Now that you have the basic tools at your fingertips, you can create more elaborate effects by applying the methods to different setups and by combining different techniques. For example, by attaching emitters to objects, you can create the illusion of the objects being on fire. You can also add wind, combine smoke with the fire, add sparks, and so on. Talking of sparks, let's go create some now.

Creating fireworks

The end result we want to achieve in this section is an animation that looks like fireworks: a series of little explosions each generating colorful sparks that fall under gravity with drag. In order to get there, we will first modify the Particle class and then create a sparks animation using the updated Particle class before finally adding extra features to make the fireworks.

Modifying the Particle class

Something like a spark exists for only a certain duration, after which it will need to be removed or recycled. It makes sense therefore to introduce a couple of new properties in the Particle class to represent the lifetime and age of a Particle instance. To do this, we add the following code to the Particle class:

// lifetime and age in seconds
private var _lifetime:Number = 1e8;
private var _age:Number = 0;

public function get lifetime():Number{
        return _lifetime;
}
public function set lifetime(plifetime:Number):void{
        _lifetime = plifetime;
}
public function get age():Number{
        return _age;
}
public function set age(page:Number):void{
        _age = page;
}

This creates the two properties, lifetime and age, with default values of 108 and 0, respectively. These represent the lifetime and age of a Particle instance in seconds. The value of 108 seconds is equal to approximately three years: so as long as your Flash movie does not run for longer than three years, a particle with the default lifetime value should never be destroyed. Of course, we could have specified any other value that is large enough. The age is the current time in seconds since the Particle instance was created.

It is up to your external code that creates the Particle instance to update its age as time progresses and to check and take the appropriate action if its age exceeds its lifetime.

Making sparks

Let us now apply the newly created lifetime and age properties to create sparks that exist for a specified duration. The new setup file is called Sparks.as and looks like this:

package{
        import flash.display.Sprite;
        import flash.filters.GlowFilter;

        public class Sparks extends Sprite{
                public function Sparks():void{
                        init();
                }
                private function init():void{
                        var sprite:Sprite = new Sprite();
                        addChild(sprite);

                        var mover:SparksMover=new SparksMover(sprite);
                        mover.startTime();

                        filters = [new GlowFilter()];
                }
        }
}

This is very similar to the setup files in the previous examples, except that we are now using a different filter, a GlowFilter() with default parameters. (Again, see the Adobe online AS3.0 reference to learn more about this filter and its parameters.) The SparksMover class is a modified version of the FireMover class, with the modifyParticles() method replaced by a new ageParticles() method (which calls a new removeParticle() method) as well a few small changes, as highlighted in bold in the following code:

package {
        import flash.display.Sprite;
        import com.physicscodes.motion.MultiForcer;
        import com.physicscodes.motion.Forces;
        import com.physicscodes.objects.Particle;
        import com.physicscodes.objects.Ball;
        import com.physicscodes.math.Vector2D;

        public class SparksMover extends MultiForcer{

                private var _sprite:Sprite;
                private var _particles:Array = new Array();
                private var _maxParticles:uint = 200;
                private var _g:Number = 10;
                private var _k:Number = 0.005;
                private var _vx:Number = 100;
                private var _vy:Number = -100;

                public function SparksMover(psprite:Sprite):void{
                        _sprite = psprite;
                        super(_particles);
                }

                override protected function calcForce(pparticle:Particle):void{
                        var gravity:Vector2D = Forces.constantGravity(pparticle.mass,_g);
                        var drag:Vector2D = Forces.drag(_k,pparticle.velo2D);
                        force = Forces.add([gravity, drag]);
                }

                override protected function moveObject():void{
                        super.moveObject();
                        createNewParticles(new Vector2D(_sprite.mouseX,_sprite.mouseY));
                        limitParticles();
                        ageParticles();
                }

                private function createNewParticles(ppos:Vector2D):void{
                        var newBall:Ball = new Ball(2,0xffff00,1,0,false);
                        newBall.pos2D = ppos;
                        newBall.velo2D = new Vector2D((Math.random()-0.5)*_vx,Image
(Math.random()-0.5)*_vy);
                        newBall.lifetime = 2 + 2*Math.random();
                        _sprite.addChild(newBall);
                        _particles.push(newBall);
                }

                private function limitParticles():void{
                        if (_particles.length > _maxParticles){
                                _sprite.removeChild(_particles[0]);
                                _particles.shift();
                        }
                }

                private function ageParticles():void{
                        for (var i:uint=0; i<_particles.length; i++){
                                var particle:Ball = _particles[i];
                                particle.age += dt;
                                particle.alpha += -0.005;
                                if (particle.age > particle.lifetime){
                                        removeParticle(particle,i);
                                }
                        }
                }

                private function removeParticle(pparticle:Particle,pnum:uint):void{
                        _sprite.removeChild(pparticle);
                        _particles.splice(pnum,1);
                }

        }
}

So we now have 200 particles and give them values of _g, _k, _vx, and _vy of 10, 0.005, 100, and –100, respectively (which you can change if you want). In createNewParticles(), each particle is given the same radius of 2 pixels. Because sparks can fly in any direction, the particles are given random velocities in any direction. Then their lifetime is set to a random value between two and four seconds.

The new ageParticles() method loops over all the particles in the _particles array, updating their age every timestep and decreasing their alpha value. This makes them fade with time. The particle's age is then checked against its lifetime: if it exceeds the lifetime, the removeParticle() method is called, which removes the particle from the stage and from the _particles array.

Run the simulation and you'll get to see some nice sparks following the mouse (see Figure 12-6).

Image

Figure 12-6. Making sparks

Making the fireworks animation

Now that we have sparks, we can use them to produce fireworks. Instead of producing the sparks continuously, we want to do that in a series of little explosions. We'll have an initial explosion that will create a bunch of sparks with random velocities; these sparks will then fall under gravity with drag, fading with time. When each spark reaches the end of its lifetime, it will explode, producing further sparks. Obviously, we want to stop this process at some point. The easiest way to do this is to limit the duration of the animation by imposing a global cutoff time.

To get started, we produce a setup file named Fireworks.as that is practically identical with the Sparks.as file except for the relevant class names. We also need a corresponding FireworksMover class. This is modified from the SparksMover class and looks like the following, with modified bits highlighted in bold:

package {
        import flash.display.Sprite;
        import com.physicscodes.motion.MultiForcer;
        import com.physicscodes.motion.Forces;
        import com.physicscodes.objects.Particle;
        import com.physicscodes.objects.Ball;
        import com.physicscodes.math.Vector2D;

        public class FireworksMover extends MultiForcer{

                private var _sprite:Sprite;
                private var _particles:Array = new Array();
                private var _maxParticles:uint = 200;
                private var _g:Number = 10;
                private var _k:Number = 0.005;
                private var _vx:Number = 150;
                private var _vy:Number = -100;
                // number of sparks per explosion
                private var _numSparks:uint = 10;
                //minimum and maximum lifetime of sparks in seconds
                private var _minLife:Number = 2;
                private var _maxLife:Number = 4;
                //maximum duration of fireworks in seconds
                private var _duration:Number = 6;

                public function FireworksMover(psprite:Sprite):void{
                        _sprite = psprite;
                        super(_particles);
                        createNewParticles(new Vector2D(400,200),0xffff00);
                }

                override protected function calcForce(pparticle:Particle):void{
                        var gravity:Vector2D = Forces.constantGravity(pparticle.mass,_g);
                        var drag:Vector2D = Forces.drag(_k,pparticle.velo2D);
                        force = Forces.add([gravity, drag]);
                }

                override protected function moveObject():void{
                        super.moveObject();
                        limitParticles();
                        ageParticles();
                }

                private function createNewParticles(ppos:Vector2D,pcol:uint):void{
                        for (var i:uint=0; i<_numSparks; i++){
                                var newBall:Ball = new Ball(2,pcol,1,0,false);
                                newBall.pos2D = ppos;
                                newBall.velo2D = new Vector2D((Math.random()-0.5)*_vx,Image
(Math.random()-0.5)*_vy);
                                newBall.lifetime = _minLife + (_maxLife-_minLife)Image
*Math.random();
                                _sprite.addChild(newBall);
                                _particles.push(newBall);
                        }
                }

                private function limitParticles():void{
                        if (_particles.length > _maxParticles){
                                _sprite.removeChild(_particles[0]);
                                _particles.shift();
                        }
                }

                private function ageParticles():void{
                        for (var i:uint=0; i<_particles.length; i++){
                                var particle:Ball = _particles[i];
                                particle.age += dt;
                                particle.alpha += -0.005;
                                if (particle.age > particle.lifetime){
                                        if (time<_duration){
                                                explode(particle);
                                        }
                                        removeParticle(particle,i);
                                }
                        }
                }

                private function explode(pparticle:Particle){
                        createNewParticles(pparticle.pos2D,0x00ff00);
                }

                private function removeParticle(pparticle:Particle,pnum:uint):void{
                        _sprite.removeChild(pparticle);
                        _particles.splice(pnum,1);
                }

        }
}

First note that we changed the value of _vx from 100 to 150. This is just so that the sparks spread horizontally more than vertically. We added four new private variables: _numSparks, _minLife, _maxLife, and _duration to store the number of sparks, their minimum and maximum lifetimes, and the duration of the animation, respectively. The next change is that the createNewParticles() method is now called from within the constructor, not within the moveObject() method. This means that the particles are created initially, not at every timestep. The createNewParticles() method is modified to accept a new argument that specifies the color of the sparks, to create a number _numSparks of particles at a time instead of just one, and to assign to those particles a random lifetime between _minLife and _maxLife.

The ageParticles() method is modified so that, as long as _duration has not been exceeded, any particle that exceeds its lifetime explodes before being removed. The explosion is handled by the explode() method, which simply calls the createNewParticles() method and specifies the position of the dying particle as the location for the explosion and gives a new color for the next generation of sparks.

Run the code and you will be treated to a fireworks display (see Figure 12-7). Needless to say, you can tweak the animation endlessly to improve the appearance and add further effects such as trailing sparks, smoke, and even sound effects. We've done our job of showing you how to implement the basic physics. Now go for it!

In the source code, we have also included a modified version of the simulation in files named Fireworks2.as and FireworksMover2.as. This is an interactive version where you click and hold down the mouse anywhere on the stage. The number of sparks _numSparks will be set to equal the duration in seconds for which you hold down the mouse, so the longer you press the mouse the greater the number of sparks that will be produced per explosion. When you release the mouse, the initial explosion will take place at the location of the mouse. The colors of the sparks are also randomized in this version. Try it out!

Image

Figure 12-7. Fireworks!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.112.160