《那朵花》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 最佳實踐(性能篇)


推薦閱讀:
相关文章