[JavaScript][微信礼花][算法]JavaScript实现类似微信礼花算法(已实现封装)

JavaScript实现类似微信礼花算法

预览

[JavaScript][微信礼花][算法]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>

使用

  1. 先将模块导入项目
    import f from './Fireworks.js'
  2. new 一个对象
    var a = new f.Fireworks()
  3. 配置参数(下边有详细参数列表)
  4. 发射
    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上很火的一个抵制加班的项目宣言,有效的工作、充实美好的生活这样子,正是我所追求的
我不想每天起早上班,然后加班到晚上,回家累的葛优躺,连周末的好心态也被搞的躺床上不想动
我想工作有工作的需求代码,回家也有自己的想法去写代码,然后练练吉他、画会画,学些有意思的东西
我们并不是廉价劳动力,也不要做廉价劳动力,我们有自己的生活
最后领导给我说,只有你在工作时写代码,有甲方给提需求,这样代码才能更好,这样才有意义
我没怎么回答,如果连自己都做不了,哪能去漫天星河
最终还是辞掉了工作,尽管我非常喜欢这个团队氛围,我领导技术也非常厉害,但是我更喜欢生活

遗留问题

  1. 由于使用requestAnimationFrame()函数实现动画,所以会存在性能问题
    比如突然切换进窗口的同时发射了礼花,礼花就会聚集,原因不详
    比如第一炮礼花未完全消失的同时发射第二炮,会导致第二炮礼花距离更远,原因不详
    但是当窗口稳定、礼花已完全落下时,比较稳定
    以上两个问题并无特别大的影响
    不过会有一个有趣的现象,短时间内连续发射礼花,会导致后面的礼花越来越远,以至于越过视窗
    仔细排查代码无任何问题,最终把问题定位到时间点上,因为通过反向推测,只能是时间点的问题,但我甚至核查每个礼花碎片的时间点也没有异常,所以我暂时把问题归咎到这个函数上了,等有机会深入了解此函数再继续排查

结束

GitHub主页
GitHub项目地址

上一篇:Spirit带你彻底了解事件捕获和冒泡机制


下一篇:JAVA的图标和由来