move scripts/styles into dirs

This commit is contained in:
Rhiannon Morris 2024-12-10 02:51:34 +01:00
parent f69277f67f
commit b3e45e855a
8 changed files with 5 additions and 5 deletions

View file

@ -0,0 +1,391 @@
import * as R from './rand.js';
const max = Math.max;
const min = Math.min;
export type Luma = number;
export type Chroma = number;
export type Hue = number;
export type Alpha = number;
export type HueDistance = number;
const MAXL: Luma = 0.9;
const MINL: Luma = 0.4;
const MINL_LIGHT: Luma = 0.7;
const MAXL_DARK: Luma = 0.65;
const MINC_LIGHT: Chroma = 0.08;
const MAXC_LIGHT: Chroma = 0.1;
const MINC_DARK: Chroma = 0.12;
const MAXC_DARK: Chroma = 0.175;
// max spread for a sequence of analogous colors. unless that would put them
// too close together
const MAXH_WIDTH: HueDistance = 80;
// minimum distance between adjacent analogous colors
const MINH_SEP: HueDistance = 5;
// size of the wedge a "complementary" color can be in
const MAXH_COMPL: HueDistance = 40;
// size of the wedge a "triadic" color can be in
const MAXH_TRIAD: HueDistance = 25;
type LD = 'light' | 'dark';
export namespace Oklch {
export type Channel = 'l' | 'c' | 'h';
export type Channels = Record<Channel, number>;
export type ChannelMap = (x: number) => number;
export type ChannelMaps = Record<Channel, ChannelMap>;
// a function, or constant value, for each channel;
// or nothing, which indicates identity function
export type With = Partial<Record<Channel, number | ChannelMap>>;
export type With1 = ChannelMap | number | undefined;
}
function isLight(l: Luma): boolean { return l >= MINL_LIGHT; }
export namespace Rand { export type State = R.State; }
export class Rand extends R.Rand {
constructor(seed?: R.State) { super(seed); }
lightFor(baseL: Luma): Luma { return this.float(baseL, MAXL); }
darkFor(baseL: Luma): Luma { return this.float(MINL, baseL); }
brightFor(l: Luma, baseC: Chroma): Chroma {
return this.float(baseC, isLight(l) ? MAXC_LIGHT : MAXC_DARK);
}
dullFor(l: Luma, baseC: Chroma): Chroma {
return this.float(baseC, isLight(l) ? MINC_LIGHT : MINC_DARK);
}
analogous1(baseH: Hue): Hue {
const size = this.float(MINH_SEP, 2 * MINH_SEP);
return this.boolean() ? baseH + size : baseH - size;
}
analogous(baseH: Hue, count: number): Hue[] {
const minWidth = min(count * MINH_SEP, MAXH_WIDTH * 0.8);
const width = this.float(minWidth, MAXH_WIDTH);
const sep = width / (count - 1);
const start = baseH - (width / 2);
const numbers = Array.from({length: count}, (_u, i) => start + i * sep);
return this.boolean() ? numbers : numbers.reverse();
}
complementary1(baseH: Hue): Hue {
return this.analogous1((baseH + 180) % 360);
}
complementary(baseH: Hue, count: number): Hue[] {
const angle = this.float(180 - MAXH_COMPL/2, 180 + MAXH_COMPL/2);
return this.analogous(baseH + angle, count);
}
triad(baseH: Hue): [Hue, Hue] {
const angle = this.float(120 - MAXH_TRIAD/2, 120 + MAXH_TRIAD/2);
return [baseH - angle, baseH + angle];
}
baseLuma(ld?: LD): Luma {
if (ld == 'light') {
return this.float(MINL_LIGHT, MAXL);
} else if (ld == 'dark') {
return this.float(MINL, MAXL_DARK);
} else {
return this.float(MINL, MAXL);
}
}
baseChroma(l: Luma): Chroma {
if (l >= MINL_LIGHT) {
return this.float(MINC_LIGHT, MAXC_LIGHT);
} else {
return this.float(MINC_DARK, MAXC_DARK);
}
}
baseHue(): Hue { return this.float(360); }
}
export class Oklch {
readonly l: Luma; readonly c: Chroma; readonly h: Hue;
static normHue(h: Hue) { return (h = h % 360) < 0 ? h + 360 : h; }
constructor(l: Luma, c: Chroma, h: Hue);
constructor(r: Rand, ld?: LD);
constructor(cs: Oklch.Channels);
constructor(ll: Luma | Oklch.Channels | Rand, cc?: Chroma | LD, hh?: Hue) {
if (hh !== undefined) {
this.l = ll as Luma;
this.c = cc as Chroma;
this.h = hh as Hue;
} else if (typeof ll == 'object' && 'l' in ll) {
const {l, c, h} = ll as Oklch.Channels;
this.l = l; this.c = c; this.h = h;
} else {
const r = ll as Rand;
this.l = r.baseLuma(cc as LD | undefined);
this.c = r.baseChroma(this.l);
this.h = r.baseHue();
}
}
css(alpha: number = 1): string {
const l = (this.l * 100).toFixed(0);
const c = (this.c * 250).toFixed(0);
const h = this.h.toFixed(0);
if (alpha != 1) { return `oklch(${l}% ${c}% ${h} / ${alpha})`; }
else { return `oklch(${l}% ${c}% ${h})`; }
}
with(maps: Oklch.With): Oklch {
function call(comp: Oklch.With1, x: number) {
switch (typeof comp) {
case 'number': return comp;
case 'function': return comp(x);
default: return x;
}
}
return new Oklch({
l: call(maps.l, this.l),
c: call(maps.c, this.c),
h: call(maps.h, this.h),
});
}
rgb(): Rgb { return toRgbViaCanvas(this); }
}
export type SchemeType = 'triad' | 'fin-belly' | 'fin-body';
export type OuterLayer = 'outer' | 'spines' | 'vitiligo1';
export type SockLayer = 'stripes' | 'cuffs';
export type FinLayer = 'fins1' | 'fins2' | 'fins3' | 'vitiligo4';
export type BellyLayer = 'belly1' | 'vitiligo3' | 'belly2' | 'vitiligo2';
export type MiscLayer = 'eyes' | 'masks' | 'claws' | 'lines';
export type Layer =
OuterLayer | SockLayer | FinLayer | BellyLayer | MiscLayer;
export type ColsOf<A extends string> = Record<A, Oklch>;
export type OuterCols = ColsOf<OuterLayer>;
export type SockCols = ColsOf<SockLayer>;
export type FinCols = ColsOf<FinLayer>;
export type BellyCols = ColsOf<BellyLayer>;
export type MiscCols = ColsOf<MiscLayer>;
export type Colors = ColsOf<Layer>;
export type Scheme = Colors & {type: SchemeType};
export const allLayers: Layer[] =
['outer', 'spines', 'stripes', 'cuffs', 'fins1', 'fins2', 'fins3',
'belly1', 'belly2', 'masks', 'claws', 'vitiligo1', 'vitiligo2', 'vitiligo3',
'vitiligo4', 'eyes', 'lines'];
export function makeColorInfo<A>(f: (l: Layer) => A): Record<Layer, A> {
return Object.fromEntries(allLayers.map(l => [l, f(l)])) as Record<Layer, A>;
}
export function colors(r: Rand = new Rand()): Scheme {
const outer = new Oklch(r, 'dark');
let outerCols: OuterCols =
{ outer, spines: mkSpines(r, outer), vitiligo1: mkVitiligo(r, outer) };
const stripes = mkStripes(r);
let sockCols: SockCols = { stripes, cuffs: mkCuffs(r, stripes) };
let finCols: FinCols, bellyCols: BellyCols, type: SchemeType;
const whichBody = r.float();
if (whichBody > 2/3) {
type = 'triad';
const [f, b] = r.triad(outer.h);
finCols = mkFins(r, f, outer); bellyCols = mkBelly(r, b);
} else if (whichBody > 1/3) {
type = 'fin-belly';
const [f, b] = r.complementary(outer.h, 2);
finCols = mkFins(r, f!, outer); bellyCols = mkBelly(r, b!);
} else {
type = 'fin-body';
finCols = mkFins(r, r.analogous1(outer.h), outer);
bellyCols = mkBelly(r, r.complementary1(outer.h));
}
let miscCols = mkMisc(r, outerCols, finCols, bellyCols);
return merge(outerCols, sockCols, finCols, bellyCols, miscCols, type);
}
function mkSpines(r: Rand, outer: Oklch): Oklch {
return outer.with({
l: x => x * 0.8,
c: x => x * 1.1,
h: x => r.float(x + 12, x - 12),
})
}
function mkVitiligo(r: Rand, outer: Oklch): Oklch {
return outer.with({
l: x => r.float(max(x, 0.94), 0.985), // exception to MAXL
c: x => r.float(min(x, 0.1), MINC_LIGHT),
});
}
function mkStripes(r: Rand): Oklch {
return new Oklch({
l: r.float(0.8, MAXL),
c: r.float(MINC_LIGHT, MAXC_LIGHT),
h: r.baseHue(),
});
}
function mkCuffs(r: Rand, sock: Oklch): Oklch {
return sock.with({
l: l => r.float(l * 0.85, l * 0.65),
c: c => r.float(c, MAXC_LIGHT),
h: h => r.float(h + 8, h - 8),
});
}
function mkFins(r: Rand, h: Hue, outer: Oklch): FinCols {
const [fin1Hue, fin2Hue, fin3Hue] = r.analogous(h, 3);
const direction: 'lighter' | 'darker' = r.choice(['lighter', 'darker']);
function ll(l: Luma): Luma {
return direction == 'lighter' ? r.lightFor(l) : r.darkFor(l);
}
function cc(l: Luma, c: Chroma): Chroma {
return direction == 'lighter' ? r.dullFor(l, c) : r.brightFor(l, c);
}
const fins1 = new Oklch(ll(outer.l), cc(outer.l, outer.c), fin1Hue!);
const fins2 = new Oklch(ll(fins1.l), cc(fins1.l, fins1.c), fin2Hue!);
const fins3 = new Oklch(ll(fins2.l), cc(fins2.l, fins2.c), fin3Hue!);
const vitiligo4 = mkVitiligo(r, fins1);
return { fins1, fins2, fins3, vitiligo4 };
}
function mkBelly(r: Rand, h: Hue): BellyCols {
const [belly1Hue, belly2Hue] = r.analogous(h, 2);
const belly1 = new Oklch({
l: r.float(0.7, MAXL),
c: r.baseChroma(1),
h: belly1Hue!
});
const belly2 = belly1.with({
l: x => min(MAXL, x * 1.1),
c: x => x * 0.9,
h: belly2Hue!,
});
const vitiligo3 = mkVitiligo(r, belly1);
const vitiligo2 = mkVitiligo(r, belly2);
return { belly1, belly2, vitiligo2, vitiligo3 };
}
function mkMisc(r: Rand, o: OuterCols, f: FinCols, b: BellyCols): MiscCols {
const masks = new Oklch({
l: r.float(0.8, MAXL),
c: r.float(0.01, 0.06),
h: r.analogous1(r.choice([o.outer, b.belly1, f.fins1]).h)
});
return {
masks,
eyes: new Oklch({
l: r.baseLuma('light'),
c: r.float(0.28, MAXC_LIGHT),
h: r.boolean() ? r.analogous1(o.outer.h) : r.complementary1(o.outer.h)
}),
claws: masks.with({
l: x => min(MAXL, x + r.float(0, 0.1)),
c: r.float(0.01, 0.06),
h: h => r.analogous1(h),
}),
lines: new Oklch({
l: r.float(0.01, 0.06),
c: r.baseChroma(0),
h: r.analogous1(o.outer.h)
}),
};
}
function merge({ outer, spines, vitiligo1 }: OuterCols,
{ stripes, cuffs }: SockCols,
{ fins1, fins2, fins3, vitiligo4 }: FinCols,
{ belly1, vitiligo3, belly2, vitiligo2 }: BellyCols,
{ eyes, masks, claws, lines }: MiscCols,
type: SchemeType): Scheme {
return {
outer, spines, vitiligo1, stripes, cuffs, fins1, fins2, fins3, vitiligo4,
belly1, vitiligo3, belly2, vitiligo2, eyes, masks, claws, lines, type
};
}
export namespace Rgb {
export type Channel = number;
export type Channels = { r: number, g: number, b: number };
}
export class Rgb {
readonly r: Rgb.Channel;
readonly g: Rgb.Channel;
readonly b: Rgb.Channel;
static clamp(x: Rgb.Channel) {
return min(max(0, Math.floor(x)), 255);
}
constructor(r: Rgb.Channel, g: Rgb.Channel, b: Rgb.Channel);
constructor({r, g, b}: Rgb.Channels);
constructor(rr: Rgb.Channel | Rgb.Channels, gg?: Rgb.Channel, bb?: Rgb.Channel) {
const C = Rgb.clamp;
if (typeof rr == 'number') {
this.r = C(rr!); this.g = C(gg!); this.b = C(bb!);
} else {
this.r = C(rr.r); this.g = C(rr.g); this.b = C(rr.b);
}
}
css() {
function h(x: Rgb.Channel) {
let s = x.toString(16);
return s.length == 2 ? s : '0' + s;
}
return `#${h(this.r)}${h(this.g)}${h(this.b)}`
}
}
export type Rgbs = Record<Layer, Rgb>;
let rgbBuf: OffscreenCanvasRenderingContext2D;
export function toRgbViaCanvas(col: Oklch): Rgb {
rgbBuf ??= new OffscreenCanvas(1, 1).getContext('2d')!;
rgbBuf.fillStyle = col.css();
rgbBuf.fillRect(0, 0, 1, 1);
const rgb = rgbBuf.getImageData(0, 0, 1, 1).data;
return new Rgb(rgb[0]!, rgb[1]!, rgb[2]!);
}
export function toRgbs(col: Colors): Rgbs {
return makeColorInfo(l => col[l].rgb());
}
export function toHex({r, g, b}: Rgb): string {
function chan(n: number) {
let a = Math.floor(n).toString(16);
return a.length == 1 ? `0${a}` : a;
}
return `#${chan(r)}${chan(g)}${chan(b)}`;
}

378
rainbow-quox/script/quox.ts Normal file
View file

@ -0,0 +1,378 @@
import * as Color from './color.js';
type State = Color.Rand.State;
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(`couldn't load file: ${url}`));
});
img0.src = url;
return createImageBitmap(await img);
}
type Buffer = OffscreenCanvasRenderingContext2D;
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);
}
async function loadDataLocking(url: string, buf: Buffer): Promise<ImageData> {
return loadBitmap(url).then(i =>
navigator.locks.request('imagebuf', () => dataViaBuffer(i, buf)));
}
async function loadDataFresh(url: string): Promise<ImageData> {
const img = await loadBitmap(url);
let buf = new OffscreenCanvas(img.width, img.height).getContext('2d')!;
return dataViaBuffer(img, buf);
}
function loadImageData(url: string, buf?: Buffer): Promise<ImageData> {
if (buf && navigator.locks) return loadDataLocking(url, buf);
else return loadDataFresh(url);
}
const WIDTH = 1000;
const HEIGHT = 673;
function makeBuffer(width = WIDTH, height = HEIGHT): Buffer {
return new OffscreenCanvas(width, height).getContext('2d')!;
}
function makeBufferIfLocks(width?: number, height?: number): Buffer | undefined {
if (navigator.locks) makeBuffer(width, height);
else return undefined;
}
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'];
function makeLayerInfo<A>(f: (l: Layer) => A): Record<Layer, A> {
return Object.fromEntries(allLayers.map(l => [l, f(l)])) as Record<Layer, A>;
}
async function makeLayerInfoAsync<A>(f: (l: Layer) => Promise<A>):
Promise<Record<Layer, A>> {
let list = await Promise.all(allLayers.map(l => f(l).then(res => [l, res])));
return Object.fromEntries(list);
}
function loadLayers(dir: string): Promise<Record<Layer, ImageData>> {
let buf = makeBufferIfLocks(WIDTH, HEIGHT);
return makeLayerInfoAsync(l => loadImageData(`./${dir}/${l}.webp`, buf));
}
type Position = [x: number, y: number];
type Positions = Record<Layer, Position>;
async function loadPos(dir: string): Promise<Positions> {
return (await fetch(`./${dir}/pos.json`)).json();
}
type Side = 'front' | 'back';
function swapSide(s: Side): Side {
return s == 'front' ? 'back' : 'front';
}
type SideData = Record<Layer, [ImageData, Position]>;
type LayerData = {
front: SideData, back: SideData,
frontImage?: ImageData, backImage?: ImageData,
};
type ComposedData = Required<LayerData>;
async function loadData(): Promise<LayerData> {
let [fl, fp, bl, bp] = await Promise.all([
loadLayers('front'), loadPos('front'),
loadLayers('back'), loadPos('back')
]);
return {
front: makeLayerInfo(l => [fl[l], fp[l]]),
back: makeLayerInfo(l => [bl[l], bp[l]]),
}
}
function singleColor({ data }: ImageData, { r, g, b }: Color.Rgb) {
for (let i = 0; i < data.length; i += 4) {
data[i] = r; data[i+1] = g; data[i+2] = b;
}
}
async function recolorLayers(layers: LayerData, cols: Color.Rgbs) {
await Promise.all(Color.allLayers.map(l => {
singleColor(layers.front[l][0], cols[l]);
singleColor(layers.back[l][0], cols[l]);
}));
delete layers.frontImage; delete layers.backImage;
}
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);
}
async function
ensureComposed(buf: Buffer, data: LayerData): Promise<ComposedData> {
let { 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 {
let [i, p] = sdata[l];
return [i, p, l == 'eyeshine' ? 'luminosity' : 'source-over'];
}
}
async function redraw(ctx: CanvasRenderingContext2D,
buf: Buffer, data: ComposedData, side: Side) {
await ensureComposed(buf, data);
ctx.putImageData(data[`${side}Image`], 0, 0);
}
function message(msg: string, error = false) {
const ctx = getCanvasCtx('main');
const size = error ? 30 : 100;
ctx.save();
ctx.clearRect(0, 0, WIDTH, HEIGHT);
ctx.font = `bold ${size}px Muller, sans-serif`;
ctx.textAlign = 'center';
ctx.fillText(msg, WIDTH/2, HEIGHT/2, WIDTH-10);
ctx.restore();
}
function urlState(): State | undefined {
const str = document.location.hash;
if (str?.match(/^#\d+$/)) return parseInt(str.substring(1));
}
function updateUrl(state: State): void {
history.replaceState({}, '', `#${state}`);
}
type ApplyStateOpts =
{ side: Side, state: State, firstLoad: boolean, buf: Buffer, done: Done };
async function
applyState(data: LayerData, opts: Partial<ApplyStateOpts>): Promise<State> {
let { side, state, firstLoad, buf, done } = opts;
side ??= 'front';
firstLoad ??= false;
buf ??= new OffscreenCanvas(WIDTH, HEIGHT).getContext('2d')!;
let r = new Color.Rand(state);
const initState = r.state;
const oklch = Color.colors(r);
const rgb = Color.toRgbs(oklch);
const newState = r.state;
await recolorLayers(data, rgb);
updateBg(oklch);
updatePalette(oklch, rgb);
updateLabel(initState);
if (firstLoad) {
await instantUpdateImage(side, await ensureComposed(buf, data));
} else {
updateUrl(initState);
await animateUpdateImage(buf, side, data, done ?? (() => {}));
}
return newState;
}
type CanvasId = 'main' | 'aux';
function getCanvasCtx(id: CanvasId) {
const canvas = document.getElementById(id) as HTMLCanvasElement;
return canvas.getContext('2d')!;
}
async function
instantUpdateImage(side: Side, data: ComposedData) {
getCanvasCtx('main').putImageData(data[`${side}Image`], 0, 0);
}
type Done = () => void;
async function
animateUpdateImage(buf: Buffer, side: Side, data: LayerData, done: Done) {
const duration = 200;
const main = getCanvasCtx('main');
const aux = getCanvasCtx('aux');
document.documentElement.dataset.running = 'reroll';
const cdata = await ensureComposed(buf, data);
redraw(aux, buf, cdata, side);
aux.canvas.addEventListener('animationend', async () => {
await redraw(main, buf, cdata, side);
aux.canvas.style.removeProperty('animation');
delete document.documentElement.dataset.running;
done();
});
setTimeout(() =>
aux.canvas.style.animation = `${duration}ms ease forwards fade-in`
);
}
async function
animateSwapImage(buf: Buffer, newSide: Side, data: ComposedData, done: Done) {
const duration = 400;
const main = getCanvasCtx('main');
const aux = getCanvasCtx('aux');
document.documentElement.dataset.running = 'swap';
await redraw(aux, buf, data, newSide);
aux.canvas.addEventListener('animationend', async () => {
const image = aux.getImageData(0, 0, WIDTH, HEIGHT);
main.putImageData(image, 0, 0);
main.canvas.style.removeProperty('animation');
aux.canvas.style.removeProperty('animation');
delete document.documentElement.dataset.running;
done();
});
main.canvas.style.animation = `${duration}ms ease swap1`;
aux.canvas.style.animation = `${duration}ms ease swap2`;
}
function updateBg(cols: Color.Colors) {
document.documentElement.style.setProperty('--hue', `${cols.outer.h}`);
}
function updateLabel(st: State) {
const stateLabel = document.getElementById('state');
if (stateLabel) stateLabel.innerHTML = `${st}`;
}
function updatePalette(oklch: Color.Colors, rgb: Color.Rgbs) {
const paletteObj = document.getElementById('palette') as HTMLObjectElement;
const palette = paletteObj.contentDocument as XMLDocument | null;
if (!palette) return;
palette.documentElement.style.setProperty('--hue', `${oklch.outer.h}`);
const get = (id: string) => palette.getElementById(id);
for (const layer of Color.allLayers) {
let col = rgb[layer].css();
let elem;
// main group
if (elem = get(`i-${layer}`)) {
if (oklch[layer].l < 0.6) {
elem.classList.add('light'); elem.classList.remove('dark');
} else {
elem.classList.add('dark'); elem.classList.remove('light');
}
elem.style.setProperty('--col', col);
}
// label
if (elem = get(`c-${layer}`)) elem.innerHTML = col;
// minor swatch, if applicable
if (elem = get(`s-${layer}`)) elem.style.setProperty('--col', col);
}
}
async function setup() {
message('loading layers…');
let data = await loadData().catch(e => { message(e, true); throw e });
let buf = new OffscreenCanvas(WIDTH, HEIGHT).getContext('2d')!;
let state = urlState();
let side: Side = 'front';
state = await applyState(data, { state, buf, firstLoad: true });
const reroll = document.getElementById('reroll')!;
const swap = document.getElementById('swap')!;
addListeners();
document.documentElement.dataset.ready = '1';
async function run(task: (k: Done) => Promise<void>): Promise<void> {
removeListeners();
await task(addListeners);
}
function updateFromUrl() {
run(async k => {
const newState = urlState();
if (newState) {
state = await applyState(data, { side, state: newState, buf, done: k });
}
});
}
function runReroll() {
run(async k => {
state = await applyState(data, { side, state, buf, done: k });
});
}
function runSwap() {
run(async k => {
side = swapSide(side);
const cdata = await ensureComposed(buf, data);
await animateSwapImage(buf, side, cdata, k);
});
}
function addListeners() {
window.addEventListener('hashchange', updateFromUrl);
reroll.addEventListener('click', runReroll);
swap.addEventListener('click', runSwap);
}
function removeListeners() {
window.removeEventListener('hashchange', updateFromUrl);
reroll.removeEventListener('click', runReroll);
swap.removeEventListener('click', runSwap);
}
}
document.addEventListener('DOMContentLoaded', setup);

View file

@ -0,0 +1,45 @@
// https://stackoverflow.com/a/424445 thanks my dude
export type State = number;
const M = 0x80000000;
const A = 1103515245;
const C = 12345;
export class Rand {
state: number;
constructor(state?: State) {
this.state = typeof state == 'number' && !isNaN(state) ?
state : Math.floor(Math.random() * (M - 1));
}
#next(): number { return this.state = (A * this.state + C) % M; }
#float(x?: number, y?: number): number {
const [lo, hi] =
x === undefined ? [0, 1] :
y === undefined ? [0, x] :
[Math.min(x, y), Math.max(x, y)];
return lo + this.#next() / (M - 1) * (hi - lo);
}
int(): number; // whole int32 range
int(x: number): number; // [0, x)
int(x: number, y: number): number; // [x, y)
int(x?: number, y?: number): number {
return x === undefined ? this.#next() : Math.floor(this.#float(x, y));
}
float(): number; // [0, 1)
float(x: number): number; // [0, x)
float(x: number, y: number): number; // [x, y)
float(x?: number, y?: number): number { return this.#float(x, y); }
choice<A>(array: A[]): A {
return array[this.int(0, array.length)]!;
}
boolean(): boolean { return this.float() > 0.5; }
}