上一节我们学习了如何通过 Canvas
来实现烟花效果,这节我们学习另一种效果 —— 五彩纸屑。具体效果如下:
功能设计
如上图所以,要实现五彩纸屑效果,需要在屏幕左右两侧向上发射粒子。上一节,我们放烟花时也发射了粒子,这里可以继续复用上节课粒子相关代码。上一节我们绘制的是圆形,这节课通过最终效果来看绘制的是椭圆。
首先我们先抽象出一个 Confetti
类,该类控制展示五彩纸屑,它拥有一个核心方法就是 show
。有了 Confetti
类我们还需要渲染每一个例子,当然需要一个 Particle
类,该类似于烟花的粒子,我们抄一抄就行。当每次调用 confetti.show()
的时候需要创建一堆纸屑粒子,纸屑粒子朝着特定的方向发射,后面随着重力落下,整体流程差不多就是这样。这里考虑到一次创建上百个粒子直接由 Confetti
类来管理,Confetti
类做的事情稍微有点多,所以我们再抽象出一层,一般的粒子效果把这一层叫发射器 Emitter
;出于业务考虑,我们这里就抽象出批次这么个概念,每次发生一批粒子,用 ConfettiBatch
类来表示。Confetti
类每次创建一批粒子,它可能同时渲染好几批粒子,而每一批粒子,又分别由左右两个部分,每一部分又有好多纸屑粒子,一个简易的类图原型我们就有了。当然这里我们还有一些没有考虑进去的地方,如动画主循环等,不过不影响我们在此基础上进行开发。
Particle 类
我们先从 Particle
类开始,它的代码跟上次的烟花的粒子几乎是一样的:
1 | class Particle { |
Particle
类的核心逻辑是通过 this.context.ellipse()
方法绘制了一个椭圆。五彩纸屑粒子相比于烟花粒子多了一个 rotation
属性,用来控制粒子旋转的角度。radius
属性表示椭圆的半径,这里我们把它又拆分成 radiusX
和 radiusY
分别是椭圆 X轴
和 Y轴
的半径,当两者相同的时候椭圆就是一个圆形,后面我们通过修改 radiusX
和 radiusY
来显示椭圆。
ConfettiBatch 类
ConfettiBatch
类用来处理一批粒子,包括左右两部分粒子。代码如下:
1 | class ConfettiBatch { |
ConfettiBatch
类中我们创建了2个数组 leftParticles
和 rightParticles
分别表示左右两部分粒子,每一个数组默认有80个粒子。我们生成粒子的速度是 8 ~ 12px
的随机数,角度是 15 ~ 82°
。由于左侧的粒子是朝右上角发射的,而右边的粒子是从左上角发射的,所以左侧粒子的 vx
的值是正数,右侧粒子的 vx
的值是负数。两者都是向上发射的,所以 vy
都是负数(Canvas
y轴向下)。最后我们把两部分粒子放在 particles
数组中,以方便在更新和绘制的时候,一个循环就搞定了。
Confetti 类
Confetti
类核心方法是 show()
方法,该方法用来创建一批粒子并启动动画循环。此外我们还添加 clear()
方法用来清空渲染的批次,destroy()
方法用来销毁 Canvas
,代码如下:
1 | class Confetti { |
Confetti
类构造函数的参数中需要传一个 canvas
,用来告诉我们需要绘制在哪里,但是更多时候我们需要绘制的是整个屏幕,此时就不需要再传 canvas
了,这里通过 createCanvas()
方法来创建一个全屏的不可交互的 canvas
, 具体代码如下:
1 | function createCanvas() { |
此时,你兴高采烈的运行代码,结果发现屏幕上什么都没有!到底发生什么事了?我们的代码哪里有 BUG ?我们看看 createCanvas()
方法,该方法通过CSS修改了 canvas
的大小,但是 canvas
高度实际上是默认的 300px * 150px
,因为上面并没有通过 HTML 或者 JS 的方式设置宽高,原来问题出在这里。现在我们修复这个 BUG,让每次绘制的时候都将 canvas
的宽高设置成真实显示的 Canvas
宽高,具体代码如下:
1 | function normalizeComputedStyleValue(string) { |
这里也可以考虑通过 devicePixelRatio
来根据设备像素比进行对 Canvas
缩放以保证更清晰的显示效果,由于我们这的文章主要内容是绘制五彩纸屑的思想,为了使代码更易懂就不考虑设备像素比了。由于我们在每次循环中都把 Canvas
的宽高设置为实际显示的宽高,所以这里也不需要像烟花的代码一样监听 resize
来处理视口的变化。
现在的效果如下:
3D旋转粒子
上面效果还是比较生硬的,没有纸片翻转的感觉。正常3D翻转如下,可用CSS轻松实现。
我们这里每一个粒子都需要像上面这样旋转,对于单个粒子(圆)来说,在3D旋转过程中半径是不变的,角度可以看成是线性变化的,所以高度可以通过三角函数来 r * cos(θ)
来计算。不过这里有一种更简单的做法来近似计算,就是线性修改椭圆的高度。虽然效果上来说并不是真正的3D旋转,但在较小的粒子上跟真实的3D旋转效果差距不大,而且计算量更小,所以我们这里通过线性修改 radiusY
的值来近似模拟粒子3D旋转效果。
Particle
类我们新增了2个参数 radiusYSpeed
和 rotationSpeed
分别表示y轴半径变化的速度和,圆形旋转的速度,同时还新增了一个 radiusYDirection
属性(可选值down
、up
),用来表示当前y轴半径变化的方向,当值为 down
的时候,表示圆的Y轴半径变小;当值为 up
的时候,表示圆的Y轴半径变大。在 update
方法中我们通过 radiusYDirection
来修改 radiusY
的值,如果是 down
的时候,radiusY
每次减去它的速度直到小于0后反向,同样的当值为 up
的时候,radiusY
每次加上它的速度直到大于圆的半径后反向。如下:
1 | class Particle { |
ConfettiBatch
类在创建粒子的时候也需要传递新增的参数。
1 | new Particle(this.context, { |
此时的效果已经很OK了,如下:
清除已完成的粒子
上面粒子离开屏幕后我们并没有清除已完成的粒子,这样会造成性能下降,如果多调用几次 confetti.show()
将会越来越卡。现在我们需要清除已完成的粒子。
首先 Particle
添加一个 isDone()
方法,用来判断是否完成,这里认为超出 Canvas
高度 100px
后粒子就完成了自己的使命。
1 | class Particle { |
然后在 ConfettiBatch
类中添加了 clearDone()
和 isDone()
方法。clearDone()
用来清除已经绘制完的粒子,本质就是通过 particles.filter
方法过滤掉已完成的粒子。这里需要传递 canvasHeight
,是因为每一帧 canvasHeight
都可能会不同。isDone()
方法也比较简单,如果粒子都被清空则表示该批次已经完成自己的使命。
1 | class ConfettiBatch { |
最后在 Confetti
类中,也需要清理对应的批次,一个批次的粒子有挺多的,清空这一批次实际上是低概率的事情,我们没必要每一帧都去检测是否需要清空当前批次(当然如果每一帧去检测也可以),这里我们每10帧检测一次。代码如下:
1 | class Confetti { |
此时的效果跟上面一样,可以点击这里查看。
主循环优化
我们之前的代码在调用 confetti.show()
的时候开启主循环,如果多次调用 confetti.show()
就会开启多个主循环,这肯定是不行的。现在我们处理一下这个问题。我们新增一个 isRunning
属性来标记是否开启主循环,confetti.show()
的时候如果没开启则开始主循环,否则说明主循环已经处于开启状态,就没必要再多次开启。另外当所有的批次都结束后应该关闭主循环,代码如下:
1 | class Confetti { |
现在我们的五彩纸屑效果就完成了,具体效果可以点击这里。
今天的课程到这里就结束了,当然我们的五彩纸屑还可以添加更多的功能,比如添加一些方形的五彩纸屑,也添加一些 emoji
;另外我们的水平速度和垂直速度都应该根据视口的宽高来生成,而不是直接给一个随机值。我相信通过本章的学习,聪明的你一定可以自己实现更好的效果。加油吧少年,愿你的生活能像五彩碎屑一样多姿多彩!! 😜