import * as Color from './color.js'; async function loadBitmap(url: string): Promise { const img0 = new Image; const img: Promise = 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(f: (l: Layer) => A): Record { return Object.fromEntries(allLayers.map(l => [l, f(l)])) as Record; } export async function makeLayerInfoAsync(f: (l: Layer) => Promise): Promise> { const list = await Promise.all(allLayers.map(l => f(l).then(res => [l, res]))); return Object.fromEntries(list) as Promise>; } export async function loadLayers(dir: string, buf: Buffer): Promise> { 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; export async function loadPos(dir: string): Promise { return (await fetch(`./${dir}/pos.json`)).json() as Promise; } export type Side = 'front' | 'back'; export function swapSide(s: Side): Side { return s == 'front' ? 'back' : 'front'; } export type SideData = Record; export type Data = { front: SideData, back: SideData, frontImage?: ImageData, backImage?: ImageData, }; export type ComposedData = Required; export async function loadData(buf: Buffer): Promise { 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 { 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 { const { front, back } = data; data.frontImage ??= await composeLayers(front); data.backImage ??= await composeLayers(back); return data as ComposedData; function composeLayers(sdata: SideData): Promise { 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); }