Day 1 - JavaScript Drum Kit

前言

JS 30 是由加拿大的全端工程师 Wes Bos 免费提供的 JavaScript 简单应用课程,课程主打 No FrameworksNo CompilersNo LibrariesNo Boilerplate 在30天的30部教学影片里,建立30个JavaScript的有趣小东西。

另外,Wes Bos 也很无私地在 Github 上公开了所有 JS 30 课程的程序码,有兴趣的话可以去 fork 或下载。


本日目标

透过监听 transitionend 事件keydown 事件,调整 CSS 设定以及播放音效,最终建立一组爵士鼓并依照按下按键的不同播放不同的音效伴随网页上的 CSS 动画效果。


解析程序码

HTML 部分

基本结构是由最外层的 "keys" 包住内层9个 "key" 的巢状结构,内层的 "key" 都有 data-key 属性并有相异的数值,这些相异值在接下来判断要播放哪个音档和套用 CSS 设定到哪个 "key" 时很重要。

<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-key="68" class="key">
      <kbd>D</kbd>
      <span class="sound">kick</span>
    </div>
    <div data-key="70" class="key">
      <kbd>F</kbd>
      <span class="sound">openhat</span>
    </div>
    <div data-key="71" class="key">
      <kbd>G</kbd>
      <span class="sound">boom</span>
    </div>
    <div data-key="72" class="key">
      <kbd>H</kbd>
      <span class="sound">ride</span>
    </div>
    <div data-key="74" class="key">
      <kbd>J</kbd>
      <span class="sound">snare</span>
    </div>
    <div data-key="75" class="key">
      <kbd>K</kbd>
      <span class="sound">tom</span>
    </div>
    <div data-key="76" class="key">
      <kbd>L</kbd>
      <span class="sound">tink</span>
    </div>
</div>
补充说明1:

HTML5 新增了 data-* 自定义属性(data attributes),让我们能以 data- 为开头,建立自订的属性和值并随时可以读写在元素上的资料数值,而不会影响到整个版面。

程序码中的 data-key 就是一个不错的例子。

补充说明2:

<kbd> 是一个行内元素 (inline element) ,用来标示键盘符号。

JS 部分

首先,我们先依照按下按键的 keyCode 取得特定的音档和 div 标签。

/*JS*/
window.addEventListener("keydown",function(){
    const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
    const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
})

因为实际上会有作用的按键只有9个,按到没作用的按键时,理论上我们应该要终止执行方法避免错误,所以新增 if 判断是否成功取得音档,没有取得就终止方法的执行。

/*JS*/
window.addEventListener("keydown",function(){
    const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
    const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
    if(!audio)return; /*如果keyCode不存在则中止执行方法*/
})

接着是播放音档的部分,如果只单用 audio.play()的话,则在连续按下同一按键时,会出现声音不连贯的效果,此时需要将每次播放音档的时间轴都设为0,让每次播放都是从头开始。

/*JS*/
window.addEventListener("keydown",function(){
    const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
    const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
    if(!audio)return; /*如果keyCode不存在则中止执行方法*/
    
    audio.currentTime = 0; /*确保每一次按下键盘都从头播放音档*/
    audio.play(); /*播放音档*/
})

最後处理 CSS 的动画效果,我们会套用 .playing 的 CSS 设定到播放音档的 div 标签上。

/*CSS*/
.playing {
  transform: scale(1.1); /*让 div 标签变大1.1倍*/
  border-color: #ffc600; /*改变 border 的颜色*/
  box-shadow: 0 0 1rem #ffc600; /*产生阴影*/
}
/*JS*/
window.addEventListener("keydown",function(){
    const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
    const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
    if(!audio)return; /*如果keyCode不存在则中止执行方法*/
    
    audio.currentTime = 0; /*确保每一次按下键盘都从头播放音档*/
    audio.play(); /*播放音档*/
    
    key.classList.add('playing');
})

一定时间後,我们必须拿掉 .playing 的 CSS 设定,让 div 标签回到未被按键触发的状态。因此我们可以在每个 key 上都注册 transitionend 事件 的监听器,并用 removeTransition() 处理该事件。

/*JS*/
function removeTransition(e){ 
    this.classList.remove('playing');
}

/*取得所有的 key 标签*/
const keys = document.querySelectorAll(`.key`);
/*在每个 key 上都注册 "transitionend 事件" 的监听器*/
keys.forEach(key => key.addEventListener('transitionend',removeTransition));

到这里基本上就完成了,但是如果把 removeTransition()transitionend 事件 印到 console,可以发现 transitionend 事件 不只有一个。

我们可以选择以 transform 结束触发的那个 transitionend 事件,作为移除 CSS 设定的时机点。

/*JS*/
/*当transform完成就移除.playing*/
function removeTransition(e){
    if(e.propertyName != 'transform') return; 
    this.classList.remove('playing');
}

整理後的完整JS如下:

/*JS*/
function playSound(e){
    const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
    const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
    if(!audio)return; /*如果keyCode不存在则中止执行方法*/
    audio.currentTime = 0; /*确保每一次按下键盘都从头播放音档*/
    audio.play(); /*播放音档*/
    key.classList.add('playing');
  }
  
/*当transform完成就移除.playing*/
function removeTransition(e){
    if(e.propertyName != 'transform') return; 
    this.classList.remove('playing');
}

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

window.addEventListener("keydown",playSound)
补充说明:

上面 `.key` 的用法是 JavaScript ES6 中新增的模版字符串(template literals)

在过去我们需要用以下写法在JS 的字串中放入 HTML 内容:


/*JS*/
let component_es5 = '<header>\n'+
'<div class="banner">\n'+
'<img src="img1.jpg"\n'+
'</div>\n'+
'</header>'

上面的写法相当冗长,而且不具备易读性。在 ES6 中我们可以用反引号快速的解决这样的状况:

/*JS*/
let component_es6 = `
<header>
    <div class='banner'>
        <img src="img1.jpg>
    </div>
</header>
`

补充资料: [笔记] JavaScript ES6 中的模版字符串(template literals)和标签模版(tagged template)

范例网页请按此



<<:  [iT铁人赛Day1]JAVA下载与执行

>>:  Day1.认识GUI和Tkinter

第十二天:初探 Gradle 任务

任务(Task)是 Gradle 运行时的基本单位,基本上所有我们输入的 Gradle 指令都是对应...

[Python]如何Speech to Text: SpeechRecognition

https://pypi.org/project/SpeechRecognition/ pip3 i...

Day-25 ImageView

ImageView为显示图片, 但在图片显示前, 必须先了解如何插入图片: Step1:於资料夹选取...

第30天~TTS(文字转语音)+STT(语音转文字)

TTS(文字转语音) 开新档案 布置XML档- 按钮也是要绑onClick- 步骤: 1.先宣告 2...

第 08 天 再接再厉坚持不懈( leetcode 300 347 )

https://leetcode.com/problems/longest-increasing-...