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