canvas动画开发

最近在开发H5上的网页挂机游戏,其中有一个需求是,挂机场景中要很多小人在移动,但因为整体还是网页实现的,为了其中这一小块引入一个框架属实没有必要,所以还是决定自己再造一个小轱辘,能走就成

一、全局动画渲染进程

动画的绘制,是使用window.requestAnimationFrame这个方法来实现的,根据文档,这个方法相比setTimeout会更加的丝滑。由于这个方法本身几乎没有调用时间间隔,所以需要人工来实现一下帧率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
window.requestAnimationFrame(mainDraw);
let drawLastTime = 0
const mainDraw = (time) => {
// 间隔是10毫秒 100帧
if (time - drawLastTime < 10) {
window.requestAnimationFrame(mainDraw);
return;
}
// TODO main Animate
for (let i in animateCb) {
animateCb[i] && animateCb[i]()
}
drawLastTime = time;
window.requestAnimationFrame(mainDraw);
}

其中animateCb为动画绘制回调方法集,交由画布对象来实现绘制。另外还暴露两个方法,供添加或删除回调。

1
2
3
4
5
6
7
let animateCb = {}
const addAnimateCb = (key, fun) => {
animateCb[key] = fun
}
const removeAnimateCb = (key) => {
animateCb[key] && delete animateCb[key]
}

二、图片加载及初始化

在启动动画进行之前,先把所有要用到的图片进行缓存。在所有图片加载完之后,启动动画进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const theimgs = [
'images/image1.png',
'images/image2.png',
'images/image3.png',
'images/image4.png',
'images/image5.png',
'images/image6.png',
'images/image7.png',
'images/image8.png',
]

const pageMainAnimate = async () => {
// 缓存图片对象
let imgs1 = []
let idx1 = 0
for (let i in theimgs) {
const img = new Image()
img.src = theimgs[i]
imgs1.push(img)
img.onload = () => {
if (++idx1 >= littleimgs.length) {
window.requestAnimationFrame(mainDraw);
}
}
}
await store.dispatch('system/saveNewImg', {key: 'man', imgs: imgs1})
}

onMounted(() => {
pageMainAnimate()
})

三、画布组件

由于考虑到页面会有多个场景,所以画布采用组件的形式,在初始化时,将自身的绘制方法,写入回调方法集中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import TheMan from '@/modules/Man.js'

const theWidth = 800
const theHeight = 400
const theCanvas = ref()
let theCtx

const { proxy, root } = getCurrentInstance()

let manList = []

const startTimer = () => {
// 初始化30个小人儿
// for (let i = 0; i < 30; i++) {
// manList.push(new TheMan(theWidth, theHeight, true))
// }
root.exposed.addAnimateCb('main', mainTimer)
}

onMounted(() => {
theCtx = proxy.$refs.theCanvas.getContext("2d");
startTimer()
})

onUnmounted(() => {
root.exposed.removeAnimateCb('main' + props.type)
})

在绘画回调中,针对对象的各种属性,进行绘制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const maxMan = 50
let deading = false

let addTimeLast = 0
const addSpace = 50

const mainTimer = () => {
theCtx.clearRect(0, 0, theWidth, theHeight)

// 50帧加一个小人 50 * 100 5秒加一个
if (manList.length <= 0 || (addTimeLast >= addSpace && manList.length <= maxMan)) {
manList.push(new TheMan(theWidth, theHeight))
addTimeLast = 0
}
addTimeLast++

// 根据对象创建的时间戳排序
manList.sort((a, b) => a.ts - b.ts)
if (manList.length > maxMan && !deading) {
deading = true
// 数量上限,删除小人
manList[0].doDead()
}

// 根据Y轴排序,确保下面的覆盖在上面
manList.sort((a, b) => a.y - b.y)
for (let i in manList) {
let man = manList[i]

if (man.alpha < 1 && !man.deading) {
man.showMe()
} else if (man.alpha >= 1) {
man.doMove()
}
theCtx.save();
theCtx.globalAlpha = man.alpha

if (man.deading) {
theCtx.translate(man.x + Math.floor(man.myWidth / 2), man.y + man.myHeight);
theCtx.rotate(Math.PI / 180 * man.deadAngle);
man.doDeading()
theCtx.drawImage(imageStorage.value['man'][man.getStep()], -1 * man.myWidth / 2, -1 * man.myHeight);
} else {
theCtx.drawImage(imageStorage.value['man'][man.getStep()], man.x, man.y);
}
theCtx.restore();

// 在消失阶段,小人会旋转90度,超过时,标记删除
if (man.deadAngle >= 90) {
man.remove = true
}
}

// 删除被标记的小人
let deadidx = manList.findIndex(e => e.remove)
if (deadidx >= 0) {
deading = false
manList.splice(deadidx, 1)
}
}

四、动画对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import { getRandom } from '@/util/main.js'
export default class Msk {
x = 0
y = 0
theWidth = 0
theHeight = 0
alpha = 0
myWidth = 49
myHeight = 83
theStep = 0

moveNum = 0
moveDir = 0

deading = false
deadAngle = 0

ts = 0

heightLimit = 0.1

TheMainFrame = 5
moveFrame = this.TheMainFrame // 10帧一动
stepFrame = this.TheMainFrame
deadFrame = this.TheMainFrame
showFrame = this.TheMainFrame

constructor(cW, cH, inMove = false) {
this.ts = new Date().getTime()
this.theWidth = cW
this.theHeight = cH
// 控制小人只出现在下半部分
if (inMove) {
this.alpha = 1
}
this.x = getRandom(0, cW - this.myWidth)
this.y = getRandom(this.theHeight * this.heightLimit, cH - this.myHeight) - 10
}

showMe () {
if (this.showFrame < this.TheMainFrame) {
this.showFrame--
if (this.showFrame === 0) {
this.showFrame = this.TheMainFrame
}
return
}
this.showFrame--

this.alpha += 0.2
this.y += 2
if (this.alpha > 1) {
this.alpha = 1
}
}

doMove () {
if (this.moveFrame < this.TheMainFrame) {
this.moveFrame--
if (this.moveFrame === 0) {
this.moveFrame = this.TheMainFrame
}
return
}
this.moveFrame--
if (this.moveNum === 0) {
// 开始移动
// 随机方向
this.moveDir = getRandom(0, 50) > 25 ? 1 : -1
// 随机步数
this.moveNum = getRandom(20, 50)
}
this.y += getRandom(0, 50) > 25 ? -1 : 1
if (this.y < this.theHeight * this.heightLimit) {
this.y = Math.floor(this.theHeight * this.heightLimit)
} else if (this.y > this.theHeight - this.myHeight) {
this.y = this.theHeight - this.myHeight
}

this.x += this.moveDir * 2
this.moveNum--
if (this.x < 0) {
this.x = 0
this.moveNum = 0
} else if (this.x > this.theWidth - this.myWidth) {
this.x = this.theWidth - this.myWidth
this.moveNum = 0
}
}

doDead () {
this.deading = true
this.alpha -= 0.1
}

doDeading () {
if (this.deadFrame < this.TheMainFrame) {
this.deadFrame--
if (this.deadFrame === 0) {
this.deadFrame = this.TheMainFrame
}
return
}
this.deadFrame--

this.deadAngle += 9
this.alpha -= 0.1
}

// 获取当前小人序列帧的索引
getStep () {
if (this.theStep > 7) {
this.theStep = 0
}
if (this.stepFrame <= 0) {
this.stepFrame = this.TheMainFrame
// if (this.theStep > 7) {
// this.theStep = 0
// }
return this.theStep++
} else {
this.stepFrame--
return this.theStep
}
}
}