Before doing anything interesting with the sprites, we need to:
Load the sprite sheet(s),
Extract the different postures and store them in an array of sprites,
Choose the appropriate one, and draw it within the animation loop, taking into account elapsed time. We cannot draw a different image of the woman walking at 60 updates per second. We will have to create a realistic "delay" between each change of sprite image.
In this lesson, let's construct an interactive tool to present the principles of sprite extraction and animation.
In this example, we'll move the slider to extract the sprite indicated by the slider value. See the red rectangle? This is the sprite image currently selected! When you move the slider, the corresponding sprite is drawn in the small canvas. As you move the slider from one to the next, see how the animation is created?
HTML code:
<html lang="en">
<head>
<title>Extract and draw sprite</title>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
Sprite width: 48, height: 92, rows: 8, sprites per posture: 13<p>
<label for="x">x: <input id="x" type="number" min=0><br/>
<label for="y">y: <input id="y" type="number" min=0><br/>
<label for="width">width: <input id="width" type="number" min=0><br/>
<label for="height">height: <input id="height" type="number" min=0><p>
Select current sprite: <input type=range id="spriteSelect" value=0> <output id="spriteNumber">
<p/>
<canvas id="canvas" width="48" height="92" />
</p>
<canvas id="spritesheet"></canvas>
</body>
</html>
Notice that we use an <input type="range"> to select the current sprite, and we have two canvases: a small one for displaying the currently-selected sprite, and a larger one that contains the sprite sheet and in which we draw a red square to highlight the selected sprite.
Here's an extract from the JavaScript. You don't have to understand all the details, just look at the part in bold which extracts the individual sprites:
var SPRITE_WIDTH = 48; // Characteristics of the sprites and spritesheet
var SPRITE_HEIGHT = 92;
var NB_ROWS = 8;
var NB_FRAMES_PER_POSTURE = 13;
// the different input and output fields
var xField, yField, wField, hField, spriteSelect, spriteNumber;
// The two canvases and respective contexts
var canvas, canvasSpriteSheet, ctx1, ctx2;
window.onload = function() {
canvas = document.getElementById("canvas");
ctx1 = canvas.getContext("2d");
canvasSpriteSheet = document.getElementById("spritesheet");
ctx2 = canvasSpriteSheet.getContext("2d");
xField = document.querySelector("#x");
yField = document.querySelector("#y");
wField = document.querySelector("#width");
hField = document.querySelector("#height");
spriteSelect = document.querySelector("#spriteSelect");
spriteNumber = document.querySelector("#spriteNumber");
// Update values of the input fields in the page
wField.value = SPRITE_WIDTH;
hField.value = SPRITE_HEIGHT;
xField.value = 0;
yField.value = 0;
// Set attributes for the slider depending on the number of sprites on the
// sprite sheet
spriteSelect.min = 0;
spriteSelect.max=NB_ROWS*NB_FRAMES_PER_POSTURE - 1;
// By default the slider is disabled until the sprite sheet is fully loaded
spriteSelect.disabled = true;
spriteNumber.innerHTML=0;
// Load the spritesheet
spritesheet = new Image();
spritesheet.src="https://i.imgur.com/3VesWqx.png";
// Called when the spritesheet has been loaded
spritesheet.onload = function() {
// enable slider
spriteSelect.disabled = false;
// Resize big canvas to the size of the sprite sheet image
canvasSpriteSheet.width = spritesheet.width;
canvasSpriteSheet.height = spritesheet.height;
// Draw the whole spritesheet
ctx2.drawImage(spritesheet, 0, 0);
// Draw the first sprite in the big canvas, corresponding to sprite 0
// wireframe rectangle in the sprite sheet
drawWireFrameRect(ctx2, 0 , 0, SPRITE_WIDTH, SPRITE_HEIGHT, 'red', 3);
// small canvas, draw sub image corresponding to sprite 0
ctx1.drawImage(spritesheet, 0, 0, SPRITE_WIDTH, SPRITE_HEIGHT,
0, 0, SPRITE_WIDTH, SPRITE_HEIGHT);
};
// input listener on the slider
spriteSelect.oninput = function(evt) {
// Current sprite number from 0 to NB_FRAMES_PER_POSTURE * NB_ROWS
var index = spriteSelect.value;
// Computation of the x and y position that corresponds to the sprite
// number index as selected by the slider
var x = index * SPRITE_WIDTH % spritesheet.width;
var y = Math.floor(index / NB_FRAMES_PER_POSTURE) * SPRITE_HEIGHT;
// Update fields
xField.value = x;
yField.value = y;
// Clear big canvas, draw wireframe rect at x, y, redraw stylesheet
ctx2.clearRect(0, 0, canvasSpriteSheet.width, canvasSpriteSheet.height);
ctx2.drawImage(spritesheet, 0, 0);
drawWireFrameRect(ctx2, x , y, SPRITE_WIDTH, SPRITE_HEIGHT, 'red', 3);
// Draw the current sprite in the small canvas
ctx1.clearRect(0, 0, SPRITE_WIDTH, SPRITE_HEIGHT);
ctx1.drawImage(spritesheet, x, y, SPRITE_WIDTH, SPRITE_HEIGHT,
0, 0, SPRITE_WIDTH, SPRITE_HEIGHT);
// Update output elem on the right of the slider
spriteNumber.innerHTML = index;
};
};
function drawWireFrameRect(ctx, x, y, w, h, color, lineWidth) {
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.strokeRect(x , y, w, h);
ctx.restore();
}
Lines 1-4: characteristics of the sprite sheet. How many rows, i.e., how many sprites per row, etc.
Lines 11-39: initializations that run just after the page has been loaded. We first get the canvas and contexts. Then we set the minimum and maximum values of the slider (an <input type=range>) at lines 31-32, and disable it at line 34 (we cannot slide it before the sprite sheet image has been loaded). We display the current sprite number 0 in the <output> field to the right of the slider (line 35). Finally, in lines 37-39, we load the sprite sheet image.
Lines 42-58: this callback is run once the sprite sheet image has been loaded. We enable the slider, set the big canvas to the size of the loaded image, and then draw it (line 51). We also draw the first sprite from the sprite sheet in the small canvas and draw a red wireframe rectangle around the first sprite in the sprite sheet (lines 52-58).
Lines 61-87: the input listener callback, called each time the slider moves. Lines 65-68 are the most important ones here: we compute the x and y position of the sprite selected with the slider. We take into account the number of sprites per posture, the number of rows, and the dimensions of each sprite. Then, as in the previous step, we draw the current sprite in the small canvas and highlight the current sprite with a red rectangle in the sprite sheet.
The code is generic enough to work with different kinds of sprite sheets. Adjust the global parameters in bold at lines 1-5 and try the extractor.
This is the same application with another sprite sheet.
We just changed these parameter values: try the same code but with another sprite sheet (the one with the robot) - see on JSBin.
Now it's time to see how we can make a small sprite animation framework!