大家好!我是Fly哥,最近做了很多粒子动画, 对canvas 实现粒子动画有了一点小感悟,前几天北方都下雪了, 身在魔都的我们, 一点点雪的影子都没有。而下雪动画作为粒子动画中我觉得算是比较简单的好理解, 先把这篇文章讲完,后面再去给大家讲 酷炫的 canvas 文字烟花动画。本篇文章大概阅读花费7分钟, 你可以学到如何去实现一个粒子动画,我觉得把思路学会了, 后面产品假设提出任何需求你都是可以cover 住的。废话不多说, 直接先看先看效果:
snow
看着是不是像辣么回事,哈哈哈哈, fly哥写的基础版,圣诞节快到了,你可以在我的基础上做下面👇🏻几个优化
- 使用雪花贴图
- 为每一个粒子增加重力效果
- 性能优化的角度考虑下,考虑使用离屏canvas(数量比较多的情况下,可以去使用)
粒子动画
可能有的人到现在还不是很清楚啥是粒子动画:
粒子
粒子是什么?粒子是一种微小的物体,比如像我们周边环境中的雪花,火星等物体。因此在游戏中一般都用粒子特效来模拟咱们现实生活中的许多自然现象。
粒子系统
粒子系统是众多粒子的集合。一般具有具有粒子的更新,显示,销毁及其创建粒子等性质。不同的粒子系统具有不同的粒子行为,所以所具有的性质会略有区别。
综合
粒子动画其实很容易理解了,就是很多个粒子按照某个特定的运动轨迹组合起来的动画,所以做粒子类属性 会带有一些物理属性, 比如重力,风的阻力, 加速度。。。这些东西,其实主要为了更加贴近自然,更加真实。物理比较差的同学记得把物理知识补一下。
雪粒子
简单介绍上面的概念后,大家可以结合上面的动画简单分析一下,雪粒子这个类应该有哪些属性呢???
首先我的雪花粒子是用「ctx.arc」 去画的,所以呢??「肯定有 x 和 y 轴 坐标 ,还有 radius 半径」
这时候有同学开始抢答了?说fly哥,你上面提到了粒子的速度, 雪花肯定每一个下的速度不一样, 「肯定有一个 vx, vy」
哎呦不错哦, 现在已经5个属性了, 还有没有属性呢, 这时候有个细心的妹子站出来了, 我看到了透明度的变化,确实 还是妹子细心哇!!
小姐姐
大家在思考下, 还有没有什么属性呢?看着同学们鸦雀无声, 我给一个提醒, 如何保持雪一直下,难道我要不断添加粒子嘛, 有没有边界啥的??
有同学就说:不亏是fly哥,思考问题就是全面哇,哈哈哈哈其实这里涉及到 就一个属性 边界检测呗, 不过没辣么夸张, 「就是是一个最大距离 maxDistance」, 如果超过了 最大距离 我们就让它从一开始落呗, 不就实现了永动机的效果!
编码实现
理论说的都差不多了,我这里写的都是伪代码,本着 「授人以鱼不如授人以渔」 的目的, 你如果真的想学, 跟着我的思路做一篇肯定很没问题!
创建canvas
创建canvas 拿到canvas 的上下文 这个应该不用多讲了
const canvas = document.getElementById( 'canvas' ); const ctx = canvas.getContext( '2d' );
创建snow 类
主要是每一个小的雪花粒子:
class Snow { dia: number fill: string vy: number vx: number z: number y: number x: number maxDistance: number width: number height: number constructor(width: number, height: number, maxDistance: number) { this.x = Math.random() * (width + maxDistance) - maxDistance / 2 this.y = Math.random() * (height + maxDistance) - maxDistance / 2 this.maxDistance = maxDistance this.width = width this.height = height this.z = Math.random() * 0.5 + 0.5 this.vx = (Math.random() * 2 - 0.5) * this.z this.vy = (Math.random() * 1.5 + 1.5) * this.z this.fill = 'rgba(255,255,255,' + (0.5 * Math.random() + 0.5) + ')' this.dia = (Math.random() * 2.5 + 1.5) * this.z } draw(ctx: CanvasRenderingContext2D) { ctx.beginPath() ctx.strokeStyle = 'transparent' ctx.fillStyle = this.fill ctx.arc(this.x, this.y, this.dia, 0, 2 * Math.PI) ctx.closePath() ctx.stroke() ctx.fill() return this } update() { this.x += this.vx this.y += this.vy if (this.x > this.width + this.maxDistance / 2) { this.x = -(this.maxDistance / 2) } else if (this.x < -(this.maxDistance / 2)) { this.x = this.width + this.maxDistance / 2 } if (this.y > this.height + this.maxDistance / 2) { this.y = -(this.maxDistance / 2) } else if (this.y < -(this.maxDistance / 2)) { this.y = this.height + this.maxDistance / 2 } } }
这里面 把上面的所说的属性 都讲到了, 其实无论你做任何粒子, 他都有一个创建 和 更新 ,因为你是做动画,
比如第一帧画面 是没有雪花粒子的, 所以第一帧 就是创建粒子,然后后面每一帧其实就是改变粒子的位置 就好了,不断重复这样的过程 配合 requestanimation 去实现。后面每一帧 就是更新 「update」 函数,
我这里还是解释下:粒子不断加 一个固定的速度 「vx vy」 ,然后做了边界判断 , 你可以自己去修改的。由于 有不同的粒子, 粒子的大小 透明度 、速度 都是不同的, 所以 使用了「random」
动画实现
动画实现 很简单 就两步骤
- 创建粒子
- 更新粒子
// 第一帧 创建1000 个 for (let i = 0; i < 1000; i++) { points.push(new Snow(100, 100, 100)) } // 后面都是更新 ctx.clearRect(0, 0, view.width, view.height) ctx.fillStyle = 'rgba(0,128,255,1)' ctx.fillRect(0, 0, view.width, view.height) // 调用每个粒子的更新 函数 points.forEach((point) => { point.draw(ctx).update() })