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 😃!
最后更新于