Demo展示

这里以Tom猫(多年前热门的移动端互动小游戏)为例:

【FFH】canvas组件播放帧动画及封装(OpenHarmony JS UI)_封装

实现思路

首先要了解帧动画播放的原理——正如我们平时看电视看视频,视频通过每一帧图片按顺序快速切换来产生“动”起来的效果。

因此可以通过canvas组件提供的drawImage加定时器的方法来实现快速绘帧、渲染的效果。


代码封装

【FFH】canvas组件播放帧动画及封装(OpenHarmony JS UI)_帧动画_02

(这里我封装在model的cv.js)

当项目中有很多动画播放的地方,代码需要复用,就要把播放动画的代码封装起来,以便提供给其它页面或组件调用,减少冗余代码量。


canvas绘制图像

首先先来看一下canvas如何绘制对象,以在页面onShow生命周期上绘制初始画面为例:

<canvas class="canvas" ref="canvas_1" style="width : 640px; height : 640px"></canvas>
onShow(e) { //---显示初始画面
let e1e = this.$refs.canvas_1;
let ctx = e1e.getContext('2d');
let img_1 = new Image();
img_1.src = 'common/images/eat/eat_01.jpg';
ctx.drawImage(img_1, 0, 0, 640, 640); //---这里绘制了一个刚好覆盖画布的图像(640px; 640px)
},

因为绘制的是2d图像,则先用let e1e = this.$refs.canvas_1获取画布。let ctx = e1e.getContext('2d')设置为'2d',返回值为2D绘制对象,该对象可用于在画布组件上绘制矩形、文本、图片等。注意getContext不支持在onInit和onReady生命周期中进行调用。

下面是官方给出的aip文档:

getContext(type: "2d", options?: ContextAttrOptions): CanvasRenderingContext2D;
/**
* Obtains the context of webgl canvas drawing.
* Only parameters related to webgl canvas drawing are supported.
* The return value is a webgl drawing object that provides specific webgl drawing operations.
* @param type identifier defining the drawing context associated to the canvas.
* @param options use this context attributes to creating rendering context.
* @since 6
*/

drawImage(image: Image, dx: number, dy: number, dWidth: number, dHeight: number): void;
/**
* Draws an image.
* @param image Image resource.
* @param sx X-coordinate of the upper left corner of the rectangle used to crop the source image.---用于裁剪源图像的矩形左上角的 X 坐标
* @param sy Y-coordinate of the upper left corner of the rectangle used to crop the source image.
* @param sWidth Target width of the image to crop.
* @param sHeight Target height of the image to crop.
* @param dx X-coordinate of the upper left corner of the drawing area on the canvas.---相对于画布左上角的X坐标
* @param dy Y-coordinate of the upper left corner of the drawing area on the canvas.---相对于画布左上角的Y坐标
* @param dWidth Width of the drawing area.---绘制宽度
* @param dHeight Height of the drawing area.---绘制高度
* @since 4
*/


动画播放

播放动画大致能分成三步:

  1. 获取画布及待播放动画信息
  2. 动画预加载
  3. 动画播放

这里选择以下方式储存动画对象信息:

let srcS = [ //---储存动画信息---这里存了两种动画信息eat和knock
{
id: "eat", //---ID
src: "common/images/eat/eat_", //---路径前段
len: 40, //---图片数
setInt: 90, //---定时器间隔(ms)---根据帧数自行调整
width: 640, //---绘制宽度
height: 1024, //---绘制高度
x: 0, //---相对于画布左上角的X坐标
y: 0, //---相对于画布左上角的Y坐标
format: '.jpg'
},
{
id: "knock",
src: "common/images/knockOut/knockout_",
len: 80,
setInt: 90,
width: 640,
height: 1024,
x: 0,
y: 0,
format: '.jpg'
},
]

【FFH】canvas组件播放帧动画及封装(OpenHarmony JS UI)_帧动画_03

通过id匹配动画信息:

function selectInfo(id, cb) { //---找到对应的动画对象
let obj = srcS.find(e => e.id === id);
cb(obj);
}

匹配后进行预加载:

function imgLoad(obj, cb) { //----预加载
let imgArray = []; //---存储Image对象
let len = obj.len;
for (let i = 0; i < len; i++) {
let j = '0';
if (i > 9) j = '';
let str = j + i.toString();
let img = new Image();
img.src = obj.src + str + obj.format; //---设置Image对象路径,假设这里图片的路径编号是00,01,02...10...39
imgArray.push(img);
}
cb(imgArray, len, obj.x, obj.y, obj.width, obj.height, obj.setInt); //---回调函数中传递参数
}

传入加载好的数组进行动画播放:

function Action(obj, ctx) { //---动画播放
return new Promise((resolve) => { //---采用Promise进行动画播放,执行完再释放线程,避免多次点击播放造成混乱
let i = 0, x, y, w, h;
let len, imgArray, interval;
imgLoad(obj, (imgArray_, len_, x_, y_, w_, h_, interval_) => {
console.info("预加载完毕");
imgArray = imgArray_; //---设置drawImage参数
len = len_;
x = x_;
y = y_;
w = w_;
h = h_;
interval = interval_;

Iv[count++] = setInterval(() => { //---定时器
if (i < len) {
if (i !== 0)ctx.clearRect(x, y, w, h); //---不断绘制新图-清除旧图
ctx.drawImage(imgArray[i], x, y, w, h);
++i;
} else {
ctx.drawImage(imgArray[len - 1], x, y, w, h); //---可选择保留结尾动作
clearInterval(Iv[count - 1]); //---清除定时器,动画结束
resolve("false");
}
}, interval);
})
})
}

注意(drawImage)每绘制一次需要手动清除(clearRect)上一张的图片,不然上一张图片会存留在页面中。

对外提供的接口:

export function ActionReady(id, ctx) { //---接口---参数:(动作id,画布2d对象)
let obj;
selectInfo(id, (obj_) => {
obj = obj_;
})
return Action(obj, ctx);
}

代码调用

本例子展示的是Tom猫的两种动作:eat和knock,通过点击不同部位按钮的方式完成交互。

这里获取动作id的方法是通过按钮(在CSS中设置成透明)点击后获取按钮元素属性id,也可以用其它方式如 屏幕坐标判断 等方式自行定义id。

<div class="container" on:click="null" grab:doubleclick.capture="allTouchStart">
<stack>
<canvas class="canvas" ref="canvas_1" style="width : {{wight}}px; height : {{height}}px"></canvas>
<button id="eat" class="eatBtn" onclick="play" disabled="{{ isDisable }}"></button>
<button id="knock" class="knockOutBtn" onclick="play" disabled="{{ isDisable }}"></button>
</stack>
</div>

导入封装好的cv.js模块和接口

import { ActionReady } from '../../common/model/cv'
import prompt from '@system.prompt';
export default {
data: {
isDisable: false,
offsetY: 0,
offsetX: 0,
wight: 640,
height: 1024
},

onShow(e) { //---显示初始画面
let e1e = this.$refs.canvas_1;
let ctx = e1e.getContext('2d');
this.offsetX = 0;
this.offsetY = 0;
let img_1 = new Image();
img_1.src = 'common/images/eat/eat_01.jpg';
ctx.drawImage(img_1, this.offsetX, this.offsetY, this.wight, this.height);
},

play(e) { //---多个组件调用同一函数,通过id进行区别播放
this.showToast("开始播放");
let id = e.target.id; //---获取动作id
let e1e = this.$refs.canvas_1; //---获取对应的画布
let ctx = e1e.getContext('2d');
let promise = ActionReady(id, ctx); //---调用封装好的获取异步结果
this.isDisable = true; //---动作播放结束前按钮不可点击
promise.then((res) => {
this.isDisable = res; //---动画播放完毕,恢复按钮
this.showToast("播放完毕");
})
},

showToast(mes) {
prompt.showToast({
message: mes,
duration: 2000,
})
}
}
.container {

flex-direction: column;
justify-content: center;
align-items: center;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
}

.eatBtn {
margin-top: 335px;
margin-left: 150px;
width: 350px;
height: 210px;
position: absolute;
opacity: 0;
}

.knockOutBtn{
margin-top: 180px;
margin-left: 130px;
width: 370px;
height: 10%;
position: absolute;
opacity: 0;
}

这样就通过调用封装好的动画播放完成了一个小Demo,可以看到渲染页面的代码量很少。