rainbow quox canvas stuff
This commit is contained in:
parent
d16ea49d62
commit
d0099fbf19
80 changed files with 497 additions and 415 deletions
|
@ -12,83 +12,36 @@ export const allLayers: Layer[] =
|
|||
'belly1', 'belly2', 'masks', 'claws', 'vitiligo1', 'vitiligo2', 'vitiligo3',
|
||||
'vitiligo4', 'eyes', 'eyeshine', 'lines'];
|
||||
|
||||
export type Image = ImageData;
|
||||
export type Images = Record<Layer, Image>;
|
||||
|
||||
export type ComposedImages = Images & {comp?: ImageData};
|
||||
|
||||
export type Side = 'front' | 'back';
|
||||
|
||||
export function flip(s: Side): Side {
|
||||
return s == 'front' ? 'back' : 'front';
|
||||
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 type Position = [x: number, y: number];
|
||||
export type Positions = Record<Layer, Position>;
|
||||
|
||||
let buffer = new OffscreenCanvas(WIDTH, HEIGHT);
|
||||
let bufferCtx = buffer.getContext('2d')!;
|
||||
export async function loadPos(side: SideName): Promise<Positions> {
|
||||
let req = new Request(`./${side}/pos.json`);
|
||||
return fetch(req).then(resp => resp.json());
|
||||
}
|
||||
|
||||
type Positions = Record<Layer, [number, number]>;
|
||||
export type LayerInfo1 = {data: ImageData, pos: Position};
|
||||
|
||||
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],
|
||||
export type LayerInfo = {
|
||||
layers: Record<Layer, LayerInfo1>,
|
||||
comp?: ImageData,
|
||||
};
|
||||
|
||||
|
||||
type Rgb = [number, number, number];
|
||||
type Rgbs = Record<Color.Layer, Rgb>;
|
||||
|
||||
let rgbBuf = new OffscreenCanvas(1, 1).getContext('2d')!;
|
||||
|
||||
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);
|
||||
|
||||
rgbBuf.fillStyle = col.css();
|
||||
rgbBuf.fillRect(0, 0, 1, 1);
|
||||
const rgb = rgbBuf.getImageData(0, 0, 1, 1).data;
|
||||
return [rgb[0]!, rgb[1]!, rgb[2]!];
|
||||
}
|
||||
|
||||
|
@ -97,98 +50,92 @@ function toRgbs(col: Color.Colors): Rgbs {
|
|||
}
|
||||
|
||||
|
||||
function setImageDataRgb([r, g, b]: Rgb, img: Image): void {
|
||||
let data = img.data;
|
||||
function setImageDataRgb({ data }: ImageData, [r, g, b]: Rgb, a: number = -1) {
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
data[i] = r; data[i+1] = g; data[i+2] = b;
|
||||
if (a >= 0) data[i+3]! *= a;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function wait(img: HTMLImageElement): Promise<HTMLImageElement> {
|
||||
if (img.complete) {
|
||||
return new Promise((r, _) => r(img));
|
||||
} else {
|
||||
return new Promise((r, _) => img.addEventListener('load', () => r(img)));
|
||||
}
|
||||
async function loadImage(url: string): Promise<ImageBitmap> {
|
||||
const img0 = new Image; img0.src = url;
|
||||
const img: ImageBitmapSource = img0.complete ? img0 :
|
||||
await new Promise(r => img0.addEventListener('load', () => r(img0)));
|
||||
return createImageBitmap(img);
|
||||
}
|
||||
|
||||
async function load(side: Side, layer: Layer): Promise<Image> {
|
||||
async function load(buf: OffscreenCanvasRenderingContext2D,
|
||||
side: SideName, layer: Layer): Promise<ImageData> {
|
||||
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);
|
||||
let bmp = await loadImage(`./${side}/${layer}.webp`);
|
||||
return navigator.locks.request('buf', async () => {
|
||||
buf.clearRect(0, 0, WIDTH, HEIGHT); // ?
|
||||
buf.drawImage(bmp, 0, 0);
|
||||
return buf.getImageData(0, 0, WIDTH, HEIGHT)
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSide(side: Side): Promise<ComposedImages> {
|
||||
const res: Partial<ComposedImages> = { };
|
||||
for (const l of allLayers) { res[l] = await load(side, l); }
|
||||
return res as ComposedImages;
|
||||
async function loadSide(buf: OffscreenCanvasRenderingContext2D,
|
||||
side: SideName): Promise<LayerInfo> {
|
||||
const layers: Partial<Record<Layer, LayerInfo1>> = { };
|
||||
const pos = await loadPos(side);
|
||||
const images = Object.fromEntries(
|
||||
await Promise.all(
|
||||
allLayers.map(l => load(buf, side, l).then(res => [l, res])))
|
||||
) as Record<Layer, ImageData>;
|
||||
console.log(images);
|
||||
for (const l of allLayers) {
|
||||
layers[l] = { data: images[l], pos: pos[l] };
|
||||
}
|
||||
return { layers: layers as Record<Layer, LayerInfo1> };
|
||||
}
|
||||
|
||||
function setColors(cols: Rgbs, layers: ComposedImages): void {
|
||||
bufferCtx.save();
|
||||
|
||||
function setColors(cols: Rgbs, info: LayerInfo): void {
|
||||
for (const l of allLayers) {
|
||||
if (l == 'static' || l == 'eyeshine') continue;
|
||||
setImageDataRgb(cols[l], layers[l]);
|
||||
setImageDataRgb(info.layers[l].data, cols[l]);
|
||||
}
|
||||
|
||||
delete layers.comp;
|
||||
|
||||
bufferCtx.restore();
|
||||
delete info.comp;
|
||||
}
|
||||
|
||||
async function compose(layers: Images, pos: Positions): Promise<ImageData> {
|
||||
bufferCtx.save();
|
||||
bufferCtx.clearRect(0, 0, WIDTH, HEIGHT);
|
||||
async function compose(buf: OffscreenCanvasRenderingContext2D,
|
||||
{ layers }: LayerInfo): Promise<ImageData> {
|
||||
return navigator.locks.request('buf', async () => {
|
||||
buf.save();
|
||||
buf.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);
|
||||
}
|
||||
for (const l of allLayers) {
|
||||
const [x, y] = layers[l].pos;
|
||||
buf.globalCompositeOperation =
|
||||
l == 'eyeshine' ? 'luminosity' : 'source-over';
|
||||
buf.drawImage(await createImageBitmap(layers[l].data), x, y);
|
||||
}
|
||||
|
||||
let res = bufferCtx.getImageData(0, 0, WIDTH, HEIGHT);
|
||||
bufferCtx.restore();
|
||||
|
||||
return res;
|
||||
buf.restore();
|
||||
return buf.getImageData(0, 0, WIDTH, HEIGHT);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function redraw(ctx: CanvasRenderingContext2D,
|
||||
layers: ComposedImages, pos: Positions) {
|
||||
let data = layers.comp ??= await compose(layers, pos);
|
||||
buf: OffscreenCanvasRenderingContext2D,
|
||||
info: LayerInfo) {
|
||||
let data = info.comp ??= await compose(buf, info);
|
||||
ctx.putImageData(data, 0, 0);
|
||||
}
|
||||
|
||||
|
||||
async function loadingMessage(): Promise<ImageBitmap> {
|
||||
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;
|
||||
function message(msg: string, ctx: CanvasRenderingContext2D) {
|
||||
ctx.save();
|
||||
ctx.clearRect(0, 0, WIDTH, HEIGHT);
|
||||
ctx.font = 'bold 100px Muller, sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(msg, WIDTH/2, HEIGHT/2);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -201,17 +148,38 @@ 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; }
|
||||
type SideName = 'front' | 'back';
|
||||
|
||||
class Side {
|
||||
cur: SideName = 'front';
|
||||
fronts: LayerInfo;
|
||||
backs: LayerInfo;
|
||||
|
||||
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;
|
||||
private constructor(fronts: LayerInfo, backs: LayerInfo) {
|
||||
this.fronts = fronts; this.backs = backs;
|
||||
}
|
||||
|
||||
static async init(buf: OffscreenCanvasRenderingContext2D) {
|
||||
return new Side(await loadSide(buf, 'front'), await loadSide(buf, 'back'));
|
||||
}
|
||||
|
||||
flip() { this.cur = (this.cur == 'front' ? 'back' : 'front'); }
|
||||
layers() { return this.cur == 'front' ? this.fronts : this.backs; }
|
||||
|
||||
async recolorOn(ctx: CanvasRenderingContext2D,
|
||||
buf: OffscreenCanvasRenderingContext2D) {
|
||||
const cols = Color.colors();
|
||||
const rgbs = toRgbs(cols);
|
||||
setColors(rgbs, this.fronts);
|
||||
setColors(rgbs, this.backs);
|
||||
await redraw(ctx, buf, this.layers());
|
||||
return cols.outer.h;
|
||||
}
|
||||
|
||||
async ensureComposed(buf: OffscreenCanvasRenderingContext2D) {
|
||||
this.fronts.comp ??= await compose(buf, this.fronts);
|
||||
this.backs.comp ??= await compose(buf, this.backs);
|
||||
}
|
||||
}
|
||||
|
||||
function setBg(hue: number) {
|
||||
|
@ -219,56 +187,77 @@ function setBg(hue: number) {
|
|||
}
|
||||
|
||||
|
||||
async function animateReroll(_e: Event, done: () => void) {
|
||||
const duration = 400;
|
||||
const hue = await recolorOn(pic2Ctx);
|
||||
pic2.style.animation = `${duration}ms ease fade-in`;
|
||||
async function animateReroll(ctx1: CanvasRenderingContext2D,
|
||||
ctx2: CanvasRenderingContext2D,
|
||||
buf: OffscreenCanvasRenderingContext2D,
|
||||
side: Side,
|
||||
done: () => void) {
|
||||
const duration = 200;
|
||||
const hue = await side.recolorOn(ctx2, buf);
|
||||
ctx2.canvas.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');
|
||||
await redraw(ctx1, buf, side.layers()).then(() => {
|
||||
ctx2.canvas.style.removeProperty('animation');
|
||||
ctx2.canvas.style.removeProperty('opacity');
|
||||
done();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function animateSwap(_e: Event, done: () => void) {
|
||||
const duration = 1000;
|
||||
pic.style.animation = `${duration}ms ease swap`;
|
||||
setTimeout(swapImage, duration/2);
|
||||
async function animateSwap(picCtx: CanvasRenderingContext2D,
|
||||
pic2Ctx: CanvasRenderingContext2D,
|
||||
side: Side,
|
||||
buf: OffscreenCanvasRenderingContext2D,
|
||||
done: () => void) {
|
||||
const duration = 400;
|
||||
|
||||
side.flip();
|
||||
await Promise.all([
|
||||
side.ensureComposed(buf),
|
||||
redraw(pic2Ctx, buf, side.layers()),
|
||||
]);
|
||||
|
||||
picCtx.canvas.style.animation = `${duration}ms ease swap1`;
|
||||
pic2Ctx.canvas.style.animation = `${duration}ms ease swap2`;
|
||||
setTimeout(swap, duration/2);
|
||||
setTimeout(finish, duration);
|
||||
|
||||
async function swapImage() {
|
||||
side = flip(side);
|
||||
await redraw(picCtx, layers(), pos());
|
||||
function swap() {
|
||||
let data = pic2Ctx.getImageData(0, 0, WIDTH, HEIGHT);
|
||||
picCtx.putImageData(data, 0, 0);
|
||||
}
|
||||
|
||||
function finish() {
|
||||
pic.style.removeProperty('animation');
|
||||
picCtx.canvas.style.removeProperty('animation');
|
||||
pic2Ctx.canvas.style.removeProperty('animation');
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
pic = document.getElementById('pic') as HTMLCanvasElement;
|
||||
picCtx = pic.getContext('2d')!;
|
||||
let pic = document.getElementById('pic') as HTMLCanvasElement;
|
||||
let picCtx = pic.getContext('2d')!;
|
||||
|
||||
pic2 = document.getElementById('pic2') as HTMLCanvasElement;
|
||||
pic2Ctx = pic2.getContext('2d')!;
|
||||
let pic2 = document.getElementById('pic2') as HTMLCanvasElement;
|
||||
let pic2Ctx = pic2.getContext('2d')!;
|
||||
|
||||
picCtx.drawImage(await loadingMessage(), 0, 0);
|
||||
let buf = new OffscreenCanvas(WIDTH, HEIGHT).getContext('2d')!;
|
||||
|
||||
fronts = await loadSide('front');
|
||||
backs = await loadSide('back');
|
||||
side = 'front';
|
||||
message('loading layers…', picCtx);
|
||||
|
||||
let hue = await recolorOn(picCtx);
|
||||
let side: Side = await Side.init(buf).catch(err => {
|
||||
message(err, picCtx);
|
||||
throw err;
|
||||
});
|
||||
|
||||
let hue = await side.recolorOn(picCtx, buf);
|
||||
setBg(hue);
|
||||
await redraw(picCtx, layers(), pos());
|
||||
await redraw(picCtx, buf, side.layers());
|
||||
|
||||
const reroll = document.getElementById('reroll')!;
|
||||
const swap = document.getElementById('swap')!;
|
||||
|
@ -285,19 +274,25 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
swap.removeEventListener('click', handleSwap);
|
||||
}
|
||||
|
||||
function handleReroll(e: Event) { wrap(reroll, animateReroll, 'reroll', e); }
|
||||
function handleSwap(e: Event) { wrap(swap, animateSwap, 'swap', e); }
|
||||
function handleReroll(e: Event) {
|
||||
wrap(reroll, done => animateReroll(picCtx, pic2Ctx, buf, side, done),
|
||||
'reroll', e);
|
||||
}
|
||||
|
||||
type HandlerWithFinish = (e: Event, finish: () => void) => void;
|
||||
function handleSwap(e: Event) {
|
||||
wrap(swap, done => animateSwap(picCtx, pic2Ctx, side, buf, done),
|
||||
'swap', e);
|
||||
}
|
||||
|
||||
function wrap(elem: Element, f: HandlerWithFinish,
|
||||
name: string, e: Event) {
|
||||
type Handler = (finish: () => void) => void;
|
||||
|
||||
function wrap(elem: Element, f: Handler, name: string, e: Event) {
|
||||
if (elem != e.target) return;
|
||||
e.stopPropagation();
|
||||
if (isRunning()) return;
|
||||
removeListeners();
|
||||
startAnim(name);
|
||||
f(e, () => { finishAnim(); addListeners(); });
|
||||
f(() => { finishAnim(); addListeners(); });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue