style(ScrollingLyrics): adjust layout and improve animations for better user experience
This commit is contained in:
parent
6bf9c493aa
commit
8a16f3cde8
|
@ -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,16 +45,13 @@
|
|||
</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>
|
||||
|
||||
<!-- 底部填充 -->
|
||||
<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
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
|
@ -68,8 +54,7 @@ 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
|
||||
|
||||
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,
|
||||
{ opacity: 0, x: -20 },
|
||||
{ opacity: 1, x: 0, duration: 0.3, ease: 'power2.out', stagger: 0.05 }, '<0.2')
|
||||
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')
|
||||
|
||||
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
|
||||
gsap.set(playQueueDialog.value, { x: -384 })
|
||||
gsap.set(playQueueDialogContainer.value, { backgroundColor: 'transparent' })
|
||||
if (playQueueDialog.value) {
|
||||
gsap.set(playQueueDialog.value, { x: -384 })
|
||||
}
|
||||
if (playQueueDialogContainer.value) {
|
||||
gsap.set(playQueueDialogContainer.value, { backgroundColor: 'transparent' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
tl.to(playQueueDialog.value.children, {
|
||||
opacity: 0, x: -20, duration: 0.2, ease: 'power2.in', stagger: 0.03
|
||||
}).to(playQueueDialog.value, {
|
||||
if (playQueueDialog.value.children.length > 0) {
|
||||
tl.to(playQueueDialog.value.children, {
|
||||
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'
|
||||
}, '<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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user