iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 1
1
Modern Web

寫JS30天系列 第 1

JS 30 - 01 - drum kit

JS30的第一天
要將鍵盤變成一個爵士鼓,首先看一下所拿到的.html檔

  <div class="keys">
    <div data-key="65" class="key">
      <kbd>A</kbd>
      <span class="sound">clap</span>
    </div>
    <div data-key="83" class="key">
      <kbd>S</kbd>
      <span class="sound">hihat</span>
    </div>
    .
    .
    .
  </div>

data-*

data-*是用來存放與該元素有關的小型資料,可以自己給屬性值,一個tag可以有許多data-*
接著我們要監聽鍵盤被按下這個keydown事件

window.addEventListener('keydown', playSound);
function playSound(e) {
    console.log('鍵盤被按下');
};

由於window是包著DOM的物件,所以可以偵測到keydown事件,並觸發functionplaySound

值得注意的是這裡不能使用keypress原因在此

接著我們要播放音樂了,可以在參數內放一個econsole.log(e)看看keypress事件,並可以看到一些值在裡面供我們使用,要知道「哪一顆按鈕被按下」,keyCode正是我們的目標

function playSound(e) {
    let audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
    if (!audio) return;
    audio.play();
};

playSound(e)
傳入監聽事件的物件,在宣告函式時,通常用參數e來表示event,去接收該事件
這裡的e會接收keydown事件

document.querySelector()
DOM的選取方法,()內填入CSS selector組成的字串

如果按a,s,d,f,g,h,j,k,l這九個按鈕,會得到相對應的標籤,其他按鈕則會回傳null
因為null的布林值是false

Boolean(null); //false

因此如果是null,就直接return終止函式

if(!audio) return;

剩下的就播放audio

但此時連點按鍵卻無法連續的打擊鼓面,因為上一次點擊所播放的聲音還沒有結束
因此,我們要讓每次函式.play()前,使用.currentTime將進度條調整至0:00,這樣就可以連續敲擊了

function playSound(e) {
    let audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
    if (!audio) return;
    audio.currentTime = 0;
    audio.play();
};

聲光效果中的聲音已經完成,接下來是光的效果了
首先,我們先看看css怎麼寫的

.key {
  border: .4rem solid black;
  border-radius: .5rem;
  margin: 1rem;
  font-size: 1.5rem;
  padding: 1rem .5rem;
  transition: all .07s ease; /*這個是動畫的時間*/
  width: 10rem;
  text-align: center;
  color: white;
  background: rgba(0,0,0,0.4);
  text-shadow: 0 0 .5rem black;
}

.playing {
  transform: scale(1.1); /*會放大元素並改變border顏色*/
  border-color: #ffc600;
  box-shadow: 0 0 1rem #ffc600;
}

我們先監聽的.key,並使用[data-key=e.keyCode]來確認取到的key
並使用.classList.add()新增class。.classList.add()可以讓你包含原本的class,並新增想要的class

function playSound(e) {
  let audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
  let key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
  if (!audio) return; //keydown without a,s,d,f,g,h,j,k,l
  audio.currentTime = 0; //set the currentTime to start
  audio.play();
  key.classList.add("playing");
};

我們會發現螢幕上對應的按鈕會亮了,但是不會消失,可以使用setTimeout(),隔一段時間後再執行函式

setTimeout(function(){
  key.classList.remove("playing");
},70)

但是這個方法需要一直去看transition過了多久
再設定時間過多久才開始執行
所以我們可以偵測另一個事件transitionend
就會等到transition結束才執行該函式
方法如下:

let keys = document.querySelectorAll('.key');
console.log(keys); //NodeList(9)[div.key, div.key, div.key, div.key, div.key, div.key, div.key, div.key, div.key];

.querySelectorAll會將所有的.key以一個list顯示,這個list長得很像array
但是他只是個類陣列array-like,並不是一個真的陣列
所以不能使用array特有的method
而這裡所產生的是Nodelist
剛好NodeList也有支援.forEach,所以可以使用

let keys = document.querySelectorAll('.key');
keys.forEach(key => key.addEventListener('transitionend', removeTransition));

傳入類陣列內的每個元素去監聽它們,等到transitionend後才執行removeTransition

function removeTransition(e) {
    if (e.propertyName !== "transform") return;
    this.classList.remove("playing");
};

如果這時候console.log(e)
會發現跑出一大串
其實這是對transformborder-colorbox-shadow等等的屬性
因為新增了.playing所產生的transition
當轉場結束後
會觸發很多個transitionend
這時候我們要只需要其中一種propertyName即可
因此將非transform的propertyName濾掉

最後使用this指定到.key並移除.playing
也有找到方法是使用e.target來指定

function removeTransition(e) {
  ...
  e.target.classList.remove("playing");
};

e.target永遠會指向觸發event的DOM元素
this則是指向掛addEventListener的元素
用這種方式也可以刪除.playing

這樣就大功告成了!

完整程式碼
Demo

參考資料

  1. 比較 keydown, keypress, keyup 的差異
  2. advanced-javascript-objects-arrays-and-array-like-objects

下一篇
JS 30 - 02 - JS and CSS Clock
系列文
寫JS30天30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言