perf(immersive): fix critical CPU usage issue in background blur
Performance problem: - WebKitWebProcess consuming 97% CPU on one core - Cause: CSS filter: blur(120px) on large image is extremely expensive in WebKit/GTK which cannot GPU-accelerate this operation Solution: - Replace CSS blur with Canvas-based tiny thumbnail approach - Draw image to 8x8 canvas, apply color adjustments, scale up via CSS - Natural pixelation creates blur-like effect with ZERO CPU cost - Remove backdrop-filter: blur() from header elements (use solid bg) Also fixed: - Change Mic icon to Mic2 to match rest of app - Add missing i18n translations for player.credits and player.suggestions
This commit is contained in:
1 parent
4d5a78ff24
commit
f012ba33f4
4 files changed
+85
-37
No files matched your search
@@ -5,32 +5,81 @@
|
||||
|
||||
let { artwork }: Props = $props();
|
||||
|
||||
let canvasRef: HTMLCanvasElement | undefined = $state();
|
||||
let isLoading = $state(true);
|
||||
let currentArtwork = $state('');
|
||||
|
||||
// Track artwork changes for fade transition
|
||||
// Generate a tiny blurred version using Canvas (GPU-free approach)
|
||||
// Instead of CSS blur(120px) which kills WebKit performance,
|
||||
// we resize the image to 8x8 pixels and scale it up - natural blur effect
|
||||
async function generateBlurredBackground(imageUrl: string): Promise<void> {
|
||||
if (!canvasRef || !imageUrl) return;
|
||||
|
||||
const ctx = canvasRef.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
|
||||
img.onload = () => {
|
||||
// Draw to tiny size (8x8) - this creates natural blur when scaled
|
||||
const tinySize = 8;
|
||||
canvasRef!.width = tinySize;
|
||||
canvasRef!.height = tinySize;
|
||||
|
||||
// Draw image scaled down to 8x8
|
||||
ctx.drawImage(img, 0, 0, tinySize, tinySize);
|
||||
|
||||
// Slightly boost saturation by adjusting colors
|
||||
const imageData = ctx.getImageData(0, 0, tinySize, tinySize);
|
||||
const data = imageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// Simple saturation boost
|
||||
const r = data[i];
|
||||
const g = data[i + 1];
|
||||
const b = data[i + 2];
|
||||
const avg = (r + g + b) / 3;
|
||||
|
||||
// Saturation factor (1.3x)
|
||||
const satFactor = 1.3;
|
||||
data[i] = Math.min(255, avg + (r - avg) * satFactor);
|
||||
data[i + 1] = Math.min(255, avg + (g - avg) * satFactor);
|
||||
data[i + 2] = Math.min(255, avg + (b - avg) * satFactor);
|
||||
|
||||
// Brightness reduction (0.6x)
|
||||
data[i] = data[i] * 0.6;
|
||||
data[i + 1] = data[i + 1] * 0.6;
|
||||
data[i + 2] = data[i + 2] * 0.6;
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
isLoading = false;
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
isLoading = false;
|
||||
};
|
||||
|
||||
img.src = imageUrl;
|
||||
}
|
||||
|
||||
// Track artwork changes
|
||||
$effect(() => {
|
||||
if (!artwork || artwork === currentArtwork) return;
|
||||
currentArtwork = artwork;
|
||||
isLoading = true;
|
||||
generateBlurredBackground(artwork);
|
||||
});
|
||||
|
||||
function handleImageLoad() {
|
||||
isLoading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="immersive-background" class:loading={isLoading}>
|
||||
<!-- Heavily blurred artwork - just color blobs -->
|
||||
{#if artwork}
|
||||
<img
|
||||
src={artwork}
|
||||
alt=""
|
||||
class="background-image"
|
||||
aria-hidden="true"
|
||||
onload={handleImageLoad}
|
||||
/>
|
||||
{/if}
|
||||
<!-- Canvas-generated tiny image, scaled up via CSS -->
|
||||
<canvas
|
||||
bind:this={canvasRef}
|
||||
class="background-canvas"
|
||||
aria-hidden="true"
|
||||
></canvas>
|
||||
|
||||
<!-- Dark overlay for better contrast -->
|
||||
<div class="dark-overlay"></div>
|
||||
@@ -45,21 +94,22 @@
|
||||
background-color: #0a0a0b;
|
||||
}
|
||||
|
||||
.background-image {
|
||||
.background-canvas {
|
||||
position: absolute;
|
||||
/* Extend beyond viewport to hide blur edges */
|
||||
inset: -100px;
|
||||
width: calc(100% + 200px);
|
||||
height: calc(100% + 200px);
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
/* Heavy blur - should be unrecognizable, just color blobs */
|
||||
filter: blur(120px) saturate(1.3) brightness(0.6);
|
||||
transform: scale(1.2);
|
||||
/* Extend beyond viewport for seamless edges */
|
||||
inset: -50px;
|
||||
width: calc(100% + 100px);
|
||||
height: calc(100% + 100px);
|
||||
/* Scale up tiny canvas - creates natural blur effect */
|
||||
/* Using image-rendering: auto for smooth interpolation */
|
||||
image-rendering: auto;
|
||||
/* GPU layer for smooth transitions */
|
||||
transform: scale(1.1) translateZ(0);
|
||||
will-change: opacity;
|
||||
transition: opacity 500ms ease-out;
|
||||
}
|
||||
|
||||
.loading .background-image {
|
||||
.loading .background-canvas {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { X, Disc3, LayoutGrid, Mic, ListMusic, Music2, Info, Radio } from 'lucide-svelte';
|
||||
import { X, Disc3, LayoutGrid, Mic2, ListMusic, Music2, Info, Radio } from 'lucide-svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
export type ImmersiveTab = 'lyrics' | 'credits' | 'suggestions' | 'queue';
|
||||
@@ -39,7 +39,7 @@
|
||||
const displayModes: { id: DisplayMode; icon: typeof Disc3; title: string }[] = [
|
||||
{ id: 'coverflow', icon: Disc3, title: 'Coverflow (1)' },
|
||||
{ id: 'split', icon: LayoutGrid, title: 'Split View (2)' },
|
||||
{ id: 'lyrics-focus', icon: Mic, title: 'Lyrics Focus (3)' },
|
||||
{ id: 'lyrics-focus', icon: Mic2, title: 'Lyrics Focus (3)' },
|
||||
{ id: 'queue-focus', icon: ListMusic, title: 'Queue Focus (4)' },
|
||||
];
|
||||
|
||||
@@ -123,9 +123,7 @@
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid var(--alpha-10, rgba(255, 255, 255, 0.1));
|
||||
border-radius: 10px;
|
||||
}
|
||||
@@ -163,9 +161,7 @@
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid var(--alpha-10, rgba(255, 255, 255, 0.1));
|
||||
border-radius: 12px;
|
||||
}
|
||||
@@ -209,9 +205,7 @@
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid var(--alpha-10, rgba(255, 255, 255, 0.1));
|
||||
border-radius: 50%;
|
||||
color: var(--alpha-70, rgba(255, 255, 255, 0.7));
|
||||
|
||||
@@ -253,6 +253,8 @@
|
||||
"lyrics": "Lyrics",
|
||||
"noLyrics": "No lyrics available",
|
||||
"fetchingLyrics": "Fetching lyrics...",
|
||||
"credits": "Credits",
|
||||
"suggestions": "Suggestions",
|
||||
"shuffle": "Shuffle",
|
||||
"repeat": "Repeat",
|
||||
"repeatAll": "Repeat All",
|
||||
|
||||
@@ -253,6 +253,8 @@
|
||||
"lyrics": "Letras",
|
||||
"noLyrics": "No hay letras disponibles",
|
||||
"fetchingLyrics": "Buscando letras...",
|
||||
"credits": "Créditos",
|
||||
"suggestions": "Sugerencias",
|
||||
"shuffle": "Aleatorio",
|
||||
"repeat": "Repetir",
|
||||
"repeatAll": "Repetir Todo",
|
||||
|
||||
Reference in new issue
Block a user