《那朵花》ED 花雨效果实现

《那朵花》ED 花雨效果实现

特别喜欢《未闻花名》,ED 听了很久,对片尾的花雨效果映像特别深刻。所以试着实现一个 5 毛版本的花雨效果。


可能还有很多小伙伴还没有看过《花名》,这里也小小的安利一下,动画里面的花雨效果是这样的:《那朵花》片尾花雨效果

大体就是花朵从上往下落,然后有个过渡静止、缩小后改变颜色后反向运动。


然后这个是我们 5 毛实现的版本:5 毛花雨效果-codepen 源码



让我们先来画一朵花

这个动画的核心就是这些纷纷攘攘的花朵,先来看一下动画中的花朵是怎样的:

动画场景中都是这种 5 个花瓣的花朵,花朵的原型是勿忘我,常见的还是蓝色居多。

动效果要用到大量不同颜色形状的花朵,所以使用背景图片肯定是不行的。花瓣的图案还是比较简单的,用 AI 或者 Sketch 的钢笔工具可以很方便描出花朵的路径然后导出到 SVG 中使用,但 SVG 并不适合大批量的绘制图形,性能是个问题。所以这里我们使用 canvas 来实现我们想要的效果。

直接用 canvas 画一朵花还是很没头绪的,但我们可以参考与花朵形状类似的五角星:

画一个五角星

画一朵花

比较上下两个图形,花朵的形状其实就是由五角星过渡过来的,只是中间多了一层控制点将尖角边变成了弧线而已。所以我们只要取到三个圆弧上的绘制点,将中间圆上的点作为二次贝塞尔曲线的控制点,依次连接曲线就能花朵的形状了。

圆上点的坐标通过简单的几何计算就能得到了:

// 获取指定圆心、角度、半径圆上的点坐标
function getPoint (ox, oy, ang, radius) {
  const rad = Math.PI * ang / 180
  return {
    x: ox + radius * Math.sin(rad),
    y: oy + radius * Math.cos(rad)
  }
}

是不是很简单,具体的花朵的绘制可以参考上面的例子。花朵绘制出来了,但是规规整整的没什么花的形韵,所以我们可以稍稍处理一下,给中间圆上的控制点设置一些随机偏移,使得画出的花瓣显自然一点。

控制点随机偏移

最终画出来的效果是这样的:

自然的花



为花朵加一些动效

花朵的绘制完成了,接下来就可以为花朵加一些动效了,看看还缺些什么:

  • 花朵随机出现在画面上,有不同的大小、颜色与透明度
  • 每朵花的运动速度都是不一样的
  • 花朵下落或者上漂时伴随则向左或者向右移动
  • 花朵下落时颜色为灰色调,旋转的方向为顺时针
  • 花朵静止反向上飘时颜色转为粉色调,旋转的方向变为逆时针

所以我们需要一个 Flower 的类用生成各种属性不一样的花朵:

function Flower (cw, ch, radius, colors, alpha, vy, vr) {
  // 随机出现在 canvas 上
  this.x = random() * cw
  this.y = random() * ch
  this.vy = vy
  // -0.5 < vx < 0.5
  this.vx = random() * 1 - 0.5
  this.vr = vr
  this.cw = cw
  this.ch = ch
  this.alpha = alpha
  this.radius = radius
  this.color = '#ccc'
  // colors[0] 灰色调、colors[1] 粉色调
  this.colors = colors
  this.count = count
  this.rotate = 0
  // 1 表示向下运动 0 静止 -1 向上运动
  this.vertical = 1
  this.points = []
  ......
} 

然后花朵需要利用上面绘制花朵的方法创建出自身的路径进行绘制,还需要一些方法改变花朵的运动方向与颜色:

// 静止过后将 vertical 设置为 1 花朵开始向上反向运动
Flower.prototype.reverse = function reverse () {
  this.vertical = -1
}
// 将 vertical 设置为 0 画面静止,改变花朵的颜色
Flower.prototype.zoom = function zoom () {
  this.vertical = 0
  this.setColor()
}
// 设置花朵的颜色时,花朵刚开始向下落时取的是灰色调的颜色
// 待到画面静止取的是粉色调颜色列表的里面的色彩
Flower.prototype.setColor = function setColor () {
  if (this.vertical === 1) {
    this.color = this.colors[0]
  } else {
    this.color = this.colors[1]
  }
}

最后是在做动画循环时更新花朵的位置,旋转角度与边界检测。这里的花朵都是进行简单的线性运动,所以动画更新还是比较简单的:

// 动画循环时用于更新花朵的位置与大小、边界检测
Flower.prototype.update = function update () {
  // vertical 为 0 时,花朵停止运动,进行缩放
  if (!this.vertical && this.scale >= 0.9) {
    this.scale *= 0.99
    return
  }
  var halfRadius = this.halfRadius
  this.rotate += this.vr * this.vertical
  this.x += this.vx * this.vertical
  this.y += this.vy * this.vertical
  // 花瓣到达边界时重新设置花瓣的位置
  if (this.x < -halfRadius || this.x > this.cw + halfRadius) {
    this.x = this.x > 0 ? -halfRadius : this.cw + halfRadius
  }
  if (this.y < -halfRadius || this.y > this.ch + halfRadius) {
    this.y = this.y > 0 ? -halfRadius : this.ch + halfRadius
    this.x = random() * this.cw + this.halfRadius
  }
}

花的类已经实现好了,接着就是构建多个花的实例,将它们绘制到 canvas 上了。

完整代码实现在这里,基本效果算是出来了,随机渲染了 100 朵花:


接下来进行下一步,让我们先来渲染 1000 一个背景层的花朵看看效果如何。机智的你可能已经猜到结果了,就是画面变得巨卡无比。 可以看看渲染 1000 花朵的效果

这到底是由什么造成的呢?

动画循环方面我们已经使用 requestAnimationFrame 进行优化了,所以这肯定不是性能的瓶颈所在。实际的问题出在 flower.drow 的操作上。 在 requestAnimationFrame 循环更新动画时,每一帧 flower.drow 都会调用 canvas api 进行花朵的重绘,而 canvas 的 api 调用恰巧又是极其占用 CPU 资源的,再加上绘制后 UI 渲染更新,绘制的花朵数量多了画面自然显得卡顿。那有什么方法可以进行优化吗?

答案是肯定的,下面介绍一种常用的 canvas 性能优化方案:离屏渲染。


使用离屏渲染对动画进行优化

既然性能的瓶颈是由于 flower.drow() 重绘造成,那我们是不是可以通过某些方法将花朵的重绘次数将至最低,以减少 canvas 重绘操作呢?或者说我们是不是可以将绘制好的图形缓存起来以重复利用呢?

实际上离屏渲染的实现思路就是利用无界面的 canvas 元素将绘制完成的图案进行缓存,无界面的 canvas 的元素在绘制图案时不需要渲染,所以不会有 UI 渲染的开销。在下一次进行动画更新时直接将缓存好的绘制图案直接输出到目标 canvas 之上,不再进行绘制操作。

首选我们需要为每一朵花添加一个自身的无界面 canvas 元素,用于对绘制的图案进行缓存。并且 canvas 的大小应当与绘制图案的大小相当,这样不会造成资源的浪费。因为在不考虑绘制图案复杂情况下,canvas 的大小越小自然缓存的数据量也就越小,所占的资源也就越少:

function Flower (cw, ch, radius, colors, alpha, vy) {
  var cacheCanvas = document.createElement('canvas')
  // 这里的 canvas 就是一个长宽为圆直径的正方形,花朵就是绘制在这上面
  cacheCanvas.width = radius * 2
  cacheCanvas.height = radius * 2
  ......
  this.canva = cacheCanvas
  this.ctx = cacheCanvas.getContext('2d')
  ......
  this.cache()
}
// 先在自身的离屏 canvas 缓存绘制出花瓣图案
Flower.prototype.cache = function cache () {
  ......
  this.ctx.drow...
}
......
// 这里不再进行绘制
// 而是使用 context.drawImage 将缓存的 canvas 绘制到需要渲染的 context 上
Flower.prototype.drow = function drow (context) {
  context.save()
  context.translate(this.x, this.y)
  context.rotate(this.rotate)
  context.scale(this.scale, this.scale)
  context.drawImage(this.canva, -this.radius, -this.radius)
  context.restore()
}

Flower 初始化创建一个无界面的 canvas 元素,然后调用 cache 绘制出花朵的图案,这样绘制花朵的就保存在内部的 canvas 上。

cache 现在只负责绘制图案,所以与动画相关的 translate、rotate、scale 变换操作,全都移交给 drow 方法,并在渲染的目标 canvas 的 context 上进行操作。这里需要注意特别注意坐标 translate 变化。

所以现在每次动画循环时,flower 是不进行再绘制操作的,它只是将自身缓存的绘制图案通过目标的 context 的 drawImage 方法输出到渲染的 context 之上。少了 flower 的重绘画,渲染效率自然就就提高了。

这是优化后的花雨效果,渲染了 1000 朵花,不再有使用离屏渲染前卡顿了:


为花雨效果分层

解决完动画的性能问题,继续我们的 5 毛效果,看看还缺少些什么?

细看动画中的效果,整个场景是有明显的分层:

  • 背景的层的花朵数量众多、花朵偏小、颜色偏浅而且运动速度比前景层更快
  • 前景层的花朵数量较少,花朵偏大、颜色偏深、运动速度较慢
  • 中间层的花朵数量比前景数量更多,但远不及背景层,颜色大小与运动速度同理

我们所要做的就是按照这个规则,分别来绘制不同层级的花雨,所以这里我们将引入一个 Layer 类用于管理每层的花雨:

function Layer (options) {
  const { ctx, count, size, alpha, vy, vr, colors1, colors2 } = options
  const flowers = []
  for (let i = 0; i < count; i++) {
    const rsize = (random() * (size.max - size.min) + size.min) | 1
    const ralpha = random() * (alpha.max - alpha.min) + alpha.min
    const rvy = random() * (vy.max - vy.min) + vy.min
    const rvr = random() * (vr.max - vr.min) + vr.min
    const colors = [getRandomColor(colors1), getRandomColor(colors2)]
    flowers.push(new Flower(VW, VH, rsize, colors, ralpha, rvy, rvr))
  }
  this.context = context
  this.flowers = flowers
}
......

花雨将分为背景层、中间层与前景层三层来分别绘制,最终效果:


完整实例可以参考-花雨的完整实例 源码

到此为止一个粗糙的 5 毛花雨特效算是做好了,算是有几分形似吧。



参考资料:

Foundation HTML5 Animation with JavaScript

Canvas 最佳实践(性能篇)

发布于 2017-07-12