暂无歌词
享受纯音乐的美妙
@@ -196,25 +164,25 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
originalTime: '[00:00]'
}
]
-
+
const lines = lrcText.split('\n')
const tempParsedLines: (LyricsLine | GapLine)[] = []
-
+
const timeRegex = /\[(\d{1,2}):(\d{2})(?:\.(\d{1,3}))?\]/g
-
+
for (const line of lines) {
const matches = [...line.matchAll(timeRegex)]
if (matches.length === 0) continue
-
+
const text = line.replace(/\[\d{1,2}:\d{2}(?:\.\d{1,3})?\]/g, '').trim()
-
+
for (const match of matches) {
const minutes = parseInt(match[1])
const seconds = parseInt(match[2])
const milliseconds = match[3] ? parseInt(match[3].padEnd(3, '0')) : 0
-
+
const totalSeconds = minutes * 60 + seconds + milliseconds / 1000
-
+
if (text) {
tempParsedLines.push({
type: 'lyric',
@@ -231,29 +199,29 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
}
}
}
-
+
tempParsedLines.sort((a, b) => a.time - b.time)
-
+
const finalLines: (LyricsLine | GapLine)[] = []
const lyricLines = tempParsedLines.filter(line => line.type === 'lyric') as LyricsLine[]
const gapLines = tempParsedLines.filter(line => line.type === 'gap') as GapLine[]
-
+
if (lyricLines.length === 0) return tempParsedLines
-
+
for (let i = 0; i < gapLines.length; i++) {
const gapLine = gapLines[i]
const nextLyricLine = lyricLines.find(lyric => lyric.time > gapLine.time)
-
+
if (nextLyricLine) {
const duration = nextLyricLine.time - gapLine.time
gapLine.duration = duration
-
+
if (duration >= minGapDuration) {
finalLines.push(gapLine)
}
}
}
-
+
finalLines.push(...lyricLines)
const sortedLines = finalLines.sort((a, b) => a.time - b.time)
// 在最前面插入一个空行
@@ -285,24 +253,24 @@ function findCurrentLineIndex(time: number): number {
// 使用 GSAP 滚动到指定行
function scrollToLine(lineIndex: number, smooth = true) {
if (!lyricsContainer.value || !lyricsWrapper.value || !lineRefs.value[lineIndex]) return
-
+
const container = lyricsContainer.value
const wrapper = lyricsWrapper.value
const lineElement = lineRefs.value[lineIndex]
-
+
const containerHeight = container.clientHeight
const containerCenter = containerHeight / 2
-
+
// 计算目标位置
const lineOffsetTop = lineElement.offsetTop
const lineHeight = lineElement.clientHeight
const targetY = containerCenter - lineOffsetTop - lineHeight / 2
-
+
// 停止之前的滚动动画
if (scrollTween) {
scrollTween.kill()
}
-
+
if (smooth) {
// 使用 GSAP 平滑滚动
scrollTween = gsap.to(wrapper, {
@@ -321,14 +289,14 @@ function scrollToLine(lineIndex: number, smooth = true) {
// 高亮当前行动画
function highlightCurrentLine(lineIndex: number) {
if (!lineRefs.value[lineIndex]) return
-
+
const lineElement = lineRefs.value[lineIndex]
-
+
// 停止之前的高亮动画
if (highlightTween) {
highlightTween.kill()
}
-
+
// 重置所有行的样式
lineRefs.value.forEach((el, index) => {
if (el && index !== lineIndex) {
@@ -340,7 +308,7 @@ function highlightCurrentLine(lineIndex: number) {
})
}
})
-
+
// 高亮当前行
highlightTween = gsap.to(lineElement, {
scale: 1.05,
@@ -356,7 +324,7 @@ function highlightCurrentLine(lineIndex: number) {
// 处理鼠标滚轮
function handleWheel(event: WheelEvent) {
event.preventDefault()
-
+
if (!lyricsWrapper.value || !lyricsContainer.value) return
userScrolling.value = true
@@ -403,18 +371,18 @@ function handleLineClick(line: LyricsLine | GapLine, index: number) {
// 这里可以发出事件让父组件处理音频跳转
// emit('seek', line.time)
}
-
+
// 滚动到点击的行
scrollToLine(index, true)
-
+
// 添加点击反馈动画
if (lineRefs.value[index]) {
- gsap.fromTo(lineRefs.value[index],
+ gsap.fromTo(lineRefs.value[index],
{ scale: 1 },
- {
- scale: 1.1,
- duration: 0.1,
- yoyo: true,
+ {
+ scale: 1.1,
+ duration: 0.1,
+ yoyo: true,
repeat: 1,
ease: "power2.inOut"
}
@@ -426,21 +394,21 @@ function handleLineClick(line: LyricsLine | GapLine, index: number) {
function toggleAutoScroll() {
autoScroll.value = !autoScroll.value
userScrolling.value = false
-
+
// 按钮点击动画
if (controlPanel.value) {
gsap.fromTo(controlPanel.value.children[0],
{ scale: 1 },
- {
- scale: 0.95,
- duration: 0.1,
- yoyo: true,
+ {
+ scale: 0.95,
+ duration: 0.1,
+ yoyo: true,
repeat: 1,
ease: "power2.inOut"
}
)
}
-
+
if (autoScroll.value && currentLineIndex.value >= 0) {
nextTick(() => {
scrollToLine(currentLineIndex.value, true)
@@ -451,35 +419,35 @@ function toggleAutoScroll() {
// 重置滚动
function resetScroll() {
if (!lyricsWrapper.value) return
-
+
// 停止所有动画
if (scrollTween) scrollTween.kill()
if (highlightTween) highlightTween.kill()
-
+
// 重置位置
gsap.to(lyricsWrapper.value, {
y: 0,
duration: 0.3,
ease: "power2.out"
})
-
+
autoScroll.value = true
userScrolling.value = false
-
+
// 按钮点击动画
if (controlPanel.value) {
gsap.fromTo(controlPanel.value.children[1],
{ scale: 1 },
- {
- scale: 0.95,
- duration: 0.1,
- yoyo: true,
+ {
+ scale: 0.95,
+ duration: 0.1,
+ yoyo: true,
repeat: 1,
ease: "power2.inOut"
}
)
}
-
+
if (currentLineIndex.value >= 0) {
nextTick(() => {
scrollToLine(currentLineIndex.value, true)
@@ -487,16 +455,34 @@ function resetScroll() {
}
}
+// gap 圆点透明度计算
+function getGapDotOpacities(line: GapLine) {
+ // 获取 gap 的持续时间
+ const duration = line.duration ?? 0
+ if (duration <= 0) return [0.3, 0.3, 0.3]
+ // 当前播放时间
+ const now = playQueueStore.currentTime
+ // gap 起止时间
+ const start = line.time
+ // 计算进度
+ let progress = (now - start) / duration
+ progress = Math.max(0, Math.min(1, progress))
+ // 每个圆点的阈值
+ const thresholds = [1 / 4, 2 / 4, 3 / 4]
+ // 透明度从 0.3 到 1
+ return thresholds.map(t => progress >= t ? 1 : progress >= t - 1 / 3 ? 0.6 : 0.3)
+}
+
// 监听播放时间变化
watch(() => playQueueStore.currentTime, (time) => {
const newIndex = findCurrentLineIndex(time)
-
+
if (newIndex !== currentLineIndex.value && newIndex >= 0) {
currentLineIndex.value = newIndex
-
+
// 高亮动画
highlightCurrentLine(newIndex)
-
+
// 自动滚动
if (autoScroll.value && !userScrolling.value) {
nextTick(() => {
@@ -512,14 +498,14 @@ watch(() => props.lrcSrc, async (newSrc) => {
// 重置状态
currentLineIndex.value = -1
lineRefs.value = []
-
+
// 停止所有动画
if (scrollTween) scrollTween.kill()
if (highlightTween) highlightTween.kill()
-
+
if (newSrc) {
loading.value = true
-
+
// 加载动画
if (loadingIndicator.value) {
gsap.fromTo(loadingIndicator.value,
@@ -527,20 +513,20 @@ watch(() => props.lrcSrc, async (newSrc) => {
{ opacity: 1, scale: 1, duration: 0.3, ease: "back.out(1.7)" }
)
}
-
+
try {
const response = await axios.get(newSrc)
parsedLyrics.value = parseLyrics(response.data)
console.log('Parsed lyrics:', parsedLyrics.value)
-
+
autoScroll.value = true
userScrolling.value = false
-
+
// 重置滚动位置
if (lyricsWrapper.value) {
gsap.set(lyricsWrapper.value, { y: 0 })
}
-
+
} catch (error) {
console.error('Failed to load lyrics:', error)
parsedLyrics.value = []
@@ -549,7 +535,7 @@ watch(() => props.lrcSrc, async (newSrc) => {
}
} else {
parsedLyrics.value = []
-
+
// 重置滚动位置
if (lyricsWrapper.value) {
gsap.set(lyricsWrapper.value, { y: 0 })
@@ -566,17 +552,17 @@ onMounted(() => {
{ opacity: 0, x: 0, duration: 0.2, ease: "power2.out", delay: 0.2 }
)
}
-
+
// 歌词行入场动画
nextTick(() => {
lineRefs.value.forEach((el, index) => {
if (el) {
gsap.fromTo(el,
{ opacity: 0, y: 30 },
- {
- opacity: 1,
- y: 0,
- duration: 0.2,
+ {
+ opacity: 1,
+ y: 0,
+ duration: 0.2,
ease: "power2.out",
delay: index * 0.1
}