歌词滚动效果

请添加图片描述
项目技术栈:Vue3+Vite+TS、NativeUI、tailwindcss
歌词文件lrc.ts

export default [
  '[00:00:00]反方向的钟',
  '[00:02:00]周杰伦',
  '[00:03:00]Jay',
  '',
  '[00:04.74]词: 方文山 曲: 周杰伦',
  '[00: 45.86]迷迷蒙蒙 你给的梦',
  '[00: 47.94]出现裂缝 隐隐作痛',
  '[00: 50.53]怎么沟通你都没空',
  '[00: 53.04]说我不懂 说了没用',
  '[00: 55.64]他的笑容 有何不同',
  '[00: 58.17]在你心中 我不再受宠',
  '[01:00.64]我的天空 是雨是风 还是彩虹',
  '[01:04.54]你在操纵',
  '[02: 48.08][01:07.13]恨自己真的没用情绪激动',
  '[02: 52.92][01: 11.47]一颗心到现在还在抽痛',
  '[02: 57.80][01: 16.42]还为分手前那句抱歉在感动',
  '[03: 48.46][03:07.80][01: 26.26]穿梭时间的画面的钟',
  '[03: 53.42][03: 12.77][01: 31.31]从反方向 开始移动',
  '[03: 58.36][03: 17.79][01: 36.44]回到当初爱你的时空',
  '[04:03.43][03: 22.85][01: 41.49]停格内容 不忠',
  '[04:08.70][03: 28.06][01: 46.55]所有回忆对着我进攻',
  '[03: 33.13][01: 51.60]我的伤口 被你拆封',
  '[03: 38.19][01: 56.67]誓言太沉重泪被纵容',
  '[03: 43.24][02:01.78]脸上汹涌 失控',
  '[02: 17.12]城市霓虹 不安跳动',
  '[02: 18.77]染红夜空 过去种种',
  '[02: 21.90]像一场梦 不敢去碰',
  '[02: 23.22]一想就痛 心碎内容',
  '[02: 24.65]每一秒钟 都有不同',
  '[02: 25.70]你不懂 连一句珍重',
  '[02: 26.25]也有苦衷 也不想送',
  '[02: 27.51]寒风中 废墟烟囱',
  '[02: 28.61]停止转动 一切落空',
  '[02: 30.10]在人海中盲目跟从',
  '[02: 31.91]别人的梦 全面放纵',
  '[02: 33.51]恨没有用 疗伤止痛',
  '[02: 34.82]不在感动 没有梦',
  '[02: 35.98]痛不知轻重',
  '[02: 36.86]泪水鲜红 全面放纵',
]
<!-- 首页 -->
<template>
  <div class="music-container">
    <div class="mask" />
    <div class="singer-logo" />
    <div ref="lrcContainer" class="music-lrc">
      <ul ref="lrcUl">
        <li v-for="(item, index) in lrcData" :key="index" :class="lrcActive === index && 'active'">{{ item.words }}</li>
      </ul>
    </div>
    <AudioController @change="setOffset" />
  </div>
</template>

<script lang='ts' setup>
import lrc from './lrc'
import AudioController from '@/components/Test/Music/AudioController.vue'
interface lrcType {
  time: number,
  words?: string
}
const lrcData = ref<lrcType[]>() // 歌词
const lrcContainer = ref<HTMLDivElement>() // 歌词外框
const lrcUl = ref<HTMLDivElement>() // 歌词ul
const lrcActive = ref<number | null>(null) // 当前激活歌词
onMounted(() => {
  lrcData.value = lrcFormat(lrc)
})

/**
 * @description 歌词格式化
*/
const lrcFormat = (lrc: any) => {
  const lrcRes: lrcType[] = []
  lrc.forEach((line: string) => {
    const strArr = line.split(']')
    const words = strArr.pop()
    strArr.forEach((time: string) => {
      const timeStr = time.substring(1)
      lrcRes.push({
        time: timeFormat(timeStr),
        words
      })
    })
  });
  return lrcRes.sort((a, b) => a.time - b.time)
}

/**
 * @description 时间格式化
*/
const timeFormat = (time: string) => {
  const timeArr = time.split(':')
  return +timeArr[0] * 60 + +timeArr[1]
}

/**
 * @description 设置歌词位置
*/
const setOffset = (time: number) => {
  const afterIndex = lrcData.value?.findIndex(v => v.time > time)
  const curIndex = afterIndex ? afterIndex - 1 : 0
  lrcActive.value = curIndex // 设置当前激活歌词

  const lrcContainerHeight = ((lrcContainer.value?.clientHeight || 0) / 2) - 45
  const lrcHeight = curIndex * 30
  let offsetY = 0
  if (lrcContainerHeight <= lrcHeight) {
    offsetY = lrcContainerHeight - lrcHeight
  }
  if (lrcUl.value) {
    lrcUl.value.style.transform = `translateY(${offsetY}px)`
  }
}


</script>
<style lang='scss'>
.music-container {
  height: 100%;
  position: relative;
  font-family: '微软雅黑';

  .mask {
    background: rgba(0, 0, 0, .35);
    z-index: -1;
  }

  .singer-logo {
    background-image: url('../../../assets/images/Jay.webp');
    background-repeat: no-repeat;
    background-size: cover;
    z-index: -2;
    filter: blur(65px);

  }

  .mask,
  .singer-logo {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
  }

  .music-lrc {
    height: 70%;
    position: absolute;
    top: 50%;
    width: 100%;
    transform: translateY(-50%);
    overflow: hidden;
    text-align: center;
  }

  ul {
    list-style: none;
    user-select: none;
    cursor: pointer;
    transition: .2s;

    li {
      height: 30px;
      line-height: 30px;
      color: rgba(255, 255, 255, .5);
      transition: .2s;

      &.active {
        color: #31c27c;
        transform: scale(1.3);
        font-weight: 700;
      }
    }
  }
}
</style>

控制器组件 AudioController.vue


<!-- 音频控制器 -->
<template>
  <div class="audio-controller">
    <audio class="hidden" ref="audio" src="/static/audio/music.mp3" controls />
    <div class="relative logo w-14 h-14">
      <img class="w-full h-full rounded-full" :class="[musicPlay && 'rotate']" src="~@/assets/images/Jay.webp">
      <div class="mask w-14 h-14 rounded-full" />
      <n-icon v-show="!musicPlay" class="absolute p-icon" color="#fff" size="40" :component="PlayCircleOutline"
        @click.native="audioPlay" />
      <n-icon v-show="musicPlay" class="absolute p-icon" color="#fff" size="40" :component="PauseCircleOutline"
        @click.native="audioSuspend" />
    </div>
    <div class="relative w-2/4 px-4 ">
      <div class="text-[#c4c3c3] select-none">周杰伦-反方向的钟</div>
      <div class="absolute top-0 right-4 text-[rgba(255,255,255,.4)] select-none">{{ sec_to_time(audio?.currentTime || 0)
      }} /
        {{ sec_to_time(audio?.duration || 0) }}</div>
      <n-slider class="mt-2" v-model:value="musicStep" :tooltip="false" @update-value="handleSliderClick">
        <template #thumb>
          <n-icon-wrapper :size="20" :border-radius="12">
            <n-icon :size="12" :component="MusicalNotes" />
          </n-icon-wrapper>
        </template>
      </n-slider>
    </div>
  </div>
</template>
 
<script lang='ts' setup>
import { PlayCircleOutline, PauseCircleOutline, MusicalNotes } from '@vicons/ionicons5'
const musicStep = ref(0) // 进度条进度
const musicPlay = ref<boolean>(false)
const audio = ref<HTMLAudioElement>() // 播放器
const duration = ref<number>() // 歌曲时长
const emit = defineEmits(['change'])

onMounted(() => {
  audio.value?.addEventListener('timeupdate', () => {
    emit('change', audio.value?.currentTime || 0)
    if (audio.value && duration.value) {
      // 设置进度条进度
      musicStep.value = (audio.value.currentTime / duration.value) * 100
    }
  })
  audio.value?.addEventListener('canplaythrough', () => {
    duration.value = audio.value?.duration
  })
})

// 进度条点击事件
const handleSliderClick = (curStep: number) => {
  if (audio.value) {
    audio.value.currentTime = (curStep / 100) * audio.value?.duration
  }
}

// 播放
const audioPlay = () => {
  musicPlay.value = true
  audio.value?.play()
}

// 暂停
const audioSuspend = () => {
  musicPlay.value = false
  audio.value?.pause()
}

// 将秒转换为分秒格式
const sec_to_time = (s: number) => {
  let t = '';
  if (s > -1) {
    let min = Math.floor(s / 60) % 60;
    let sec = ~~s % 60;
    if (min < 10) t += "0"
    t += min + ":";
    if (sec < 10) t += "0"
    t += sec;
  }
  return t;
}
</script>
<style lang='scss' scoped>
.audio-controller {
  position: absolute;
  bottom: 0;
  width: 100%;
  height: 80px;
  background: rgba(0, 0, 0, .2);
  display: flex;
  align-items: center;
  justify-content: center;

  .logo {
    cursor: pointer;

    &:hover {

      .mask,
      .p-icon {
        transition: .4s;
        opacity: 1;
      }
    }
  }

  .mask {
    opacity: 0;
    position: absolute;
    background: rgba(0, 0, 0, .5);
    z-index: 9;
  }

  .p-icon {
    opacity: 0;
    left: 50%;
    top: 50%;
    z-index: 10;
    transform: translate(-50%, -50%);
  }
}

.rotate {
  animation: rotate 8s linear infinite;
}

@keyframes rotate {
  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(360deg);
  }
}
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值