style(ScrollingLyrics): adjust layout and improve animations for better user experience

This commit is contained in:
Astrian Zheng 2025-05-26 18:23:24 +10:00
parent 6bf9c493aa
commit 8a16f3cde8
Signed by: Astrian
SSH Key Fingerprint: SHA256:rVnhx3DAKjujCwWE13aDl7uV6+9U1MvydLkNRXJrBiA
2 changed files with 76 additions and 89 deletions

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="relative overflow-hidden h-full w-full bg-gradient-to-b from-black/5 via-transparent to-black/5" class="relative overflow-hidden h-full w-[40rem]"
ref="lyricsContainer" ref="lyricsContainer"
@wheel="handleWheel" @wheel="handleWheel"
> >
@ -15,16 +15,16 @@
<div <div
v-for="(line, index) in parsedLyrics" v-for="(line, index) in parsedLyrics"
:key="index" :key="index"
:ref="el => setLineRef(el, index)" :ref="el => setLineRef(el as HTMLElement, index)"
class="py-8 px-16 cursor-pointer transition-all duration-300 hover:scale-105" class="py-8 px-16 cursor-pointer transition-all duration-300 hover:scale-105"
@click="handleLineClick(line, index)" @click="handleLineClick(line, index)"
> >
<div v-if="line.type === 'lyric'" class="relative text-center"> <div v-if="line.type === 'lyric'" class="relative">
<!-- 背景模糊文字 --> <!-- 背景模糊文字 -->
<div <div
class="text-3xl font-bold transition-all duration-500" class="text-3xl font-bold transition-all duration-500"
:class="[ :class="[
currentLineIndex === index ? 'text-black/80 blur-3xl' : 'text-black/20 blur-3xl' currentLineIndex === index ? 'text-black/80 blur-xl' : 'text-black/20 blur-2xl'
]" ]"
> >
{{ line.text }} {{ line.text }}
@ -36,8 +36,8 @@
currentLineIndex === index currentLineIndex === index
? 'text-white scale-110' ? 'text-white scale-110'
: index < currentLineIndex : index < currentLineIndex
? 'text-white/60' ? userScrolling ? 'text-white/60' : 'text-white/60 blur-sm'
: 'text-white/40' : userScrolling ? 'text-white/40' : 'text-white/40 blur-sm'
]" ]"
> >
{{ line.text }} {{ line.text }}
@ -45,16 +45,13 @@
</div> </div>
<div v-else-if="line.type === 'gap'" class="flex justify-center items-center py-4"> <div v-else-if="line.type === 'gap'" class="flex justify-center items-center py-4">
<div class="w-16 h-px bg-gradient-to-r from-transparent via-white/30 to-transparent rounded-full"></div> <div class="w-16 h-px rounded-full"></div>
</div> </div>
</div> </div>
<!-- 底部填充 --> <!-- 底部填充 -->
<div class="h-1/2 pointer-events-none"></div> <div class="h-1/2 pointer-events-none"></div>
</div> </div>
<!-- 中央指示线 -->
<div class="absolute top-1/2 left-16 right-16 h-px bg-gradient-to-r from-transparent via-white/20 to-transparent pointer-events-none transform -translate-y-1/2"></div>
<!-- 歌词控制面板 --> <!-- 歌词控制面板 -->
<div <div
@ -161,7 +158,7 @@ const noLyricsIndicator = ref<HTMLElement>()
// GSAP // GSAP
let scrollTween: gsap.core.Tween | null = null let scrollTween: gsap.core.Tween | null = null
let highlightTween: gsap.core.Tween | null = null let highlightTween: gsap.core.Tween | null = null
let userScrollTimeout: number | null = null let userScrollTimeout: NodeJS.Timeout | null = null
// Props // Props
const props = defineProps<{ const props = defineProps<{
@ -332,7 +329,7 @@ function highlightCurrentLine(lineIndex: number) {
highlightTween = gsap.to(lineElement, { highlightTween = gsap.to(lineElement, {
scale: 1.05, scale: 1.05,
opacity: 1, opacity: 1,
duration: 0.5, duration: 0.2,
ease: "back.out(1.7)", ease: "back.out(1.7)",
onComplete: () => { onComplete: () => {
highlightTween = null highlightTween = null
@ -449,7 +446,7 @@ function resetScroll() {
// //
gsap.to(lyricsWrapper.value, { gsap.to(lyricsWrapper.value, {
y: 0, y: 0,
duration: 0.6, duration: 0.3,
ease: "power2.out" ease: "power2.out"
}) })
@ -552,7 +549,7 @@ onMounted(() => {
if (controlPanel.value) { if (controlPanel.value) {
gsap.fromTo(controlPanel.value, gsap.fromTo(controlPanel.value,
{ opacity: 0, x: 20 }, { opacity: 0, x: 20 },
{ opacity: 0, x: 0, duration: 0.5, ease: "power2.out", delay: 0.5 } { opacity: 0, x: 0, duration: 0.2, ease: "power2.out", delay: 0.2 }
) )
} }
@ -565,7 +562,7 @@ onMounted(() => {
{ {
opacity: 1, opacity: 1,
y: 0, y: 0,
duration: 0.5, duration: 0.2,
ease: "power2.out", ease: "power2.out",
delay: index * 0.1 delay: index * 0.1
} }

View File

@ -3,7 +3,7 @@ import { usePlayQueueStore } from '../stores/usePlayQueueStore'
import { artistsOrganize } from '../utils' import { artistsOrganize } from '../utils'
import gsap from 'gsap' import gsap from 'gsap'
import { Draggable } from "gsap/Draggable" import { Draggable } from "gsap/Draggable"
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted, nextTick } from 'vue'
import { useTemplateRef } from 'vue' import { useTemplateRef } from 'vue'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { usePreferences } from '../stores/usePreferences' import { usePreferences } from '../stores/usePreferences'
@ -34,27 +34,13 @@ const progressBarContainer = useTemplateRef('progressBarContainer')
const playQueueDialogContainer = useTemplateRef('playQueueDialogContainer') const playQueueDialogContainer = useTemplateRef('playQueueDialogContainer')
const playQueueDialog = useTemplateRef('playQueueDialog') const playQueueDialog = useTemplateRef('playQueueDialog')
const controllerRef = useTemplateRef('controllerRef') const controllerRef = useTemplateRef('controllerRef')
const mainContainer = useTemplateRef('mainContainer')
const lyricsSection = useTemplateRef('lyricsSection') const lyricsSection = useTemplateRef('lyricsSection')
const scrollingLyrics = useTemplateRef('scrollingLyrics')
const albumCover = useTemplateRef('albumCover') const albumCover = useTemplateRef('albumCover')
const songInfo = useTemplateRef('songInfo') const songInfo = useTemplateRef('songInfo')
const progressSection = useTemplateRef('progressSection')
const controlButtons = useTemplateRef('controlButtons')
const favoriteButton = useTemplateRef('favoriteButton')
const volumeButton = useTemplateRef('volumeButton')
const queueButton = useTemplateRef('queueButton')
const prevButton = useTemplateRef('prevButton')
const playButton = useTemplateRef('playButton') const playButton = useTemplateRef('playButton')
const nextButton = useTemplateRef('nextButton')
const lyricsButton = useTemplateRef('lyricsButton')
const moreButton = useTemplateRef('moreButton')
const presentQueueListDialog = ref(false) const presentQueueListDialog = ref(false)
const controllerHeight = ref(500)
let resizeObserver: ResizeObserver | null = null
let heightSyncTween: gsap.core.Tween | null = null
onMounted(async () => { onMounted(async () => {
Draggable.create(progressBarThumb.value, { Draggable.create(progressBarThumb.value, {
@ -68,8 +54,7 @@ onMounted(async () => {
} }
}) })
thumbUpdate() thumbUpdate()
setupHeightSync()
setupEntranceAnimations() setupEntranceAnimations()
}) })
@ -125,37 +110,6 @@ function playPrevious() {
} }
} }
function setupHeightSync() {
if (controllerRef.value && lyricsSection.value) {
updateLyricsHeight()
resizeObserver = new ResizeObserver(() => {
updateLyricsHeight()
})
resizeObserver.observe(controllerRef.value)
}
}
function updateLyricsHeight() {
if (!controllerRef.value || !lyricsSection.value) return
const newHeight = controllerRef.value.offsetHeight
if (Math.abs(newHeight - controllerHeight.value) > 5) {
controllerHeight.value = newHeight
if (heightSyncTween) {
heightSyncTween.kill()
}
heightSyncTween = gsap.to(lyricsSection.value, {
height: newHeight,
duration: 0.3,
ease: "power2.out"
})
}
}
function setupEntranceAnimations() { function setupEntranceAnimations() {
if (controllerRef.value) { if (controllerRef.value) {
gsap.fromTo(controllerRef.value.children, gsap.fromTo(controllerRef.value.children,
@ -184,15 +138,35 @@ function handlePlayPause() {
playQueueStore.isPlaying = !playQueueStore.isPlaying playQueueStore.isPlaying = !playQueueStore.isPlaying
} }
}) })
} else {
playQueueStore.isPlaying = !playQueueStore.isPlaying
} }
} }
function toggleShuffle() { function toggleShuffle() {
playQueueStore.playMode.shuffle = !playQueueStore.playMode.shuffle playQueueStore.playMode.shuffle = !playQueueStore.playMode.shuffle
playQueueStore.shuffleCurrent = false playQueueStore.shuffleCurrent = false
nextTick(() => {
const shuffleBtn = playQueueDialog.value?.querySelector('.flex-1:first-child') as HTMLElement
if (shuffleBtn) {
gsap.to(shuffleBtn, {
scale: 0.95, duration: 0.1, yoyo: true, repeat: 1, ease: "power2.inOut"
})
}
})
} }
function toggleRepeat() { function toggleRepeat() {
nextTick(() => {
const repeatBtn = playQueueDialog.value?.querySelector('.flex-1:last-child') as HTMLElement
if (repeatBtn) {
gsap.to(repeatBtn, {
rotateZ: 360, scale: 0.95, duration: 0.3, ease: "back.out(1.7)"
})
}
})
switch (playQueueStore.playMode.repeat) { switch (playQueueStore.playMode.repeat) {
case 'off': playQueueStore.playMode.repeat = 'all'; break case 'off': playQueueStore.playMode.repeat = 'all'; break
case 'all': playQueueStore.playMode.repeat = 'single'; break case 'all': playQueueStore.playMode.repeat = 'single'; break
@ -203,30 +177,52 @@ function toggleRepeat() {
function makePlayQueueListPresent() { function makePlayQueueListPresent() {
presentQueueListDialog.value = true presentQueueListDialog.value = true
const tl = gsap.timeline() nextTick(() => {
tl.to(playQueueDialogContainer.value, { if (!playQueueDialogContainer.value || !playQueueDialog.value) return
backgroundColor: '#17171780', duration: 0.3, ease: 'power2.out'
}).to(playQueueDialog.value, { const tl = gsap.timeline()
x: 0, duration: 0.4, ease: 'power3.out' tl.to(playQueueDialogContainer.value, {
}, '<0.1').fromTo(playQueueDialog.value.children, backgroundColor: '#17171780', duration: 0.3, ease: 'power2.out'
{ opacity: 0, x: -20 }, }).to(playQueueDialog.value, {
{ opacity: 1, x: 0, duration: 0.3, ease: 'power2.out', stagger: 0.05 }, '<0.2') x: 0, duration: 0.4, ease: 'power3.out'
}, '<0.1')
if (playQueueDialog.value.children.length > 0) {
tl.fromTo(playQueueDialog.value.children,
{ opacity: 0, x: -20 },
{ opacity: 1, x: 0, duration: 0.3, ease: 'power2.out', stagger: 0.05 }, '<0.2')
}
})
} }
function makePlayQueueListDismiss() { function makePlayQueueListDismiss() {
if (!playQueueDialogContainer.value || !playQueueDialog.value) {
presentQueueListDialog.value = false
return
}
const tl = gsap.timeline({ const tl = gsap.timeline({
onComplete: () => { onComplete: () => {
presentQueueListDialog.value = false presentQueueListDialog.value = false
gsap.set(playQueueDialog.value, { x: -384 }) if (playQueueDialog.value) {
gsap.set(playQueueDialogContainer.value, { backgroundColor: 'transparent' }) gsap.set(playQueueDialog.value, { x: -384 })
}
if (playQueueDialogContainer.value) {
gsap.set(playQueueDialogContainer.value, { backgroundColor: 'transparent' })
}
} }
}) })
tl.to(playQueueDialog.value.children, { if (playQueueDialog.value.children.length > 0) {
opacity: 0, x: -20, duration: 0.2, ease: 'power2.in', stagger: 0.03 tl.to(playQueueDialog.value.children, {
}).to(playQueueDialog.value, { opacity: 0, x: -20, duration: 0.2, ease: 'power2.in', stagger: 0.03
})
}
tl.to(playQueueDialog.value, {
x: -384, duration: 0.3, ease: 'power2.in' x: -384, duration: 0.3, ease: 'power2.in'
}, '<0.1').to(playQueueDialogContainer.value, { }, playQueueDialog.value.children.length > 0 ? '<0.1' : '0')
.to(playQueueDialogContainer.value, {
backgroundColor: 'transparent', duration: 0.2, ease: 'power2.in' backgroundColor: 'transparent', duration: 0.2, ease: 'power2.in'
}, '<') }, '<')
} }
@ -239,13 +235,7 @@ function getCurrentTrack() {
} }
} }
function calculateStickyTop() {
return (window.innerHeight - (controllerRef.value?.clientHeight ?? 0)) / 2
}
onUnmounted(() => { onUnmounted(() => {
if (resizeObserver) resizeObserver.disconnect()
if (heightSyncTween) heightSyncTween.kill()
}) })
// New: Watch for track changes and animate // New: Watch for track changes and animate
@ -273,13 +263,13 @@ watch(() => playQueueStore.currentIndex, () => {
<div class="bg-transparent w-full h-full absolute top-0 left-0" /> <div class="bg-transparent w-full h-full absolute top-0 left-0" />
</div> </div>
<!-- Main content area - new flex layout --> <!-- Main content area - new centered flex layout -->
<div class="absolute top-0 left-0 flex justify-center h-screen w-screen overflow-y-auto z-10 select-none"> <div class="absolute top-0 left-0 flex justify-center h-screen w-screen overflow-y-auto z-10 select-none">
<div class="flex items-center justify-center gap-16 h-fit my-auto" ref="mainContainer"> <div class="flex items-center justify-center gap-16 h-fit my-auto" ref="mainContainer">
<!-- Controller area --> <!-- Controller area -->
<div class="flex flex-col w-96 gap-4" ref="controllerRef"> <div class="flex flex-col w-96 gap-4" ref="controllerRef">
<!-- Album cover with hover effect --> <!-- Album cover with enhanced hover effect -->
<div ref="albumCover" class="relative"> <div ref="albumCover" class="relative">
<img :src="getCurrentTrack().album?.coverUrl" <img :src="getCurrentTrack().album?.coverUrl"
class="rounded-2xl shadow-2xl border border-white/20 w-96 h-96 transition-transform duration-300 hover:scale-105" /> class="rounded-2xl shadow-2xl border border-white/20 w-96 h-96 transition-transform duration-300 hover:scale-105" />
@ -318,7 +308,7 @@ watch(() => playQueueStore.currentIndex, () => {
</button> </button>
</div> </div>
<!-- Progress section with enhanced interactions --> <!-- Progress section -->
<div class="flex flex-col gap-1" ref="progressSection"> <div class="flex flex-col gap-1" ref="progressSection">
<!-- ...existing progress bar code... --> <!-- ...existing progress bar code... -->
<div class="w-full p-[0.125rem] bg-white/20 shadow-[0_.125rem_1rem_0_#00000010] rounded-full backdrop-blur-3xl"> <div class="w-full p-[0.125rem] bg-white/20 shadow-[0_.125rem_1rem_0_#00000010] rounded-full backdrop-blur-3xl">
@ -348,7 +338,7 @@ watch(() => playQueueStore.currentIndex, () => {
</div> </div>
</div> </div>
<!-- Control buttons with enhanced animations --> <!-- Control buttons -->
<div class="w-full flex justify-between items-center" ref="controlButtons"> <div class="w-full flex justify-between items-center" ref="controlButtons">
<div class="flex-1 text-left flex gap-1"> <div class="flex-1 text-left flex gap-1">
<button class="h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110" <button class="h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110"
@ -462,8 +452,8 @@ watch(() => playQueueStore.currentIndex, () => {
</div> </div>
</div> </div>
<!-- Lyrics section - height synced with controller --> <!-- Lyrics section - full screen height -->
<div class="w-96" ref="lyricsSection"> <div class="w-[40rem] h-screen" ref="lyricsSection">
<ScrollingLyrics :lrcSrc="getCurrentTrack().song.lyricUrl ?? undefined" <ScrollingLyrics :lrcSrc="getCurrentTrack().song.lyricUrl ?? undefined"
class="h-full" ref="scrollingLyrics" /> class="h-full" ref="scrollingLyrics" />
</div> </div>