使用 JavaScript 制作 Sprite Sheet 动画
让我们看看如何使用 JavaScript 在 HTML5 画布上为精灵表制作动画。
一些设置
首先,让我们创建画布元素。
<canvas width="300" height="200"></canvas>
添加边框(以便我们可以看到可用区域)。
canvas {
border: 1px solid black;
}
并加载精灵表 ( https://opengameart.org/content/green-cap-character-16x18 )。趁着这个机会,我们来访问画布及其 2D 上下文。
let img = new Image();
img.src = 'https://opengameart.org/sites/default/files/Green-Cap-Character-16x18.png';
img.onload = function() {
init();
};
let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
function init() {
// future animation code goes here
}
该init
函数在图像加载后通过 调用img.onload
。这是为了确保图像在我们尝试处理之前已加载完毕。所有动画代码都将放在该init
函数中。就本教程而言,这样做是可行的。如果我们要处理多张图片,我们可能需要使用 Promises 等待所有图片加载完毕后再进行任何操作。
精灵表
现在我们已经设置好了,让我们看一下图像。
每一行代表一个动画周期。第一行(顶部)表示角色向下行走,第二行表示向上行走,第三行表示向左行走,第四行(底部)表示向右行走。严格来说,左侧一列表示站立(无动画),中间和右侧一列表示动画帧。不过,我觉得我们可以同时使用这三列动画来获得更流畅的行走动画。😊
Context 的drawImage
方法
在开始为图像制作动画之前,让我们先了解一下drawImage
context 方法,因为我们将使用该方法自动切分精灵表并将其应用到画布上。
哇,这个方法里参数好多啊!尤其是第三种形式,也就是我们接下来要用到的。别担心,其实没那么难。它有逻辑分组。
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
参数image
是源图像。接下来的四个(sx
、sy
、sWidth
和sHeight
)与源图像(精灵表)相关。最后四个(dx
、dy
、dWidth
和dHeight
)与目标(画布)相关。
“x” 和 “y” 参数(sx
, sy
, dx
, dy
)分别与精灵表(源)和画布(目标)的起始位置相关。它本质上是一个网格,左上角从 (0, 0) 开始,向右下方正方向移动。换句话说,(50, 30) 表示向右移动 50 像素,向下移动 30 像素。
“宽度”和“高度”参数(sWidth
、sHeight
、dWidth
和dHeight
)分别表示精灵表和画布的宽度和高度,分别从它们各自的“x”和“y”位置开始。我们将其分解成一个部分,比如源图像。如果源参数(sx
、sy
、sWidth
、sHeight
)为 (10, 15, 20, 30),则起始位置(网格坐标系)为 (10, 15),并拉伸至 (30, 45)。然后,结束坐标的计算公式为 ( sx
+ sWidth
、sy
+ sHeight
)。
绘制第一帧
现在我们已经了解了该drawImage
方法,让我们实际看看它的实际效果。
我们的精灵表的角色帧大小在文件名 ( 16x18
) 中很方便地标记出来,这样我们就有了宽度和高度属性。第一帧将从 (0, 0) 开始,到 (16, 18) 结束。让我们把它绘制到画布上。我们将从画布上的 (0, 0) 开始绘制这一帧,并保持比例不变。
function init() {
ctx.drawImage(img, 0, 0, 16, 18, 0, 0, 16, 18);
}
我们现在有了第一帧!不过它有点小。我们把它放大一点,这样更容易看清。
将上面的内容改为:
const scale = 2;
function init() {
ctx.drawImage(img, 0, 0, 16, 18, 0, 0, 16 * scale, 18 * scale);
}
您应该会看到画布上绘制的图像在水平和垂直方向上都放大了一倍。通过更改dWidth
和 的dHeight
值,我们可以将原始图像在画布上缩放。不过,操作时要小心,因为处理的是像素,它很快就会变得模糊。尝试更改 的scale
值,看看输出如何变化。
下一帧
要绘制第二帧,我们唯一需要做的就是更改源集的某些值。具体来说,sx
和sy
。每个帧的宽度和高度相同,所以我们永远不需要更改这些值。实际上,让我们提取这些值,创建几个缩放值,然后在当前帧的右侧绘制接下来的两个帧。
const scale = 2;
const width = 16;
const height = 18;
const scaledWidth = scale * width;
const scaledHeight = scale * height;
function init() {
ctx.drawImage(img, 0, 0, width, height, 0, 0, scaledWidth, scaledHeight);
ctx.drawImage(img, width, 0, width, height, scaledWidth, 0, scaledWidth, scaledHeight);
ctx.drawImage(img, width * 2, 0, width, height, scaledWidth * 2, 0, scaledWidth, scaledHeight);
}
现在看起来是这样的:
现在,我们得到了精灵表的整个顶行,但位于三个独立的帧中。如果你查看调用ctx.drawImage
,会发现只有 4 个值发生了变化—— sx
、sy
、dx
和dy
。
让我们稍微简化一下。既然如此,我们就使用精灵表中的帧编号,而不是处理像素。
将所有ctx.drawImage
调用替换为:
function drawFrame(frameX, frameY, canvasX, canvasY) {
ctx.drawImage(img,
frameX * width, frameY * height, width, height,
canvasX, canvasY, scaledWidth, scaledHeight);
}
function init() {
drawFrame(0, 0, 0, 0);
drawFrame(1, 0, scaledWidth, 0);
drawFrame(0, 0, scaledWidth * 2, 0);
drawFrame(2, 0, scaledWidth * 3, 0);
}
我们的drawFrame
函数处理精灵表数学,所以我们只需要传入帧号(从 0 开始,像一个数组,所以“x”帧是 0、1 和 2)。
画布的“x”和“y”值仍然采用像素值,以便我们更好地控制角色的位置。scaledWidth
在函数内部移动乘数(即scaledWidth * canvasX
)意味着所有内容都会一次移动/改变整个缩放角色的宽度。如果角色每帧移动 4 或 5 个像素,那么这对于行走动画就不起作用。所以我们保持原样。
调用列表中还多了一行drawFrame
。这行代码是为了展示动画循环的样子,而不是仅仅绘制精灵图的前三帧。动画循环不再重复“左步,右步”,而是重复“站立,左步,站立,右步”——这是一个稍微好一点的动画循环。不过两种方法都可以——80 年代的许多游戏都使用了两步动画。
这是我们目前的情况:
让我们为这个角色制作动画!
现在我们可以开始为角色添加动画了!我们来看看MDNrequestAnimationFrame
文档。
这就是我们用来创建循环的方法。我们也可以使用setInterval
,但它requestAnimationFrame
已经有一些很好的优化,比如以每秒 60 帧(或尽可能接近)的速度运行,并在浏览器/标签失去焦点时停止动画循环。
本质上,这requestAnimationFrame
是一个递归函数——为了创建动画循环,我们将requestAnimationFrame
再次调用作为参数传递的函数。像这样:
window.requestAnimationFrame(step);
function step() {
// do something
window.requestAnimationFrame(step);
}
walk
函数开始循环之前单独调用,然后在循环内部不断调用。
在开始使用它之前,我们需要了解并使用另一个上下文方法 - clearRect
(MDN 文档)。在画布上绘制时,如果我们反复drawFrame
在同一个位置调用该方法,它将一直绘制在已有内容之上。为了简单起见,我们将在每次绘制之间清除整个画布,而不仅仅是我们绘制的区域。
因此,我们的绘制循环看起来会像清除、绘制第一帧、清除、绘制第二帧等等。
换句话说:
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawFrame(0, 0, 0, 0);
// repeat for each frame
好了,让我们来给这个角色制作动画吧!我们先创建一个循环数组 (0, 1, 0, 2),并记录当前循环的进度。然后,创建一个step
函数,作为主动画循环。
步进函数清除画布,绘制框架,前进(或重置)我们在循环循环中的位置,然后通过调用自身requestAnimationFrame
。
const cycleLoop = [0, 1, 0, 2];
let currentLoopIndex = 0;
function step() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawFrame(cycleLoop[currentLoopIndex], 0, 0, 0);
currentLoopIndex++;
if (currentLoopIndex >= cycleLoop.length) {
currentLoopIndex = 0;
}
window.requestAnimationFrame(step);
}
为了开始动画,让我们更新该init
函数。
function init() {
window.requestAnimationFrame(step);
}
那个角色进展很快!😂
放慢速度!
看起来我们的角色有点失控了。如果浏览器允许,角色将以每秒 60 帧的速度绘制,或者尽可能接近这个速度。我们来限制一下,让它每 15 帧绘制一次。我们需要跟踪当前处于哪一帧。然后,在step
函数中,每次调用时都会增加计数器,但仅在 15 帧后绘制。15 帧过后,重置计数器,然后绘制该帧。
const cycleLoop = [0, 1, 0, 2];
let currentLoopIndex = 0;
let frameCount = 0;
function step() {
frameCount++;
if (frameCount < 15) {
window.requestAnimationFrame(step);
return;
}
frameCount = 0;
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawFrame(cycleLoop[currentLoopIndex], 0, 0, 0);
currentLoopIndex++;
if (currentLoopIndex >= cycleLoop.length) {
currentLoopIndex = 0;
}
window.requestAnimationFrame(step);
}
好多了!
其他方向
到目前为止,我们只处理了向下的方向。我们稍微修改一下动画,让角色在每个方向上完成完整的4步循环怎么样?
记住,在我们的代码中,“向下”的帧位于第 0 行(精灵表的第一行),向上是第 1 行,向左是第 2 行,向右是第 3 行(精灵表的底行)。循环保持每行 0、1、0、2 的顺序。由于我们已经处理了循环变化,因此唯一需要更改的是行号,它是drawFrame
函数的第二个参数。
我们将添加一个变量来跟踪当前的方向。为了简单起见,我们将按照精灵表的顺序(下、上、左、右)进行,这样它就是连续的(0、1、2、3,重复)。
当循环重置时,我们将转向下一个方向。一旦我们完成了每个方向,我们就会重新开始。因此,我们更新后的step
函数和相关变量如下所示:
const cycleLoop = [0, 1, 0, 2];
let currentLoopIndex = 0;
let frameCount = 0;
let currentDirection = 0;
function step() {
frameCount++;
if (frameCount < 15) {
window.requestAnimationFrame(step);
return;
}
frameCount = 0;
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawFrame(cycleLoop[currentLoopIndex], currentDirection, 0, 0);
currentLoopIndex++;
if (currentLoopIndex >= cycleLoop.length) {
currentLoopIndex = 0;
currentDirection++; // Next row/direction in the sprite sheet
}
// Reset to the "down" direction once we've run through them all
if (currentDirection >= 4) {
currentDirection = 0;
}
window.requestAnimationFrame(step);
}
就这样!我们的角色可以向四个方向行走,所有动画都来自一张图片。
文章来源:https://dev.to/martyhimmel/animating-sprite-sheets-with-javascript-ag3