feat(web): dominant-color accent strip on PlayerBar + bigger Home tiles
test-web / test (push) Successful in 36s
test-web / test (push) Successful in 36s
#9 — new lib/media/dominantColor.ts: load cover image, downsample to 1x1 canvas, read pixel. Approximates the dominant tone via the browser's bilinear mean — close enough for an ambient accent without the 5KB ColorThief dependency. Same-origin cover URLs so no CORS dance. Result cached by URL so revisits are free. PlayerBar samples the current track's cover and pipes the resulting rgb into a 2px accent strip above both the compact and desktop variants. Transparent until the first resolve; 300ms transition on colour change so track-skips fade rather than snap. #10 — slight bump to Home tile widths so a typical viewport shows roughly 5-6 across instead of cramming 8-9: Playlists w-56, all remaining AlbumCard rows w-48 (Recently Added + both Rediscover album scrollers). Replaces the wave-2 sizes that were still showing 7-8 across on wider screens.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
// Cheap dominant-color extractor: loads an image, draws it scaled
|
||||
// to a 1x1 canvas, reads the resulting pixel. The browser's bilinear
|
||||
// downsample produces an arithmetic-mean color, which approximates
|
||||
// the "dominant" tone close enough for an ambient accent strip.
|
||||
// Not aiming for ColorThief-grade clustering — that adds ~5KB and
|
||||
// noticeable extraction latency. The user-visible feature is just
|
||||
// "the page subtly takes on the current track's hue" and a mean
|
||||
// color delivers that.
|
||||
//
|
||||
// Cover URLs are same-origin (/api/albums/<id>/cover), so no CORS
|
||||
// dance is needed. Returns null on any failure (load error, decode
|
||||
// error, opaque-canvas getImageData reject) so the caller can fall
|
||||
// back to the static accent.
|
||||
|
||||
export type Rgb = { r: number; g: number; b: number };
|
||||
|
||||
const cache = new Map<string, Promise<Rgb | null>>();
|
||||
|
||||
export function dominantColorFromUrl(url: string): Promise<Rgb | null> {
|
||||
const cached = cache.get(url);
|
||||
if (cached) return cached;
|
||||
const p = sample(url);
|
||||
cache.set(url, p);
|
||||
return p;
|
||||
}
|
||||
|
||||
function sample(url: string): Promise<Rgb | null> {
|
||||
if (typeof window === 'undefined') return Promise.resolve(null);
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return resolve(null);
|
||||
ctx.drawImage(img, 0, 0, 1, 1);
|
||||
const data = ctx.getImageData(0, 0, 1, 1).data;
|
||||
resolve({ r: data[0], g: data[1], b: data[2] });
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
img.onerror = () => resolve(null);
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
export function rgbToCssString({ r, g, b }: Rgb): string {
|
||||
return `rgb(${r} ${g} ${b})`;
|
||||
}
|
||||
Reference in New Issue
Block a user