[實作] 唱盤音樂播放器

學習到的知識點

HTML

  • 從 fontawsome 引入圖片

CSS

  • linear-gradient
  • animation
  • animation-play-state
  • ::after 偽元素
  • z-index

JS

  • Jquery
  • clientX,screenX,pageX
  • favicon

簡介

Demo

一個有唱盤會旋轉的音樂播放器。

HTML

音樂訊息與 Progrees info

包住整個結構的.container,與存放 progress bar。

1
2
3
4
5
6
7
8
9
10
11
12
<body>
<h1>Music Player</h1>
<div class="music-container" id="music-container">
<div class="music-info">
<h4 id="title"></h4>
<div class="progress-container" id="progress-container">
<div class="progress" id="progress"></div>
</div>
</div>
<audio id="audio" src="music/smallguitar.mp3"></audio>
</div>
</body>

照片與播放 Button

圖片容器.img-container,還有三個從Font-awasome網站引入的撥放、倒退、快轉鍵。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="img-container">
<img src="images/smallguitar.jpg" alt="music-cover" id="cover" />
</div>
<div class="navigation">
<button id="prev" class="action-btn">
<i class="fas fa-backward"></i>
</button>
<button id="play" class="action-btn action-btn-big">
<i class="fas fa-play"></i>
</button>
<button id="next" class="action-btn">
<i class="fas fa-forward"></i>
</button>
</div>

CSS

將所有元素置中

背景漸層的效果

使用linear-gradient,在 Amos 切版也有使用到。

1
2
3
4
5
6
body {
background-image: linear-gradient(
0deg,
rgba(247, 247, 247, 1) 23.8%,
rgba(252, 221, 221, 1) 92%
);

這部分的 Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@import url("https://fonts.googleapis.com/css?family=Lato&display=swap");

* {
box-sizing: border-box;
}

body {
background-image: linear-gradient(
0deg,
rgba(247, 247, 247, 1) 23.8%,
rgba(252, 221, 221, 1) 92%
);
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: "Lato", sans-serif;
margin: 0;
}

外層容器

img-container

relative可以定位到上層元素.music-container,而不會定位到最外層導致跑版。

1
2
3
4
.img-container {
position: relative;
width: 110px;
}

這部分的 Code

1
2
3
4
5
6
7
8
9
10
.music-container {
background-color: #fff;
border-radius: 15px;
box-shadow: 0 20px 20px 0 rgba(252, 169, 169, 0.6);
display: flex;
padding: 20px 30px;
position: relative;
margin: 100px 0;
z-index: 10;
}

設置音樂圖片

圖片設置跟外容器一樣寬

使用width: inherit,瀏覽器會將上層元素img-container的寬度賦值給他,就不需要再img-container上設定position:relative

參考文章

定位照片

使用絕對定位

1
2
3
4
5
{
position: absolute;
bottom: 0;
left: 0;
}

animation

animation: 動畫名稱 | 動畫持續時間 | 動畫加速度函式 | 動畫播放次數

animation: animation-name|name-duration | animation-timing-function | animation-iteration-count

animation-play-state 為動畫播放或暫停狀態。
running:預設值,表示動畫運行。
paused:表示動畫暫停

1
2
3
4
{
animation: rotate 3s linear infinite;
animation-play-state: paused;
}

音樂播放時開始旋轉

這部分是由 JS 控制,按下播放音樂的 Button,才將.play 的 class 加上去。

1
2
3
.music-container.play .img-container img {
animation-play-state: running;
}

這部分的 Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.img-container {
position: relative;
width: 110px;
}

.img-container img {
border-radius: 50%;
object-fit: cover;
height: 110px;
width: inherit;
position: absolute;
bottom: 0;
left: 0;
animation: rotate 3s linear infinite;
animation-play-state: paused;
}

.music-container.play .img-container img {
animation-play-state: running;
}

圖片上的白點

將圖片上的白點移到中間,使用到偽元素::after

1
2
3
4
5
6
7
8
9
10
11
.img-container::after {
content: "";
background-color: #fff;
border-radius: 50%;
position: absolute;
bottom: 100%;
left: 50%;
width: 20px;
height: 20px;
transform: translate(-50%, 50%);
}

設定動畫

1
2
3
4
5
6
7
8
9
@keyframes rotate {
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
}

播放欄

去除點擊後出現的框框

1
2
3
.action-btn:focus {
outline: 0;
}

這部分的 Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.navigation {
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}

.action-btn {
background-color: #fff;
border: 0;
color: #dfdbdf;
font-size: 20px;
cursor: pointer;
padding: 10px;
margin: 0 20px;
}

/* 放大播放鍵 */
.action-btn.action-btn-big {
color: #cdc2d0;
font-size: 30px;
}

.action-btn:focus {
outline: 0;
}

Music Info

要做出 pop up 的效果(按下播放時,才出現),所以一開始要設定transform: translateY(0%)opacity: 0,不讓 Info 面板出現。

transform的座標別於我們一般認知的座標,以下面這張圖為例,右下在一般認知裡為(100,-100),但transform原點在左上,所以往他的右下為(100,100),相反的往他的左上為(-100,-100)。

這邊使用到transition:ease-in效果,意思為緩慢的開始。

width: calc(100% - 40px),100%為上層元素的最大寬度,calc()為動態計算,不用在一個一個計算現在的寬度為多少,可以和max()、min()、clamp或是 CSS 的變數互相搭配,中間是減號,前後要留兩個空格。

完整的 Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 未播放時不出現 */
.music-info {
background-color: rgba(255, 255, 255, 0.5);
border-radius: 15px 15px 0 0;
position: absolute;
top: 0;
left: 20px;
width: calc(100% - 40px);
padding: 10px 10px 10px 150px;
opacity: 0;
transform: translateY(0%);
transition: transform 0.3s ease-in, opacity 0.3s ease-in;
z-index: 0;
}

/* 播放時出現 */
.music-container.play .music-info {
opacity: 1;
transform: translateY(-100%);
}

/* 去除h4的預設margin */
.music-info h4 {
margin: 0;
}

Progress

.progress裡面的width改變他的長度,會使用 Js 動態改變。

transition:新增效果的CSS屬性 | 效果的持續時間 | 動畫執行的計算方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 外面的白色框框 */
.progress-container {
background: #fff;
border-radius: 5px;
cursor: pointer;
margin: 10px 0;
height: 4px;
width: 100%;
}

/* 裡面的進度條 */
.progress {
background-color: #fe8daa;
border-radius: 5px;
height: 100%;
width: 0%;
transition: width 0.1s linear;
}

JS

新增變數

JS

1
2
3
4
5
6
7
8
9
10
const musicContainer = document.getElementById("music-container");
const playBtn = document.getElementById("play");
const prevBtn = document.getElementById("prev");
const nextBtn = document.getElementById("next");

const audio = document.getElementById("audio");
const progress = document.getElementById("progress");
const progressContainer = document.getElementById("progress-container");
const title = document.getElementById("title");
const cover = document.getElementById("cover");

Jquery

直接選取元素

1
2
3
4
5
6
7
8
9
$("#musicContainer")
$("#playBtn")
$("#prevBtn")
$("#nextBtn")
$("#audio")
$("#progress")
$("#progressContainer")
$("#title")
$("#cover")

宣告歌曲陣列,檢索,呼叫loadSong()

設定以歌為 id 的陣列

JS 和 Jquery 都要宣告

1
2
3
4
5
6
7
8
9
10
11
// Song titles
// 設定以歌為id的陣列
const songs = ["energy", "inspire", "smallguitar"];

// Keep track of song
// 宣告檢索歌曲的變數
let songIndex = 0;

// Initially load song details into DOM
// 呼叫loadSong函數,用檢索呼叫歌曲
loadSong(songs[songIndex]);

載入歌曲loadSong()

將歌曲的名字,音檔,照片讀入。

JS

1
2
3
4
5
6
7
// Update song details
function loadSong(song) {
// Can change the song
title.innerText = song;
audio.src = `music/${song}.mp3`;
cover.src = `images/${song}.jpg`;
}

Jquery

  • audio.src =轉成$("#audio").attr()
1
2
3
4
5
function loadSong(song) {
$("#title").Text(song);
$("#audio").attr(`music/${song}.mp3`);
$("#cover").attr(`images/${song}.jpg`);
}

播放按鈕的事件監聽

JS

1
2
3
4
5
6
7
8
9
10
// Event Listeners
playBtn.addEventListener("click", () => {
const isPlaying = musicContainer.classList.contains("play");

if (isPlaying) {
pauseSong();
} else {
playSong();
}
});

Jquery

  • playBtn.addEventListener("click" 轉成 $("#playBtn").click(function(){
  • .classList.contains轉成.hasClass
1
2
3
4
5
6
7
8
$("#playBtn").click(function(){
const isPlaying =$("musicContainer").hasClass("play");
if (isPlaying true) {
pauseSong();
} else {
playSong();
}
});

播放歌曲 playSong(),暫停歌曲pauseSong()

播放和暫停功能一樣,只有 icon 交換和audio.play()audio.pause()不同。

JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Play song
function playSong() {
musicContainer.classList.add("play");
playBtn.querySelector("i.fas").classList.remove("fa-play");
playBtn.querySelector("i.fas").classList.add("fa-pause");

audio.play();

// Pause song
function pauseSong() {
musicContainer.classList.remove("play");
playBtn.querySelector("i.fas").classList.add("fa-play");
playBtn.querySelector("i.fas").classList.remove("fa-pause");

audio.pause();
}

Jquery

從 id 去找到 i,再將圖檔替換掉。

  • playBtn.querySelector("i.fas") 轉成 $("#play").find("i").attr("class", "fas fa-pause")
  • audio.play()轉成$("#audio")[0].play();

.play.pause都是 DOM 的操作方法。$queryselector 為 jquery 物件而不是原始的 DOM 元素,所以我們需要成 $("#audio")[0].play()

一開始不理解這種用法,搜尋了一下後得到解答,意思為: 利用 Jquery 選擇器,得到 id 為 audio 的第一個 DOM 對象。

1
2
3
4
5
6
7
8
9
10
11
function playSong() {
$("#music-container").addClass("play");
$("#play").find("i").attr("class", "fas fa-pause");
$("#audio")[0].play();
}

function pauseSong() {
$("#music-container").removeClass("play");
$("#play").find("i").attr("class", "fas fa-play");
$("#audio")[0].pause();
}

按下前一首prevSong()、下一首歌nextSong()

JS

利用songIndex,控制歌曲的撥放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Next song
function nextSong() {
songIndex++;
// 如果songIndex大於總數2時
if (songIndex > songs.length - 1) {
// 就歸0
songIndex = 0;
}
// 重新更換歌曲名字,音檔,圖片
loadSong(songs[songIndex]);
playSong();
}

// Previous song
function prevSong() {
songIndex--;
// 如果songIndex小於0時
if (songIndex < 0) {
// 總數3-1=2,會撥放第3首音樂,形成一個循環
songIndex = songs.length - 1;
}
loadSong(songs[songIndex]);
playSong();
}

// Change song
prevBtn.addEventListener("click", prevSong);
nextBtn.addEventListener("click", nextSong);

Jquery

  • prevBtn.addEventListener("click", prevSong)轉成$("#next").click(function(){nextSong();})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Next song
function nextSong() {
songIndex++;
if (songIndex > songs.length - 1) {
songIndex = 0;
}
loadSong(songs[songIndex]);
playSong();
}

// Previous song
function prevSong() {
songIndex--;
if (songIndex < 0) {
songIndex = songs.length - 1;
}
loadSong(songs[songIndex]);
playSong();
}

// Change song
$("#pre").click(function(){
prevSong();
});
$("#next").click(function(){
nextSong();
});

進度條

JS

duration是整首歌的時間,currentTime是過了多久。

event.offsetX、event.offsetY,滑鼠相對於事件源元素(srcElement)的 X,Y 坐標。

JS 一秒區分 clientX,offsetX,screenX,pageX 之間關係

Audio duration Property

1
2
3
4
5
6
7
8
9
10
11
12
13
// Update progress bar
// duration是整首歌的時間,currentTime是過了多久。
// e.srcElement 代表當前的事件源
function updateProgress(e) {
const { duration, currentTime } = e.srcElement;
// (當下的音樂時間/總時長)*100就會得到百分比
const progressPercent = (currentTime / duration) * 100;
// 百分比變數放到progress上
progress.style.width = `${progressPercent}%`;
}
// Time/song update
audio.addEventListener("timeupdate", updateProgress);

Jquery

.on 就是 addEventListener

  • audio.addEventListener()轉成$("#audio").on()
  • const { duration, currentTime } = e.srcElement轉成const duration = $("#audio")[0].duration
  • progress.style.width = ()轉成$("#progress").css
1
2
3
4
5
6
$("#audio").on("timeupdate", function () {
const duration = $("#audio")[0].duration;
const progressPercent = $("#audio")[0].currentTime;
const progressPercent = (currentTime / duration) * 100;
$("#progress").css(width, `${progressPercent}%`);
});

可以點選進度條

JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 滑鼠點擊進度條移動Set progress bar
function setProgress(e) {
// this指向progress-container它的width就是216
const width = this.clientWidth;
// clickX為滑鼠點擊progress-container的位置
const clickX = e.offsetX;
// duration will get from audio api
// 音檔總時長
const duration = audio.duration;
// 算出百分比
audio.currentTime = (clickX / width) * duration;
}

// Click on progress bar
progressContainer.addEventListener("click", setProgress);

Jquery

這邊有兩種寫法:

  1. 照著原作者方式改寫
  2. 參考別人寫法

Bug
這邊的 width 抓取要使用#progress-container,如果使用e.target會導致這種錯誤。目前還不太明白這個錯誤是甚麼意思,待了解…

目前使用$("#progress-container")可以解決。

使用 clientX 與 width 取百分比

1
2
3
4
5
6
7
$("#progress-container").click(function (e) {
const duration = $("#audio")[0].duration;
const width = $("#progress-container").width();
const clickX = e.offsetX;
const percent = (clickX / width) * duration;
$("#audio")[0].currentTime = percent;
});

使用到.offset().left

  • this.clientWidth轉成$('#progress-container').width()
1
2
3
4
5
6
7
8
9
10
11
12
13
$('#progress-container').click(function(e) {
// 取得progress-container的長度
const width = $('#progress-container').width()
// offsetX為點擊物件最左側的距離
const offsetX = $(e.target).offset().left;
// clickX為點擊的位置,與畫面最左側的距離
const clickX = e.pageX
// 取得歌曲的總長度
const duration = $('#audio')[0].duration;
// 利用距離算出百分比
const percent = ((clickX - offsetX) / width)
$('#audio')[0].currentTime = percent * duration
});

播放完畢後,自動跳下一首

JS

1
audio.addEventListener("ended", nextSong);

Jquery

audio.addEventListener()轉成$("audio").on

1
2
3
$("audio").on("ended",function(){
nextSong();
})

遇到的問題

1

Bug

在輸入 commit 時,少加了結束的引號,而出現了 dquite>的這串文字,按^Z 也無效,最後 Google 一下,找到解法

2

Bug
使用 Chrome 會出現此錯誤,但使用 edge 和 Firefox 時不會。

解決方法是,在 index.html 加上<link rel="shortcut icon" href="#" />

但是,favicon 是什麼?

它的是全名是 Favorite Icon,就是網頁旁邊出現的小圖案。

參考文章

The Embed Audio element

CSS

width:100%和 width:inherit

完整解析 CSS 動畫 ( CSS Animation )

CSS transition 各種速率

CSS 沒有極限 - CSS 的神奇 Calc 運算

css3 transition 原理

Javascript

jQuery attr() Method

原生 JS 替代 jQuery 的各種方法彙總

JS 一秒區分 clientX,offsetX,screenX,pageX 之間關係

Audio duration Property


[實作] 唱盤音樂播放器
https://phoebeho.com/Portfolio/20210123/3546601703/
作者
Phoebe
發布於
2021年1月23日
許可協議