前端特效移层,用Canvas畫一只會(huì)跟著鼠標(biāo)走的小狗,文末有福利分享

以前經(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) ?微信 代碼 程序員

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市懂从,隨后出現(xiàn)的幾起案子授段,更是在濱河造成了極大的恐慌,老刑警劉巖番甩,帶你破解...
    沈念sama閱讀 223,002評(píng)論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侵贵,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡缘薛,警方通過查閱死者的電腦和手機(jī)窍育,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門卡睦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蔫骂,你說我怎么就攤上這事么翰。” “怎么了辽旋?”我有些...
    開封第一講書人閱讀 169,787評(píng)論 0 365
  • 文/不壞的土叔 我叫張陵浩嫌,是天一觀的道長。 經(jīng)常有香客問我补胚,道長码耐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,237評(píng)論 1 300
  • 正文 為了忘掉前任溶其,我火速辦了婚禮骚腥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瓶逃。我一直安慰自己束铭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,237評(píng)論 6 398
  • 文/花漫 我一把揭開白布厢绝。 她就那樣靜靜地躺著契沫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪昔汉。 梳的紋絲不亂的頭發(fā)上懈万,一...
    開封第一講書人閱讀 52,821評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音靶病,去河邊找鬼会通。 笑死,一個(gè)胖子當(dāng)著我的面吹牛娄周,可吹牛的內(nèi)容都是我干的涕侈。 我是一名探鬼主播,決...
    沈念sama閱讀 41,236評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼煤辨,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼裳涛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起掷酗,我...
    開封第一講書人閱讀 40,196評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤调违,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后泻轰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體技肩,經(jīng)...
    沈念sama閱讀 46,716評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,794評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了虚婿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片旋奢。...
    茶點(diǎn)故事閱讀 40,928評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖然痊,靈堂內(nèi)的尸體忽然破棺而出至朗,到底是詐尸還是另有隱情,我是刑警寧澤剧浸,帶...
    沈念sama閱讀 36,583評(píng)論 5 351
  • 正文 年R本政府宣布锹引,位于F島的核電站,受9級(jí)特大地震影響唆香,放射性物質(zhì)發(fā)生泄漏嫌变。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,264評(píng)論 3 336
  • 文/蒙蒙 一躬它、第九天 我趴在偏房一處隱蔽的房頂上張望腾啥。 院中可真熱鬧,春花似錦冯吓、人聲如沸倘待。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凸舵。三九已至,卻和暖如春锣披,著一層夾襖步出監(jiān)牢的瞬間贞间,已是汗流浹背贿条。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評(píng)論 1 274
  • 我被黑心中介騙來泰國打工雹仿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人整以。 一個(gè)月前我還...
    沈念sama閱讀 49,378評(píng)論 3 379
  • 正文 我出身青樓胧辽,卻偏偏與公主長得像,于是被迫代替她去往敵國和親公黑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子邑商,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,937評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,343評(píng)論 25 707
  • 一:canvas簡介 1.1什么是canvas? ①:canvas是HTML5提供的一種新標(biāo)簽 ②:HTML5 ...
    GreenHand1閱讀 4,693評(píng)論 2 32
  • 用到的組件 1凡蚜、通過CocoaPods安裝 2人断、第三方類庫安裝 3、第三方服務(wù) 友盟社會(huì)化分享組件 友盟用戶反饋 ...
    SunnyLeong閱讀 14,629評(píng)論 1 180
  • 文/舒夏 和一個(gè)同學(xué)聊天,他向我抱怨奈附,感覺最近自己的狀態(tài)特別差全度,每天都在虛度光陰,頹廢的想抽自己一耳光斥滤。她最近...
    舒夏閱讀 467評(píng)論 6 4