Download the code for Interactive Animation
Last week, we created a cycle and animated it in JavaScript. This week, we'll add some interaction to our animation.
We'll interact with an animated sprite by clicking on it. Each mouse click turns on or off an animated behavior. Click once and the behavior starts; click again and the behavior stops. Each click acts like a push-on, push-off switch.
We'll create five switch behaviors that flip animation on and off or increase or decrease a sprite's property. The five behaviors affect the sprite's animation cycle, rotation, movement, scale, and opacity. All five behaviors have some code in common. We're going to create a parent class and all five behaviors inherit code from the parent class.
The parent class, called SwitcherBehavior, looks like this:
var SwitcherBehavior = function(switcherBehaviorArgs) {
"use strict";
this.switchOn = false;
var mySwitcherBehavior = this;
this.switcher = new Switcher({callback: function(switchOn){mySwitcherBehavior.setSwitchOn(switchOn);}});
this.clickable = new Clickable({mouseDownCallback: function(){mySwitcherBehavior.switcher.flipIt();}});
return this;
};
SwitcherBehavior.prototype.execute = function(sprite, context, time) {
"use strict";
//call clickable so it can update its mouseIsOver property with the sprite's current mouseIsOver property
this.clickable.execute(sprite, context, time);
};
SwitcherBehavior.prototype.setSwitchOn = function(switchOn) {
"use strict";
this.switchOn = switchOn;
};
Take a look at SwitcherBehavior's constructor—the constructor is the function we call when we instantiate the object. The constructor starts with var SwitcherBehavior set equal to a function. Let's look at the code inside the function. First we set this.switchOn to false. We'll use that switchOn property to turn on and off our animation.
Next, we create a variable, mySwitcherBehavior; that variable stores a reference to this, the current SwitcherBehavior object. We keep a reference to SwitcherBehavior to take advantage JavaScript's closure—closure gives any child function defined within a parent function access to all the variables of the parent.
We're about to define a couple callbacks that need a way to get back to the current SwitcherBehavior object. We can't use the this keyword to point to SwitcherBehavior, because the callback functions are run in other objects where this refers to that other object.
To maintain a link to SwitcherBehavior, we create a variable that stores a reference to the SwitcherBehavior object, and then we use that variable inside our callback function. Because the callback is a child function, it has access to its parent's variables and we maintain a link to SwitcherBehavior. It can seem a little strange at first, but this procedure helps us get back to an object when we're currently running code in a completely different object.
The next two lines of code instantiate Switcher and Clickable. Switcher flips a boolean value—a data type that can be either true or false—from its current value to its opposite value. If the boolean is true, it's flipped to false; if it's false, it's flipped to true.
Clickable tracks a common set of mouse events—mousemove, mousedown, and mouseup. We'll use these mouse events to determine if the mouse is over a sprite, if the mouse clicked on a sprite, and if the mouse was released after it clicked down.
Here's the code that creates Switcher and Clickable:
this.switcher = new Switcher({callback: function(switchOn){mySwitcherBehavior.setSwitchOn(switchOn);}});
this.clickable = new Clickable({mouseDownCallback: function(){mySwitcherBehavior.switcher.flipIt();}});
We pass callbacks to both Switcher and Clickable. Let's look at Switcher's callback. We're passing it an object literal with a name-value pair. The name is callback and the value is an anonymous function. Let's focus on that anonymous function:
function(switchOn){mySwitcherBehavior.setSwitchOn(switchOn);}
The function has parameter of switchOn—it's right between the parentheses that follow the function keyword. Switcher flips a boolean from true to false or false to true and passes that boolean along with its callback function. It does all of that in its flipIt method. Here's the flipIt code:
Switcher.prototype.flipIt = function() {
"use strict";
if (this.switchOn) {
this.switchOn = false;
} else {
this.switchOn = true;
}
this.callback(this.switchOn);
};
The last line of the flipIt method, triggers the callback—the anonymous function we defined in SwitcherBehavior. Notice that the callback passes along a boolean, this.switchOn, to the anonymous function.
Back to our anonymous function:
function(switchOn){mySwitcherBehavior.setSwitchOn(switchOn);}
The code inside the function's curly brackets runs when we trigger the callback inside the flipIt method. That code targets the SwitcherBehavior object and calls its setSwitchOn method.
So what's happening here? Switcher's flipIt triggers a callback which calls the SwitcherBehavior's setSwitchOn method. setSwitchOn uses the boolean that's passed to it to sets its own switch—a property called this.switchOn. switchOn, checked in each switch behavior's execute method, turns on or off things like cycling, rotating, or moving. We'll see more on that in a moment.
Next let's look at Clickable's callback. Here's the line of code where we instantiate Clickable:
this.clickable = new Clickable({mouseDownCallback: function(){mySwitcherBehavior.switcher.flipIt();}});
We pass to Clickable a name-and-value object literal with a name of mouseDownCallback. Clickable defines two callbacks: mouseDownCallback and mouseUpCallback. For our SwitcherBehavior class, we need just the mouseDownCallback. Like Switcher's callback, we set mouseDownCallback to an anonymous function. Here's the function by itself:
function(){mySwitcherBehavior.switcher.flipIt();}
This anonymous function has no parameters, so we just have two parentheses after the function keyword. Then we have a line of code within the function's curly brackets. Here's that line of code:
mySwitcherBehavior.switcher.flipIt();
We're using the same mySwitcherBehavior variable that holds a reference to the current SwitcherBehavior object. Then we target switcher—a SwitcherBehavior property that contains a reference to Switcher—and run switcher's flipIt method.
(Remember that dot syntax lets us access a property or method on an object. mySwitcher.switcher.flipIt() gives us access to switcher's flipIt method and switcher is a child of the parent object mySwitcherBehavior. We connect each object, property, or method to its parent object with a period or dot.)
So how does the Clickable's callback work?
Clickable has a mouseDown method that's called by MyEventManager whenever a mousedown event occurs. Clickable's mouseDown method checks if the mouse is currently over the sprite, and if it is, it runs the mouseDownCallback function. mouseDownCallback houses an anonymous function that calls Switcher's flipIt method. flipIt turns its boolean from false to true or true to false—flipping its value—and calls back SwitcherBehavior's setSwitchOn, which flips its own boolean.
Switcher's callback runs a method on SwitcherBehavior; Clickable's callback runs a method on Switcher. In both cases, the callback lets an object run a method on a different object. We could simplify things and put all the logic of Switcher and Clickable inside our SwitcherBehavior class—that would work fine. But we've separated the logic into different modules to make the functionality of Switcher and Clickable available to lots of behaviors. Rather than re-create the logic for each new behavior, we bundle that logic in its own class to make it accessible to any behavior that instantiates the class.
SwitcherBehavior, like all behaviors, has an execute method. Its execute method is overwritten by each child switch behavior. Those child behaviors implement specific logic inside their execute methods to do things like rotate, cycle, or move a sprite.
We have one last method that we want to share with all our switch behaviors. That's the setSwitchOn method. This method turns on and off our switchOn property. Recall that this method is called by Switcher's flipIt method, which is in turn called when Clickable's mouseDownCallback runs.
Here's that last method:
SwitcherBehavior.prototype.setSwitchOn = function(switchOn) {
"use strict";
this.switchOn = switchOn;
};
Now let's turn our attention to a behavior that lets us switch on and off a sprite's animated cycle.
We'll call our script cycling-on-off.js and name its class CyclingOnOff.
Let's look at CyclingOnOff's constructor and two lines of code that follow that constructor.
var CyclingOnOff = function(cyclingOnOffArgs) {
"use strict";
SwitcherBehavior.call(this, cyclingOnOffArgs);
if (!cyclingOnOffArgs){
cyclingOnOffArgs = {};
}
this.cycleTime = cyclingOnOffArgs.cycleTime || 60;
this.lastTime = 0.0;
this.name = "cyclingOnOff";
return this;
};
CyclingOnOff.prototype = Object.create(SwitcherBehavior.prototype);
CyclingOnOff.prototype.constructor = CyclingOnOff;
Before we walk through the constructor, we'll focus on the two lines of code that follow the constructor.
CyclingOnOff.prototype = Object.create(SwitcherBehavior.prototype);
CyclingOnOff.prototype.constructor = CyclingOnOff;
CyclingOnOff inherits properties and methods from SwitcherBehavior—it's a child class of the parent class SwitcherBehavior. The above two lines of code turn CyclingOnOff into a child class of SwitcherBehavior, giving it access to SwitcherBehavior's functionality.
We first set CyclingOnOff's prototype—its shared set of methods and properties—equal to SwitcherBehavior's prototype. Object—the parent of all objects—has a create method that transfers SwitcherBehavior's prototype to CyclingOnOff. After Object's create method runs, CyclingOnOff incorporates all SwitcherBehavior's methods and properties.
Next we set CyclingOnOff's constructor—the function we call to instantiate a CyclingOnOff object—to CyclingOnOff. When CyclingOnOff inherits SwitcherBehavior's prototype, its constructor is set to SwitcherBehavior. We need to restore it to its own constructor, CyclingOnOff.
All of our switch behaviors inherit from SwitcherBehavior and they have similar lines of code that transfer to them SwitcherBehavior's prototype and restore their own constructors.
Let's look now at CyclingOnOff's constructor. Look at the second line of code inside the constructor.
SwitcherBehavior.call(this, cyclingOnOffArgs);
We call CyclingOnOff's parent class, SwitcherBehavior, and pass it cyclingOnOffArgs. That call ensures the parent's properties are set up and available for the child class.
Next we check if cyclingOnOffArgs is undefined—cyclingOnOffArgs contains the object literal passed to CyclingOnOff when it's instantiated. If cyclingOnOffArgs is undefined, we set it to an empty object—just two curly brackets with nothing inside.
if (!cyclingOnOffArgs) {
cyclingOnOffArgs = {};
}
If no arguments are passed to CyclingOnOff's, cyclingOnOffArgs is undefined. If we use an undefined cyclingOnOffArgs to initialize a property, we'll trigger a code error that stops our script. To avoid that, we set an undefined cyclingOnOffArgs to an empty object literal (cyclingOnOffArgs = {};). We also include default values when we initialize a property, ensuring that the property has a value even if no arguments are passed to the object.
Next we set the property cycleTime to the cyclingOnOffArgs.cycleTime property.
this.cycleTime = cyclingOnOffArgs.cycleTime || 60;
Let's talk about this syntax. We're setting this.cycleTime on the left equal to a value on the right. But we have two values on the right instead of just one. Those two pipe lines that separate the values are a short-hand way to say, if the first value doesn't exist, take the second value.
In the above code, if CyclingOnOffArgs doesn't have a cycleTime property, we use a default of 60—that's 60 milliseconds. There are a thousand milliseconds in one second. We use cycleTime in the execute method to determine if it's time to advance to the next sprite-sheet frame. If more the 60 milliseconds have passed since we last advanced a frame, its time to display a new frame.
With a cycleTime of 60, we're updating sprite-sheet frames 16.7 times a second. Decrease cycleTime to increase the cycling frame rate; increase the cycleTime to decrease the frame rate.
Just a couple more properties to initialize. We set this.lastTime to zero—we use lastTime to keep track of the time that's passed since we last displayed a new sprite-sheet frame.
We give CyclingOnOff a unique name and return a reference to the object using the this keyword.
Next, let's look at CyclingOnOff's execute method:
CyclingOnOff.prototype.execute = function(sprite, context, time) {
"use strict";
//call clickable so it can update its mouseIsOver property with the sprite's current mouseIsOver property
this.clickable.execute(sprite, context, time);
if (this.switchOn) {
if (time - this.lastTime >= this.cycleTime) {
sprite.parent.advanceCel();
this.lastTime = time;
}
}
};
The first thing we do is call Clickable's execute. We pass to Clickable a reference to the sprite so it can keep track of the sprite's position relative to the mouse's position. Clickable needs access to the sprite to determine when the mouse clicks down on the sprite.
After we call Clickable's execute method, we check our switch, switchOn. If it's set to true, we cycle our animation. If it's false, we leave the static image in place.
When switchOn is true, we check to see if enough time has passed to move to the next frame on the sprite sheet. cycleTime determines the amount of time we wait before advancing a sprite-sheet frame.
Let's look at the code inside the conditional statement.
if (time - this.lastTime >= this.cycleTime) {
sprite.parent.advanceCel();
this.lastTime = time;
}
We first perform a calculation that subtracts lastTime from the current time. If the result of the calculation is greater than or equal to cycleTime—enough time has passed since we last displayed a new frame, so we can display our next frame. We call the sprite's parent advanceCel() method, which moves forward one frame on the sprite sheet.
(This is what happens behind the scenes: advanceCel increments the sprite's celIndex property. If it's at the end of the sprite sheet's frames, it sets the celIndex back to zero and we start a new cycle. The sprite's painter uses that celIndex to determine which cel or frame within the sprite sheet to render. See js/sprite/all-sprite.js, BitmapGraphic.prototype.advanceCel and js/painters/all-painters.js, BitmapPainter.prototype.typePaint.)
After advancing the sprite sheet's frame, we set lastTime to the current time. lastTime now holds the time when we last advanced a frame. The next time CyclingOnOff's execute is called, we subtract lastTime from the current time. If the result is greater than cyclingTime, we advance to the next frame in the sprite sheet. That's how we cycle through our animation.
Just a few things to finish up. We need to create our set-up and start scripts for this project and create the HTML page that houses the canvas and includes the scripts. The set-up script is similar to all our other set-up scripts. We define the sprite's properties, painters, and behaviors, instantiate the sprite, and store it in our sprites array. Here's the set-up code:
var CyclingOnOffSetup = function (setupArgs) {
"use strict";
//setupArgs passes in a reference to the project instantiation of the Main class
if (!setupArgs) {
setupArgs = {};
}
this.blueRect = {
spriteProps:{name:'blue rect', x: 600, y: 400, w: 1200, h: 800, fillColor:'#0e5190', visible:true},
painters:[new RectPainter()],
behaviors:[]
};
this.myBackground = new ShapeLineGraphic(this.blueRect);
this.catSheet = [
{ x:0, y:0, w:400, h:220 },
{ x:400, y:0, w:400, h:220 },
{ x:800, y:0, w:400, h:220 },
{ x:1200, y:0, w:400, h:220 },
{ x:1600, y:0, w:400, h:220 },
{ x:0, y:220, w:400, h:220 },
{ x:400, y:220, w:400, h:220 },
{ x:800, y:220, w:400, h:220 },
{ x:1200, y:220, w:400, h:220 },
{ x:1600, y:220, w:400, h:220 },
{ x:0, y:440, w:400, h:220 },
{ x:400, y:440, w:400, h:220 },
{ x:800, y:440, w:400, h:220 },
{ x:1200, y:440, w:400, h:220 },
{ x:1600, y:440, w:400, h:220 },
];
this.catAnim = {
spriteProps:{name:'catAnim', x:410, y:340, w:400, h:220, graphics:['img/cat-sprite-sheet.png'], sheets:[this.catSheet], rotation: 0.2, visible:true},
painters:[new BitmapPainter()],
behaviors:[new CyclingOnOff({cycleTime:60})]
};
this.myCatAnim = new BitmapGraphic(this.catAnim);
this.sprites = [this.myBackground, this.myCatAnim];
return this;
};
CyclingOnOffSetup.prototype.getSprites = function() {
"use strict";
return this.sprites;
};
We first define and instantiate our background sprite; it's a rectangle that provides the backdrop for our canvas.
We define the sprite sheet's frames—take a look at this.catSheet.
As usual, we define the sprite's properties, painters, and behaviors. The spriteProps and painters are the same as last week's example of cycling animation. The behaviors, however, are different. We instantiate CyclingOnOff and pass it a cycleTime of 60 milliseconds—cycleTime determines how quickly the sprite cycles through its sprite-sheet frames.
We instantiate our sprite as a BitmapGraphic, passing in all our sprite data. And finally we add our background and animating sprite in the sprites array.
To see the start script, look at js/project/interactive-animation/cycling-on-off-start.js. The html is called 01-cycling-switch.html—it includes a complete list of scripts we use for this project. Try building your own on-off animation using a sprite sheet of your own creation.
Also, take a look at the behavior that turns rotation on and off, js/behaviors/rotating-on-off.js. It has a similar structure to cycling-on-off.js. The main difference is that you pass the behavior radians per second and the sprite spins instead of cycles.