diff --git a/src/services/webp.js b/src/services/webp.js new file mode 100644 index 0000000..a0f3e66 --- /dev/null +++ b/src/services/webp.js @@ -0,0 +1,88 @@ +const LIB_URLS = [ + 'https://esm.sh/wasm-webp@0.1.0', + 'https://cdn.jsdelivr.net/npm/wasm-webp@0.1.0/+esm' +]; + +let webpLib = null; +let loadingPromise = null; + +async function loadWebpLib() { + if (webpLib) return webpLib; + if (loadingPromise) return loadingPromise; + + loadingPromise = (async () => { + let lastErr = null; + for (const url of LIB_URLS) { + try { + const mod = await import(url); + if (typeof mod?.encodeAnimation !== 'function') { + throw new Error(`encodeAnimation export not found from ${url}`); + } + webpLib = mod; + return webpLib; + } catch (err) { + lastErr = err; + } + } + throw lastErr || new Error('Failed to load animated WebP encoder.'); + })(); + + return loadingPromise; +} + +function loadImage(src) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.decoding = 'async'; + img.onload = () => resolve(img); + img.onerror = () => reject(new Error('Failed to load frame image')); + img.src = src; + }); +} + +export async function assembleAnimatedWebp(frames, fps, maxSize, onProgress) { + if (!frames?.length) throw new Error('No frames to assemble.'); + const { encodeAnimation } = await loadWebpLib(); + + const images = await Promise.all(frames.map(loadImage)); + let w = images[0].naturalWidth; + let h = images[0].naturalHeight; + + if (maxSize && (w > maxSize || h > maxSize)) { + if (w > h) { + h = Math.round((h * maxSize) / w); + w = maxSize; + } else { + w = Math.round((w * maxSize) / h); + h = maxSize; + } + } + + const delay = Math.max(1, Math.round(1000 / Math.max(1, fps))); + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d', { willReadFrequently: true }); + if (!ctx) throw new Error('Could not create 2D canvas context.'); + + const animFrames = []; + for (let i = 0; i < images.length; i++) { + ctx.clearRect(0, 0, w, h); + ctx.drawImage(images[i], 0, 0, w, h); + const imgData = ctx.getImageData(0, 0, w, h); + + animFrames.push({ + data: new Uint8Array(imgData.data), + duration: delay + }); + + onProgress?.(Math.round(((i + 1) / images.length) * 90), `packing frame ${i + 1} of ${images.length}...`); + } + + onProgress?.(95, 'encoding animated webp...'); + const webpData = await encodeAnimation(w, h, true, animFrames); + if (!webpData) throw new Error('Failed to encode animated WebP.'); + + onProgress?.(100, 'animated webp ready'); + return new Blob([webpData], { type: 'image/webp' }); +}