Particle Emitter Animation in JavaFX

I was a little intrigued with emitter animation effects, like water flowing out of a shower head or snow flakes falling to the ground (see http://en.wikipedia.org/wiki/Particle_system). So I decided to play around and create a reusable Emitter in JavaFX based on the javafx.animation.transition.Transition  class. A view of the application is shown in the following screen capture:


There are three basic classes in this implementation. "Particle" represents the particle that is emitted, "Emitter" generates the particles, and "ParticleTransition" implements the Particle's animation.

First lets examine the Particle.

The Particle is a Resizable CustomNode, that is it extends javafx.scene.layout.Resizable  and javafx.scene.CustomNode. To support the Resizable aspect, Particle implements the following code:

    public override function getPrefHeight(h: Number) : Number {
         5.0 // default height
    }
    public override function getPrefWidth(w: Number) : Number {
        5.0 // default width
    }

    override var layoutBounds = bind lazy {
        BoundingBox {
            minX: 0
            minY: 0
            width: this.width
            height: this.height
        }
    }

    postinit {
        if (not isInitialized(width)) then width = getPrefWidth(-1);
        if (not isInitialized(height)) then height = getPrefHeight(-1);
    }

For the actual particle node, there is a function variable, createParticle.

public var createParticle: function(particle:Particle) : Node;

This is used in the create():Node overloaded function from CustomNode, to create the visible node for the Particle.

    public override function create(): Node {
        return Group {
            content: createParticle(this)
        };
    }

This allows any type of Node to be created to represent a particle. In our main test application, we used a javafx.scene.shape.Ellipse

A particle has a lifespan and a current age defined with javafx.lang.Duration objects. When the lifespan is expired, the Particle will die by invoking its die() function. The default implementation of the die() function is to remove the particle from the parent group, thusly the particle is removed from the scene.

While the particle is alive, there is an "interpolate" variable that holds a function that manipulates the Particle as it ages. In the default implementation, the Particle is moved based on its velocity.

    public var velocity: Point2D = Point2D {  // Pixels per millisecond
        x: 1
        y: 1
    } 
    public var interpolate: function(particle:Particle):Void = function(particle:Particle) {
        var millis = age.toMillis();
        translateX = millis \* velocity.x;
        translateY = millis \* velocity.y;
    }

Next, the Emitter class causes Particles to be emitted on a periodic basis, controlled by a javafx.animation.Timeline  object.  For each period, the emitter will create a certain number of Particles ± a variance. Each Particle will also have variance in its initial location relative to the emitter, as well as variances in velocity and lifespan. The variance is a random distribution across a variance range. By using variances, a randomness is introduced to the emitter effect.

For each period, the Emitter calls the "emit()" function. Each time, it creates a ParticleTransition object that represents a transition timeline, it then adds the generated Particles to the transition. Lastly, it starts the transition animation by calling the ParticleTransition's play() function.

    var transitions: ParticleTransition[];
    var count: Integer;
    function emit():Void {
        var eCount = Math.round(getVariance(emitCountVariance) \* emitCount);
        var pt:ParticleTransition = ParticleTransition {
            id: count++
            action: function() {
                delete pt from transitions;
            }

        }
        for(i in [0..<eCount]) {
            var particle = createParticle(this);
            particle.layoutX = this.layoutX + getVariance(initialLocationVariance);
            particle.layoutY = this.layoutY + getVariance(initialLocationVariance);
            insert particle into group.content;
            insert particle into pt.particles;
        }
        insert pt into transitions;
        pt.play();
    }

The ParticleTransition holds a sequence of Particles and  overloads the "rebuildKeyFrames()" function from the javafx.animation.transition.Transition  class. The Transition class contains the animation framework that loads the KeyFrames and controls the animation play.

public class ParticleTransition extends Transition {

    /\*\*
     \* holds the particles that participate in the transition
     \*/
    public var particles: Particle[] on replace oldValues[lo..hi] = newValues {
        markDirty();
    }

    /\*\*
     \* Holds a unique id for this Transition
     \*/
    public-init var id: Integer;

    protected override function rebuildKeyFrames():KeyFrame[] {
        var sortedParticles = Sequences.sort(particles, Comparator {
            public override function compare(a: Object, b: Object):Integer {
                var ap = a as Particle;
                var bp = b as Particle;
                ap.lifespan.compareTo(bp.lifespan);
            }
        });

        [
            KeyFrame {
                time: 0ms
                values: for(p in sortedParticles) {
                    (p as Particle).alpha => 0.0 tween interpolator;
                }
            },
            for(p in sortedParticles) {
                KeyFrame {
                    time: (p as Particle).lifespan
                    values: [(p as Particle).alpha => 1.0 tween interpolator]
                    canSkip: false
                    action: function() {
                        if(indexof p >= sizeof sortedParticles - 1)
                            action();
                        (p as Particle).die();
                    }
                }
             }
        ]
    }
} 

When building the KeyFrames, the particles are sorted from shortest to longest lifespan so that the keyframes are built in time order.

The "Main" class kicks off the test by creating an Emitter object, including it in the SceneGraph, and starting its animation. The "createParticle" function on the Emitter creates a new Particle that defines itself as an Ellipse shape. The emitter is defined as:

def emitter = Emitter {
    layoutX: 20
    layoutY: 20
    initialLocationVariance: 5
    createParticle: function(emitter: Emitter) {
        Particle{
           velocity: Point2D {
               x: emitter.velocity.x + emitter.getVariance(emitter.velocityVariance.x)
               y: emitter.velocity.y + emitter.getVariance(emitter.velocityVariance.y)
           }
           width: 5 + emitter.getVariance(2.0)
           height: 4 + emitter.getVariance(2.0)
           lifespan: 10s + 5s.mul(emitter.getVariance(1000.0))
           createParticle: function (particle: Particle):Node {
                var rx = bind particle.width/2.0 - emitter.getVariance(1.0);
                var ry = bind particle.height/2.0 - emitter.getVariance(1.0);
                Ellipse {
                    centerX: bind rx
                    centerY: bind ry
                    radiusX: bind rx
                    radiusY: bind ry
                    fill: UtilsFX.deriveColor(Color.SKYBLUE, emitter.rand.nextFloat())
                    effect: MotionBlur {
                        angle: 45
                        radius: bind rx
                    }
                }
           }
       }
    }
} 

The entire NetBeans project can be downloaded at

ParticleEmitter project.


Comments:

Hi!

Congratulations! Your readers have submitted and voted for your blog at The Daily Reviewer. We compiled an exclusive list of the Top 100 ria Blogs, and we are glad to let you know that your blog was included! You can see it at http://thedailyreviewer.com/top/ria/3

You can claim your Top 100 Blogs Award here : http://thedailyreviewer.com/pages/badges/ria

P.S. This is a one-time notice to let you know your blog was included in one of our Top 100 Blog categories. You might get notices if you are listed in two or more categories.

P.P.S. If for some reason you want your blog removed from our list, just send an email to angelina@thedailyreviewer.com with the subject line "REMOVE" and the link to your blog in the body of the message.

Cheers!

Angelina Mizaki
Selection Committee President
The Daily Reviewer
http://thedailyreviewer.com

Posted by The Daily Reviewer on September 23, 2009 at 10:39 PM EDT #

Post a Comment:
  • HTML Syntax: NOT allowed
About

jimclarke

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today