141 lines
4.3 KiB
TypeScript
141 lines
4.3 KiB
TypeScript
import * as Color from './color.js';
|
|
|
|
async function loadBitmap(url: string): Promise<ImageBitmap> {
|
|
const img0 = new Image;
|
|
const img: Promise<ImageBitmapSource> = new Promise((ok, err) => {
|
|
img0.addEventListener('load', () => ok(img0));
|
|
img0.addEventListener('error', () => {
|
|
err(new Error(`couldn't load file: ${url}`));
|
|
});
|
|
});
|
|
img0.src = url;
|
|
return createImageBitmap(await img);
|
|
}
|
|
|
|
|
|
export type Buffer =
|
|
CanvasCompositing & CanvasDrawImage & CanvasImageData &
|
|
CanvasRect & CanvasState;
|
|
|
|
function dataViaBuffer(bmp: ImageBitmap, buf: Buffer): ImageData {
|
|
buf.clearRect(0, 0, bmp.width, bmp.height);
|
|
buf.drawImage(bmp, 0, 0);
|
|
return buf.getImageData(0, 0, bmp.width, bmp.height);
|
|
}
|
|
|
|
|
|
export const WIDTH = 1040;
|
|
export const HEIGHT = 713;
|
|
|
|
export type Layer = 'stroke' | 'static' | 'eyeshine' | Color.Layer;
|
|
|
|
// in compositing order
|
|
export const allLayers: Layer[] =
|
|
['stroke', 'static', 'outer', 'spines', 'stripes', 'cuffs', 'fins1', 'fins2',
|
|
'fins3', 'belly1', 'belly2', 'masks', 'claws', 'vitiligo1', 'vitiligo2',
|
|
'vitiligo3', 'vitiligo4', 'eyes', 'eyeshine', 'lines'];
|
|
|
|
export function makeLayerInfo<A>(f: (l: Layer) => A): Record<Layer, A> {
|
|
return Object.fromEntries(allLayers.map(l => [l, f(l)])) as Record<Layer, A>;
|
|
}
|
|
|
|
export async function makeLayerInfoAsync<A>(f: (l: Layer) => Promise<A>):
|
|
Promise<Record<Layer, A>> {
|
|
const list = await Promise.all(allLayers.map(l => f(l).then(res => [l, res])));
|
|
return Object.fromEntries(list) as Promise<Record<Layer, A>>;
|
|
}
|
|
|
|
|
|
export async function
|
|
loadLayers(dir: string, buf: Buffer): Promise<Record<Layer, ImageData>> {
|
|
const bitmaps =
|
|
await makeLayerInfoAsync(l => loadBitmap(`./${dir}/${l}.webp`));
|
|
return makeLayerInfo(l => dataViaBuffer(bitmaps[l], buf));
|
|
}
|
|
|
|
|
|
export type Position = [x: number, y: number];
|
|
export type Positions = Record<Layer, Position>;
|
|
|
|
export async function loadPos(dir: string): Promise<Positions> {
|
|
return (await fetch(`./${dir}/pos.json`)).json() as Promise<Positions>;
|
|
}
|
|
|
|
|
|
export type Side = 'front' | 'back';
|
|
|
|
export function swapSide(s: Side): Side {
|
|
return s == 'front' ? 'back' : 'front';
|
|
}
|
|
|
|
export type SideData = Record<Layer, [ImageData, Position]>;
|
|
|
|
export type Data = {
|
|
front: SideData, back: SideData,
|
|
frontImage?: ImageData, backImage?: ImageData,
|
|
};
|
|
|
|
export type ComposedData = Required<Data>;
|
|
|
|
export async function loadData(buf: Buffer): Promise<Data> {
|
|
const [fl, fp, bl, bp] = await Promise.all([
|
|
loadLayers('front', buf), loadPos('front'),
|
|
loadLayers('back', buf), loadPos('back')
|
|
]);
|
|
return {
|
|
front: makeLayerInfo(l => [fl[l], fp[l]]),
|
|
back: makeLayerInfo(l => [bl[l], bp[l]]),
|
|
}
|
|
}
|
|
|
|
|
|
function recolor({ data }: ImageData, col: Color.Color) {
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
data[i] = col.red; data[i+1] = col.green; data[i+2] = col.blue;
|
|
}
|
|
}
|
|
|
|
export async function recolorAll(layers: Data, cols: Color.Colors) {
|
|
await Promise.all(Color.allLayers.map(l => {
|
|
recolor(layers.front[l][0], cols[l]);
|
|
recolor(layers.back[l][0], cols[l]);
|
|
}));
|
|
delete layers.frontImage; delete layers.backImage;
|
|
}
|
|
|
|
export type ComposeLayer = [ImageData, Position, GlobalCompositeOperation];
|
|
|
|
async function compose(buf: Buffer, layers: ComposeLayer[],
|
|
width: number, height: number): Promise<ImageData> {
|
|
buf.save();
|
|
buf.clearRect(0, 0, width, height);
|
|
const bmps = await Promise.all(layers.map(async ([l, [x, y], m]) =>
|
|
[await createImageBitmap(l), x, y, m] as const));
|
|
for (const [bmp, x, y, m] of bmps) {
|
|
buf.globalCompositeOperation = m;
|
|
buf.drawImage(bmp, x, y);
|
|
}
|
|
buf.restore();
|
|
return buf.getImageData(0, 0, width, height);
|
|
}
|
|
|
|
export async function
|
|
ensureComposed(buf: Buffer, data: Data): Promise<ComposedData> {
|
|
const { front, back } = data;
|
|
data.frontImage ??= await composeLayers(front);
|
|
data.backImage ??= await composeLayers(back);
|
|
return data as ComposedData;
|
|
|
|
function composeLayers(sdata: SideData): Promise<ImageData> {
|
|
return compose(buf, allLayers.map(l => makeLayer(l, sdata)), WIDTH, HEIGHT);
|
|
}
|
|
function makeLayer(l: Layer, sdata: SideData): ComposeLayer {
|
|
const [i, p] = sdata[l];
|
|
return [i, p, l == 'eyeshine' ? 'luminosity' : 'source-over'];
|
|
}
|
|
}
|
|
|
|
|
|
export function redraw(ctx: CanvasImageData, data: ComposedData, side: Side) {
|
|
ctx.putImageData(data[`${side}Image`], 0, 0);
|
|
}
|