import * as Color from './color.js'; export const WIDTH = 1000; export const HEIGHT = 673; export type Layer = 'static' | 'eyeshine' | Color.Layer; // in compositing order export const allLayers: Layer[] = ['static', 'outer', 'spines', 'stripes', 'cuffs', 'fins1', 'fins2', 'fins3', 'belly1', 'belly2', 'masks', 'claws', 'vitiligo1', 'vitiligo2', 'vitiligo3', 'vitiligo4', 'eyes', 'eyeshine', 'lines']; export type Image = ImageData; export type Images = Record; export type ComposedImages = Images & {comp?: ImageData}; export type Side = 'front' | 'back'; export function flip(s: Side): Side { return s == 'front' ? 'back' : 'front'; } let buffer = new OffscreenCanvas(WIDTH, HEIGHT); let bufferCtx = buffer.getContext('2d')!; type Positions = Record; const FRONT_POS: Positions = { belly1: [187, 105], belly2: [186, 91], claws: [3, 168], cuffs: [42, 160], eyes: [223, 52], eyeshine: [223, 52], fins1: [381, 31], fins2: [387, 35], fins3: [495, 140], lines: [1, 0], masks: [173, 3], outer: [28, 43], spines: [372, 23], static: [50, 52], stripes: [50, 168], vitiligo1: [34, 23], vitiligo2: [198, 92], vitiligo3: [214, 312], vitiligo4: [647, 71], }; const BACK_POS: Positions = { belly1: [39, 67], belly2: [92, 95], claws: [191, 334], cuffs: [221, 215], eyes: [685, 42], eyeshine: [685, 42], fins1: [227, 60], fins2: [226, 61], fins3: [229, 195], lines: [0, 0], masks: [643, 1], outer: [2, 22], spines: [337, 50], static: [219, 41], stripes: [219, 221], vitiligo1: [4, 22], vitiligo2: [46, 48], vitiligo3: [101, 134], vitiligo4: [221, 56], }; type Rgb = [number, number, number]; type Rgbs = Record; function toRgb(col: Color.Oklch): Rgb { // :) const prev = bufferCtx.getImageData(0, 0, 1, 1); bufferCtx.save(); bufferCtx.fillStyle = Color.oklch(col); bufferCtx.fillRect(0, 0, 1, 1); bufferCtx.restore(); const rgb = bufferCtx.getImageData(0, 0, 1, 1).data; bufferCtx.putImageData(prev, 0, 0); return [rgb[0]!, rgb[1]!, rgb[2]!]; } function toRgbs(col: Color.Colors): Rgbs { return Color.makeColorInfo(l => toRgb(col[l])); } function setImageDataRgb([r, g, b]: Rgb, img: Image): void { let data = img.data; for (let i = 0; i < data.length; i += 4) { data[i] = r; data[i+1] = g; data[i+2] = b; } } async function wait(img: HTMLImageElement): Promise { if (img.complete) { return new Promise((r, _) => r(img)); } else { return new Promise((r, _) => img.addEventListener('load', () => r(img))); } } async function load(side: Side, layer: Layer): Promise { if (layer == 'eyeshine') layer = 'eyes'; let img = new Image; img.src = `./${side}/${layer}.png`; let bmp = await createImageBitmap(await wait(img)); bufferCtx.clearRect(0, 0, WIDTH, HEIGHT); // ? bufferCtx.drawImage(bmp, 0, 0); return bufferCtx.getImageData(0, 0, WIDTH, HEIGHT); } async function loadSide(side: Side): Promise { const res: Partial = { }; for (const l of allLayers) { res[l] = await load(side, l); } return res as ComposedImages; } function setColors(cols: Rgbs, layers: ComposedImages): void { bufferCtx.save(); for (const l of allLayers) { if (l == 'static' || l == 'eyeshine') continue; setImageDataRgb(cols[l], layers[l]); } delete layers.comp; bufferCtx.restore(); } async function compose(layers: Images, pos: Positions): Promise { bufferCtx.save(); bufferCtx.clearRect(0, 0, WIDTH, HEIGHT); for (const l of allLayers) { const [x, y] = pos[l]; bufferCtx.globalCompositeOperation = l == 'eyeshine' ? 'luminosity' : 'source-over'; bufferCtx.drawImage(await createImageBitmap(layers[l]), x, y); } let res = bufferCtx.getImageData(0, 0, WIDTH, HEIGHT); bufferCtx.restore(); return res; } async function redraw(ctx: CanvasRenderingContext2D, layers: ComposedImages, pos: Positions) { let data = layers.comp ??= await compose(layers, pos); ctx.putImageData(data, 0, 0); } async function loadingMessage(): Promise { bufferCtx.save(); bufferCtx.clearRect(0, 0, WIDTH, HEIGHT); bufferCtx.font = 'bold 100px Muller, sans-serif'; bufferCtx.textAlign = 'center'; bufferCtx.fillText('loading layers…', WIDTH/2, HEIGHT/2); let res = createImageBitmap(buffer); bufferCtx.restore(); return await res; } let pic: HTMLCanvasElement; let picCtx: CanvasRenderingContext2D; let pic2: HTMLCanvasElement; let pic2Ctx: CanvasRenderingContext2D; let fronts: Images; let backs: Images; let side: Side = 'front'; function startAnim(name: string) { document.documentElement.dataset.running = name; } function isRunning(): boolean { return !!document.documentElement.dataset.running; } function finishAnim() { delete document.documentElement.dataset.running; } function layers(): Images { return side == 'front' ? fronts : backs; } function pos(): Positions { return side == 'front' ? FRONT_POS : BACK_POS; } async function recolorOn(ctx: CanvasRenderingContext2D) { const cols = Color.colors(); const rgbs = toRgbs(cols); setColors(rgbs, fronts); setColors(rgbs, backs); await redraw(ctx, layers(), pos()); return cols.outer.h; } function setBg(hue: number) { document.documentElement.style.setProperty('--hue', `${hue}`); } async function animateReroll(_e: Event, done: () => void) { const duration = 400; const hue = await recolorOn(pic2Ctx); pic2.style.animation = `${duration}ms ease fade-in`; setBg(hue); setTimeout(finish, duration); async function finish() { await redraw(picCtx, layers(), pos()).then(() => { pic2.style.removeProperty('animation'); pic2.style.removeProperty('opacity'); done(); }); } } function animateSwap(_e: Event, done: () => void) { const duration = 1000; pic.style.animation = `${duration}ms ease swap`; setTimeout(swapImage, duration/2); setTimeout(finish, duration); async function swapImage() { side = flip(side); await redraw(picCtx, layers(), pos()); } function finish() { pic.style.removeProperty('animation'); done(); } } document.addEventListener('DOMContentLoaded', async function() { pic = document.getElementById('pic') as HTMLCanvasElement; picCtx = pic.getContext('2d')!; pic2 = document.getElementById('pic2') as HTMLCanvasElement; pic2Ctx = pic2.getContext('2d')!; picCtx.drawImage(await loadingMessage(), 0, 0); fronts = await loadSide('front'); backs = await loadSide('back'); side = 'front'; let hue = await recolorOn(picCtx); setBg(hue); await redraw(picCtx, layers(), pos()); const reroll = document.getElementById('reroll')!; const swap = document.getElementById('swap')!; addListeners(); function addListeners() { reroll.addEventListener('click', handleReroll); swap.addEventListener('click', handleSwap); } function removeListeners() { reroll.removeEventListener('click', handleReroll); swap.removeEventListener('click', handleSwap); } function handleReroll(e: Event) { wrap(reroll, animateReroll, 'reroll', e); } function handleSwap(e: Event) { wrap(swap, animateSwap, 'swap', e); } type HandlerWithFinish = (e: Event, finish: () => void) => void; function wrap(elem: Element, f: HandlerWithFinish, name: string, e: Event) { if (elem != e.target) return; e.stopPropagation(); if (isRunning()) return; removeListeners(); startAnim(name); f(e, () => { finishAnim(); addListeners(); }); } }); export { load, loadSide };