這個章節呢,同樣會以實作為主,在解決問題中帶大家學習,逐漸引入JS的語言特性,前面一樣會從簡單的開始,後面八成會開始越講越快,若有不明白的歡迎留言詢問!
身為程式人,要懂得拆解和觀察,可以先想想看"什麼叫做動畫",一幅會動的畫?還是一個移動中的圖像?在Canvas上畫畫我們已經學會了,上個章節透過不斷更新頻譜數據,畫出來的直方圖,也是一種持續變化的動畫,因此要思考的是"動"這件事,要滿足動,也就是位移,只要能掌握路徑,便能掌握動畫,咦?最長在動的不就是咱們的滑鼠嗎,那今天就用滑鼠帶大家學動畫吧!
參數回顧:
讓我們沿用上個章節的架構:
let mouseImg = new Image();
mouseImg.src = "../mouse.png";
let mouseX = 0, mouseY = 0;
canvas.addEventListener('mousemove', function (e) {
let Rect = canvas.getBoundingClientRect();
mouseX = (e.pageX - Rect.left) * RATIO;
mouseY = (e.pageY - Rect.top) * RATIO;
}, false);
function MouseAnime(){
context.drawImage(MouseImg, mouseX, mouseY);
}
pageX, pageY 對應到的實際畫面大小,計算其在畫布上的相對位置,要乘上我們當時設計的 RATIO
若考量到RWD所用到的touchmove,給大家參考:
if(window.innerWidth < 992)
canvas.addEventListener('touchmove', function (e) {
let Rect = canvas.getBoundingClientRect();
mouseX = (e.touches[0].pageX - Rect.left) * RATIO;
mouseY = (e.touches[0].pageY - Rect.top) * RATIO;
}, false);
用breakpoint來判斷,不是mousemove就是touchmove,才不會在移動端重覆執行
這個坑實在有點不想踩,又要拿一些進階的名詞,是後面章節才會提到的,簡單來說就是 Image 實例化的物件(mouseImg)並不會在你賦予它圖片路徑後,就直接讀取圖片,要是圖片太大讀取太久,那程式不就會卡住嗎?因此它用到的是非同步的方式載入,等到圖片載入完畢後,就會觸發 load 事件,我們可以自行定義其內容,來決定「當圖片載入後,要做什麼反應」。
如果你正心想:「什麼是類名?什麼是實例化?什麼是非同步?」這些不知道沒關係,我們未來有機會再娓娓道來,這邊只需要知道「如果在設定圖片路徑後,立刻呼叫drawImage,則瀏覽器會拋出錯誤,彷彿mouseImg不存在一樣」,因此這邊最直覺的解決方案便是:
mouseImg.onload = () => {
AnimationLoop();
}
在圖片載入完畢後,再呼叫動畫循環
這樣解決雖然很直白,但把整個動畫循環的開始,完全依賴於一個圖片的讀取,實在是不太靠譜,萬一這個檔案就丟了,讀取不到怎麼辦呢?因此較好的做法應該是以下這兩種:
function MouseAnime(){
if(MouseImg.complete) context.drawImage(mouseImg, mouseX, mouseY);
}
圖片物件身上有complete屬性,圖片載入時為false,完成後為true,若MouseImg未讀取完畢,則暫時不進行繪製
不過有一種狀況例外,就是當圖片的路徑有問題時,由於是非同步載入,這個路徑的錯誤並不會中斷整個網頁的運行,這固然是一個優點(畢竟網路上的圖片總會經常弄丟),只是問題在於由於載入的過程被中斷了,MouseImg.complete
仍然會是預設值true,並沒有進到載入中的狀態,直接用上面的代碼就會讓人有「摁?complete == true
已經載入完成了,卻還是沒有圖片」的錯覺,於是我們還可以這麼做:
function MouseAnime(){
if(mouseImg.width) return; // 若寬度為0,表示圖片來源未正確設定,直接中斷
if(MouseImg.complete) context.drawImage(mouseImg, mouseX, mouseY);
else context.drawImage(MouseImg, mouseX, mouseY);
}
圖片物件建立之初,會預設寬高屬性為0,因此我們可以選用width來檢查代碼
聰明的你是不是也想到了什麼呢?沒錯,上個章節用到的HTMLMediaElement(aduio),其實也是需要讀取的!相比load事件,它的事件名稱長了些,叫做loadeddata。那當初為什麼程式沒出錯?可以說我們運氣好,也可以說我們的架構設計的還不錯,這些細節的部份在最後的章節,將會一口氣對整個架構進行細節的調整。
於是乎,這樣就有一個基本的圖形會隨時在滑鼠的位置了,不過它的貼圖位置是以坐標點為左上角,因此會發現滑鼠總是在圖案的左上角,因此若要置中,可以用第二種寫法來設定寬高,並把圖片偏移左上,使中心剛好為滑鼠座標,這邊為了方便,以長寬1:1的圖形舉例:
function MouseAnime(){
let size = WIDTH * 0.1;
context.drawImage(mouseImg, mouseX-size/2, mouseY-size/2, size, size);
}
考量到RWD,大小也是用WIDTH決定(畫布總寬度)
context.drawImage(mouseImg, 0, 0, mouseImg.width, mouseImg.height,
mouseX-size/2, mouseY-size/2, size, size);
↑這邊也同步提供第三種格式給大家參考,前面四個參數可以讓你對圖片進行裁切,不過我個人是不建議把事情做得這麼複雜,最好的方式是在製作素材的時候就先設計好寬高的比例,然後取得寬高比,這樣,不論是什麼圖片,都不用重新做設定:
function MouseAnime(){
let imgRatio = mouseImg.height / mouseImg.width;
let w = WIDTH * 0.1;
let h = WIDTH * 0.1 * imgRatio;
context.drawImage(mouseImg, mouseX-w/2, mouseY-h/2, w, h);
}
那麼,終於讓滑鼠總是待在圖片的正中心了,不過,單純只有這樣的話,你會發現,隨著鼠標的移動,圖片有非常明顯的瞬移現象,動畫的品質是低劣的,因此我們要來設計一個緩衝的方案了,一開始談數學公式太複雜了,我們就先思考「希望圖案不要立刻到滑鼠的位置」這件事,那是不是就不要馬上衝過去,可以讓位移變少?
這幾種方案是不是就馬上浮現腦袋了,接下來分析他們會遇到的問題,用減法的話,比方說緩衝設為100,那是不是圖案最後只會停在距離滑鼠100的地方?那顯然不行,如果用固定每次移動5格呢?欸這辦法似乎挺不錯的,只是當距離超過500,那是不是要移動100次才夠呢?似乎再稍微修改一下,就可以得到答案了。
因應上面的問題,我們需要一個方案:「在距離小的時候,移動變少;在距離大的時候,移動變多」,巧了,這意思不就是距離和位移呈正比嗎,並且位移要比距離還小,那麼只要這麼設計就大功告成了:
位移 = 距離 / 緩衝
function MouseAnime(){
cursorX+= (mouseX - cursorX) / 5;
cursorY+= (mouseY - cursorY) / 5;
let size = WIDTH * 0.1;
context.drawImage(mouseImg, cursorX-size/2, cursorY-size/2, size, size);
}
如果有格式控覺得不太好看的話,還有另一種相同效果的版本
cursorX = (cursorX * 4 + mouseX) / 5;
cursorY = (cursorY * 4 + mouseY) / 5;
此時我們發現,圖案就像是跟隨著滑鼠一樣,有自己的運動軌跡,看起來也更加順暢了,嚴格來說,它已經不是鼠標了,就是個小跟班
接下來就可以練習嘗試做個函式,在加入時間這個概念前,先來製作一個基本的互動模型,讓滑鼠點擊後,讓動畫暫停,釋放滑鼠後,再讓動畫繼續進行:
let isPathing = false;
canvas.addEventListener('mousedown', () => isPathing = true);
canvas.addEventListener('mousemove', GetMouse);
canvas.addEventListener('mouseup', () => isPathing = false);
canvas.addEventListener('mouseout', () => isPathing = false);
function GetMouse(e){
let Rect = canvas.getBoundingClientRect();
mouseX = (e.pageX - Rect.left) * RATIO;
mouseY = (e.pageY - Rect.top) * RATIO;
}
function MouseAnime(){
if(!isPathing){
cursorX+= (mouseX - cursorX) / 5;
cursorY+= (mouseY - cursorY) / 5;
}
let size = WIDTH * 0.1;
context.drawImage(mouseImg, cursorX-size/2, cursorY-size/2, size, size);
}
在滑鼠按下的時候,先鎖住游標圖案的座標,讓它保持不變,等放開滑鼠時,再一次釋放它!有沒有很像要喊三二一讓賽車衝出去的感覺呢?
接下來我們給它一定的時間,單位為偵數,60偵大約為1秒
let period = 0, timer = 0;
let distanceX = 0, distanceY = 0;
canvas.addEventListener('mouseup', GetDistance);
canvas.addEventListener('mouseout', GetDistance);
function GetDistance(){
distanceX = (mouseX - cursorX);
distanceY = (mouseY - cursorY);
period = 90;
timer = 90;
isPathing = false;
}
function MouseAnime(){
if(!isPathing){
if(timer > 0){
cursorX+= distanceX / period;
cursorY+= distanceY / period;
timer--;
}
else{
cursorX+= (mouseX - cursorX) / 5;
cursorY+= (mouseY - cursorY) / 5;
}
}
let size = WIDTH * 0.1;
context.drawImage(mouseImg, cursorX-size/2, cursorY-size/2, size, size);
}
這樣就完成一個等速動畫了,可以參考 Demo,接下來幾天會陸續更新!
時間也不早了,出個思考題吧
如果想要讓圖案啟動時,慢慢出發,一邊加速一邊抵達鼠標的位置,可以怎麼做呢?