今年聖誕節我們做了一個軌跡特效套件

fin
嗨,世界
Published in
8 min readDec 24, 2021
live demo: https://codesandbox.io/s/cursor-trail-demo-fqsd4?file=/index.html

緣起

偶然在網路上看到個上古時代的語法

<MARQUEE><BLINK>你覺得這訊息會怎麼顯示?</BLINK></MARQUEE>

除了驚訝 marquee 仍然可以用以外

safari 歷經千辛萬苦終於在 15.1 支援了 marquee (咦?)

回想起曾經流行過的滑鼠軌跡效果,但估狗後發現大部分是告訴你如何在 window 上面設定變成這樣

最後看到的是這個 90’s Cursor Effects (tholman.com) ,曾經被 stackoverflow 拿來做愚人節復古特效的元素之一,這其中的雪花效果最後變成了 Yourator 聖誕節慶的元素,以下是 Yourator 首頁的動態影片。為此,我們還把這特效弄成了套件,以下是這個功能開發的一些紀錄。

https://www.yourator.co/ 只到這個週末的特效,為此,我們還把這特效弄成了套件

先來看一下原始的 Cursor-effect

src: https://github.com/tholman/cursor-effects/blob/master/src/snowflakeCursor.js

原始碼不長,挑幾個重點來看。首先, 程式架構從init 展開

整個功能的基本結構都在這裡了:

  1. 初始化雪花放置的環境 canvas
  2. 把 possibleEmoji 內的字串(emoji)變成一個個獨立的 canvas
  3. bindEvents 監聽滑鼠&觸控事件
  4. loop 持續更新每個雪花的動態

透過 bindevent 監聽滑鼠&觸控事件

onMouseMove onTouchMove 裡面做的是一樣的事情,在游標 / 觸控點移動時,呼叫 addParticle 把雪花建立在游標所在位置。

onWindowResize 則是負責把 canvas 調整成當前容器的大小

你可能會想問可以用 CSS width: 100%, height: 100% 嗎?
答案是不行,canvas 是一張固定解析度的畫布,透過 style 只是讓瀏覽器把畫布做變形,但畫布本身的解析度是不變的。這會造成一圖片被壓縮或放大、位置偏離等問題。所以必須在每次視窗尺寸變動時也跟著調整 canvas 寬高

init 的最後還呼叫了一個 loop ,建立了無窮迴圈來更新雪花動態,呼叫所有 particles 的 update 方法以及清除過期的 particle,這方法是整個功能最常被執行到的地方 ( 152-178 行)。每一個 Particle 每一個 requestAnimationFrame 都會被呼叫到。注意這裡用的是 requestAnimationFrame,只要跟繪製渲染有關的,都建議使用此方法把動畫效果與瀏覽器渲染的時間連結起來。

particle update 本身則是負責記錄剩餘 lifespan、位置、旋轉以及把自己的位置更新到 canvas context 上

還有一些工作要做

原有的 cursor-effect 滿足了八成我們的需求,但想實際拿來在網站上使用,還有一些問題要解決:

  • 使用雪花 emoji ❄️ ,無法自行替換為圖片
  • 除了雪花想替換為自己的圖片之外,也想控制雪花的出現頻率以及速度、時間等
  • 觸控裝置會同時觸發 mousemove & touchstart,單純點擊容易導致同時畫出兩個雪花
  • 引用方式有點老舊,無法使用套件管理

開始調整

使用雪花 emoji ❄️,無法自行替換為圖片

原來是使用 fillText 加上一些自行設定,使用圖片時比較簡單直接改為 drawImage 加上位置資訊等,順便加了個透明度的設定 globalAlpha 。因為這段對 canvas context 有諸多的操作 globalAlpha, rotate, translate,所以在前後加上 save & restore 避免這些 context 環境影響到其他 particle

因為圖片載入是非同步的,加上有可能出現問題,因此另外以一個 loadImage 函式處理

加上 Promise all 來處理多張圖片的載入(其實也可以使用 Promise.allSettled 來處理),注意這裡會把 null 濾掉因為在載入時如果有錯誤就會 resolve null。如此即會等到載入圖片後才做 init 的呼叫。

控制雪花的出現頻率以及速度、時間等

希望可以有更多的彈性比如自訂顯示圖片、控制雪化的數量、顯示時間等等。抽成參數的具體好處就是討論上更快速了,可以在討論時即時調整參數甚至交由 stakeholder 自己去調整參數來玩。

參數化很簡單,原有的進入點多吃幾個參數即可,注意提供預設值。再把有用到的地方替換成相關參數即可

觸控裝置單純點擊會同時畫出兩個雪花

前述 bindEvent 針對 mousemove touchstart touchmove 做監聽,不過在觸控裝置上的觸碰會同時觸發 touchstart 以及 mousemove ,因而產生兩個 particle,原因見此。這裡就需要另外做裝置類型的判斷

引用方式老舊

原有寫法直接掛在 window 下,因此直接改成 npm module,這樣比較好做套件管理,後續維護上也比較單純。

之後有需要就 npm install cursor-trails 即可

yourator/cursor-trails (github.com)

關於 Canvas 與 Transition

一開始拿 cursor-effect 改為客製化圖片之後效能變得很差,原本以為是 canvas 在大量動態繪製圖片的問題,還用 CSS Transition 重寫了一個版本。後來才發現是 SVG 惹的禍,大量的 SVG 產生不管是 canvas 還是 transition 都會有明顯的 FPS 降低狀況。

以 cursor trail 來說 canvas 的確是比較理想的方法,因為是大量的圖片在固定的區域裡面重複繪製。Transition 則是比較適合用來讓頁面上(通常是已經有的) DOM 元素產生一些動態。比起在指標移動的當下產生 img 元素,直接把圖片畫到 canvas 上明顯快上許多。

這裡就要講到 Chrome Devtool 下的算繪功能了,閃動顯示繪製區域&畫格算會統計資料很好用

直接用實際作用影片來展示

這是某一版用 css transition 的,可以看到只有新的 image 會被 repaint

其他考量

特效本身看起來酷炫,但考量到 Yourator 網站每天面對大量求職者以及企業的使用,該避免過度的特效造成使用者本身的不適。幾經討論後決定只把特效放在首頁的主要搜尋區塊內,搭配上季節風格的 logo ,在門面帶入一些聖誕的氛圍,同時也希望不過度影響使用者日常的操作。

另外有些運算能力比較不足的裝置仍有可能遇上卡頓的問題,本來想用 prefers-reduced-motion 來處理,但時間有限,這部分就待下次用到時再來加強了。

這次針對聖誕節,動態部分固定就是模擬雪花落下的路徑,之後希望可以直接指定不同的類型,讓動態可以有不同的變化,ex: 固定、漂浮、淡出等。

總之,大家來玩玩
https://github.com/yourator/cursor-trails

參考資料

  • 事後紀錄時查了資料才發現,原來前年 Stackoverflow 也有做過類似的事情

--

--