feat(immersive): use full artwork background like Cider

Replace blur approach with full resolution artwork:
- Artwork scaled 115% to hide edges
- Strong vignette overlay fading to black at edges
- Gradient overlays for text readability (header, controls, lyrics)
- Slight desaturation for better contrast
- Simpler and more visually impactful
This commit is contained in:
vicrodh committed 2026-01-30 16:53:14 -06:00
1 parent eb0166b709
commit 484ef87ee7
2 files changed
+49 -76

No files matched your search

@@ -1,82 +1,42 @@
<script lang="ts"> <script lang="ts">
import { generateBlurredBackground, extractDominantColors, createGradientFromColors } from '$lib/utils/imageBlur';
interface Props { interface Props {
artwork: string; artwork: string;
mode?: 'blur' | 'gradient' | 'solid';
} }
let { artwork, mode = 'blur' }: Props = $props(); let { artwork }: Props = $props();
let backgroundUrl = $state('');
let useCanvasBlur = $state(false);
let isLoading = $state(true); let isLoading = $state(true);
let currentArtwork = $state(''); let currentArtwork = $state('');
// Generate background when artwork changes // Track artwork changes for fade transition
$effect(() => { $effect(() => {
if (!artwork || artwork === currentArtwork) return; if (!artwork || artwork === currentArtwork) return;
currentArtwork = artwork; currentArtwork = artwork;
isLoading = true; isLoading = true;
useCanvasBlur = false;
if (mode === 'blur') {
// Try canvas blur first (most efficient)
generateBlurredBackground(artwork)
.then((dataUrl) => {
backgroundUrl = dataUrl;
useCanvasBlur = true;
isLoading = false;
})
.catch((err) => {
// CORS error - fallback to CSS blur (works but uses more GPU)
console.warn('[ImmersiveBackground] Canvas blur failed (CORS), using CSS blur fallback');
backgroundUrl = artwork;
useCanvasBlur = false;
isLoading = false;
});
} else if (mode === 'gradient') {
fallbackToGradient();
} else {
backgroundUrl = '';
isLoading = false;
}
}); });
async function fallbackToGradient() { function handleImageLoad() {
try {
const colors = await extractDominantColors(artwork);
backgroundUrl = createGradientFromColors(colors);
useCanvasBlur = true; // Gradient doesn't need CSS blur
} catch {
backgroundUrl = 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)';
useCanvasBlur = true;
}
isLoading = false; isLoading = false;
} }
</script> </script>
<div class="immersive-background" class:loading={isLoading}> <div class="immersive-background" class:loading={isLoading}>
<!-- Background: either pre-blurred canvas or CSS blur fallback --> <!-- Full artwork as background -->
{#if useCanvasBlur} {#if artwork}
<!-- Pre-computed blur (efficient) -->
<div
class="background-image canvas-blur"
style="background-image: url({backgroundUrl});"
></div>
{:else if backgroundUrl}
<!-- CSS blur fallback (works with CORS-restricted images) -->
<img <img
src={backgroundUrl} src={artwork}
alt="" alt=""
class="background-image css-blur" class="background-image"
aria-hidden="true" aria-hidden="true"
onload={handleImageLoad}
/> />
{/if} {/if}
<!-- Overlay for consistent darkness and vignette --> <!-- Vignette overlay - strong fade to black at edges -->
<div class="background-overlay"></div> <div class="vignette-overlay"></div>
<!-- Gradient overlay for text readability -->
<div class="gradient-overlay"></div>
</div> </div>
<style> <style>
@@ -85,42 +45,55 @@
inset: 0; inset: 0;
overflow: hidden; overflow: hidden;
z-index: 0; z-index: 0;
background-color: #0a0a0b;
} }
.background-image { .background-image {
position: absolute; position: absolute;
inset: -40px; /* Overflow to avoid blur edge artifacts */ /* Scale larger than viewport to hide edges */
transition: opacity 300ms ease-out; top: 50%;
} left: 50%;
transform: translate(-50%, -50%) scale(1.15);
/* Canvas pre-blurred version (efficient - no GPU filter) */ min-width: 115%;
.background-image.canvas-blur { min-height: 115%;
background-size: cover; width: auto;
background-position: center; height: auto;
background-repeat: no-repeat;
transform: scale(1.1);
}
/* CSS blur fallback for CORS-restricted images */
.background-image.css-blur {
width: calc(100% + 80px);
height: calc(100% + 80px);
object-fit: cover; object-fit: cover;
filter: blur(60px) saturate(1.2) brightness(0.5); object-position: center;
transform: scale(1.2); transition: opacity 400ms ease-out;
/* Slight desaturation for better text contrast */
filter: saturate(0.85) brightness(0.9);
} }
.loading .background-image { .loading .background-image {
opacity: 0; opacity: 0;
} }
.background-overlay { /* Strong vignette - fades to black at edges */
.vignette-overlay {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: radial-gradient(
ellipse 80% 80% at 50% 45%,
transparent 0%,
transparent 30%,
rgba(0, 0, 0, 0.4) 60%,
rgba(0, 0, 0, 0.85) 100%
);
pointer-events: none;
}
/* Vignette effect + ensure readability */ /* Gradient for text areas - darker where text appears */
.gradient-overlay {
position: absolute;
inset: 0;
background: background:
radial-gradient(ellipse at center, transparent 0%, rgba(0, 0, 0, 0.3) 100%), /* Top gradient for header */
linear-gradient(to bottom, rgba(0, 0, 0, 0.2) 0%, transparent 30%, transparent 70%, rgba(0, 0, 0, 0.4) 100%); linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, transparent 15%),
/* Bottom gradient for controls */
linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, transparent 25%),
/* Right side gradient for lyrics panel */
linear-gradient(to left, rgba(0, 0, 0, 0.4) 0%, transparent 40%);
pointer-events: none;
} }
</style> </style>
@@ -214,7 +214,7 @@
tabindex="-1" tabindex="-1"
> >
<!-- Background --> <!-- Background -->
<ImmersiveBackground {artwork} mode="blur" /> <ImmersiveBackground {artwork} />
<!-- Header with tabs --> <!-- Header with tabs -->
<ImmersiveHeader <ImmersiveHeader