使用JavaScript制作动画精灵表

原始地址:https://dev.to/martyhimmel/animating-sprite-sheets-with-javascript-ag3

让我们来看一下如何使用JavaScript在HTML5画布上实现精灵表动画。

A Little Setup
首先,让我们创建画布元素。

添加一个边框(这样我们就可以看到可用区域)。
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() {
// 未来的动画代码放在这里
}
init函数在图像加载完成后,通过img.onload调用。这样可以确保在尝试与图像交互之前图像已加载完毕。所有的动画代码将放在init函数中。对于本教程来说,这样做是可以的。如果我们处理多个图像,我们可能会希望使用Promises来等待它们全部加载完成再执行任何操作。

The Spritesheet
现在我们已经设置好了,让我们来看一下这个图片。
每一行代表一个动画循环。第一行(顶部)是角色向下走,第二行是向上走,第三行是向左走,第四行(底部)是向右走。从技术上讲,左列是静止的(没有动画),中间和右列是动画帧。不过我认为我们可以将所有三个列用于更平滑的行走动画。😊

Context’s drawImage Method
在我们开始为图像创建动画之前,让我们先来看一下drawImage 上下文方法,因为这是我们将用于自动切割精灵表并将其应用到画布上的方法。
哇,这个方法有很多参数!特别是第三种形式,这正是我们将使用的。别担心,它并没有看起来那么糟糕。它有一个逻辑分组。
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像素。
“Width”和“Height”参数(sWidth, sHeight, dWidth和dHeight)指的是精灵表和画布的宽度和高度,从它们各自的“x”和“y”位置开始。让我们把它分解成一个部分,比如源图像。如果源参数(sx, sy, sWidth, sHeight)是(10, 15, 20, 30),那么网格坐标的起始位置将是(10, 15),结束位置将是(30, 45)。那么结束坐标计算公式为(sx + sWidth, sy + sHeight)。

Drawing The First Frame
现在,我们已经了解了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的值,看看输出如何改变。

Next Frames
要绘制第二个帧,我们只需要更改一些源集的值。具体来说,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个像素,那样就行不通了。所以我们保持原样。在ctx.drawImage调用列表中还有一行额外的代码。这是为了显示我们的动画循环将会是什么样子,而不仅仅是绘制精灵表的前三个帧。相比之下,它将重复“站立,左移,站立,右移” - 这是一个稍微更好的动画循环。不过任何一个都可以 - 80年代的许多游戏都使用了两个步骤的动画。
现在我们目前的进展如下:
Let’s Animate This Character!
现在我们可以开始为角色创建动画了!让我们来看看MDN文档中的requestAnimationFrame。
这是我们将用来创建循环的方法。我们也可以使用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);
// 为每一帧重复上述步骤
好的,让我们给这个角色加上动画吧!让我们为循环循环(0, 1, 0, 2)创建一个数组,并创建一个变量来跟踪在该循环中的位置。然后,我们将创建一个step函数,它将充当主要的动画循环。
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);
}
这个角色正在迅速前进!😂
Slow Down There!
看起来我们的角色有点失控。如果浏览器允许,角色将以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);
}
好多了!
The Other Directions
到目前为止,我们只处理了向下的方向。我们如何稍微修改一下动画,使角色在每个方向都完成一个完整的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++; // 下一个行/方向在精灵表中
}
// 当我们运行完所有方向时,重置为“下”方向
if (currentDirection >= 4) {
currentDirection = 0;
}
window.requestAnimationFrame(step);
}
我们完成了!我们的角色现在在所有四个方向上行走,这一切都来自于单个图像。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值