canvas精灵图动画

214 阅读6分钟

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 轴坐标。

dWidth:

image 在目标画布上绘制的宽度。

dHeight:

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()