准备工作:
VScode 编辑器 设计稿 (无图不页面)
设计稿分析:
- 从图上首先我们可以分析出来一整个页面从上至下可以划分为 6 个区域
- 最中间的主页面我们可以考虑用两个圆环的 svg 图去撑起来,并且panel1,2,3一共三个圆环按照相反的方向转动
- 下面的按钮栏,我们用几个点赞,下载,分享,评论的 svg 来填充就好,需要考虑是否添加每个按钮的跳转效果
- 最后一行也是用播放,暂停,上一曲,下一曲等按钮组成
- 总体难度系数较低,复杂的部分主要是歌曲播放的进度条部分,注意要做进度条拖动就快进到对应的歌词
HTML 和 SCSS 部分实现
区域一二部分实现
<div class="header">
<h1>我肯定在几百年前就说过...</h1>
<p>告五人-爱人错过</p>
<div class="balls">
<span class="current"></span>
<span></span>
</div>
</div>
规定使用较多的颜色和区域1、2部分的 SCSS
$backgroundColor: #060a3d;
$color: #fff;
$color1: #868aaf;
$color2: #db3baa;
$color2-dark: darken($color2, 10%);
$color2-dark: darken($color2, 30%);
$color3: #0025f1;
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
}
* {
margin: 0;
padding: 0;
}
#player {
height: 100%;
background: $backgroundColor;
.header {
text-align: center;
height: 110px;
h1 {
color: $color;
font-size: 20px;
padding-top: 20px;
}
p {
color: $color1;
font-size: 12px;
}
.balls {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
span {
display: block;
width: 5px;
height: 5px;
border-radius: 50%;
background: $color2-dark;
margin: 0 4px;
&.current {
width: 8px;
height: 8px;
background: $color2;
}
}
}
}
区域3实现
<div class="panels panel1">
<div class="panel-effect">
<div class="effect">
<div class="effect-1"></div>
<div class="effect-2"></div>
<div class="effect-3"></div>
</div>
<div class="lyrics">
<p class="current"></p>
<p></p>
</div>
</div>
<div class="panel-lyrics">
<div class="container"></div>
<div class="maoboli"></div>
</div>
</div>
区域4按钮(喜欢,下载,分享,评论)svg 引入
<div class="buttons">
<svg class="btn-download"
id="icon-download"
stroke="none"
stroke-width="1"
fill="none"
fill-rule="evenodd"
opacity="0.8">
</svg>
</div>
//以此类推都引入
区域5播放进度条
<div class="area-bar">
<span class="time-start">00:00</span>
<span class="time-end">00:00</span>
<div class="bar">
<div class="progress">
<div class="progress-button"></div>
</div>
</div>
</div>
区域6播放暂停svg引入
<div class="actions">
<img src="./src/svg/循环模式.svg" alt="" />
<img class="play-prev" src="./src/svg/上一首.svg" alt="" />
<div class="play-pause"></div>
<img class="play-next" src="./src/svg/下一首.svg" alt="" />
<img src="./src/svg/音乐.svg" alt="" />
</div>
CSS 部分实现
区域3 SCSS实现
.panels {
height: calc(100% - 270px);
align-items: center;
width: 100vw;
display: flex;
transition: transform 0.3s;
overflow: visible;
&.panel1 { //歌词左滑之后的页面
transform: translateX(0vw);
}
&.panel2 { //歌词右滑之后的页面
transform: translateX(-100vw);
}
.panel-effect {
width: 100vw;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
flex-shrink: 0;
.effect {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 40vh;
> div {
background: contain;
position: absolute;
}
.effect-1 { //外层的大园环
background: url(../svg/effect-no-move.svg) 0 0 no-repeat;
width: 70vw;
height: 70vw;
animation: rotate 20s linear infinite;//添加动效
}
.effect-2 { //中层的圆环
background: url(../svg/effect-move1.svg) 0 0 no-repeat;
width: 60vw;
height: 60vw;
animation: rotate 10s linear infinite reverse;//添加动效
}
.effect-3 { //最内层的圆环
background: url(../svg/effect-move2.svg) 0 0 no-repeat;
width: 24vw;
height: 24vw;
animation: rotate 10s linear infinite;//添加动效
}
}
// 右滑时的歌词panel实现
.lyrics { // 歌词左滑页面,三个圆环动效下面的歌词效果
text-align: center;
p {
font-size: 13px;
color: $color1;
margin-top: 8px;
&.current {
color: $color;
}
}
}
.panel-lyrics { //歌词右滑页面的效果
position: relative;
flex-shrink: 0;
width: 100vw;
height: 100%;
text-align: center;
line-height: 2;
overflow: hidden;
.container { //歌词的上下滚动效果
transition: all .3s;
transform: translateY(-100px);
p {
font-size: 14px;
color: $color1;
&.current {
color: $color;
}
}
}
.maoboli { //歌词的毛玻璃效果
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(rgba(6, 10, 61, 0.822), rgba(6, 10, 61, 0), rgba(6, 10, 61, 0.829));
}
}
}
区域4 SCSS 实现
.buttons {
display: flex;
justify-content: center;
> svg,//svg的样式
> div {
width: 24px;
height: 24px;
margin: 0 20px;
}
}
区域5 SCSS实现
.area-bar {
color: $color1;
font-size: 12px;
display: flex;
padding: 0 20px;
margin-top: 20px;
align-items: center;
.time-start {
order: 1;
width: 32px;
}
.time-end {
order: 3;
width: 32px;
}
.bar {
order: 2;
flex: 1;
height: 4px;
background: $color3;
border-radius: 2px;
margin: 0 18px;
.progress {
width: 0%;
height: 100%;
border-radius: 2px;
background: $color2;
position: relative;
.progress-button { //拖动小球来快进
position: absolute;
right: -8px;
top: 50%;
display: block;
width: 20px;
height: 20px;
background: url(../svg/progress.svg) 0 0 no-repeat;
background-size: 12px 12px;
background-position: center;
transform: translateY(-50%);
}
}
}
}
区域6 SCSS实现
.actions {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20px;
padding: 0 20px;
.play-pause {
width: 50px;
height: 50px;
background-size: contain;
background-repeat: no-repeat;
background-image: url("../svg/播放.svg");
&.playing {
background-image: url("../svg/暂停.svg");
}
}
img {
width: 33px;
height: 50px;
}
}
添加动效
@keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
JS部分
左右滑动效果swiper
class Swiper {
constructor(node) {
if(!node) throw new Error('需要传递需要绑定的DOM元素')
let root = typeof node === 'string' ? document.querySelector(node) : node
let eventHub = {'swipLeft': [],'swipRight':[]}
let initx
let newX
let clock
root.ontouchstart = function(e) {
initx = e.changedTouches[0].pageX
}
root.ontouchmove = function(e){
if(clock) clearInterval(clock)
clock = setTimeout(() =>{
newX = e.changedTouches[0].pageX
if(newX - initx > 10){
eventHub['swipRight'].forEach(fn=>fn(root))
}else if(initx - newX > 10){
eventHub['swipLeft'].forEach(fn=>fn(root))
}
},100)
}
this.on = function (type, fn){
if(eventHub[type]) {
eventHub[type].push(fn)
}
}
this.off = function (type,fn){
let index = eventHub[type].indexOf(fn)
if(index !== -1) {
eventHub[type].splice(index,1)
}
}
}
}
export default Swiper
主要JS部分
class Player {//绑定事件
constructor(node) {
this.$ = (selector) => this.root.querySelector(selector);
this.$$ = (selector) => this.root.querySelectorAll(selector);
this.root = typeof node === "string" ? document.querySelector(node) : node;
this.songList = [];
this.currentIndex = 0;
this.audio = new Audio();
this.start();
this.bind();
}
start() {
this.songList = [
{
id: "-1",
title: "only my railgun",
author: "fripside",
albumn: "某科学的超电磁炮",
lyric: onlyMyRailgun,
url: mOnlyMyRailgun,
},
.....//加入你想要的音乐
];
this.renderSong();
}
bind() {
this.audio.src = this.songList[this.currentIndex].url;
this.$(".play-pause").onclick = (e) => {//点击播放暂停
if (e.target.classList.contains("playing")) {
this.audio.pause();
e.target.classList.remove("playing");
} else {
this.audio.play();
e.target.classList.add("playing");
}
};
this.$(".play-prev").onclick = () => {//播放上一首
this.playPrevSong();
};
this.$(".play-next").onclick = () => {//播放下一首
this.playNextSong();
};
this.$(".area-bar .progress-button").ontouchstart = (e) => {//判断开始拖动小球的动作
this.progressButtonTouchStart = {
x: e.touches[0].clientX,
left: e.target.offsetLeft,
};
};
this.$(".area-bar .progress-button").ontouchmove = (e) => {//判断持续拖动的过程
const delta = e.touches[0].clientX - this.progressButtonTouchStart.x;
const bar = this.$(".area-bar .bar");
const progress = this.$(".area-bar .progress");
progress.style.width =
Math.min(
Math.max(//
((delta + this.progressButtonTouchStart.left) / bar.offsetWidth) *
100,
0
),
100
) + "%";
};
this.$(".area-bar .progress-button").ontouchend = () => {
this.progressButtonTouchStart = undefined;
const progress = this.$(".area-bar .progress");
this.audio.currentTime =
(this.audio.duration * parseInt(progress.style.width)) / 100;
};
this.audio.ontimeupdate = () => {
this.locateLyric();
if (!this.progressButtonTouchStart) {
this.setProgerssBar();
}
};
let swiper = new Swiper(this.$(".panels"));
swiper.on("swipLeft", (e) => {//向左滑动
e.classList.remove("panel1");
e.classList.add("panel2");
this.$$(".header .balls span")[1].classList.add("current");
this.$$(".header .balls span")[0].classList.remove("current");
this.$(".footer .buttons").style.opacity = 0;
this.$(".footer .buttons").style.pointerEvents = "none";
});
swiper.on("swipRight", (e) => {//向右滑动
e.classList.remove("panel2");
e.classList.add("panel1");
this.$$(".header .balls span")[1].classList.remove("current");
this.$$(".header .balls span")[0].classList.add("current");
this.$(".footer .buttons").style.opacity = 1;
this.$(".footer .buttons").style.pointerEvents = "auto";
});
}
重新渲染歌曲
renderSong() {
let songObj = this.songList[this.currentIndex];
this.$(".header h1").innerText = songObj.title;
this.$(".header p").innerText = songObj.author + "-" + songObj.albumn;
this.audio.onloadedmetadata = () =>
(this.$(".time-end").innerText = this.formateTime(this.audio.duration));
this.loadLyrics();
}
playPrevSong() {
this.currentIndex =
(this.songList.length + this.currentIndex - 1) % this.songList.length;
this.audio.src = this.songList[this.currentIndex].url;
this.audio.play();
this.$(".play-pause").classList.add("playing");
this.renderSong();
}
playNextSong() {
this.currentIndex = (this.currentIndex + 1) % this.songList.length;
this.audio.src = this.songList[this.currentIndex].url;
this.audio.play();
this.$(".play-pause").classList.add("playing");
this.renderSong();
}
loadLyrics() {
this.setLyrics(this.songList[this.currentIndex].lyric.lrc.lyric);
}
locateLyric() {
const currentTime = (this.audio?.currentTime ?? 0) * 1000;
for (let index = 0; index < this.lyricsArr.length; index++) {
if (this.lyricsArr[index][0] < currentTime) {
this.lyricIndex = index;
} else {
break;
}
}
let node = this.$(
'[data-time="' + this.lyricsArr[this.lyricIndex][0] + '"]'
);
if (node) {
this.setLyricToCenter(node);
}
this.$$(".panel-effect .lyrics p")[0].innerText =
this.lyricsArr[this.lyricIndex][1];
this.$$(".panel-effect .lyrics p")[1].innerText = this.lyricsArr[
this.lyricIndex + 1
]
? this.lyricsArr[this.lyricIndex + 1][1]
: "";
}
setLyrics(lyrics) {
this.lyricIndex = 0;
let fragment = document.createDocumentFragment();
this.lyricsArr = [];
lyrics
.split(/\n/)
.filter((str) => str.match(/\[.+?\]/))
.forEach((line) => {
let str = line.replace(/\[.+?\]/g, "");
line.match(/\[.+?\]/g).forEach((t) => {
t = t.replace(/[\[\]]/g, "");
let milliseconds =
parseInt(t.slice(0, 2)) * 60 * 1000 +
parseInt(t.slice(3, 5)) * 1000 +
parseInt(t.slice(6));
this.lyricsArr.push([milliseconds, str]);
});
});
this.lyricsArr = this.lyricsArr
.filter((line) => line[1].trim() !== "")
.sort((v1, v2) => {
if (v1[0] > v2[0]) {
return 1;
} else {
return -1;
}
});
this.lyricsArr.forEach((line) => {
let node = document.createElement("p");
node.setAttribute("data-time", line[0]);
node.innerText = line[1];
fragment.appendChild(node);
});
this.$$(".panel-effect .lyrics p")[0].innerText =
this.lyricsArr[this.lyricIndex][1];
this.$$(".panel-effect .lyrics p")[1].innerText = this.lyricsArr[
this.lyricIndex + 1
]
? this.lyricsArr[this.lyricIndex + 1][1]
: "";
this.$(".panel-lyrics .container").innerHTML = "";
this.$(".panel-lyrics .container").appendChild(fragment);
}
setLyricToCenter(node) {
let offset = node.offsetTop - this.$(".panel-lyrics").offsetHeight / 2;
this.$(
".panel-lyrics .container"
).style.transform = `translateY(${-offset}px)`;
this.$$(".panel-lyrics p").forEach((node) => {
node.classList.remove("current");
});
node.classList.add("current");
}
setProgerssBar() {
let percent = (this.audio.currentTime * 100) / this.audio.duration + "%";
this.$(".bar .progress").style.width = percent;
this.$(".time-start").innerText = this.formateTime(this.audio.currentTime);
}
formateTime(secondsTotal) {
let minutes = parseInt(secondsTotal / 60);
minutes = minutes >= 10 ? "" + minutes : "0" + minutes;
let seconds = parseInt(secondsTotal % 60);
seconds = seconds >= 10 ? "" + seconds : "0" + seconds;
return minutes + ":" + seconds;
}
}
window.p = new Player("#player");