feat(immersive): integrate WebGL2 renderer into ImmersiveBackground
- ImmersiveBackground now checks WebGL2 availability on mount - Dynamically imports ImmersiveAmbientCanvas when WebGL2 available - Falls back to CSS blur when WebGL2 unavailable - New props: enableAmbient (default true), ambientIntensity Automatic renderer selection: 1. Check BUILD_IMMERSIVE_ENABLED (compile-time) 2. Check isRuntimeEnabled() (user preference) 3. Check isWebGL2Available() (browser capability) 4. If all pass: use WebGL2 with ambient motion 5. Otherwise: use CSS fallback (original behavior)
This commit is contained in:
1 parent
c94857fc87
commit
34710e92e1
1 file changed
+100
-27
@@ -1,17 +1,70 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Immersive Background
|
||||
*
|
||||
* Renders the blurred album artwork background for the immersive player.
|
||||
* Uses WebGL2 when available (with ambient motion), falls back to CSS blur.
|
||||
*/
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
BUILD_IMMERSIVE_ENABLED,
|
||||
isWebGL2Available,
|
||||
isRuntimeEnabled,
|
||||
} from '$lib/immersive';
|
||||
|
||||
interface Props {
|
||||
artwork: string;
|
||||
/** Enable ambient motion effect (WebGL2 only) */
|
||||
enableAmbient?: boolean;
|
||||
/** Ambient motion intensity 0-1 (WebGL2 only) */
|
||||
ambientIntensity?: number;
|
||||
}
|
||||
|
||||
let { artwork }: Props = $props();
|
||||
let { artwork, enableAmbient = true, ambientIntensity }: Props = $props();
|
||||
|
||||
// Determine which renderer to use
|
||||
let useWebGL = $state(false);
|
||||
let useFallback = $state(false);
|
||||
let WebGLCanvas: typeof import('$lib/immersive/ImmersiveAmbientCanvas.svelte').default | null = $state(null);
|
||||
|
||||
// Fallback state (CSS-based blur)
|
||||
let canvasRef: HTMLCanvasElement | undefined = $state();
|
||||
let isLoading = $state(true);
|
||||
let currentArtwork = $state('');
|
||||
|
||||
// Generate a small blurred version using Canvas
|
||||
// Key insight: blur(30px) on a 64x64 image is MUCH cheaper than blur(120px) on full image
|
||||
// The small source + small blur = smooth result with minimal CPU
|
||||
// Check capabilities and load WebGL component if available
|
||||
onMount(async () => {
|
||||
if (BUILD_IMMERSIVE_ENABLED && isRuntimeEnabled() && isWebGL2Available()) {
|
||||
try {
|
||||
// Dynamic import - only loads if WebGL2 is available
|
||||
const module = await import('$lib/immersive/ImmersiveAmbientCanvas.svelte');
|
||||
WebGLCanvas = module.default;
|
||||
useWebGL = true;
|
||||
console.log('[ImmersiveBackground] Using WebGL2 renderer');
|
||||
} catch (e) {
|
||||
console.warn('[ImmersiveBackground] Failed to load WebGL canvas:', e);
|
||||
useFallback = true;
|
||||
}
|
||||
} else {
|
||||
useFallback = true;
|
||||
console.log('[ImmersiveBackground] Using CSS fallback');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle WebGL fallback (if WebGL init fails)
|
||||
*/
|
||||
function handleWebGLFallback(): void {
|
||||
console.warn('[ImmersiveBackground] WebGL failed, switching to CSS fallback');
|
||||
useWebGL = false;
|
||||
useFallback = true;
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// CSS Fallback Implementation (original code)
|
||||
// =====================================================
|
||||
|
||||
async function generateBlurredBackground(imageUrl: string): Promise<void> {
|
||||
if (!canvasRef || !imageUrl) return;
|
||||
|
||||
@@ -22,15 +75,12 @@
|
||||
img.crossOrigin = 'anonymous';
|
||||
|
||||
img.onload = () => {
|
||||
// Use 64x64 - large enough to avoid blocky pixels, small enough to be efficient
|
||||
const size = 64;
|
||||
canvasRef!.width = size;
|
||||
canvasRef!.height = size;
|
||||
|
||||
// Draw image scaled down
|
||||
ctx.drawImage(img, 0, 0, size, size);
|
||||
|
||||
// Apply color adjustments
|
||||
const imageData = ctx.getImageData(0, 0, size, size);
|
||||
const data = imageData.data;
|
||||
|
||||
@@ -40,13 +90,11 @@
|
||||
const b = data[i + 2];
|
||||
const avg = (r + g + b) / 3;
|
||||
|
||||
// Saturation boost (1.3x)
|
||||
const satFactor = 1.3;
|
||||
let newR = avg + (r - avg) * satFactor;
|
||||
let newG = avg + (g - avg) * satFactor;
|
||||
let newB = avg + (b - avg) * satFactor;
|
||||
|
||||
// Brightness reduction (0.55x) - slightly darker for better text contrast
|
||||
data[i] = Math.min(255, Math.max(0, newR * 0.55));
|
||||
data[i + 1] = Math.min(255, Math.max(0, newG * 0.55));
|
||||
data[i + 2] = Math.min(255, Math.max(0, newB * 0.55));
|
||||
@@ -63,22 +111,38 @@
|
||||
img.src = imageUrl;
|
||||
}
|
||||
|
||||
// Track artwork changes
|
||||
// Track artwork changes for fallback renderer
|
||||
$effect(() => {
|
||||
if (!artwork || artwork === currentArtwork) return;
|
||||
currentArtwork = artwork;
|
||||
isLoading = true;
|
||||
generateBlurredBackground(artwork);
|
||||
if (useFallback && artwork && artwork !== currentArtwork) {
|
||||
currentArtwork = artwork;
|
||||
isLoading = true;
|
||||
generateBlurredBackground(artwork);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="immersive-background" class:loading={isLoading}>
|
||||
<!-- Canvas-generated tiny image, scaled up via CSS -->
|
||||
<canvas
|
||||
bind:this={canvasRef}
|
||||
class="background-canvas"
|
||||
aria-hidden="true"
|
||||
></canvas>
|
||||
<div class="immersive-background">
|
||||
{#if useWebGL && WebGLCanvas}
|
||||
<!-- WebGL2 Renderer with ambient motion -->
|
||||
<WebGLCanvas
|
||||
artworkUrl={artwork}
|
||||
enableMotion={enableAmbient}
|
||||
intensity={ambientIntensity}
|
||||
onFallback={handleWebGLFallback}
|
||||
/>
|
||||
{:else if useFallback}
|
||||
<!-- CSS Fallback Renderer -->
|
||||
<div class="fallback-container" class:loading={isLoading}>
|
||||
<canvas
|
||||
bind:this={canvasRef}
|
||||
class="background-canvas"
|
||||
aria-hidden="true"
|
||||
></canvas>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Loading state while determining renderer -->
|
||||
<div class="loading-placeholder"></div>
|
||||
{/if}
|
||||
|
||||
<!-- Dark overlay for better contrast -->
|
||||
<div class="dark-overlay"></div>
|
||||
@@ -93,19 +157,21 @@
|
||||
background-color: #0a0a0b;
|
||||
}
|
||||
|
||||
/* CSS Fallback styles */
|
||||
.fallback-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.background-canvas {
|
||||
position: absolute;
|
||||
/* Extend beyond viewport to hide blur edges */
|
||||
inset: -80px;
|
||||
width: calc(100% + 160px);
|
||||
height: calc(100% + 160px);
|
||||
/* Smooth interpolation when scaling up */
|
||||
image-rendering: auto;
|
||||
/* Small blur to smooth out the 64x64 canvas - MUCH cheaper than 120px on full image */
|
||||
filter: blur(40px);
|
||||
/* GPU layer for performance */
|
||||
transform: scale(1.15) translateZ(0);
|
||||
will-change: opacity, filter;
|
||||
will-change: opacity;
|
||||
transition: opacity 500ms ease-out;
|
||||
}
|
||||
|
||||
@@ -113,11 +179,18 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Subtle dark overlay */
|
||||
.loading-placeholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: #0a0a0b;
|
||||
}
|
||||
|
||||
/* Dark overlay */
|
||||
.dark-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user