ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

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

2021-10-16 21:58:24  阅读:225  来源: 互联网

标签:JavaScript 礼花 微信 .# ctx transformation item var spirit


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()

参数

参数名类型描述
quantitynumber碎片数量,建议大于shape数组参数的长度
rangenumber发射范围,以angle参数为中心的角度领域
speednumber发射初速度,此参数仅对抛物模式有作用
anglenumber发射角度,角度制支持正负
positionarray[number, number]发射初始位置,以左上为(0,0)点的坐标系
colorsarray[string, …]颜色,RGB制(仅当颜色模式为value时才可为任意颜色制)
colorsModestring颜色模式,仅有range范围模式和value值模式
launchModestring发射模式,仅有firework烟花模式和parabola抛物模式
shapearray[object, …]碎片形状,仅支持arc圆形rect矩形text文字三类对象(下面表格详解)
gravitynumber环境重力,对两种发射模式的落体运动有影响
isTransformbool飘落模式,为真则会变形以实现飘落
  • 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项目地址

标签:JavaScript,礼花,微信,.#,ctx,transformation,item,var,spirit
来源: https://blog.csdn.net/qq_31103801/article/details/120802646

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有