以前經(jīng)晨瓯浚看到這種效果:在網(wǎng)頁右下角放一個(gè)人憔鬼,然后他的眼珠會(huì)跟著鼠標(biāo)轉(zhuǎn),效果如下:
請(qǐng)點(diǎn)擊此處輸入圖片描述
這個(gè)例子來自于CodePen胃夏,它是根據(jù)鼠標(biāo)的位置設(shè)置兩個(gè)眼球的transform: rotate屬性做的效果逊彭。
這種跟著鼠標(biāo)移動(dòng)的小交互一般都比較好玩,所以我突然想到构订,能不能做一只會(huì)跟著鼠標(biāo)走的小狗侮叮,最后的效果如下所示:
請(qǐng)點(diǎn)擊此處輸入圖片描述
我們一步步來實(shí)現(xiàn)這個(gè)效果。
1. 小狗走的動(dòng)畫
小狗走的動(dòng)畫應(yīng)該怎么實(shí)現(xiàn)呢悼瘾?如果用一張gif囊榜,然后根據(jù)鼠標(biāo)的位置移動(dòng)這張gif,那么當(dāng)鼠標(biāo)停下來小狗不動(dòng)的效果就做不了亥宿,因?yàn)間if一直在循環(huán)播放代碼控制不了這個(gè)行為卸勺。所以這種簡單方案是不可行的。
然后又想到之前用CSS的animation做過這種逐幀動(dòng)畫:
請(qǐng)點(diǎn)擊此處輸入圖片描述
所以就有思路了烫扼,小狗的動(dòng)畫也是使用逐幀的動(dòng)畫曙求,并且用JS控制它的播放。
在網(wǎng)上搜羅了一番映企,還沒有人做過類似的動(dòng)畫悟狱,不過找到了小狗的素材,這位老兄在教人怎么畫行走的動(dòng)物堰氓,剛好可以拿來當(dāng)做我們的素材挤渐,把小狗摳出來:
請(qǐng)點(diǎn)擊此處輸入圖片描述
2. 畫一只在原地踏步的小狗
動(dòng)畫的第一步先讓小狗原地踏步,即先讓這個(gè)動(dòng)畫能播放起來双絮,然后再做移動(dòng)的動(dòng)畫浴麻。所謂逐幀動(dòng)畫就是每隔一小會(huì)就播放一幀得问,這樣連起來就是在動(dòng)了。
寫一個(gè)canvas標(biāo)簽软免,然后把它固定到頁面的底部:
然后設(shè)置寬度為頁面的100%:
let canvas = document.querySelector("#dog-walking");
canvas.width = window.innerWidth;
canvas.height = 200;
這樣我們就有一個(gè)畫布了宫纬。接著要把圖片畫讓去,先要把圖片加載下來膏萧,上面我們準(zhǔn)備了9張png:0.png ~ 8.png漓骚,其中0.png是小狗停住不動(dòng)的圖片,1.png ~ 8.png是小狗在走的圖片向抢。
在JS里面怎么加載圖片呢,用新建一個(gè)Image實(shí)例的方式胚委,如下代碼所示:
let img = new Image();
img.onload = function() {
? ? beginDraw(img);
};
img.src = "dog/0.png";
由于圖片比較多挟鸠,我們用類的方式組織我們的代碼,把數(shù)據(jù)當(dāng)作類的屬性亩冬,方便存取艘希。如下代碼所示:
請(qǐng)點(diǎn)擊此處輸入圖片描述
把狗的圖片放到dogPictures數(shù)組里面,在loadResources里面進(jìn)行加載硅急,如下代碼所示:
請(qǐng)點(diǎn)擊此處輸入圖片描述
這段加載圖片的代碼借助了Promise覆享,把每張圖片的加載都當(dāng)作一個(gè)Promise的任務(wù),統(tǒng)一放到一個(gè)數(shù)組里面营袜,然后再借助Promise.all就知道所有的任務(wù)都完成了撒顿。這樣就拿到了所有已onload的img對(duì)象,然后就可以拿來畫了荚板。
在start函數(shù)里面添加一個(gè)畫的函數(shù)walk的執(zhí)行:
async start() {
? ? // 等待資源加載完
? ? await this.loadResources();
? ? this.walk();?
}
walk() {
}
實(shí)際上為了畫逐幀動(dòng)畫凤壁,我們要使用window.requestAnimationFrame,這個(gè)函數(shù)在瀏覽器畫它自己的動(dòng)畫的下一幀之前會(huì)先調(diào)一下這個(gè)函數(shù)跪另,理想情況下拧抖,1s有60幀,即幀率為60 fps免绿。因?yàn)椴还苁遣シ乓曨l還是瀏覽網(wǎng)頁它們都是逐幀的唧席,例如往下滾動(dòng)網(wǎng)頁的時(shí)候就是一個(gè)滾動(dòng)的動(dòng)畫,所以瀏覽器本身也是在不斷地在畫動(dòng)畫嘲驾,只是當(dāng)你的網(wǎng)頁停止不動(dòng)時(shí)(且頁面沒有動(dòng)畫元素)淌哟,它可能會(huì)降低幀率減少資源消耗。
所以代碼改成這樣:
async start() {
? ? await this.loadResources();
? ? // 給下一幀注冊(cè)一個(gè)函數(shù)
? ? window.requestAnimationFrame(this.walk.bind(this));
}
walk() {
? ? // 繪制狗的圖片?
? ? // 繼續(xù)給下一幀注冊(cè)一個(gè)函數(shù)
? ? window.requestAnimationFrame(this.walk.bind(this));
}
我們使用了一個(gè)bind(this)辽故,它的作用是讓walk函數(shù)的執(zhí)行上下文還是指向當(dāng)前類的實(shí)例绞绒。
現(xiàn)在怎么讓狗動(dòng)起來呢?最簡單的我們可以每隔0.1s就畫一幀榕暇,這樣就會(huì)連起來蓬衡,形成一個(gè)動(dòng)畫喻杈,為此我們需要記錄上一次畫的時(shí)間,然后判斷當(dāng)前時(shí)間與上一次的時(shí)間是否大于0.1s狰晚,如果是的話就畫下一幀筒饰,否則什么也不用干。因?yàn)樯衔奶徇^壁晒,1s最多有60幀瓷们,每一幀間隔 1s / 60 = 16.67ms。如下代碼所示秒咐,先在constructor添加幾個(gè)變量谬晕,包括一個(gè)記錄上一幀時(shí)間的變量:
constructor(canvas) {
? ? this.canvas = canvas;
? ? this.ctx = canvas.getContext("2d");
? ? // 記錄上一幀的時(shí)間
? ? this.lastWalkingTime = Date.now();?
? ? // 記錄當(dāng)前畫的圖片索引
? ? this.keyFrameIndex = -1;?
? ? this.start();
}
然后在walk函數(shù)里面進(jìn)行繪制,在畫的時(shí)候每次畫都取下張圖片携取,即下一幀的圖片攒钳,不斷循環(huán):
請(qǐng)點(diǎn)擊此處輸入圖片描述
這樣我們就有了一只在原地踏步的小狗:
請(qǐng)點(diǎn)擊此處輸入圖片描述
然后讓它往前走。
3. 讓小狗往前走
上面在drawImage的傳參固定dx = 20雷滋,如果不斷加大這個(gè)dx不撑,那么它就往前走了。為此在構(gòu)造函數(shù)里面添加一個(gè)變量記錄當(dāng)前的位移晤斩,并設(shè)置小狗的速度:
constructor(canvas) {
? ? // 小狗的速度
? ? this.dogSpeed = 0.1;
? ? // 小狗當(dāng)前的位移
? ? this.currentX = 0;
}
然后在walk函數(shù)里面計(jì)算當(dāng)前累加的位移:
// 計(jì)算位移 = 時(shí)間 * 速度
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)
但是這樣我們發(fā)現(xiàn)小狗走起路來一卡一卡的焕檬,不是很連貫:
請(qǐng)點(diǎn)擊此處輸入圖片描述
這個(gè)是因?yàn)槊?.1s畫一幀,幀率只有10fps澳泵,所以一走起來就不太行了实愚。方法一是讓它走慢點(diǎn),這樣可以減緩兔辅,但是如果想保持速度甚至提高速度的話爆侣,我們得想辦法優(yōu)化一下。
4. 算法優(yōu)化
考慮到狗的控制參數(shù)比較集中幢妄,把它們寫到一個(gè)dog的Object里面:
constructor (canvas) {
? ? this.dog = {
? ? ? ? // 一步10px
? ? ? ? stepDistance: 10,
? ? ? ? // 狗的速度
? ? ? ? speed: 0.15,
? ? ? ? // 鼠標(biāo)的x坐標(biāo)
? ? ? ? mouseX: -1
? ? };
}
主要有兩個(gè)參數(shù)兔仰,一個(gè)是狗的速度另一個(gè)是每一步走的位移,然后計(jì)算距離方式變成:
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蕉鸳,如果小于這個(gè)數(shù)的話就不走了乎赴。通過每步的位移和速度這兩個(gè)參數(shù)可以很方便地控制狗走的快慢和幀率,例如把stepDistance改小點(diǎn)潮尝,speed提高就會(huì)走得比較頻繁榕吼,能提高幀率,上面設(shè)置的幀率是14 fps. 不過幀率低的根本原因還是在于小狗走路的圖片較少勉失。
5. 走到鼠標(biāo)的位置停下
給小狗添加一個(gè)停留的位置羹蚣,包括往前走和往后走的,因?yàn)橐粋€(gè)是鼠標(biāo)在圖片前面乱凿,一個(gè)是鼠標(biāo)在圖片的后面顽素,需要區(qū)分:
this.dog = {
? ? // 往前走停留的位置
? ? frontStopX: -1,
? ? // 往回走停留的位置,
? ? backStopX: window.innerWidth,
};
然后添加一個(gè)記錄鼠標(biāo)位置的函數(shù)咽弦,主要是監(jiān)聽mousemove事件:.
請(qǐng)點(diǎn)擊此處輸入圖片描述
然后在walk函數(shù)里面用一個(gè)變量stopWalking表示小狗是否停下來,和一個(gè)direct表示小狗的方向:?
請(qǐng)點(diǎn)擊此處輸入圖片描述
如果小狗沒有停胁出,計(jì)算位置的時(shí)候乘以direct:
// 計(jì)算位置
if (!stopWalking) {
? ? this.dog.mouseX += this.dog.stepDistance * direct;
}
如果小狗停了型型,則mouseX還是上次的值。
鼠標(biāo)停留在小狗位置的那段代碼可以做個(gè)優(yōu)化全蝶,如果鼠標(biāo)在小狗中間的右邊闹蒜,則方向調(diào)整為正,否則為負(fù):
// 如果鼠標(biāo)在狗在位置
else {
? ? stopWalking = true;
? ? // 如果鼠標(biāo)在小狗圖片中間的右邊抑淫,則direct為正绷落,否則為負(fù)
? ? direct = this.dog.backStopX - this.dog.mouseX?
? ? ? ? ? ? ? ? ? ? > this.pictureWidth / 2 ? 1 : -1;?
? ? this.keyFrameIndex = -1;
}
這樣鼠標(biāo)在小狗左右來回移動(dòng)時(shí),小狗會(huì)轉(zhuǎn)頭始苇。
得到小狗的位置和方向之后就是畫上去砌烁,正方向的還好,反方向的由于沒圖片埂蕊,我們通過canvas的翻轉(zhuǎn)flip進(jìn)行繪制往弓,如下代碼所示:
請(qǐng)點(diǎn)擊此處輸入圖片描述
這樣基本上就完成了疏唾,最后一個(gè)問題是小狗初始化位置的擺放蓄氧,如果你要把它擺在右邊的話,那需要把它的方向反轉(zhuǎn)一下槐脏,擺在最左邊也需要喉童。不然你會(huì)發(fā)現(xiàn)小狗擺在左邊,但它的頭朝左了顿天,需要轉(zhuǎn)一下放在右邊堂氯。
圖片的素材和繪制過程已說得很詳細(xì),讀者可以自行實(shí)現(xiàn)牌废,或者想其它一些跟著鼠標(biāo)動(dòng)的交互效果咽白。
對(duì)想學(xué)習(xí)前端的小伙伴,小編給你們準(zhǔn)備了全套前端電子版書籍鸟缕,包含了目前大部分前端開發(fā)的書
領(lǐng)取方式晶框,加前端學(xué)習(xí)群?330336289 獲取 邀請(qǐng)碼 寂靜
??前端 特效 開發(fā) 編程語言 ?互聯(lián)網(wǎng) ?微信 代碼 程序員