JS 30 是由加拿大的全端工程師 Wes Bos 免費提供的 JavaScript 簡單應用課程,課程主打 No Frameworks
、No Compilers
、No Libraries
、No Boilerplate
在30天的30部教學影片裡,建立30個JavaScript的有趣小東西。
另外,Wes Bos 也很無私地在 Github 上公開了所有 JS 30 課程的程式碼,有興趣的話可以去 fork 或下載。
靈活運用 video
元素的相關屬性、方法,實作出一個擁有快進快退
、播放速度倍率
、控制音量大小
、拖拉時間軸
功能的簡易影片播放器。
.player
代表整個影片播放器,包含影片(.player__video)
、播放控制列(.player__controls)
兩部分。
播放控制列
內部又可細分為四個部分:
.progress
、.progress__filled
.toggle
.player__slider
.player__button
<div class="player">
<video class="player__video viewer" src="652333414.mp4"></video>
<div class="player__controls">
<div class="progress">
<div class="progress__filled"></div>
</div>
<button class="player__button toggle" title="Toggle Play">►</button>
<input type="range" name="volume" class="player__slider" min="0" max="1" step="0.05" value="1">
<input type="range" name="playbackRate" class="player__slider" min="0.5" max="2" step="0.1" value="1">
<button data-skip="-10" class="player__button">« 10s</button>
<button data-skip="25" class="player__button">25s »</button>
</div>
</div>
首先,取得所有要用到 HTML 標籤並放到對應宣告的常數中。
/*get element we need*/
const player = document.querySelector('.player');
const video = player.querySelector('.viewer');
const progress = player.querySelector('.progress');
const progressBar = player.querySelector('.progress__filled');
const toggle = player.querySelector('.toggle');
const skipButtons = player.querySelectorAll('[data-skip]');
const ranges = player.querySelectorAll('.player__slider');
下面我們將各個播放器的功能一個個拆出來做 :
功能目的 : 我們希望在點擊影片或播放/暫停鈕時,播放或暫停影片。
在video
(影片)、toggle
(播放/暫停鈕)上都註冊click 事件
監聽器,觸發事件後用togglePlay()
進行處理。
在togglePlay()
裡,我們宣告一個常數method
,經過條件判斷,當video.paused
回傳 true 則 method = play
;當video.paused
回傳 false 則method = pause
。
這邊有一個特殊的寫法video[method]();
。舉例來說,當method = play
則實際效果相當於video.play();
。
function togglePlay(){
const method = video.paused ? 'play' : 'pause';
video[method]();
}
/*控制影片的播放*/
video.addEventListener('click',togglePlay);
toggle.addEventListener('click',togglePlay);
功能目的 : 我們希望在影片播放/暫停的狀態下,同步更新圖示。
在video
(影片)上註冊play 事件
和pause 事件
兩個監聽器並都以updateButton()
進行事件處理。
在updateButton()
裡,我們宣告一個常數icon
指定當影片處於暫停狀態則icon = '►'
,接著利用toggle.textContent = icon
修改按鈕的圖示,當影片處於播放狀態的處理也是用一樣的方式。
function updateButton(){
const icon = this.paused ? '►' : '❚ ❚';
toggle.textContent = icon;
}
/*讓播放鍵的圖示改變*/
video.addEventListener('play',updateButton);
video.addEventListener('pause',updateButton);
功能目的 : 我們希望在點擊快進/快退按鈕時,同步調整影片的時間軸。
在skipButtons
(快進、快退按鈕)裡的所有button
都註冊click 事件
並以skip()
進行事件處理。
在skip()
裡,我們將video.currentTime
(影片現在播放的時間點)加上我們要快進或快退的秒數。
由於video.currentTime
本身是 float
型別,因此需要將this.dataset.skip
(快進/快退的秒數)用parseFloat()
轉換成float
型別之後再進行運算。
function skip(){
video.currentTime += parseFloat(this.dataset.skip);
}
/*調整影片的快進和倒退*/
skipButtons.forEach(button => button.addEventListener('click',skip));
功能目的 : 我們希望在滑鼠在倍率或音量條上移動改變數值時,同步反映到video
(影片)上。
在速度倍率和音量條上都註冊change 事件
和mousemove 事件
,分別在數值改變和滑鼠拖曳時觸發事件,之後用handleRangeUpdate()
進行事件處理。
在handleRangeUpdate()
裡,我們使用和之前一樣的特殊語法video[this.name] = this.value;
對video
的屬性值進行調整。舉例來說,如果this.name = volume
、this.value = 0
,則video[this.name] = this.value;
的效果和video.volume = 0;
一樣。
function handleRangeUpdate(){
video[this.name] = this.value;
}
/*調整影片的播放速度、音量*/
ranges.forEach(range => range.addEventListener('change',handleRangeUpdate));
ranges.forEach(range => range.addEventListener('mousemove',handleRangeUpdate));
功能目的 : 我們希望在影片播放的過程中,不斷地更新時間軸。
在video
(影片)上註冊timeupdate 事件
的監聽器,當影片的播放時間(currentTime
)有變動就觸發事件,之後用handleProgress()
進行事件處理。
在handleProgress()
裡,我們宣告常數percent
並放入video.currentTime
(影片現在時間)除以video.duration
(影片的總長度)再乘以100得到的比例值。
接著用progressBar.style.flexBasis = `${percent}%`;
,用percent
指定時間軸的長度佔比。
function handleProgress(){
const percent = (video.currentTime / video.duration) * 100
progressBar.style.flexBasis = `${percent}%`;
}
/*持續更新時間軸*/
video.addEventListener('timeupdate',handleProgress);
功能目的 : 我們希望按住滑鼠拖或點擊時間軸的同時,更新video
(影片)現在播放的時間。
宣告mousedown
作為 flag 判斷現在是否有按住滑鼠。
我們在progress
註冊click 事件
、mousemove 事件
、mousedown 事件
、mouseup 事件
監聽器。
觸發click 事件
時,我們可以直接就以scrub(e)
進行事件的處理。
但在觸發mousemove 事件
時,我們需要先判斷是否有按住滑鼠,所以要借助mousedown 事件
和mouseup 事件
的幫忙,在mousedown 事件
觸發地當下將 flag(mousedown
) 設為 true,反之觸發mouseup 事件
則將 flag(mousedown
) 設為 false。最後用mousedown && scrub(e)
判斷是否執行scrub(e)
,只有當flag(mousedown
) 是 true 的時候,才接著執行scrub(e)
完成事件處理。
在scrub(e)
裡,我們宣告常數scrubTime
放入將滑鼠在元素內部的X座標(e.offsetX
)除以時間軸的長度(progress.offsetWidth
)再乘以影片長度(video.duration
)所得到要前往的時間點。最後將影片現在的時間(video.currentTime
)指定為要前往的時間點(scrubTime
)。
function scrub(e){
const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration;
video.currentTime = scrubTime;
}
/*拖拉時間軸*/
let mousedown = false;
progress.addEventListener('click',scrub);
progress.addEventListener('mousemove',(e)=> mousedown && scrub(e));
progress.addEventListener('mousedown',() => mousedown = true);
progress.addEventListener('mouseup',() => mousedown = false);
HTMLVideoElement
繼承自HTMLMediaElement
所以一些video
元素的屬性都可以到HTMLMediaElement
查詢。
使用HTMLElement.offsetWidth
所取得的元素(element
)寬度包括透過 CSS 設定的width、border、padding
等等...。
HTMLMediaElement
HTMLElement.dataset
HTMLElement.offsetWidth
JS一秒區分clientX,offsetX,screenX,pageX之間關係