03.04 前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

以前經常看到這種效果:在網頁右下角放一個人,然後他的眼珠會跟著鼠標轉,效果如下:

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

這個例子來自於CodePen,它是根據鼠標的位置設置兩個眼球的transform: rotate屬性做的效果。

這種跟著鼠標移動的小交互一般都比較好玩,所以我突然想到,能不能做一隻會跟著鼠標走的小狗,最後的效果如下所示:

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

我們一步步來實現這個效果。

1. 小狗走的動畫

小狗走的動畫應該怎麼實現呢?如果用一張gif,然後根據鼠標的位置移動這張gif,那麼當鼠標停下來小狗不動的效果就做不了,因為gif一直在循環播放代碼控制不了這個行為。所以這種簡單方案是不可行的。

然後又想到之前用CSS的animation做過這種逐幀動畫:

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

所以就有思路了,小狗的動畫也是使用逐幀的動畫,並且用JS控制它的播放。

在網上搜羅了一番,還沒有人做過類似的動畫,不過找到了小狗的素材,這位老兄在教人怎麼畫行走的動物,剛好可以拿來當做我們的素材,把小狗摳出來:

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

2. 畫一隻在原地踏步的小狗

動畫的第一步先讓小狗原地踏步,即先讓這個動畫能播放起來,然後再做移動的動畫。所謂逐幀動畫就是每隔一小會就播放一幀,這樣連起來就是在動了。

寫一個canvas標籤,然後把它固定到頁面的底部:

<canvas>

然後設置寬度為頁面的100%:

let canvas = document.querySelector("#dog-walking");

canvas.width = window.innerWidth;

canvas.height = 200;

這樣我們就有一個畫布了。接著要把圖片畫讓去,先要把圖片加載下來,上面我們準備了9張png:0.png ~ 8.png,其中0.png是小狗停住不動的圖片,1.png ~ 8.png是小狗在走的圖片。

在JS裡面怎麼加載圖片呢,用新建一個Image實例的方式,如下代碼所示:

let img = new Image();

img.onload = function() {

beginDraw(img);

};

img.src = "dog/0.png";

由於圖片比較多,我們用類的方式組織我們的代碼,把數據當作類的屬性,方便存取。如下代碼所示:

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

把狗的圖片放到dogPictures數組裡面,在loadResources裡面進行加載,如下代碼所示:

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

這段加載圖片的代碼藉助了Promise,把每張圖片的加載都當作一個Promise的任務,統一放到一個數組裡面,然後再借助Promise.all就知道所有的任務都完成了。這樣就拿到了所有已onload的img對象,然後就可以拿來畫了。

在start函數里面添加一個畫的函數walk的執行:

async start() {

// 等待資源加載完

await this.loadResources();

this.walk();

}

walk() {

}

實際上為了畫逐幀動畫,我們要使用window.requestAnimationFrame,這個函數在瀏覽器畫它自己的動畫的下一幀之前會先調一下這個函數,理想情況下,1s有60幀,即幀率為60 fps。因為不管是播放視頻還是瀏覽網頁它們都是逐幀的,例如往下滾動網頁的時候就是一個滾動的動畫,所以瀏覽器本身也是在不斷地在畫動畫,只是當你的網頁停止不動時(且頁面沒有動畫元素),它可能會降低幀率減少資源消耗。

所以代碼改成這樣:

async start() {

await this.loadResources();

// 給下一幀註冊一個函數

window.requestAnimationFrame(this.walk.bind(this));

}

walk() {

// 繪製狗的圖片

// 繼續給下一幀註冊一個函數

window.requestAnimationFrame(this.walk.bind(this));

}

我們使用了一個bind(this),它的作用是讓walk函數的執行上下文還是指向當前類的實例。

現在怎麼讓狗動起來呢?最簡單的我們可以每隔0.1s就畫一幀,這樣就會連起來,形成一個動畫,為此我們需要記錄上一次畫的時間,然後判斷當前時間與上一次的時間是否大於0.1s,如果是的話就畫下一幀,否則什麼也不用幹。因為上文提過,1s最多有60幀,每一幀間隔 1s / 60 = 16.67ms。如下代碼所示,先在constructor添加幾個變量,包括一個記錄上一幀時間的變量:

constructor(canvas) {

this.canvas = canvas;

this.ctx = canvas.getContext("2d");

// 記錄上一幀的時間

this.lastWalkingTime = Date.now();

// 記錄當前畫的圖片索引

this.keyFrameIndex = -1;

this.start();

}

然後在walk函數里面進行繪製,在畫的時候每次畫都取下張圖片,即下一幀的圖片,不斷循環:

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

這樣我們就有了一隻在原地踏步的小狗:

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

然後讓它往前走。

3. 讓小狗往前走

上面在drawImage的傳參固定dx = 20,如果不斷加大這個dx,那麼它就往前走了。為此在構造函數里面添加一個變量記錄當前的位移,並設置小狗的速度:

constructor(canvas) {

// 小狗的速度

this.dogSpeed = 0.1;

// 小狗當前的位移

this.currentX = 0;

}

然後在walk函數里面計算當前累加的位移:

// 計算位移 = 時間 * 速度

let distance = (now - this.lastWalkingTime) * this.dogSpeed;

this.currentX += distance;

this.ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight,

// dx, dy, dwidth, dheight

this.currentX, 20, 186, 162)

但是這樣我們發現小狗走起路來一卡一卡的,不是很連貫:

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

這個是因為每0.1s畫一幀,幀率只有10fps,所以一走起來就不太行了。方法一是讓它走慢點,這樣可以減緩,但是如果想保持速度甚至提高速度的話,我們得想辦法優化一下。

4. 算法優化

考慮到狗的控制參數比較集中,把它們寫到一個dog的Object裡面:

constructor (canvas) {

this.dog = {

// 一步10px

stepDistance: 10,

// 狗的速度

speed: 0.15,

// 鼠標的x座標

mouseX: -1

};

}

主要有兩個參數,一個是狗的速度另一個是每一步走的位移,然後計算距離方式變成:

let now = Date.now();

let distance = (now - this.lastWalkingTime) * this.dog.speed;

if (distance < this.dog.stepDistance) {

window.requestAnimationFrame(this.walk.bind(this));

return;

}

每一步至少走10px,如果小於這個數的話就不走了。通過每步的位移和速度這兩個參數可以很方便地控制狗走的快慢和幀率,例如把stepDistance改小點,speed提高就會走得比較頻繁,能提高幀率,上面設置的幀率是14 fps. 不過幀率低的根本原因還是在於小狗走路的圖片較少。

5. 走到鼠標的位置停下

給小狗添加一個停留的位置,包括往前走和往後走的,因為一個是鼠標在圖片前面,一個是鼠標在圖片的後面,需要區分:

this.dog = {

// 往前走停留的位置

frontStopX: -1,

// 往回走停留的位置,

backStopX: window.innerWidth,

};

然後添加一個記錄鼠標位置的函數,主要是監聽mousemove事件:.

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

然後在walk函數里面用一個變量stopWalking表示小狗是否停下來,和一個direct表示小狗的方向:

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

如果小狗沒有停,計算位置的時候乘以direct:

// 計算位置

if (!stopWalking) {

this.dog.mouseX += this.dog.stepDistance * direct;

}

如果小狗停了,則mouseX還是上次的值。

鼠標停留在小狗位置的那段代碼可以做個優化,如果鼠標在小狗中間的右邊,則方向調整為正,否則為負:

// 如果鼠標在狗在位置

else {

stopWalking = true;

// 如果鼠標在小狗圖片中間的右邊,則direct為正,否則為負

direct = this.dog.backStopX - this.dog.mouseX

> this.pictureWidth / 2 ? 1 : -1;

this.keyFrameIndex = -1;

}

這樣鼠標在小狗左右來回移動時,小狗會轉頭。

得到小狗的位置和方向之後就是畫上去,正方向的還好,反方向的由於沒圖片,我們通過canvas的翻轉flip進行繪製,如下代碼所示:

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

這樣基本上就完成了,最後一個問題是小狗初始化位置的擺放,如果你要把它擺在右邊的話,那需要把它的方向反轉一下,擺在最左邊也需要。不然你會發現小狗擺在左邊,但它的頭朝左了,需要轉一下放在右邊。

圖片的素材和繪製過程已說得很詳細,讀者可以自行實現,或者想其它一些跟著鼠標動的交互效果。

對想學習前端的小夥伴,小編給你們準備了全套前端電子版書籍,包含了目前大部分前端開發的書

領取方式,先關注小編,然後私信發“前端”,就能獲取全套書籍,你還不心動嗎?心動就趕緊發私信 ,獲取資源

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

【私信方法】文章上方處點擊“作者頭像”,進入作者首頁,右上角點擊“發私信” 即可

前端特效,用Canvas畫一隻會跟著鼠標走的小狗,文末有福利分享

前端 特效 開發 編程語言 互聯網 微信 代碼 程序員


分享到:


相關文章: