feat(lyrics): smoother transitions and static lyrics at top

- Reduced transition durations from 300-400ms to 120-150ms
- Changed easing to ease-out for snappier feel
- Added will-change for GPU acceleration
- Non-synced lyrics now display at container top (no spacers)
- Added isSynced prop to all lyrics components
This commit is contained in:
vicrodh committed 2026-01-10 07:37:04 -06:00
1 parent b171a17b57
commit 340fb2a72a
6 files changed
+45 -14

No files matched your search

+3
View File
@@ -23,6 +23,7 @@
lyricsLines?: LyricsLine[];
lyricsActiveIndex?: number;
lyricsActiveProgress?: number;
lyricsSynced?: boolean;
lyricsLoading?: boolean;
lyricsError?: string | null;
}
@@ -43,6 +44,7 @@
lyricsLines = [],
lyricsActiveIndex = -1,
lyricsActiveProgress = 0,
lyricsSynced = false,
lyricsLoading = false,
lyricsError = null
}: Props = $props();
@@ -141,6 +143,7 @@
lines={lyricsLines}
activeIndex={lyricsActiveIndex}
activeProgress={lyricsActiveProgress}
isSynced={lyricsSynced}
center={false}
compact={false}
immersive={true}
@@ -9,6 +9,7 @@
lines: LyricsLine[];
activeIndex?: number;
activeProgress?: number;
isSynced?: boolean;
align?: 'left' | 'center';
title?: string;
artist?: string;
@@ -19,6 +20,7 @@
lines,
activeIndex = -1,
activeProgress = 0,
isSynced = false,
align = 'center',
title = '',
artist = '',
@@ -54,6 +56,7 @@
{lines}
{activeIndex}
{activeProgress}
{isSynced}
center={isCenter}
compact={false}
/>
+31 -14
View File
@@ -14,6 +14,7 @@
compact?: boolean;
scrollToActive?: boolean;
immersive?: boolean;
isSynced?: boolean;
}
let {
@@ -24,7 +25,8 @@
center = false,
compact = false,
scrollToActive = true,
immersive = false
immersive = false,
isSynced = false
}: Props = $props();
let container: HTMLDivElement | null = null;
@@ -99,18 +101,21 @@
class:compact
class:center
class:immersive
class:static={!isSynced}
bind:this={container}
>
{#if lines.length === 0}
<div class="lyrics-empty">No lyrics available</div>
{:else}
<!-- Spacer at top to allow first lines to scroll to center -->
<div class="lyrics-spacer"></div>
<!-- Spacer at top to allow first lines to scroll to center (only for synced) -->
{#if isSynced}
<div class="lyrics-spacer"></div>
{/if}
{#each lines as line, index}
{@const isActive = index === activeIndex}
{@const isPast = index < activeIndex}
{@const opacity = getLineOpacity(index, activeIndex)}
{@const isActive = isSynced && index === activeIndex}
{@const isPast = isSynced && index < activeIndex}
{@const opacity = isSynced ? getLineOpacity(index, activeIndex) : 1}
<div
class="lyrics-line"
class:active={isActive}
@@ -122,8 +127,10 @@
</div>
{/each}
<!-- Spacer at bottom to allow last lines to scroll to center -->
<div class="lyrics-spacer"></div>
<!-- Spacer at bottom to allow last lines to scroll to center (only for synced) -->
{#if isSynced}
<div class="lyrics-spacer"></div>
{/if}
{/if}
</div>
@@ -135,7 +142,6 @@
padding: 16px 20px;
overflow-y: auto;
height: 100%;
scroll-behavior: smooth;
scrollbar-width: thin;
scrollbar-color: var(--bg-tertiary) transparent;
}
@@ -158,6 +164,16 @@
flex-shrink: 0;
}
/* Static mode - non-synced lyrics, start at top */
.lyrics-lines.static {
justify-content: flex-start;
}
.lyrics-lines.static .lyrics-line {
opacity: 0.85;
color: var(--text-primary);
}
.lyrics-lines.center {
text-align: center;
}
@@ -199,12 +215,13 @@
letter-spacing: 0.01em;
opacity: var(--line-opacity, 1);
transition:
opacity 400ms cubic-bezier(0.4, 0, 0.2, 1),
transform 400ms cubic-bezier(0.4, 0, 0.2, 1),
font-size 300ms cubic-bezier(0.4, 0, 0.2, 1),
font-weight 300ms cubic-bezier(0.4, 0, 0.2, 1),
color 300ms cubic-bezier(0.4, 0, 0.2, 1);
opacity 150ms ease-out,
transform 150ms ease-out,
font-size 120ms ease-out,
font-weight 120ms ease-out,
color 150ms ease-out;
transform-origin: left center;
will-change: opacity, transform;
}
.lyrics-lines.center .lyrics-line {
@@ -10,6 +10,7 @@
lines: LyricsLine[];
activeIndex?: number;
activeProgress?: number;
isSynced?: boolean;
title?: string;
artist?: string;
artwork?: string;
@@ -23,6 +24,7 @@
lines,
activeIndex = -1,
activeProgress = 0,
isSynced = false,
title = '',
artist = '',
artwork,
@@ -43,6 +45,7 @@
{lines}
{activeIndex}
{activeProgress}
{isSynced}
compact={true}
center={false}
/>
@@ -12,6 +12,7 @@
lines: LyricsLine[];
activeIndex?: number;
activeProgress?: number;
isSynced?: boolean;
isLoading?: boolean;
error?: string | null;
}
@@ -22,6 +23,7 @@
lines,
activeIndex = -1,
activeProgress = 0,
isSynced = false,
isLoading = false,
error = null
}: Props = $props();
@@ -53,6 +55,7 @@
{lines}
{activeIndex}
{activeProgress}
{isSynced}
compact={true}
center={false}
/>
+2
View File
@@ -1102,6 +1102,7 @@
lines={lyricsLines.map(l => ({ text: l.text }))}
activeIndex={lyricsActiveIndex}
activeProgress={lyricsActiveProgress}
isSynced={lyricsIsSynced}
isLoading={lyricsStatus === 'loading'}
error={lyricsStatus === 'error' ? lyricsError : (lyricsStatus === 'not_found' ? 'No lyrics found' : null)}
/>
@@ -1215,6 +1216,7 @@
lyricsLines={lyricsLines.map(l => ({ text: l.text }))}
lyricsActiveIndex={lyricsActiveIndex}
lyricsActiveProgress={lyricsActiveProgress}
lyricsSynced={lyricsIsSynced}
lyricsLoading={lyricsStatus === 'loading'}
lyricsError={lyricsStatus === 'error' ? lyricsError : (lyricsStatus === 'not_found' ? 'No lyrics found' : null)}
/>