vtube-studio/services/imageService.ts
James Twose 5078d67d4f refactor: Improve avatar asset processing
Separates asset analysis into distinct steps to accurately capture face landmarks.
Introduces `fileToDataUrl` utility and modifies `stitchAssets` to accept image source strings, reducing redundant file processing and improving clarity.
2025-11-20 22:03:53 +01:00

93 lines
2.8 KiB
TypeScript

import { Rect } from '../types';
export const fileToDataUrl = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target?.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
};
export const loadImage = (src: string): Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
};
export const stitchAssets = async (
baseSrc: string,
blinkSrc?: string,
talkSrc?: string
): Promise<{ imageUrl: string; mainBody: Rect; textureClosedEye?: Rect; textureOpenMouth?: Rect }> => {
// Load images
const baseImg = await loadImage(baseSrc);
const blinkImg = blinkSrc ? await loadImage(blinkSrc) : null;
const talkImg = talkSrc ? await loadImage(talkSrc) : null;
// Layout: Base on Left. Sidebar on Right containing Blink (top) and Talk (bottom).
// Sidebar width = max(blink.width, talk.width)
const sidebarWidth = Math.max(blinkImg?.width || 0, talkImg?.width || 0);
// If there are no variants, just return the base image as is
if (sidebarWidth === 0) {
return {
imageUrl: baseSrc,
mainBody: { x: 0, y: 0, w: 1, h: 1 }
};
}
const totalWidth = baseImg.width + sidebarWidth;
const totalHeight = Math.max(baseImg.height, (blinkImg?.height || 0) + (talkImg?.height || 0));
const canvas = document.createElement('canvas');
canvas.width = totalWidth;
canvas.height = totalHeight;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error("Could not get canvas context");
// Draw Base
ctx.drawImage(baseImg, 0, 0);
// Calculate normalized rects
const mainBody: Rect = {
x: 0,
y: 0,
w: baseImg.width / totalWidth,
h: baseImg.height / totalHeight
};
let textureClosedEye: Rect | undefined;
if (blinkImg) {
ctx.drawImage(blinkImg, baseImg.width, 0);
textureClosedEye = {
x: baseImg.width / totalWidth,
y: 0,
w: blinkImg.width / totalWidth,
h: blinkImg.height / totalHeight
};
}
let textureOpenMouth: Rect | undefined;
if (talkImg) {
const yPos = blinkImg ? blinkImg.height : 0;
ctx.drawImage(talkImg, baseImg.width, yPos);
textureOpenMouth = {
x: baseImg.width / totalWidth,
y: yPos / totalHeight,
w: talkImg.width / totalWidth,
h: talkImg.height / totalHeight
};
}
return {
imageUrl: canvas.toDataURL('image/png'),
mainBody,
textureClosedEye,
textureOpenMouth
};
};