JavaScript实现类似微信礼花算法
预览
前言
关于标题使用
算法
二字说明个人认为算法是解决某一问题的方法,怕理解不当,搜索*得到以下结果(摘录):
算法(algorithm;算法),在数学(算学)和计算机科学之中,指一个被定义好的、计算机可施行其指示的有限步骤或次序,常用于计算、数据处理和自动推理。算法是有效方法,包含一系列定义清晰的指令,并可于有限的时间及空间内清楚的表述出来。
所以我认为并不是只有 排序、查找、最优解 之类的抽象代码程序,或更为复杂的 神经网络、深度学习、机器视觉 才称为算法
我想说的是,算法没那么神秘和高大上
你我在写代码解决问题或实现功能的模块中,就可能是在写算法
模块可以画成流程图,那么流程图其实就是算法的图形表现关于
封装
此代码在单独的 js 文件中,可直接导入页面调用
实现
使用了 canvas 画布实现图像绘制
抛物模式使用了简单的*落体物理公式
烟花模式则是直接位移
使用了 transform 使图形变形实现视觉飘落感
设计了良好的框架解耦
代码
- 封装好的模块
/**
* Title : Js For Fireworks
* Author : Fc_404
* Date : 2021-09-14
* Describe :
*/
const PIPI = 2 * Math.PI
const PIR = PIPI / 360
const addCanvas = function () {
var el = document.createElement('canvas')
el.height = window.innerHeight
el.width = window.innerWidth
el.style.padding = 0
el.style.margin = 0
el.style.position = 'fixed'
el.style.top = 0
el.style.left = 0
el.style.backgroundColor = 'rgba(0,0,0,1)'
document.body.prepend(el)
return el
}
class Fireworks {
//#region DEFINE
// number
quantity = 20
// angle
range = 30
// number
speed = 12
// angle
angle = 45
// dots
position = [0, 0]
// arr
colors = ["#999999", "#CCCCCC"]
// enum {range, value}
colorsMode = 'range'
// enum {firework, parabola}
launchMode = 'parabola'
// object eg.
// {"arc":range, "ratio":num}
// {"rect":[width, height], "ratio":num}
// {"text":[text, size], "ratio":num}
shape = [
{ "arc": 20, "ratio": 1 },
{ "rect": [20, 40], "ratio": 1 },
{ "text": ["Firework", 20], "ratio": 1 }
]
// num
gravity = 9.8
// transform
isTransform = true
// object
#spirits = []
#spiritsDustbin = []
//#endregion
//#region INIT
#newSpirits() {
// recalculate ratio
var totalratio = 0
for (var i in this.shape) {
totalratio += this.shape[i].ratio
}
for (var i in this.shape) {
this.shape[i].ratio = parseInt(
(this.shape[i].ratio * this.quantity) / totalratio)
}
// new spirit
var spirit = []
for (var i in this.shape) {
var items = JSON.parse(JSON.stringify(this.shape[i]))
for (var ii = 0; ii < items.ratio; ++ii) {
var iitem = JSON.parse(JSON.stringify(items))
// Init position and direction
iitem.x = this.position[0]
iitem.y = this.position[1]
// Init color
if (this.colorsMode == 'value') {
iitem.color = this.colors[
parseInt(
Math.random() * this.colors.length
)]
} else if (this.colorsMode == 'range') {
iitem.color = this.#getColor()
}
// Init angle and speed
iitem.angle = this.angle
+ Math.random() * (this.range / 2)
* (Math.random() > 0.5 ? 1 : -1)
iitem.speed = this.speed
+ Math.random() * this.speed
* (this.launchMode == 'firework' ? 2 : 0.5)
// Init Greaity
if (this.launchMode == 'firework') {
iitem.gravity = this.gravity
+ Math.random()
}
// Calculation vertical and horizontal velocity
iitem.verticalV = Math.sin(iitem.angle * PIR) * iitem.speed
iitem.horizontalV = Math.cos(iitem.angle * PIR) * iitem.speed
// Init transformation
iitem.transformation = [1, 0, 0, 1, 0, 0]
spirit.push(iitem)
}
}
this.#spirits.push([Date.now(), spirit, 0])
}
#getColor() {
var groupL = parseInt(this.colors.length / 2)
var group = parseInt(Math.random() * groupL)
var hcolor = this.colors[group * 2].slice(1)
var ecolor = this.colors[group * 2 + 1].slice(1)
try {
var hcolorR = parseInt(hcolor.slice(0, 2), 16)
var hcolorG = parseInt(hcolor.slice(2, 4), 16)
var hcolorB = parseInt(hcolor.slice(4, 6), 16)
var ecolorR = parseInt(ecolor.slice(0, 2), 16)
var ecolorG = parseInt(ecolor.slice(2, 4), 16)
var ecolorB = parseInt(ecolor.slice(4, 6), 16)
} catch (m) {
throw new TypeError('Color must be #xxxxxx')
}
var colorR = parseInt(
Math.random() *
Math.abs(ecolorR - hcolorR) +
(hcolorR < ecolorR ? hcolorR : ecolorR)
).toString(16)
if (colorR.length == 1) colorR = '0' + colorR
var colorG = parseInt(
Math.random() *
Math.abs(ecolorG - hcolorG) +
(hcolorG < ecolorG ? hcolorG : ecolorG)
).toString(16)
if (colorG.length == 1) colorG = '0' + colorG
var colorB = parseInt(
Math.random() *
Math.abs(ecolorB - hcolorB) +
(hcolorB < ecolorB ? hcolorB : ecolorB)
).toString(16)
if (colorB.length == 1) colorB = '0' + colorB
return '#' + colorR + colorG + colorB
}
//#endregion
//#region DRAW
#draw() {
this.ctx.clearRect(0, 0, window.innerWidth, window.innerHeight)
for (var g in this.#spirits) {
for (var i in this.#spirits[g][1]) {
var item = this.#spirits[g][1][i]
switch (Object.keys(item)[0]) {
case 'arc':
this.#drawArc(item)
break
case 'rect':
this.#drawRect(item)
break
case 'text':
this.#drawText(item)
break;
}
}
}
}
#drawArc(spirit) {
this.ctx.beginPath()
this.ctx.save()
this.#transform(spirit)
this.ctx.arc(
spirit.x - spirit.y * spirit.transformation[2],
spirit.y - spirit.x * spirit.transformation[1],
spirit.arc,
0, PIPI
)
this.ctx.fillStyle = spirit.color
this.ctx.fill()
this.ctx.strokeStyle = spirit.color
this.ctx.stroke()
this.ctx.closePath()
this.ctx.restore()
}
#drawRect(spirit) {
this.ctx.save()
this.#transform(spirit)
this.ctx.fillStyle = spirit.color
this.ctx.fillRect(
spirit.x - spirit.y * spirit.transformation[2],
spirit.y - spirit.x * spirit.transformation[1],
spirit.rect[0], spirit.rect[1]
)
this.ctx.strokeStyle = spirit.color
this.ctx.stroke()
this.ctx.restore()
}
#drawText(spirit) {
this.ctx.save()
this.ctx.font = spirit.text[1] + 'px sans-serif'
this.ctx.fillStyle = spirit.color
this.#transform(spirit)
this.ctx.fillText(
spirit.text[0],
spirit.x - spirit.y * spirit.transformation[2],
spirit.y - spirit.x * spirit.transformation[1]
)
this.ctx.strokeStyle = spirit.color
this.ctx.stroke()
this.ctx.restore()
}
#transform(spirit) {
var offsetX = spirit.x - spirit.x * spirit.transformation[0]
var offsetY = spirit.y - spirit.y * spirit.transformation[3]
switch (Object.keys(spirit)[0]) {
case 'rect':
offsetX -= spirit.rect[1] * spirit.transformation[2]
offsetY -= spirit.rect[1] * spirit.transformation[1]
break
case 'arc':
offsetX -= spirit.arc * spirit.transformation[2] * 2
offsetY -= spirit.arc * spirit.transformation[1] * 2
break
case 'text':
offsetX -= spirit.text[1] * spirit.transformation[2]
offsetY -= spirit.text[1] * spirit.transformation[1]
}
this.ctx.setTransform()
this.ctx.transform(spirit.transformation[0],
spirit.transformation[1],
spirit.transformation[2],
spirit.transformation[3],
spirit.transformation[4] + offsetX,
spirit.transformation[5] + offsetY)
}
//#endregion
//#region ENGINE
#moveEngine() {
for (var g in this.#spirits) {
var msec = (Date.now() - this.#spirits[g][0])
var time = msec / 1000
var spiritDustbin = []
for (var i in this.#spirits[g][1]) {
var item = this.#spirits[g][1][i]
var verticalS, horizontalS
switch (this.launchMode) {
case 'parabola':
verticalS = item.verticalV * time
- 0.5 * (this.gravity)
* Math.pow(time, 2)
horizontalS = item.horizontalV * time * 0.1
break
case 'firework':
if (time < 0.1) {
verticalS = item.verticalV * time * 64
horizontalS = item.horizontalV * time * 64
}
else {
horizontalS = 0
verticalS = item.gravity * time * -1
}
break
}
item.x += horizontalS
item.y -= verticalS
var topPosition = 0
switch (Object.keys(item)[0]) {
case 'arc':
topPosition = item.y - item.arc
break
case 'rect':
topPosition = item.y - item.rect[1]
break
case 'text':
topPosition = item.y - item.text[1] * PIPI
break
}
if (topPosition > window.innerHeight) {
spiritDustbin.push(i)
}
}
this.#spiritsDustbin.push([g, spiritDustbin])
}
}
#transformEngine() {
if (!this.isTransform)
return
for (var g in this.#spirits) {
var msec = (Date.now() - this.#spirits[g][0])
var time = msec / 1000
if (time - this.#spirits[g][2] < 0.1)
continue
for (var i in this.#spirits[g][1]) {
var item = this.#spirits[g][1][i]
if (!('polarity' in item)) {
if (Math.random() > 0.2)
continue
item.polarity = false
}
var c = item.transformation[2]
var d = item.transformation[3]
c *= 10
d *= 10
c -= 2
if (c < -20) {
c = 18
}
d += (item.polarity ? 1 : -1)
if (d <= 0) {
item.polarity = true
d = 0
}
else if (d >= 10) {
item.polarity = false
d = 10
}
item.transformation[2] = c / 10
item.transformation[3] = d / 10
}
this.#spirits[g][2] = time
}
}
#clearDustbin() {
for (var g in this.#spiritsDustbin) {
var group = this.#spiritsDustbin[g][0]
var dustbin = this.#spiritsDustbin[g][1]
for (var i in dustbin) {
this.#spirits[group][1]
.splice(dustbin[i] - i, 1)
}
}
this.#spiritsDustbin.splice(0, this.#spiritsDustbin.length)
var spiritL = this.#spirits.length
for (var i = 0; i < spiritL; ++i) {
if (this.#spirits[i][1].length == 0) {
this.#spirits.splice(i, 1)
i--
spiritL--
}
}
}
//#endregion
constructor() {
this.el = addCanvas()
this.ctx = this.el.getContext("2d")
}
launch() {
const self = this
this.#newSpirits()
var procedure = function () {
self.#moveEngine()
self.#transformEngine()
self.#draw()
self.#clearDustbin()
if (self.#spirits.length > 0)
requestAnimationFrame(procedure)
}
procedure()
}
}
export default {
Fireworks,
}
- 页面代码
<!DOCTYPE html>
<html>
<head>
<title>Fireworks</title>
</head>
<body>
</body>
</html>
<script type="module">
import f from './Fireworks.js'
var a = new f.Fireworks()
a.quantity = 120
a.speed = 12
a.angle = 45
a.range = 30
a.gravity = 2
a.launchMode = 'firework'
a.position = [100, 400]
a.shape = [
{ "text": ['English', 12], "ratio": 1 },
{ "text": ['Chinese', 12], "ratio": 1 },
{ "arc": 6, "ratio": 2 },
{ "rect": [6, 12], "ratio": 2 }
]
a.launch()
a.el.onclick = () => { a.launch() }
</script>
使用
- 先将模块导入项目
import f from './Fireworks.js'
- new 一个对象
var a = new f.Fireworks()
- 配置参数(下边有详细参数列表)
- 发射
a.launch()
参数
参数名 | 类型 | 描述 |
---|---|---|
quantity | number | 碎片数量,建议大于shape 数组参数的长度 |
range | number | 发射范围,以angle 参数为中心的角度领域 |
speed | number | 发射初速度,此参数仅对抛物模式有作用 |
angle | number | 发射角度,角度制支持正负 |
position | array[number, number] | 发射初始位置,以左上为(0,0)点的坐标系 |
colors | array[string, …] | 颜色,RGB制(仅当颜色模式为value 时才可为任意颜色制) |
colorsMode | string | 颜色模式,仅有range 范围模式和value 值模式 |
launchMode | string | 发射模式,仅有firework 烟花模式和parabola 抛物模式 |
shape | array[object, …] | 碎片形状,仅支持arc 圆形rect 矩形text 文字三类对象(下面表格详解) |
gravity | number | 环境重力,对两种发射模式的落体运动有影响 |
isTransform | bool | 飘落模式,为真则会变形以实现飘落 |
- shape对象结构
对象 | 结构 |
---|---|
arc | {“arc”: 半径, “ratio”: 数量比例} |
rect | {“rect”: [宽, 高], “ratio”: 数量比例} |
text | {“text”: [文字, 大小], “ratio”: 数量比例} |
思想
解决这个问题我首先想到的是:使项目结构化、易用化、可扩展、易维护
所以我封装起来,只需要导入模块,new一个对象即可,当然也可以自定义参数,使模块个性化
在写代码的过程中,先大体分好需要做那些动作,然后分离出来,解耦合,然后针对每个动作再细分出可重复利用的代码,降低冗余
正如你所见,大体动作被#region
符号折叠起来了,然后细分功能函数
在主流程函数launch()
中,清晰的动作逻辑,使代码阅读性大大提高,便于维护,正因如此,为了实现多炮同屏同步,也轻松了很多
本来是想使用*落体公式加阻力浮力参数去实现烟花效果,折腾了一下午无果,虽然有想法,但是就是实现不起来,还是说明我的物理以及数学功底太差,放弃使用物理公式后,我就直接在发射和爆炸期间快速移动,然后达到爆炸时间点后自然下落,由于轻物会受到浮力作用,所以此时下落不能使用*落体,便采用简单位移
至此,我已正式学软件3年,写代码约2w+行,涵盖C/C++、C#、Python、Java、PHP、Shell、JavaScript,代码量虽然不多,但每一次写代码,我都如同设计一件艺术品一样,只有这样,才能在每一次的代码中学习,也正是因为喜欢
在我前不久刚辞掉的工作中,我的领导问我为什么要走,我回答因为加班
生活和工作平衡,这也是最近GitHub上很火的一个抵制加班的项目宣言,有效的工作、充实美好的生活这样子,正是我所追求的
我不想每天起早上班,然后加班到晚上,回家累的葛优躺,连周末的好心态也被搞的躺床上不想动
我想工作有工作的需求代码,回家也有自己的想法去写代码,然后练练吉他、画会画,学些有意思的东西
我们并不是廉价劳动力,也不要做廉价劳动力,我们有自己的生活
最后领导给我说,只有你在工作时写代码,有甲方给提需求,这样代码才能更好,这样才有意义
我没怎么回答,如果连自己都做不了,哪能去漫天星河
最终还是辞掉了工作,尽管我非常喜欢这个团队氛围,我领导技术也非常厉害,但是我更喜欢生活
遗留问题
- 由于使用
requestAnimationFrame()
函数实现动画,所以会存在性能问题
比如突然切换进窗口的同时发射了礼花,礼花就会聚集,原因不详
比如第一炮礼花未完全消失的同时发射第二炮,会导致第二炮礼花距离更远,原因不详
但是当窗口稳定、礼花已完全落下时,比较稳定
以上两个问题并无特别大的影响
不过会有一个有趣的现象,短时间内连续发射礼花,会导致后面的礼花越来越远,以至于越过视窗
仔细排查代码无任何问题,最终把问题定位到时间点上,因为通过反向推测,只能是时间点的问题,但我甚至核查每个礼花碎片的时间点也没有异常,所以我暂时把问题归咎到这个函数上了,等有机会深入了解此函数再继续排查