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:
1 parent
b171a17b57
commit
340fb2a72a
6 files changed
+43
-12
No files matched your search
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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) -->
|
||||||
|
{#if isSynced}
|
||||||
<div class="lyrics-spacer"></div>
|
<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,9 +127,11 @@
|
|||||||
</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) -->
|
||||||
|
{#if isSynced}
|
||||||
<div class="lyrics-spacer"></div>
|
<div class="lyrics-spacer"></div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in new issue
Block a user