2023-09-02|閱讀時間 ‧ 約 8 分鐘

挑戰 JS30 #1 - JavaScript Drum Kit

這系列文章會記錄 JS30 當次挑戰時使用到的語法和相關知識。

JS30 官方有提供所有製作網頁的資源,不需要任何事前準備,就能無痛開始撰寫 JS,寫完之後還有 JS30 作者提供的解答,不知道怎麼下手時可以參考、寫完之後也能了解更多解法,改善自己的寫法。

本次的挑戰是「JavaScript Drum Kit」,需要達成的目標是:

「使用者按下鍵盤特定按鍵,按鍵會發光、同時播放相對應的音效。」

根據上面這個目標,大略知道該挑戰牽涉到以下面向:

  • 事件綁定、鍵盤事件
  • 流程控制
  • 操作音效物件
  • DOM 操作 - 包括樣式修改、取得 DOM 元素等

嘗試拆解流程

流程圖

這個挑戰有個小細節,就是每次按下鍵盤後,都必須將音效時間軸歸零,這樣在每次觸發時才會從頭開始播放音效。

知識點

音效物件

網頁中引入音效可以使用 <audio> 元素,原來 audio 本身有 API 可以進行操作,相當方便,以下就來了解看看吧。

<audio> 標籤

<audio​ src="filePath"></audio>

常見屬性

  • autoplay:自動播放音檔
  • controls:出現控制條
  • loop:循環播放

有看到一些文章會建議搭配 source 標籤,避免某些音檔格式瀏覽器不支援,而發生預期外的效果。如果 source 的第一個格式不支援,就會向下讀取其他項目,通常 mp3 會是最普及的格式,可以放在最後。

<audio autoplay>
<source src="xxx.ogg">
<source src="xxx.mp3">
你的瀏覽器不支援 HTML5 音訊格式。
</audio>

audio 相關 API

const audio = document.querySelector("audio")

//開始播放​
audio.play();

//暫停播放​
audio.pause();

//將音檔秒數歸零​
//audio 物件還有許多屬性,幾乎完全可以透過 JS 控制 audio
audio.currentTime = 0;

<kbd> 標籤

語意標籤,表示「鍵盤輸入按鍵」,預設樣式就會將字型變為等寬字(monospace)

附上此次實作結果

更好的寫法

參考作者的寫法,發現在取得 DOM 元素時就先篩選出符合的元素,但筆者在這邊都使用 forEach 迴圈做篩選。

下方比較兩者寫法,可以看出筆者寫法較為冗長,可讀性較低,且跑迴圈會佔較多效能。

  • 筆者的寫法
const keys = document.querySelectorAll(".key");

const filter = function (list, userKeyboard) {
let el;
list.forEach((item) => {
if (item.dataset.key === userKeyboard) {
el = item;
}
});
return el;
};

const playAudio = function (e) {
const audios = document.querySelectorAll("audio");
const userKeyboard = String(e.keyCode);
if (filter(keys, userKeyboard)) {
filter(keys, userKeyboard).classList.add("playing");
filter(audios, userKeyboard).currentTime = 0;
filter(audios, userKeyboard).play();
}
};
  • 作者的寫法
function playSound(e) {
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
const key = document.querySelector(`div[data-key="${e.keyCode}"]`);
console.log(e.keyCode);
if (!audio) return;

key.classList.add("playing");
audio.currentTime = 0;
audio.play();
}


增進使用體驗

製作完成後,發現有以下幾點可以做調整:

  1. 目前重複按下同一個鍵盤按鍵時,網頁按鈕樣式不會重複觸發,會一直亮著,雖然不影響使用,但希望有更細緻的體驗
  2. 如果完成第 1 點,會發現若鍵盤事件使用 keydown,當使用者按著按鍵不放時,事件會連續觸發,到某個程度時按鈕樣式就不再變動(簡言之「看起來」就像壞掉了XD);若鍵盤事件使用 keyup,雖然解決了事件重複觸發的問題,但筆者認為使用起來不符合預期效果,一般使用者應該會預期按下去的瞬間就發出音效,變成放開按鍵才發出音效似乎不夠即時

實作

一、重複點擊按鈕時,每次都會改變按鈕樣式

這部分是參考作者提供的解法,是使用transitionend 事件,監聽帶有 transition 屬性的元素,當 transitioned 結束時會觸發事件,有兩個情況下無法觸發該事件:

  • 在 transition 觸發以前元素就被設定 display: none
  • 有修改目標節點的 transition-property 屬性
function removeTransition(e) {
console.log(e.propertyName);
if (e.propertyName !== "transform") return;
e.target.classList.remove("playing");
}

作者是透過 propertyName 找到 transform 屬性,但似乎會導致重複觸發時按鈕樣式就不會再變動,不確定原因為何,目前想到的解決方式可往下看第二點。

二、在使用 keydown 的情況下,可以連續觸發樣式,且保持運作正常

解法1: 直接在 keyup 事件上綁定「清除按鈕樣式」的處理器,有點像買保險的感覺XD

let keys = document.querySelectorAll(".key");
const removeSound = function (e) {
keys.forEach((item) => {
item.classList.remove("playing");
});
};
window.addEventListener("keyup", removeSound);

解法2: transitionend 事件的 propertyName,找到除了 transform 以外的其他屬性,就能正常運作,這部分原因尚待釐清。

目前想到的解法是這樣,如果有更好的再補上。

題外話

發現下載後的檔案都有 favicon(瀏覽器頁籤會顯示的 icon),意外發現這個網站,可以自行替換掉後面的表情符號,蠻有趣的XD

今天就介紹到這裡,若有錯誤歡迎指正,也歡迎大家分享自己的看法。

參考資料

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.