进来这篇文章里放个烟花吧

最近在逛博客的时候发现了云游君的小站有一个鼠标点击特效,会放烟花,看着很有趣,于是就想着学习一下。在看了博主开源的valaxy-theme-yun这个主题中找到了对应的代码段,然后发现最初的代码是来自anime库的一个demo,因为该demo主要是用anime实现动画效果,我就想着能不能不用anime自己来实现这个效果。

分析

我把烟花特效单独加在这个页面内,你可以尝试点击触发这个烟花特效。当鼠标点击触发事件时,页面会在点击的位置绘制一定个数的粒子作为烟花最基础的组成部分,然后通过逐帧重绘的方式,让这些粒子向周围移动并缩小,最终变成一个烟花的动画。

所以可以确定的是,我们需要一个圆的集合,包含了每个圆的初始状态和最终状态,接着使用canvas画板绘制动画过程中每个时间点对应的圆的状态,动画的过程需要用到requestAnimationFrame这个Api来实现。

实现

创建默认配置对象,以及需要使用到的工具函数和基础数据

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
// 粒子数据模型
type Particule = {
x: number // 粒子当前所在x坐标
y: number // 粒子当前所在y坐标
currentRadius: number // 粒子当前半径
direction: ParticuleDirection // 粒子的结束位置及路径长度
color: string // 粒子的颜色
radius: number // 粒子的初始半径
}

type ParticuleDirection = {
x: number
y: number
length: number
}

const el = 'canvas.fireworks'
const particuleNum = 20
const colors = ['#FF1461', '#18FF92', '#5A87FF', '#FBF38C']
let pointerX, pointerY // 鼠标点击位置坐标
const tap = 'ontouchstart' in window ? 'touchstart' : 'mousedown'
const particules: Particule[] = [] // 粒子个数

/** 随机生成区间内整数 */
function randomInt(min: number, max: number) {
return Math.floor(Math.random() * (max + 1 - min) + min)
}

获取canvas并设置canvas大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const canvasEl = document.querySelector('canvas.fireworks')
const ctx = canvas.getContext('2d')!
function setCanvasSize(canvasEl) {
canvasEl.width = window.innerWidth * 2
canvasEl.height = window.innerHeight * 2
canvasEl.style.width = window.innerWidth + 'px'
canvasEl.style.height = window.innerHeight + 'px'
canvasEl.style.pointerEvents = 'none'
ctx.scale(2, 2)
}
setCanvasSize(canvasEl);
window.addEventListener('resize', () => {
setCanvasSize(canvasEl)
})

创建粒子

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
/** 设置粒子结束位置 */
function setParticuleDirection(x: number, y: number): ParticuleDirection {
const length = randomInt(50, 180)
const angle = randomInt(0, 360) * Math.PI / 180
const direction = [-1, 1][randomInt(0, 1)]
const xlength = Math.cos(angle) * length * direction
const ylength = Math.sin(angle) * length * direction
return {
x: x + xlength,
y: y + ylength,
length
}
}

/** 创建粒子 */
function createParticule(x: number, y: number): Particule {
const color = colors[randomInt(0, colors.length -1)]
const radius = randomInt(0, 16)
const endPoint = setParticuleDirection(x, y)
return {
x,
y,
color,
direction: endPoint,
radius,
currentRadius: radius
}
}

/** 计算粒子下一帧位置 */
function calcParticuleCoords(particule: Particule) {
const direction = particule.direction
let x = particule.x + (direction.x - pointerX) / 500 * elapsed
let y = particule.y + (direction.y - pointerY) / 500 * elapsed
const length = Math.sqrt(Math.pow(Math.abs(x - pointerX), 2) + Math.pow(Math.abs(y - pointerY), 2))
if (length > direction.length) {
x = direction.x
y = direction.y
}
return {x, y}
}

动画每一帧的操作函数

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
let startTimeStamp = 0 // 动画开始事件
let elapsed // 动画已用时间
let done = true
function drawStep(timestamp: number) {
done = false
if (!startTimeStamp) {
startTimeStamp = timestamp
}
elapsed = timestamp - startTimeStamp
ctx.clearRect(0, 0, canvasEl!.clientWidth, canvasEl!.clientHeight)
for (let i = 0; i < particules.length; i++) {
const particule = particules[i]
const direction = particule.direction
const coord = calcParticuleCoords(particule)
const radius = Math.max(particule.radius - particule.radius / 500 * elapsed, 0)
ctx.beginPath()
ctx.arc(coord.x, coord.y, radius, 0, 2 * Math.PI, true)
ctx.fillStyle = particule.color
ctx.fill()
if ((coord.x === direction.x && coord.y === direction.y) || radius === 0) {
particules.splice(i, 1)
i--
}
}
if (elapsed > 1000 || particules.length === 0) {
particules.splice(0)
done = true
} else {
requestAnimationFrame(drawStep)
}
}

添加监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
document.addEventListener(tap, function(e) {
if (!done) {
return
}
pointerX = (e as MouseEvent).clientX || (e as TouchEvent).touches[0].clientX
pointerX = (e as MouseEvent).clientY || (e as TouchEvent).touches[0].clientY
startTimeStamp = 0
elapsed = null
for (let i = 0; i < particuleNum; i++) {
particules.push(createParticule(pointerX, pointerY))
}
requestAnimationFrame(drawStep)
})

完整代码

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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
type initOption = {
el: string | HTMLCanvasElement
particuleNum: number,
colors: string[]
}

type Particule = {
x: number
y: number
currentRadius: number
direction: ParticuleDirection
color: string
radius: number
}

type ParticuleDirection = {
x: number
y: number
length: number
}

const defaultOptions: initOption = {
el: 'canvas.fireworks',
particuleNum: 20,
colors: ['#FF1461', '#18FF92', '#5A87FF', '#FBF38C']
}

function randomInt(min: number, max: number) {
return Math.floor(Math.random() * (max + 1 - min) + min)
}

function setCanvasSize(canvasEl: HTMLCanvasElement) {
canvasEl.width = window.innerWidth * 2
canvasEl.height = window.innerHeight * 2
canvasEl.style.width = window.innerWidth + 'px'
canvasEl.style.height = window.innerHeight + 'px'
canvasEl.style.pointerEvents = 'none'
canvasEl.getContext('2d')!.scale(2, 2)
}

function fireworks(options: initOption = defaultOptions) {
const canvasEl: HTMLCanvasElement | null = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
if (!canvasEl || canvasEl.nodeName.toLowerCase() !== 'canvas') {
throw new Error('未找到canvas元素')
}
setCanvasSize(canvasEl)
window.addEvnetListener('resize', () => {
setCanvasSize(canvasEl)
})
let pointerX = 0
let pointerY = 0
const particuleNum = options.particuleNum
const colors = options.colors
const ctx = (canvasEl as HTMLCanvasElement).getContext('2d')!
const tap = 'ontouchstart' in window ? 'touchstart' : 'mousedown'
const particules: Particule[] = []

function setParticuleDirection(x: number, y: number): ParticuleDirection {
const length = randomInt(50, 180)
const angle = randomInt(0, 360) * Math.PI / 180
const direction = [-1, 1][randomInt(0, 1)]
const xlength = Math.cos(angle) * length * direction
const ylength = Math.sin(angle) * length * direction
return {
x: x + xlength,
y: y + ylength,
length
}
}

function createParticule(x: number, y: number): Particule {
const color = colors[randomInt(0, colors.length - 1)]
const radius = randomInt(8, 16)
const endPoint = setParticuleDirection(x, y)
return {
x,
y,
color,
direction: endPoint,
radius,
currentRadius: radius
}
}

function calcParticuleCoords(particule: Particule) {
const direction = particule.direction
let x = particule.x + (direction.x - pointerX) / 500 * elapsed
let y = particule.y + (direction.y - pointerY) / 500 * elapsed
const length = Math.sqrt(Math.pow(Math.abs(x - pointerX), 2) + Math.pow(Math.abs(y - pointerY), 2))
if (length > direction.length) {
x = direction.x
y = direction.y
}
return {x, y}
}

let startTimeStamp = 0
let elapsed
let done = true
function drawStep(timestamp: number) {
done = false
if (!startTimeStamp) {
startTimeStamp = timestamp
}
elapsed = timestamp - startTimeStamp
ctx.clearRect(0, 0, canvasEl!.clientWidth, canvasEl!.clientHeight)
for (let i = 0; i < particules.length; i++) {
const particule = particules[i]
const direction = particule.direction
const coord = calcParticuleCoords(particule)
const radius = Math.max(particule.radius - particule.radius / 500 * elapsed, 0)
ctx.beginPath()
ctx.arc(coord.x, coord.y, radius, 0, 2 * Math.PI, true)
ctx.fillStyle = particule.color
ctx.fill()
if ((coord.x === direction.x && coord.y === direction.y) || radius === 0) {
particules.splice(i, 1)
i--
}
}
if (elapsed > 1000 || particules.length === 0) {
particules.splice(0)
done = true
} else {
requestAnimationFrame(drawStep)
}
}

document.addEventListener(tap, function (e) {
if (!done) {
return
}
pointerX = (e as MouseEvent).clientX || (e as TouchEvent).touches[0].clientX
pointerY = (e as MouseEvent).clientY || (e as TouchEvent).touches[0].clientY
startTimeStamp = 0
elapsed = null
for (let i = 0; i < particuleNum; i++) {
particules.push(createParticule(pointerX, pointerY))
}
requestAnimationFrame(drawStep)
})
}

export default fireworks