本文正在參加星光計劃3.0–夏日挑戰賽
#夏日挑戰賽#【FFH】canvas幀動畫及封裝(OpenHarmony JS UI)
Demo展示
這裡以Tom貓(多年前熱門的移動端互動小遊戲)為例:

實作思路
首先要了解幀動畫播放的原理——正如我們平時看電視看視訊,視訊通過每一幀圖檔按順序快速切換來産生“動”起來的效果。
是以可以通過canvas元件提供的drawImage加定時器的方法來實作快速繪幀、渲染的效果。
代碼封裝
(這裡我封裝在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
*/
動畫播放
播放動畫大緻能分成三步:
- 擷取畫布及待播放動畫資訊
- 動畫預加載
-
動畫播放
這裡選擇以下方式儲存動畫對象資訊:
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'
},
]
通過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,可以看到渲染頁面的代碼量很少。