drawImage 讲解
绘制图片
在Canvas中,我们使用drawImage()方法绘制图片。drawImage()方法有如下3种调用方式:
1.drawImage(image, dx, dy)
-- 参数image表示页面中的图片。
-- 参数dx表示图片左上角的横坐标;
-- 参数dy表示图片左上角的纵坐标;
2.drawImage(image, dx, dy, dw, dh)
-- 前三参数同上
-- 参数dw为图片宽度;
-- 参数dh为图片高度。
3.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
-- 参数image, dx, dy, dw, dh 同上
-- sx, sy 偏移坐标
-- sw, sh 偏移宽高
今天,我们实现如下图一个精灵动画,通过选择不同的动画类型,实现动画切换
精灵图的实现原理,是通过不断调整偏移坐标来实现。
首先我们需要确定单独一个图形的宽高,如下图
这张精灵图分辨率为6876 × 5230,而横向最多有12个图像,纵向最多有10个图像,所以单个图像的分辨率为(6876 / 12 =)573 × (5230 / 10 = =)523,所以就可以定义如下变量
const spiritWidth = 573;
const spiritHeight = 523;
初始化画布和加载图片
async function init() {
canvas.value.height = w_h;
canvas.value.width = w_h;
ctx.value = canvas.value.getContext('2d');
return new Promise(resolve => {
image.src = imgSpirit;
image.onload = function () {
resolve('success');
}
})
}
这里是通过Vue方式获取的canvas dom,如果通过纯js,如下
let canvas = document.getElementById('canvas')
或
let canvas = document.querySelector('#canvas')
Image加载是一个异步操作过程,通过Promise封装,再结合async/await将后续逻辑同步化。
动画实现方法
function draw() {
ctx.value.clearRect(0, 0, w_h, w_h);
ctx.value.drawImage(image, offsetX * spiritWidth, offsetY * spiritHeight, spiritWidth, spiritHeight
, 0, 0, spiritWidth, spiritHeight);
if (offsetX < 6) {
offsetX++;
} else {
offsetX = 0;
}
requestAnimationFrame(draw);
}
通过不断累加offsetX偏移量,不断切换图片位置。这事就实现了一个简单的动画效果。但是我们在想一下,可不可以在每一帧切换的之间加一点延迟,或者说是交替针,所以就了如下代码
if (delayAccrual % delay.value === 0) {
if (offsetX < 6) {
offsetX++;
} else {
offsetX = 0;
}
}
delayAccrual++;
通过取余的形式,控制offsetX累加的情况。但是发现这太简单,那我们就一种思路。就有如下代码
let position = Math.floor(delayAccrual / delay.value) % 6;
offsetX = position * spiritWidth;
ctx.value.drawImage(image, offsetX, offsetY * spiritHeight, spiritWidth, spiritHeight
, 0, 0, spiritWidth, spiritHeight);
delayAccrual++;
Math.floor(delayAccrual / delay.value) % 7 得到的是0-6这7个数,而一行精灵图也是从0开始的。
任意一个数字 % 一个数字(可以认为x) 得到的数据规律是 0 - (x - 1)
整体数据
但是这张精灵图是12 × 10,现在我们是写死的,下面我们就做成开头所说的那样。整备数据如下。
data.ts
interface list {
key: string,
name: string,
spirits: number
}
export const spiritList: Array<list> = [
{
key: 'idle',
name: '空闲',
spirits: 7
},
{
key: 'jump',
name: '跳动',
spirits: 7
},
{
key: 'fall',
name: '落下',
spirits: 7
},
{
key: 'run',
name: '跑',
spirits: 9
},
{
key: 'dizzy',
name: '晕眩',
spirits: 11
},
{
key: 'sit',
name: '坐',
spirits: 5
},
{
key: 'roll',
name: '滚动',
spirits: 7
},
{
key: 'bite',
name: '咬',
spirits: 7
},
{
key: 'ko',
name: '击倒',
spirits: 12
},
{
key: 'get hit',
name: '被击中',
spirits: 4
}
]
我们通过下拉框选择切换动画,拿到的数据是key数据,现在数据是一个数组,通过find或者filter有点麻烦,那我们就把数据格式化成Map形式,key就是数组中 key,这就有了如下方法
function getSpiritObj() {
spiritSelector.value.forEach(({key, spirits}, index) => {
spiritMap.set(key, {
spirits,
y: index
})
})
}
因为数组的顺序就是按照图片顺序构造的,所以index就是y坐标。
完整代码
HTML
<template>
<el-container class="frame">
<el-aside width="400px" class="aside">
<el-form class="form" label-width="80px">
<el-form-item label="动画类型">
<el-select v-model="animateType" style="width: 100%" placeholder="请选择" size="middle">
<el-option
v-for="item in spiritSelector"
:key="item.key"
:label="item.name"
:value="item.key"
/>
</el-select>
</el-form-item>
<el-form-item label="延迟">
<el-input-number v-model="delay" controls-position="right" :min="0" style="width: 100%" />
</el-form-item>
</el-form>
</el-aside>
<el-main class="main">
<div class="spirit">
<canvas ref="canvas" class="canvas"></canvas>
</div>
</el-main>
</el-container>
</template>
JS
import {onMounted, ref} from "vue";
import {spiritList} from "@/views/spirit/data";
const imgSpirit = require('../../assets/spirit/spirit1.png')
let canvas = ref(null);
let ctx = ref(null);
let w_h = 600;
const spiritWidth = 573;
const spiritHeight = 523;
const image = new Image();
let offsetX = 2;
let offsetY = 0;
let delayAccrual = 0;
let delay = ref(5);
const spiritSelector = ref(spiritList);
let animateType = ref('idle');
let spiritMap = new Map();
async function init() {
canvas.value.height = w_h;
canvas.value.width = w_h;
ctx.value = canvas.value.getContext('2d');
getSpiritObj();
return new Promise(resolve => {
image.src = imgSpirit;
image.onload = function () {
resolve('success');
}
})
}
function getSpiritObj() {
spiritSelector.value.forEach(({key, spirits}, index) => {
spiritMap.set(key, {
spirits,
y: index
})
})
}
function draw() {
let currentSpirit = spiritMap.get(animateType.value);
if (!currentSpirit) {
return;
}
ctx.value.clearRect(0, 0, w_h, w_h);
let position = Math.floor(delayAccrual / delay.value) % currentSpirit.spirits;
offsetX = position * spiritWidth;
offsetY = currentSpirit.y * spiritHeight
ctx.value.drawImage(image, offsetX, offsetY, spiritWidth, spiritHeight
, 0, 0, spiritWidth, spiritHeight);
delayAccrual++;
requestAnimationFrame(draw);
}
onMounted(async () => {
await init();
draw();
})
CSS
.frame {
height: calc(100vh);
}
.aside {
background-color: rgb(217, 235, 255);
}
.main {
background-color: rgb(236, 245, 255);
}
.spirit {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.form {
padding: 10px;
}
.canvas {
border: 2px solid #343a42;
}