[JavaScript][微信礼花][算法]JavaScript实现类似微信礼花算法(已实现封装)
2021/10/16 22:10:16
本文主要是介绍[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>
使用
- 先将模块导入项目
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()
函数实现动画,所以会存在性能问题
比如突然切换进窗口的同时发射了礼花,礼花就会聚集,原因不详
比如第一炮礼花未完全消失的同时发射第二炮,会导致第二炮礼花距离更远,原因不详
但是当窗口稳定、礼花已完全落下时,比较稳定
以上两个问题并无特别大的影响
不过会有一个有趣的现象,短时间内连续发射礼花,会导致后面的礼花越来越远,以至于越过视窗
仔细排查代码无任何问题,最终把问题定位到时间点上,因为通过反向推测,只能是时间点的问题,但我甚至核查每个礼花碎片的时间点也没有异常,所以我暂时把问题归咎到这个函数上了,等有机会深入了解此函数再继续排查
结束
GitHub主页
GitHub项目地址
这篇关于[JavaScript][微信礼花][算法]JavaScript实现类似微信礼花算法(已实现封装)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-26Mybatis官方生成器资料详解与应用教程
- 2024-11-26Mybatis一级缓存资料详解与实战教程
- 2024-11-26Mybatis一级缓存资料详解:新手快速入门
- 2024-11-26SpringBoot3+JDK17搭建后端资料详尽教程
- 2024-11-26Springboot单体架构搭建资料:新手入门教程
- 2024-11-26Springboot单体架构搭建资料详解与实战教程
- 2024-11-26Springboot框架资料:新手入门教程
- 2024-11-26Springboot企业级开发资料入门教程
- 2024-11-26SpringBoot企业级开发资料详解与实战教程
- 2024-11-26Springboot微服务资料:新手入门全攻略