Feat: animated webp assembly with alpha

This commit is contained in:
2026-03-21 00:36:11 -07:00
parent 98c4d4115e
commit 23ee7642ae

88
src/services/webp.js Normal file
View File

@@ -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' });
}