mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 02:12:12 +00:00
Feat: animated webp assembly with alpha
This commit is contained in:
88
src/services/webp.js
Normal file
88
src/services/webp.js
Normal 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' });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user