Something went wrong. Try again.
a simple but full-featured web player for OpenSubsonic music servers (Navidrome, Gonic, Ampache, etc) devins.page/tinysub
svelte typescript javascript subsonic gonic navidrome
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138// the reasoning for this is to generate blur a single time instead of relying on CSS filter.// CSS blur() filter can re-render the blur unpredictably and it's render behavior varys based on engine:// https://tangled.org/devins.page/tinysub/issues/112
import { blurRGBA } from "glur";
const blurLoadingClass = "blur-loading";
type BlurSource = { data: Uint8ClampedArray; width: number; height: number;};
const sourceCache = new WeakMap< HTMLImageElement, { src: string; source: BlurSource }>();
const getBlurSource = ( image: HTMLImageElement, src: string,): BlurSource | null => { const cached = sourceCache.get(image); if (cached?.src === src) return cached.source; if (!image.naturalWidth || !image.naturalHeight) return null;
const scale = Math.min( 1, 128 / Math.max(image.naturalWidth, image.naturalHeight), ); const width = Math.max(1, Math.round(image.naturalWidth * scale)); const height = Math.max(1, Math.round(image.naturalHeight * scale)); const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const context = canvas.getContext("2d", { willReadFrequently: true }); if (!context) return null;
context.drawImage(image, 0, 0, width, height); const source = { data: context.getImageData(0, 0, width, height).data, width, height, }; sourceCache.set(image, { src, source }); return source;};
const createBlurredDataUrl = (source: BlurSource, radius: number) => { const data = new Uint8ClampedArray(source.data); blurRGBA(data, source.width, source.height, radius);
const canvas = document.createElement("canvas"); canvas.width = source.width; canvas.height = source.height; const context = canvas.getContext("2d"); if (!context) return ""; context.putImageData(new ImageData(data, source.width, source.height), 0, 0); return canvas.toDataURL();};
export const blurImage = ( image: HTMLImageElement, src: string, radius: number, onReady: () => void,) => { image.classList.add(blurLoadingClass); let blurredSrc = ""; let isIntersecting = false; let isReady = false;
const reveal = () => image.classList.remove(blurLoadingClass); const restore = () => { if (image.isConnected && blurredSrc && image.src === blurredSrc) image.src = src; blurredSrc = ""; }; const signalReady = () => { reveal(); if (isReady) return; isReady = true; onReady(); };
const apply = () => { if ( isReady || blurredSrc || document.visibilityState !== "visible" || !isIntersecting ) return; image.classList.add(blurLoadingClass); try { const source = getBlurSource(image, src); if (!source) return; blurredSrc = createBlurredDataUrl(source, radius); if (blurredSrc) image.src = blurredSrc; else signalReady(); } catch { signalReady(); } };
const handleLoad = () => { if (blurredSrc) signalReady(); else apply(); }; const handleError = () => { if (blurredSrc) restore(); signalReady(); }; const visibilityObserver = new IntersectionObserver(([entry]) => { isIntersecting = entry?.isIntersecting ?? false; apply(); });
image.addEventListener("load", handleLoad); image.addEventListener("error", handleError); document.addEventListener("visibilitychange", apply); visibilityObserver.observe(image); if (image.complete) { if (image.naturalWidth) handleLoad(); else handleError(); }
return () => { image.removeEventListener("load", handleLoad); image.removeEventListener("error", handleError); document.removeEventListener("visibilitychange", apply); visibilityObserver.disconnect(); restore(); reveal(); };};