A few months ago I was working on an Angular app that used animated sprites across multiple sections and pages. I figured it was the perfect opportunity to create a directive that I could re-use to dynamically create and animate a sprite of any size, number of frames and orientation.
Setting up our Directive
The first thing we want to do is set up a basic framework for our directive.
(function() {
angular
.module('simple-sprite', [])
.directive('simpleSprite', simpleSprite);
simpleSprite.$inject = [];
function simpleSprite() {
return {
restrict: 'AE',
replace: false,
scope: {
},
link: function($scope, element, attributes) {
}
};
}
})(angular);
We have set up the module simple-sprite
, from which our new directive can be loaded. We have also have set up the the skeleton of our directive. So far, we have only an empty isolated scope
and link
function. Next let’s set up the properties we’ll need from the user and set them on our scope
.
(function() {
angular
.module('simple-sprite', [])
.directive('simpleSprite', simpleSprite);
simpleSprite.$inject = [];
function simpleSprite() {
return {
restrict: 'AE',
replace: false,
scope: {
src: "@",
frameWidth: "@",
frameHeight: "@",
frames: "@",
speed: "@",
loop: "@"
},
link: function($scope, element, attributes) {
var src = $scope.src,
frameWidth = parseInt($scope.framesWidth) || 0,
frameHeight = parseInt($scope.framesHeight) || 0,
frames = parseInt($scope.frames) || 0,
speed = parseInt($scope.speed) || 100,
loop = $scope.repeat == 'true';
var currentSpritePosition = {
x: 0,
y: 0
};
var animationInterval;
activate();
function activate() {
// Initializes the sprite and starts animating
}
function animate() {
// Animates the sprite
}
}
};
}
})(angular);
We have set up all of the properties we’ll need in order to create our sprite animation. We’ve included these options in our isolated scope (to be passed in by the user) and have set defaults for each one.
- The source of our sprite image
- The width and height of a single frame
- The total number of frames
- The speed of the animation interval
- Whether or not we should loop the sprite once it has finished animating
In addition, we have added a currentSpritePosition
to keep track of the position of our sprite. We have also included an empty animationInterval
variable which will hold our animation interval.
Activating our Sprite
Let’s continue by filling out our activate
function. In activate
, we will style our element using properties passed in to the isolated scope. This includes the width/height of our element (based on the width/height of our sprite) and the background-image (based on our sprite image source).
function activate() {
element.css({
"display": "block",
"width": frameWidth + "px",
"height": frameHeight + "px",
"background": "url(" + src + ") repeat",
"backgroundPosition": "0px 0px"
});
animate();
}
After we set the required styles on our element, we trigger the animate
function. This is where we will actually animate our sprite.
Animating our Sprite
We will use Angular’s $window
service to set our interval. First we’ll need to inject this service into our directive.
simpleSprite.$inject = [$window];
function simpleSprite($window) {
...
}
Next we’ll need to start writing our actual animation logic. Each time the interval is run, we need to do the following:
- Update the current sprite frame
- Check to see if our animation has completed. If it has, either replay the animation or clear it.
We’ll start by updating our current sprite position:
function animate() {
animationInterval = $window.setInterval(function() {
// TODO: Update the current sprite frame
element.css("background-position", -spritePosition.x + "px" + " " + spritePosition.y + "px");
}, speed);
}
Pretty easy so far. We set up our interval and updated our element’s frame to use currentSpritePosition
. Now we actually need to check if the animation has completed or not, and update currentSpritePosition
accordingly. To do this we will create a helper function isAnimationComplete
that will check whether or not we have reached our last frame.
function _isAnimationComplete() {
return currentSpritePosition.x >= frameWidth * frames;
}
Now we can jump back into our animate
function and update our currentSpritePosition
:
function animate() {
animationInterval = $window.setInterval(function() {
// TODO: Update the current sprite frame
element.css("background-position", -spritePosition.x + "px" + " " + spritePosition.y + "px");
if (_isAnimationComplete()) {
if (loop) {
// Restart the animation
currentSpritePosition.x = 0;
currentSpritePosition.y = 0;
} else {
// We are done animating!
$window.clearInterval(animationInterval);
}
} else {
// Update the current sprite position
spritePosition.x += frameWidth;
}
}, speed);
}
And that’s it! We have an animating sprite directive. The last thing we need to add is a handler for our scope’s $destroy
event. This event triggers whenever our directive is removed from the application scope. In our case, we just need to clear our animation interval whenever this occurs.
$scope.$on("$destroy", function() {
$window.clearInterval(animationInterval);
});
Supporting Multiple Frames Per Row
Our directive should work as is, but it currently only supports sprite-sheets with one row. We can make it more flexible by allowing users to specify the number of frames per row in sprite-sheets with multiple rows. In order to do this, we will need to add a new optional property on our scope called framesPerRow
. We will also have to update both our isAnimationComplete
and animate
functions:
Assuming we have added framesPerRow
to our scope, lets go ahead and update our functions:
function _isAnimationComplete() {
if (framesPerRow) {
var numRows = frames / framesPerRow;
return spritePosition.x >= framesPerRow * frameWidth &&
spritePosition.y >= numRows * frameHeight;
} else {
return spritePosition.x >= framesPerRow;
}
}
function animate() {
if (_isAnimationComplete()) {
...
} else {
currentSpritePosition.x += frameWidth;
// Check if we should update our y position
if (framesPerRow) {
// If we are on the last frame of the row
if (currentSpritePosition.x == frameWidth * framesPerRow) {
// ... And we are not on the last row
var numRows = frames / framesPerRow;
if (currentSpritePosition.y < numRows * frameHeight) {
currentSpritePosition.x = 0;
currentSpritePosition.y += frameHeight;
}
}
}
}
}
Seeing it in Action
Now that we’ve created our directive, lets see it in action. We’ve got our sprite-sheet ready to go.
We set up our directive like so:
<simple-sprite
src="path/to/my.sprite.png"
frame-width="100"
frame-height="155"
frames="8"
repeat="true"
speed="50">
</simple-sprite>
And the result:
And that’s all there is to it! The source code and documentation for AngularSimpleSprite
can be found on my Github account - feel free to contribute!