01 不一样の烟火

anime.js 是一个强大的 JavaScript 动画库,HeartBeat 主题的背景点击特效就是借用其官网展示效果。为了学习动画库的使用,这里用 ES6 重构了烟火代码,来一场不一样的烟火。

Anime is a lightweight JavaScript animation library. It works with any CSS Properties, individual CSS transforms, SVG or any DOM attributes, and JavaScript Objects.

不一样の烟火

在开始之前,先链上 Source Code在线预览

引入 anime.min.js

首先在引入 anime.min.js,这里使用 BootCDN 外链。然后创建一个 canvas 画布,用来呈现烟火效果。在 body 标签尾部引入 index.js,接下来就是在 index.js 完成最终的烟火代码。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>不一样の烟火</title>
<script src="https://cdn.bootcss.com/animejs/2.2.0/anime.min.js"></script>
</head>
<style>
body {
background: #000;
overflow: hidden;
}
</style>
<body>
<canvas class="fireworks"></canvas>
<script src="./index.js"></script>
</body>
</html>

初始化画布

在 index.js 中,新建一个 Firework 类,并初始化画布大小尺寸。

class Firework {
constructor() {
this.canvasEl = null // 画布元素
this.ctx = null // 画布上下文
}
// Let's go
start() {
// 初始化画布
this.setCanvasSize()
}
// 设置画布尺寸
setCanvasSize() {
// 获取画布元素
const canvasEl = document.querySelector('.fireworks')
const ctx = canvasEl.getContext('2d')
// 窗口尺寸
const innerWidth = window.innerWidth
const innerHeight = window.innerHeight
// 设置画布尺寸
canvasEl.width = innerWidth * 2
canvasEl.height = innerHeight * 2
canvasEl.style.width = innerWidth + 'px'
canvasEl.style.height = innerHeight + 'px'
ctx.scale(2, 2)
// 保存画布
this.canvasEl = canvasEl
this.ctx = ctx
}
}
const margicAnime = new Firework()
margicAnime.start()

绑定事件

接下来监听点击事件以绘制动画,并且监听窗口缩放事件,当窗口大小变化时重置画布尺寸。为了兼容不同浏览器,这里将事件绑定和解绑方法提取出公用方法。

/**
* @description 绑定事件 on(element, event, handler)
*/
const on = (function() {
if (document.addEventListener) {
return function(element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false)
}
}
} else {
return function(element, event, handler) {
if (element && event && handler) {
element.attachEvent('on' + event, handler)
}
}
}
})()
/**
* @description 解绑事件 off(element, event, handler)
*/
const off = (function() {
if (document.removeEventListener) {
return function(element, event, handler) {
if (element && event) {
element.removeEventListener(event, handler, false)
}
}
} else {
return function(element, event, handler) {
if (element && event) {
element.detachEvent('on' + event, handler)
}
}
}
})()

然后添加绑定事件,并且添加销毁方法,在销毁時解绑事件:

// 点击事件
const tap = 'ontouchstart' in window || navigator.msMaxTouchPoints ? 'touchstart' : 'mousedown'
class Firework {
// Let's go
start() {
// 初始化画布
this.setCanvasSize()
// 监听点击和窗口缩放事件
on(document, tap, this.render.bind(this))
on(window, 'resize', this.setCanvasSize.bind(this))
}
// 销毁
destroyed() {
off(document, tap, this.render.bind(this))
off(window, 'resize', this.setCanvasSize.bind(this))
this.tapFunc = this.resizeFunc = this.renderAnime = null
}
// 点击事件
render() {}
}

擦除与绘制

借助 anime.js,可以很方便在每一帧画布更新后擦除画布,通过不断清除画布内容再绘制,形成动画效果。

class Firework {
// 点击事件
render(e) {
const canvasEl = this.canvasEl
const ctx = this.ctx
// 绘制前启用擦除动画
if (!this.renderAnime) {
this.renderAnime = anime({
duration: Infinity,
update() {
// 擦除画布
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height)
}
})
}
this.renderAnime.play()
// 点击坐标
const pointerX = e.clientX || e.touches[0].clientX
const pointerY = e.clientY || e.touches[0].clientY
this.animateParticules(pointerX, pointerY)
}
// 绘制烟火
animateParticules() {}
}

绘制烟火

绘制烟火是最为核心代码,烟火由扩散圈的烟火粒子两部分组成。并在每一个动画帧更新后重新绘制粒子。

class Firework {
constructor() {
this.numberOfParticules = 30 // 粒子数量
this.colors = ['#FF1461', '#18FF92', '#5A87FF', '#FBF38C'] // 粒子颜色
}
// 创建扩散圈
createCircle(x, y) {
const ctx = this.ctx
const p = {}
p.x = x
p.y = y
p.color = '#FFF'
p.radius = 0.1
p.alpha = 0.5
p.lineWidth = 6
p.draw = function() {
ctx.globalAlpha = p.alpha
ctx.beginPath()
// 绘制正圆
ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
ctx.lineWidth = p.lineWidth
ctx.strokeStyle = p.color
ctx.stroke()
ctx.globalAlpha = 1
}
return p
}
// 创建粒子
createParticule(x, y) {
const ctx = this.ctx
const p = {}
p.x = x
p.y = y
p.color = this.colors[anime.random(0, this.colors.length - 1)]
p.radius = anime.random(16, 32)
p.endPos = this.setParticuleDirection(p)
p.draw = function() {
ctx.beginPath()
ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
ctx.fillStyle = p.color
ctx.fill()
}
return p
}
// 粒子扩散方向
setParticuleDirection(p) {
const angle = (anime.random(0, 360) * Math.PI) / 180
const value = anime.random(50, 180)
const radius = [-1, 1][anime.random(0, 1)] * value
return {
x: p.x + radius * Math.cos(angle),
y: p.y + radius * Math.sin(angle)
}
}
// 绘制粒子
renderParticule(anim) {
for (let i = 0; i < anim.animatables.length; i++) {
anim.animatables[i].target.draw()
}
}
// 绘制烟火
animateParticules(x, y) {
const circle = this.createCircle(x, y)
const particules = []
for (let i = 0; i < this.numberOfParticules; i++) {
particules.push(this.createParticule(x, y))
}
const renderParticule = this.renderParticule
anime
.timeline()
.add({
targets: particules,
x(p) {
return p.endPos.x
},
y(p) {
return p.endPos.y
},
radius: 0.1,
duration: anime.random(1200, 1800),
easing: 'easeOutExpo',
// 每一个动画帧更新后重新绘制粒子
update: renderParticule
})
.add({
targets: circle,
radius: anime.random(80, 160),
lineWidth: 0,
alpha: {
value: 0,
easing: 'linear',
duration: anime.random(600, 800)
},
duration: anime.random(1200, 1800),
easing: 'easeOutExpo',
update: renderParticule,
offset: 0
})
}
}

大功告成

最终烟火效果代码如下:

/**
* @description 绑定事件 on(element, event, handler)
*/
const on = (function() {
if (document.addEventListener) {
return function(element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false)
}
}
} else {
return function(element, event, handler) {
if (element && event && handler) {
element.attachEvent('on' + event, handler)
}
}
}
})()
/**
* @description 解绑事件 off(element, event, handler)
*/
const off = (function() {
if (document.removeEventListener) {
return function(element, event, handler) {
if (element && event) {
element.removeEventListener(event, handler, false)
}
}
} else {
return function(element, event, handler) {
if (element && event) {
element.detachEvent('on' + event, handler)
}
}
}
})()
// 点击事件
const tap = 'ontouchstart' in window || navigator.msMaxTouchPoints ? 'touchstart' : 'mousedown'
class Firework {
constructor() {
this.canvasEl = null // 画布元素
this.ctx = null // 画布上下文
this.numberOfParticules = 30 // 粒子数量
this.colors = ['#FF1461', '#18FF92', '#5A87FF', '#FBF38C'] // 粒子颜色
this.tapFunc = null
this.resizeFunc = null
this.renderAnime = null
}
// Let's go
start() {
// 初始化画布
this.setCanvasSize()
// 监听点击和窗口缩放事件
on(document, tap, this.render.bind(this))
on(window, 'resize', this.setCanvasSize.bind(this))
}
// 销毁
destroyed() {
off(document, tap, this.render.bind(this))
off(window, 'resize', this.setCanvasSize.bind(this))
this.tapFunc = this.resizeFunc = this.renderAnime = null
}
// 设置画布尺寸
setCanvasSize() {
// 获取画布元素
const canvasEl = document.querySelector('.fireworks')
const ctx = canvasEl.getContext('2d')
// 窗口尺寸
const innerWidth = window.innerWidth
const innerHeight = window.innerHeight
// 设置画布尺寸
canvasEl.width = innerWidth * 2
canvasEl.height = innerHeight * 2
canvasEl.style.width = innerWidth + 'px'
canvasEl.style.height = innerHeight + 'px'
ctx.scale(2, 2)
// 保存画布
this.canvasEl = canvasEl
this.ctx = ctx
}
// 创建点击扩散圈
createCircle(x, y) {
const ctx = this.ctx
const p = {}
p.x = x
p.y = y
p.color = '#FFF'
p.radius = 0.1
p.alpha = 0.5
p.lineWidth = 6
p.draw = function() {
ctx.globalAlpha = p.alpha
ctx.beginPath()
ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
ctx.lineWidth = p.lineWidth
ctx.strokeStyle = p.color
ctx.stroke()
ctx.globalAlpha = 1
}
return p
}
// 创建点击粒子
createParticule(x, y) {
const ctx = this.ctx
const p = {}
p.x = x
p.y = y
p.color = this.colors[anime.random(0, this.colors.length - 1)]
p.radius = anime.random(16, 32)
p.endPos = this.setParticuleDirection(p)
p.draw = function() {
ctx.beginPath()
ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
ctx.fillStyle = p.color
ctx.fill()
}
return p
}
// 粒子扩散方向
setParticuleDirection(p) {
const angle = (anime.random(0, 360) * Math.PI) / 180
const value = anime.random(50, 180)
const radius = [-1, 1][anime.random(0, 1)] * value
return {
x: p.x + radius * Math.cos(angle),
y: p.y + radius * Math.sin(angle)
}
}
// 绘制粒子
renderParticule(anim) {
for (let i = 0; i < anim.animatables.length; i++) {
anim.animatables[i].target.draw()
}
}
// 绘制烟火
animateParticules(x, y) {
const circle = this.createCircle(x, y)
const particules = []
for (let i = 0; i < this.numberOfParticules; i++) {
particules.push(this.createParticule(x, y))
}
const renderParticule = this.renderParticule
anime
.timeline()
.add({
targets: particules,
x(p) {
return p.endPos.x
},
y(p) {
return p.endPos.y
},
radius: 0.1,
duration: anime.random(1200, 1800),
easing: 'easeOutExpo',
update: renderParticule
})
.add({
targets: circle,
radius: anime.random(80, 160),
lineWidth: 0,
alpha: {
value: 0,
easing: 'linear',
duration: anime.random(600, 800)
},
duration: anime.random(1200, 1800),
easing: 'easeOutExpo',
update: renderParticule,
offset: 0
})
}
// 点击事件
render(e) {
const canvasEl = this.canvasEl
const ctx = this.ctx
// 绘制前启用擦除画布
if (!this.renderAnime) {
this.renderAnime = anime({
duration: Infinity,
update() {
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height)
}
})
}
this.renderAnime.play()
// 点击坐标
const pointerX = e.clientX || e.touches[0].clientX
const pointerY = e.clientY || e.touches[0].clientY
this.animateParticules(pointerX, pointerY)
}
}
const margicAnime = new Firework()
margicAnime.start()

Just enjoy it 😃!