canvas精灵图动画
一.概念了解
1.canvas元素
是一个可以使用脚本 (通常为JavaScript) 来绘制图形的 HTML 元素。例如,它可以用于绘制图表、制作图片构图或者制作简单的动画。
2.什么是精灵图
精灵图就是图片拼合技术,它就是把多张小图合成一张大图,通过css中的background-position属性,显示精灵图中某一个小图标。
例如下图:
二.canvas绘制图片
1.首先需要准备一个canvas标签并指定id
<canvas id="canvas1"></canvas>
2.接下来使用js脚本获取到canvas元素,并指定其画布大小,以及需要绘制的图片
const canvas = document.getElementById('canvas1')
//HTMLCanvasElement.getContext() 方法返回canvas 的上下文,如果上下文没有定义则返回 null
const ctx = canvas.getContext('2d')
//定义常量记录画布大小并赋值给canvas画布
const CANVAS_WIDTH = canvas.width = 600
const CANVAS_HEIGHT = canvas.height = 600
//创建img元素,并传入图片路径
const playerImage = new Image()
playerImage.src = '路径.png'
3.使用PS获取每个精灵的大小,并保存为常量
//要绘制的精灵的大小根据实际情况量取
const spriteWidth = 575
const spriteHeight = 523
4.绘制精灵图到canvas
4.1 drawImage方法的使用
Canvas 2D API 中的 CanvasRenderingContext2D.drawImage() 方法提供了多种在画布(Canvas)上绘制图像的方式
sx 可选 :
需要绘制到目标上下文中的,image 的矩形(裁剪)选择框的左上角 X 轴坐标。
sy 可选:
需要绘制到目标上下文中的,image 的矩形(裁剪)选择框的左上角 Y 轴坐标。
sWidth 可选:
需要绘制到目标上下文中的,image 的矩形(裁剪)选择框的宽度。
sHeight 可选:
需要绘制到目标上下文中的,image的矩形(裁剪)选择框的高度。使用负值将翻转这个图像。
dx:
image 的左上角在目标画布上 X 轴坐标。
dy:
image 的左上角在目标画布上 Y 轴坐标。
image 在目标画布上绘制的宽度。
image 在目标画布上绘制的高度。
//下面这个方法就会绘制精灵图左上角的第一个精灵
ctx.drawImage(playerImage, 0, 0, spriteWidth, spriteHeight, 0, 0, spriteWidth, spriteHeight)
4.2 Img的onload方法
playerImage.onload = function () {
// 执行 drawImage 语句
// img的加载是异步的,所以要在img加载后就立即使用drawImage方法的话要在onload函数中使用,等待img加载完毕
ctx.drawImage(playerImage, 0, 0, spriteWidth, spriteHeight, 0, 0, spriteWidth, spriteHeight)
}
结果:
三.canvas+精灵图做动画
1.简单介绍requestAnimationFrame函数
requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
2.动画绘制含税
//首先定义一个animate函数,里面放入drawImage绘制方法,并使用requestAnimationFrame调用自身,实现循环绘制
function animate() {
//clearRect方法可以清空指定范围的画布
//每次调用绘制方法前先清空画布
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
ctx.drawImage(playerImage, 0, 0, spriteWidth, spriteHeight, 0, 0, spriteWidth, spriteHeight)
requestAnimationFrame(animate)
}
animate()
3.动画参数的准备
在animate函数中我们定义了画布的绘制方法,但是现在drawImage中的参数并不会改变所以永远绘制的左上角第一个精灵,所以接下来我们需要定义几个参数,确定每一组精灵动画的x,y值实现动画效果
3.1新增下拉框选择动画类型
html中:
<!--
示例精灵图中一共有10组不同的动作,每一组为一个动画,所以我们在这里定义一个下拉框,有10个选项
-->
<div class="controls">
<label for="animations">选择动画</label>
<select name="animations" id="animations">
<option value="idle">idle</option>
<option value="jump">jump</option>
<option value="fall">fall</option>
<option value="run">run</option>
<option value="dizzy">dizzy</option>
<option value="sit">sit</option>
<option value="o">o</option>
<option value="more">more</option>
<option value="all">all</option>
<option value="over">over</option>
</select>
</div>
js中:
//在js脚本中定义下拉框默认值为'idle'
let playerState = 'idle'
//拿到select元素
const dropdown = document.getElementById('animations')
//监听change事件,当下拉框中的值改变时,把新值赋值给playerState
dropdown.addEventListener('change', (e) => {
playerState = e.target.value
})
3.2记录精灵图中每组动画中每个精灵的位置
let spriteAnimations = {}
//每一组数据中name为下拉框中对应的动画类型, frames为精灵图中对应行中精灵的个数
let animationStates = [
{
name: 'idle',
frames:7
},
{
name: 'jump',
frames:7
},
{
name: 'fall',
frames:7
},
{
name: 'run',
frames:9
},
{
name: 'dizzy',
frames:11
},
{
name: 'sit',
frames:5
},
{
name: 'o',
frames:7
},
{
name: 'more',
frames:7
},
{
name: 'all',
frames:12
},
{
name: 'over',
frames:4
},
]
//封装统一处理函数
animationStates.forEach((item, index) => {
let frames = {
loc : []
}
//遍历frames计算每一行中的每个精灵的xy值
for (let i = 0; i < item.frames; i++){
frames.loc.push({
x: i * spriteWidth,
y: index * spriteHeight,
})
}
spriteAnimations[item.name] = frames
})
3.3定义渲染位置参数
//动画开始时的精灵索引
let gameFrame = 0
//限制动画加载帧数
const staggerFrames = 4
//下面这段代码放入animate循环执行num的值分别为: 0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,每个数字四次(次数根据staggerFrames决定)循环依次递增
//同理position的值根据num值的递增也会依次变大最大值为当前动画类型的动作数,
//0%7=0,...6%7=6,7%7=0 position会在范围内按顺序循环
function animate() {
let num = Math.floor(gameFrame / staggerFrames)
let position = num % spriteAnimations[playerState].loc.length
gameFrame++
requestAnimationFrame(animate)
}
animate()
3.4在animate中绘制动画
function animate() {
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
let num = Math.floor(gameFrame / staggerFrames)
let position = num % spriteAnimations[playerState].loc.length
//此时frameX的去值会根据position每四次循环递增取数组中下一个值实现精灵动作切换
let frameX = spriteAnimations[playerState].loc[position].x
//frameY的值只有在playerState(下拉框的值)变化时会发生变化
let frameY = spriteAnimations[playerState].loc[position].y
ctx.drawImage(playerImage, frameX, frameY, spriteWidth, spriteHeight, 0, 0, spriteWidth, spriteHeight)
gameFrame++
requestAnimationFrame(animate)
}
animate()
最终效果:
完整代码:
html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<canvas id="canvas1"></canvas>
<div class="controls">
<label for="animations">选择动画</label>
<select name="animations" id="animations">
<option value="idle">idle</option>
<option value="jump">jump</option>
<option value="fall">fall</option>
<option value="run">run</option>
<option value="dizzy">dizzy</option>
<option value="sit">sit</option>
<option value="o">o</option>
<option value="more">more</option>
<option value="all">all</option>
<option value="over">over</option>
</select>
</div>
<script src="index.js"></script>
</body>
</html>
css:
/* index.css */
#canvas1 {
border: 5px solid black;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 600px;
height: 600px;
}
.controls {
position: absolute;
z-index: 10;
top: 10px;
left: 50%;
transform: translateX(-50%);
}
.controls, select, option {
font-size: 25px;
}
js:
//index.js
let playerState = 'idle'
const dropdown = document.getElementById('animations')
dropdown.addEventListener('change', (e) => {
playerState = e.target.value
})
const canvas = document.getElementById('canvas1')
const ctx = canvas.getContext('2d')
const CANVAS_WIDTH = canvas.width = 600
const CANVAS_HEIGHT = canvas.height = 600
const playerImage = new Image()
playerImage.src = '路径.png'
const spriteWidth = 575
const spriteHeight = 523
let gameFrame = 0
const staggerFrames = 4
let spriteAnimations = {}
let animationStates = [
{
name: 'idle',
frames:7
},
{
name: 'jump',
frames:7
},
{
name: 'fall',
frames:7
},
{
name: 'run',
frames:9
},
{
name: 'dizzy',
frames:11
},
{
name: 'sit',
frames:5
},
{
name: 'o',
frames:7
},
{
name: 'more',
frames:7
},
{
name: 'all',
frames:12
},
{
name: 'over',
frames:4
},
]
animationStates.forEach((item, index) => {
let frames = {
loc : []
}
for (let i = 0; i < item.frames; i++){
frames.loc.push({
x: i * spriteWidth,
y: index * spriteHeight,
})
}
spriteAnimations[item.name] = frames
})
function animate() {
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
let position = Math.floor(gameFrame / staggerFrames) % spriteAnimations[playerState].loc.length
let frameX = spriteAnimations[playerState].loc[position].x
let frameY = spriteAnimations[playerState].loc[position].y
ctx.drawImage(playerImage, frameX, frameY, spriteWidth, spriteHeight, 0, 0, spriteWidth, spriteHeight)
gameFrame++
requestAnimationFrame(animate)
}
animate()