canvas精灵图动画-基于Vue3

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 偏移宽高

今天,我们实现如下图一个精灵动画,通过选择不同的动画类型,实现动画切换
canvas精灵图动画-基于Vue3_第1张图片

精灵图的实现原理,是通过不断调整偏移坐标来实现。
首先我们需要确定单独一个图形的宽高,如下图
canvas精灵图动画-基于Vue3_第2张图片
这张精灵图分辨率为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;
}

你可能感兴趣的:(canva可画)