Procedural Animation with a Fish Simulator

By Josh Marinacci, September 24, 2008

This sample shows how to build a simple procedural animation. When you click and hold anywhere on the screen, ripples appear and the fish moves towards the center of the ripples and wags its tail. Both the ripples and fish are examples of procedural animation.

Understanding the Code

There are two basic animated objects in this example: the fish and the ripples. The code in Figure 1 shows how the fish is constructed. The fish itself is a nested set of ImageViews, each one scaled down from the previous ImageView and offset slightly, except for the last one which makes the tail. There is only one actual image, but the scaling creates the illusion of the fish body.

Source Code
public class Fish extends CustomNode {
    // controls the angle of each segment, used for the wag animation
    var ang:Number = 0.0;
    // the heading of the entire fish
    var heading:Number = 0.0;
    
    override public function create():Node {
        var g:Group = Group {
            transforms: bind Transform.rotate(heading+90,0,0)
        };

        var orig = g;

        for(i in [0..4]) {
            var scale = 0.6 * (5 - i) / 3;
            if(i==4) {
                scale = 0.7;
            }
            var newg = Group {
                transforms: bind [
                    Transform.translate(20,0),
                    Transform.rotate(ang,20,0)
                ]
                content: [
                    ImageView {
                        scaleX: scale
                        scaleY: scale
                        translateY: -50;
                        image: Image { url: "{__DIR__}scale2.png" }
                    },
                ]
            }
            insert newg into g.content;
            // add eyeballs to the first one
            if(i == 0) {
                var eyeSpacing = 10;
                var eyeOffset = 20;
                insert Circle { centerX: eyeOffset centerY: eyeSpacing radius: 10 fill: Color.WHITE } into newg.content;
                insert Circle { centerX: eyeOffset centerY: -eyeSpacing radius: 10 fill: Color.WHITE } into newg.content;
                insert Circle { centerX: eyeOffset-4 centerY: eyeSpacing radius: 5 fill: Color.BLACK } into newg.content;
                insert Circle { centerX: eyeOffset-4 centerY: -eyeSpacing radius: 5 fill: Color.BLACK } into newg.content;
            }
            g = newg;
        }

        return orig;
    }

Figure 1: Fish Creation Code

The fish itself supports two different animations. The first is the constant tail wagging. Each segment of the fish has a rotation transform bound to a single ang variable. By changing this variable, you can make the entire fish's body wag. The wag animation is implemented by the following Timeline which oscillates the ang variable from positive to negative 15 degrees. autoReverse is to to true so that it oscillates. repeatCount is set to Timeline.INDEFINITE to make it run forever.

Source Code
    //make angle loop from -10 to +10 degrees
    public var wag = Timeline {
        keyFrames: [
            at(0s) { ang=> -10.0 tween Interpolator.EASEBOTH },
            at(1s) { ang=>  10.0 tween Interpolator.EASEBOTH },
        ]
        autoReverse: true
        repeatCount: Timeline.INDEFINITE
    };

Figure 2: Wag Animation

The move animation is implemented by calculating a new angle and final position based on the coordinates passed to the goTo function. Because this function encapsulates all of the information about how to move the outside code won't have to know anything about it. It can work at the higher level of saying 'go to here' and the fish will take care of it. This encapsulation makes higher level programming possible.

Source Code
    var move:Timeline = Timeline { };
    public function goTo(x:Number, y:Number):Void {
        move.stop();
        var xoff = x - translateX;
        var yoff = y - translateY;
        var angg = calcAngle(translateX, translateY, x, y);
        var dist = Math.sqrt(xoff * xoff + yoff * yoff);
        
        var t = 1s * dist / 100.0; //speed = 100px / sec
        move = Timeline {
            keyFrames: [
                KeyFrame {
                    time: t
                    values: [
                        heading => angg tween Interpolator.LINEAR,
                        translateX => x tween Interpolator.LINEAR,
                        translateY => y tween Interpolator.LINEAR,
                    ]
                }
            ]
        }
        move.play();
    }

    function calcAngle(x1:Number, y1:Number, x2:Number, y2:Number):Number {
        var xoff = x2-x1;
        var yoff = y2-y1;
        var angle = Math.atan(yoff/xoff);
        if (xoff < 0) {
            angle = angle + Math.PI;
        }

        if (angle < 0) {
            angle += 2 * Math.PI;
        }
        if (angle > 2 * Math.PI) {
            angle -= 2 * Math.PI;
        }
        return Math.toDegrees(angle);
    }

Figure 3: goTo function

Building the Ripples

While the fish is a single character with internal animations, the ripples are more like a particle simulation. The RippleGenerator class creates a new ripple each time the createRipple function is called. This function creates a new instance of the Ripple class, puts it into the scene, and starts the animation. It also creates a second timeline to remove the ripple after three seconds.

Source Code
public class RippleGenerator extends CustomNode {
    var ripples:Node[];
    public var generatorCenterX = 100.0;
    public var generatorCenterY = 100.0;
    
    override public function create():Node {
        return Group{
            content: bind ripples;
        }
    }
    
    public function createRipple():Void {
        var rip = Ripple { 
            centerX: generatorCenterX
            centerY: generatorCenterY
        };
        insert rip into ripples;
        rip.anim.start();
        var remover = Timeline {
            keyFrames: [
                KeyFrame { 
                    time: 3s 
                    action:function() { 
                        delete rip from ripples; 
                        rip.anim.stop(); 
                    } 
                },
            ]
        };
        remover.start();
    }
    
    public var generate = Timeline {
        keyFrames: KeyFrame {
            time: 0.5s
            action: createRipple
        }
        repeatCount: Timeline.INDEFINITE
    }
}

Figure 4: RippleGenerator.fx File

The Ripple itself is a subclass of circle overridden to have particular colors. It also adds the actual growing and fading animation, as shown in Figure 5.

Source Code
class Ripple extends Circle {
    override var stroke = Color.rgb(200,200,255);
    override var fill = null;
    override var centerX = 100;
    override var centerY = 100;
    var anim = Timeline {
        keyFrames: [
            at(0s) { radius => 0 tween Interpolator.LINEAR },
            at(1s) { opacity => 1.0 tween Interpolator.LINEAR },
            at(3s) { radius => 100 tween Interpolator.LINEAR }
            at(3s) { opacity => 0.0 tween Interpolator.LINEAR }
        ]
    }
}

Figure 5: the Ripple class

Pulling it Together

To pull all of the pieces together the Main class in Figure 6 creates a fish, starts the wagging, creates a ripple generator, then combines it all into a Stage with a transparent overlay rectangle to capture the mouse clicks.

Source Code
var fish = Fish { 
    translateX: 0
    translateY: 0
};
fish.wag.start();

var ripper = RippleGenerator { };

var w = 800;
var h = 500;

Stage {
    //closeAction: function() { java.lang.System.exit(0); }
    //visible: true
    scene: Scene {
        content :
            Group { content: [
                // a colorful background
                Rectangle { width: w height: h
                    fill: RadialGradient {
                        radius:500
                        focusX: 0
                        focusY: 0
                        centerX: 300
                        centerY: 300
//                        cycleMethod:
                        proportional: false
                        stops: [
                            Stop {offset: 0 color: Color.BLACK },
                            Stop {offset: 1 color: Color.BLUE  },
                        ]
                    }
                },
                
                fish, // the fish
                ripper, // the ripple generator
                
                // an overlay to capture mouse events
                Rectangle {
                    fill: Color.rgb(255,255,255,0.0)
                    width: w
                    height: h
                    onMousePressed: function(e:MouseEvent) {
                        ripper.generatorCenterX= e.x;
                        ripper.generatorCenterY= e.y;
                        ripper.createRipple();
                        ripper.generate.start();
                        fish.goTo(e.x,e.y);
                    }
                    onMouseDragged: function(e:MouseEvent) {
                        ripper.generatorCenterX= e.x;
                        ripper.generatorCenterY= e.y;
                        fish.goTo(e.x,e.y);
                    }
                    onMouseReleased: function(e:MouseEvent) {
                        ripper.generate.stop();
                    }
                },

                ] 
            }
    }
}

Figure 6: Main.fx

Looking Ahead

A great improvement to this application in the future would be adding more fish. Right now the fish know how to move and wag but you could add more fish "smarts", such as wandering off after a while, grouping together, or chasing after smaller fish that swim by. As long as the new behavior is encapsulated inside the Fish class you can easily reuse the fish in other programs.