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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109// 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 getBlurSource = (image: HTMLImageElement): BlurSource | null => { 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); return { data: context.getImageData(0, 0, width, height).data, width, height, };};
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 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) return; try { const source = getBlurSource(image); 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(); }; image.addEventListener("load", handleLoad); image.addEventListener("error", handleError); if (image.complete) { if (image.naturalWidth) handleLoad(); else handleError(); }
return () => { image.removeEventListener("load", handleLoad); image.removeEventListener("error", handleError); restore(); reveal(); };};