Adding Animation
Setup
You may find a completed “features/display/AddingAnimation” sample project for TypeScript, Haxe, ES6 or ES5 online, the following describes how to add animation from an empty project or from the previous Displaying a Bitmap.
If you have completed the Displaying a Bitmap tutorial, you can continue using the same project files. Otherwise you can run the following commands:
mkdir AddingAnimation
cd AddingAnimation
yo openfl
If you start fresh, download an image file into your dist
directory, such as this one.
Animating Your Project
In the Displaying a Bitmap tutorial, we added an image using the openfl.display.Bitmap
class, and made it renderable. We also suggested that you try changing some properties, such as the x
or y
property of the Bitmap
object to change how it would be rendered.
Although this is a good way to understand some of the fundamental principles of the display list works in OpenFL, interactive projects often benefit by updating these properties over time to create an animation.
In the following steps, we will illustrate how you can create animation using Event.ENTER_FRAME
, using a timer, a combination of both Event.ENTER_FRAME
and time, as well as using an animation library.
Before we continue, your “src/app.ts”, “src/app.js” or “src/App.hx” should be able to load and display a bitmap, similar to the following:
import Bitmap from "openfl/display/Bitmap";
import BitmapData from "openfl/display/BitmapData";
import Sprite from "openfl/display/Sprite";
import Stage from "openfl/display/Stage";
class App extends Sprite {
constructor () {
super ();
BitmapData.loadFromFile ("openfl.png").onComplete ((bitmapData) => {
var bitmap = new Bitmap (bitmapData);
this.addChild (bitmap);
});
}
}
var stage = new Stage (550, 400, 0xFFFFFF, App);
document.body.appendChild (stage.element);
import openfl.display.Bitmap;
import openfl.display.BitmapData;
import openfl.display.Sprite;
import openfl.display.Stage;
class App extends Sprite {
public function new () {
super ();
BitmapData.loadFromFile ("openfl.png").onComplete (function (bitmapData) {
var bitmap = new Bitmap (bitmapData);
addChild (bitmap);
});
}
static function main () {
var stage = new Stage (550, 400, 0xFFFFFF, App);
js.Browser.document.body.appendChild (stage.element);
}
}
import Bitmap from "openfl/display/Bitmap";
import BitmapData from "openfl/display/BitmapData";
import Sprite from "openfl/display/Sprite";
import Stage from "openfl/display/Stage";
class App extends Sprite {
constructor () {
super ();
BitmapData.loadFromFile ("openfl.png").onComplete ((bitmapData) => {
var bitmap = new Bitmap (bitmapData);
this.addChild (bitmap);
});
}
}
var stage = new Stage (550, 400, 0xFFFFFF, App);
document.body.appendChild (stage.element);
var Bitmap = require ("openfl/display/Bitmap").default;
var BitmapData = require ("openfl/display/BitmapData").default;
var Sprite = require ("openfl/display/Sprite").default;
var Stage = require ("openfl/display/Stage").default;
var App = function () {
Sprite.call (this);
BitmapData.loadFromFile ("openfl.png").onComplete (function (bitmapData) {
var bitmap = new Bitmap (bitmapData);
this.addChild (bitmap);
}.bind (this));
}
App.prototype = Sprite.prototype;
var stage = new Stage (550, 400, 0xFFFFFF, App);
document.body.appendChild (stage.element);
Using Event.ENTER_FRAME
One of the ways that you can begin to add animation in OpenFL is by listening to the Event.ENTER_FRAME
event. This event occurs every time that OpenFL enters a new animation frame (which is by default the same as requestAnimationFrame
, but is configurable).
First we can add a new import for openfl.events.Event
:
import Event from "openfl/events/Event";
import openfl.events.Event;
import Event from "openfl/events/Event";
var Event = require ("openfl/events/Event").default;
If you prefer, you can use "enterFrame"
instead of Event.ENTER_FRAME
, but importing Event
is a standard way of accessing event names.
Now we can add some code to listen to the Event.ENTER_FRAME
event. Add it below the new Bitmap
declaration in your code, like this:
BitmapData.loadFromFile ("openfl.png").onComplete ((bitmapData) => {
var bitmap = new Bitmap (bitmapData);
this.addChild (bitmap);
// add code here
});
BitmapData.loadFromFile ("openfl.png").onComplete (function (bitmapData) {
var bitmap = new Bitmap (bitmapData);
addChild (bitmap);
// add code here
});
BitmapData.loadFromFile ("openfl.png").onComplete ((bitmapData) => {
var bitmap = new Bitmap (bitmapData);
this.addChild (bitmap);
// add code here
});
BitmapData.loadFromFile ("openfl.png").onComplete (function (bitmapData) {
var bitmap = new Bitmap (bitmapData);
this.addChild (bitmap);
// add code here
}.bind (this));
this.addEventListener (Event.ENTER_FRAME, (e:Event) => {
console.log ("Enter Frame");
});
addEventListener (Event.ENTER_FRAME, function (e) {
trace ("Enter Frame");
});
this.addEventListener (Event.ENTER_FRAME, (e) => {
console.log ("Enter Frame");
});
this.addEventListener (Event.ENTER_FRAME, function (e) {
console.log ("Enter Frame");
});
All we need to do to create an animation is update a property of our Bitmap
object (such as the x
, y
or alpha
property) in our listener. This will occur repeatedly over time, making it possible to play an animation.
The following code will animate the image from it’s initial y
position of 0
, increasing by 1
each frame until it reaches y == 200
. Then it changes direction, and moves back toward y == 0
before finally repeating:
let direction = true;
this.addEventListener (Event.ENTER_FRAME, (e:Event) => {
if (direction) {
bitmap.y += 1;
if (bitmap.y >= 200) direction = !direction;
} else {
bitmap.y -= 1;
if (bitmap.y <= 0) direction = !direction;
}
});
var direction = true;
addEventListener (Event.ENTER_FRAME, function (e) {
if (direction) {
bitmap.y += 1;
if (bitmap.y >= 200) direction = !direction;
} else {
bitmap.y -= 1;
if (bitmap.y <= 0) direction = !direction;
}
});
let direction = true;
this.addEventListener (Event.ENTER_FRAME, (e) => {
if (direction) {
bitmap.y += 1;
if (bitmap.y >= 200) direction = !direction;
} else {
bitmap.y -= 1;
if (bitmap.y <= 0) direction = !direction;
}
});
var direction = true;
this.addEventListener (Event.ENTER_FRAME, function (e) {
if (direction) {
bitmap.y += 1;
if (bitmap.y >= 200) direction = !direction;
} else {
bitmap.y -= 1;
if (bitmap.y <= 0) direction = !direction;
}
});
Using Timer
Sometimes, you may need to update a property based upon time, rather than animation frames. In general, it is recommended that updates are made within an animation frame. In the next section, we will show you can combine Event.ENTER_FRAME
with openfl.utils.getTimer
to handle time, but in the meantime, this is a short description of openfl.utils.Timer
could be used to trigger animation based on time.
Both setTimeout
and setInterval
could be used to trigger an event repeatedly, as well as a final complete event, but the Timer
class combines the two behaviors into a single utility which may be helpful for time-based behaviors.
import TimerEvent from "openfl/events/TimerEvent";
import Timer from "openfl/utils/Timer";
import openfl.events.TimerEvent;
import openfl.utils.Timer;
import TimerEvent from "openfl/events/TimerEvent";
import Timer from "openfl/utils/Timer";
var TimerEvent = require ("openfl/events/TimerEvent").default;
var Timer = require ("openfl/utils/Timer").default;
let increment = -0.005;
let timer = new Timer (1000 / 60, 200);
timer.addEventListener (TimerEvent.TIMER, (e:TimerEvent) => {
bitmap.alpha += increment;
});
timer.addEventListener (TimerEvent.TIMER_COMPLETE, (e:TimerEvent) => {
increment *= -1;
timer.reset ();
timer.start ();
});
timer.start ();
var increment = -0.005;
var timer = new Timer (1000 / 60, 200);
timer.addEventListener (TimerEvent.TIMER, function (e) {
bitmap.alpha += increment;
});
timer.addEventListener (TimerEvent.TIMER_COMPLETE, function (e) {
increment *= -1;
timer.reset ();
timer.start ();
});
timer.start ();
let increment = -0.005;
let timer = new Timer (1000 / 60, 200);
timer.addEventListener (TimerEvent.TIMER, (e) => {
bitmap.alpha += increment;
});
timer.addEventListener (TimerEvent.TIMER_COMPLETE, (e) => {
increment *= -1;
timer.reset ();
timer.start ();
});
timer.start ();
var increment = -0.005;
var timer = new Timer (1000 / 60, 200);
timer.addEventListener (TimerEvent.TIMER, function (e) {
bitmap.alpha += increment;
});
timer.addEventListener (TimerEvent.TIMER_COMPLETE, function (e) {
increment *= -1;
timer.reset ();
timer.start ();
});
timer.start ();
Using Event.ENTER_FRAME
and getTimer
When you need to update based on animation frames, but also need to know how much time has elapsed, you could use a combination of both Event.ENTER_FRAME
and openfl.utils.getTimer
.
This approach is common for many games, which need to know the amount of time that has elapsed since the last frame. It also tends to blend the best elements of both Event.ENTER_FRAME
and time-based animation.
import Event from "openfl/events/Event";
import getTimer from "openfl/utils/getTimer";
...
let speed = 60 / 1000;
let cacheTime = getTimer ();
this.addEventListener (Event.ENTER_FRAME, (e:Event) => {
let currentTime = getTimer ();
let deltaTime = currentTime - cacheTime;
bitmap.x += speed * deltaTime;
if (bitmap.x > 100) {
speed *= -1;
bitmap.x = 100;
} else if (bitmap.x < 0) {
speed *= -1;
bitmap.x = 0;
}
cacheTime = currentTime;
});
import openfl.events.Event;
import openfl.Lib.getTimer;
...
var speed = 60 / 1000;
var cacheTime = getTimer ();
addEventListener (Event.ENTER_FRAME, function (e) {
var currentTime = getTimer ();
var deltaTime = currentTime - cacheTime;
bitmap.x += speed * deltaTime;
if (bitmap.x > 100) {
speed *= -1;
bitmap.x = 100;
} else if (bitmap.x < 0) {
speed *= -1;
bitmap.x = 0;
}
cacheTime = currentTime;
});
import Event from "openfl/events/Event";
import getTimer from "openfl/utils/getTimer";
...
let speed = 60 / 1000;
let cacheTime = getTimer ();
this.addEventListener (Event.ENTER_FRAME, (e) => {
let currentTime = getTimer ();
let deltaTime = currentTime - cacheTime;
bitmap.x += speed * deltaTime;
if (bitmap.x > 100) {
speed *= -1;
bitmap.x = 100;
} else if (bitmap.x < 0) {
speed *= -1;
bitmap.x = 0;
}
cacheTime = currentTime;
});
var Event = require ("openfl/events/Event").default;
var getTimer = require ("openfl/utils/getTimer").default;
...
var speed = 60 / 1000;
var cacheTime = getTimer ();
this.addEventListener (Event.ENTER_FRAME, function (e) {
var currentTime = getTimer ();
var deltaTime = currentTime - cacheTime;
bitmap.x += speed * deltaTime;
if (bitmap.x > 100) {
speed *= -1;
bitmap.x = 100;
} else if (bitmap.x < 0) {
speed *= -1;
bitmap.x = 0;
}
cacheTime = currentTime;
});
Using an Animation Library
When you move beyond a simple illustration, and begin building real interactive projects, use of an animation (or “tween”) library is more productive for expressive animation than animating by hand. In addition to automatically animating based upon requestAnimationFrame
, an animation library may make it simpler to combine animation of multiple properties at once, make it simple to receive callbacks when the object has been updated or completes an animation, and often includes multiple “easing” equations in order to animate properties along a quadratic, exponential or elastic animation equation rather than a simple linear equation, similar to the examples above.
In the following example code, we have integrated a tween library called Actuate. If you wish, you may use the “features/display/AddingAnimation” sample (for TypeScript, Haxe, ES6 and ES5), which is already configured for use with Actuate, otherwise we can add Actuate to your current project.
First, open a command-prompt or terminal, and change to your project directory and run the following command:
npm install --save-dev actuate
Next, in order to make it easier to import modules from the Actuate library, edit “webpack.common.js”, and update the resolve
section to include an alias for Actuate:
alias: {
...
"motion": path.resolve (__dirname, "node_modules/actuate/lib/motion")
}
If you are using TypeScript, we will also need to add an alias in “tsconfig.json” so that the TypeScript compiler also knows how to resolve our aliases for Actuate:
"paths": {
...
"motion": ["node_modules/actuate/lib/motion"],
"motion/*": ["node_modules/actuate/lib/motion/*"]
},
If you are using Haxe, we will need to add a new classpath to Actuate in your “build.hxml” file:
-cp node_modules/actuate/lib
Now Actuate should be installed available, importing from “motion” or “motion/Actuate”, rather than a lengthier path.
Here is an example animation using Actuate:
import Actuate from "motion/Actuate";
import Elastic from "motion/easing/Elastic";
import Linear from "motion/easing/Linear";
import motion.Actuate;
import motion.easing.Elastic;
import motion.easing.Linear;
import Actuate from "motion/Actuate";
import Elastic from "motion/easing/Elastic";
import Linear from "motion/easing/Linear";
var Actuate = require ("motion/Actuate").default;
var Elastic = require ("motion/easing/Elastic").default;
var Linear = require ("motion/easing/Linear").default;
Actuate.tween (bitmap, 4, { y: 200 }).repeat ().reflect ();
Actuate.tween (bitmap, 4, { x: 100 }).ease (Elastic.easeOut).repeat ().reflect ();
Actuate.tween (bitmap, 4, { alpha: 0 }).ease (Linear.easeNone).repeat ().reflect ();
Actuate.tween (bitmap, 4, { y: 200 }).repeat ().reflect ();
Actuate.tween (bitmap, 4, { x: 100 }).ease (Elastic.easeOut).repeat ().reflect ();
Actuate.tween (bitmap, 4, { alpha: 0 }).ease (Linear.easeNone).repeat ().reflect ();
Actuate.tween (bitmap, 4, { y: 200 }).repeat ().reflect ();
Actuate.tween (bitmap, 4, { x: 100 }).ease (Elastic.easeOut).repeat ().reflect ();
Actuate.tween (bitmap, 4, { alpha: 0 }).ease (Linear.easeNone).repeat ().reflect ();
Actuate.tween (bitmap, 4, { y: 200 }).repeat ().reflect ();
Actuate.tween (bitmap, 4, { x: 100 }).ease (Elastic.easeOut).repeat ().reflect ();
Actuate.tween (bitmap, 4, { alpha: 0 }).ease (Linear.easeNone).repeat ().reflect ();
You can read more about the Actuate library here, or experiment with other tween libraries!
Next Steps
Keep playing! There is a lot you can do with an animation library. To keep learning, try the Handling Mouse Events tutorial to begin making your project interactive.