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>
<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"
@wheel="handleWheel"
>
@ -15,16 +15,16 @@
<div
v-for="(line, index) in parsedLyrics"
: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"
@click="handleLineClick(line, index)"
>
<div v-if="line.type === 'lyric'" class="relative text-center">
<div v-if="line.type === 'lyric'" class="relative">
<!-- 背景模糊文字 -->
<div
class="text-3xl font-bold transition-all duration-500"
: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 }}
@ -36,8 +36,8 @@
currentLineIndex === index
? 'text-white scale-110'
: index < currentLineIndex
? 'text-white/60'
: 'text-white/40'
? userScrolling ? 'text-white/60' : 'text-white/60 blur-sm'
: userScrolling ? 'text-white/40' : 'text-white/40 blur-sm'
]"
>
{{ line.text }}
@ -45,7 +45,7 @@
</div>
<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>
@ -53,9 +53,6 @@
<div class="h-1/2 pointer-events-none"></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
class="absolute top-4 right-4 flex gap-2 opacity-0 transition-opacity duration-300 hover:opacity-100"
@ -161,7 +158,7 @@ const noLyricsIndicator = ref<HTMLElement>()
// GSAP
let scrollTween: gsap.core.Tween | null = null
let highlightTween: gsap.core.Tween | null = null
let userScrollTimeout: number | null = null
let userScrollTimeout: NodeJS.Timeout | null = null
// Props
const props = defineProps<{
@ -332,7 +329,7 @@ function highlightCurrentLine(lineIndex: number) {
highlightTween = gsap.to(lineElement, {
scale: 1.05,
opacity: 1,
duration: 0.5,
duration: 0.2,
ease: "back.out(1.7)",
onComplete: () => {
highlightTween = null
@ -449,7 +446,7 @@ function resetScroll() {
//
gsap.to(lyricsWrapper.value, {
y: 0,
duration: 0.6,
duration: 0.3,
ease: "power2.out"
})
@ -552,7 +549,7 @@ onMounted(() => {
if (controlPanel.value) {
gsap.fromTo(controlPanel.value,
{ 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,
y: 0,
duration: 0.5,
duration: 0.2,
ease: "power2.out",
delay: index * 0.1
}

View File

@ -3,7 +3,7 @@ import { usePlayQueueStore } from '../stores/usePlayQueueStore'
import { artistsOrganize } from '../utils'
import gsap from 'gsap'
import { Draggable } from "gsap/Draggable"
import { onMounted, onUnmounted } from 'vue'
import { onMounted, onUnmounted, nextTick } from 'vue'
import { useTemplateRef } from 'vue'
import { ref, watch } from 'vue'
import { usePreferences } from '../stores/usePreferences'
@ -34,27 +34,13 @@ const progressBarContainer = useTemplateRef('progressBarContainer')
const playQueueDialogContainer = useTemplateRef('playQueueDialogContainer')
const playQueueDialog = useTemplateRef('playQueueDialog')
const controllerRef = useTemplateRef('controllerRef')
const mainContainer = useTemplateRef('mainContainer')
const lyricsSection = useTemplateRef('lyricsSection')
const scrollingLyrics = useTemplateRef('scrollingLyrics')
const albumCover = useTemplateRef('albumCover')
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 nextButton = useTemplateRef('nextButton')
const lyricsButton = useTemplateRef('lyricsButton')
const moreButton = useTemplateRef('moreButton')
const presentQueueListDialog = ref(false)
const controllerHeight = ref(500)
let resizeObserver: ResizeObserver | null = null
let heightSyncTween: gsap.core.Tween | null = null
onMounted(async () => {
Draggable.create(progressBarThumb.value, {
@ -69,7 +55,6 @@ onMounted(async () => {
})
thumbUpdate()
setupHeightSync()
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() {
if (controllerRef.value) {
gsap.fromTo(controllerRef.value.children,
@ -184,15 +138,35 @@ function handlePlayPause() {
playQueueStore.isPlaying = !playQueueStore.isPlaying
}
})
} else {
playQueueStore.isPlaying = !playQueueStore.isPlaying
}
}
function toggleShuffle() {
playQueueStore.playMode.shuffle = !playQueueStore.playMode.shuffle
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() {
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) {
case 'off': playQueueStore.playMode.repeat = 'all'; break
case 'all': playQueueStore.playMode.repeat = 'single'; break
@ -203,30 +177,52 @@ function toggleRepeat() {
function makePlayQueueListPresent() {
presentQueueListDialog.value = true
nextTick(() => {
if (!playQueueDialogContainer.value || !playQueueDialog.value) return
const tl = gsap.timeline()
tl.to(playQueueDialogContainer.value, {
backgroundColor: '#17171780', duration: 0.3, ease: 'power2.out'
}).to(playQueueDialog.value, {
x: 0, duration: 0.4, ease: 'power3.out'
}, '<0.1').fromTo(playQueueDialog.value.children,
}, '<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() {
if (!playQueueDialogContainer.value || !playQueueDialog.value) {
presentQueueListDialog.value = false
return
}
const tl = gsap.timeline({
onComplete: () => {
presentQueueListDialog.value = false
if (playQueueDialog.value) {
gsap.set(playQueueDialog.value, { x: -384 })
}
if (playQueueDialogContainer.value) {
gsap.set(playQueueDialogContainer.value, { backgroundColor: 'transparent' })
}
}
})
if (playQueueDialog.value.children.length > 0) {
tl.to(playQueueDialog.value.children, {
opacity: 0, x: -20, duration: 0.2, ease: 'power2.in', stagger: 0.03
}).to(playQueueDialog.value, {
})
}
tl.to(playQueueDialog.value, {
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'
}, '<')
}
@ -239,13 +235,7 @@ function getCurrentTrack() {
}
}
function calculateStickyTop() {
return (window.innerHeight - (controllerRef.value?.clientHeight ?? 0)) / 2
}
onUnmounted(() => {
if (resizeObserver) resizeObserver.disconnect()
if (heightSyncTween) heightSyncTween.kill()
})
// 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>
<!-- 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="flex items-center justify-center gap-16 h-fit my-auto" ref="mainContainer">
<!-- Controller area -->
<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">
<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" />
@ -318,7 +308,7 @@ watch(() => playQueueStore.currentIndex, () => {
</button>
</div>
<!-- Progress section with enhanced interactions -->
<!-- Progress section -->
<div class="flex flex-col gap-1" ref="progressSection">
<!-- ...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">
@ -348,7 +338,7 @@ watch(() => playQueueStore.currentIndex, () => {
</div>
</div>
<!-- Control buttons with enhanced animations -->
<!-- Control buttons -->
<div class="w-full flex justify-between items-center" ref="controlButtons">
<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"
@ -462,8 +452,8 @@ watch(() => playQueueStore.currentIndex, () => {
</div>
</div>
<!-- Lyrics section - height synced with controller -->
<div class="w-96" ref="lyricsSection">
<!-- Lyrics section - full screen height -->
<div class="w-[40rem] h-screen" ref="lyricsSection">
<ScrollingLyrics :lrcSrc="getCurrentTrack().song.lyricUrl ?? undefined"
class="h-full" ref="scrollingLyrics" />
</div>