// 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(); }; };