阅读 721
原生JS实现一个音乐播放器

原生JS实现一个音乐播放器

准备工作:

VScode 编辑器 设计稿 (无图不页面)

设计稿分析:

效果图.jpeg

  • 从图上首先我们可以分析出来一整个页面从上至下可以划分为 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");
复制代码
文章分类
前端
文章标签