history, reduced-motion, editable name box, an easter egg

This commit is contained in:
Rhiannon Morris 2024-12-13 03:14:37 +01:00
parent d52151e787
commit 0a59aa66f6
14 changed files with 782 additions and 127 deletions

View file

@ -52,11 +52,26 @@ 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); }
type CloseFar = 'close' | 'far';
lightFor(baseL: Luma): Luma { return this.float(baseL, MAXL); }
darkFor(baseL: Luma): Luma { return this.float(MINL, baseL); }
export class Rand extends R.Rand {
constructor();
constructor([a, b, c, d]: Rand.State);
constructor(str: string);
constructor(st?: Rand.State | string) {
if (st === undefined) super();
else if (typeof st === 'string') super(st);
else super(st);
}
lightFor(baseL: Luma, d: CloseFar = 'close'): Luma {
let maxl = d == 'close' ? min(MAXL, baseL * 1.25) : MAXL;
return this.float(baseL, maxl);
}
darkFor(baseL: Luma, d: CloseFar = 'close'): Luma {
let minl = d == 'close' ? max(MINL, baseL * 0.8) : MINL
return this.float(minl, baseL);
}
brightFor(l: Luma, baseC: Chroma): Chroma {
return this.float(baseC, isLight(l) ? MAXC_LIGHT : MAXC_DARK);
@ -140,14 +155,6 @@ export class Oklch {
}
}
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) {
@ -163,7 +170,23 @@ export class Oklch {
});
}
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})`; }
}
rgb(): Rgb { return toRgbViaCanvas(this); }
static validate(x: unknown): Oklch | undefined {
if (typeof x == 'object' && x != null && 'l' in x && 'c' in x && 'h' in x) {
const { l, c, h } = x;
if (typeof l == 'number' && typeof c == 'number' && typeof h == 'number')
return oklch(l, c, h);
}
}
}
@ -196,9 +219,15 @@ 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 type BaseCol = 'outer' | 'belly' | 'fins';
export type OptionalBaseCol = 'eyes' | 'stripes';
export function colors(r: Rand = new Rand()): Scheme {
const outer = new Oklch(r, 'dark');
type KnownPalette =
Record<BaseCol, Oklch> & Partial<Record<OptionalBaseCol, Oklch>>;
export function colors(r: Rand = new Rand(), base?: KnownPalette): Scheme {
const outer = base?.outer ?? new Oklch(r, 'dark');
let outerCols: OuterCols =
{ outer, spines: mkSpines(r, outer), vitiligo1: mkVitiligo(r, outer) };
@ -210,18 +239,18 @@ export function colors(r: Rand = new Rand()): Scheme {
if (whichBody > 2/3) {
type = 'triad';
const [f, b] = r.triad(outer.h);
finCols = mkFins(r, f, outer); bellyCols = mkBelly(r, b);
finCols = mkFins(r, f, outer, base); bellyCols = mkBelly(r, b, base);
} 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!);
finCols = mkFins(r, f!, outer, base); bellyCols = mkBelly(r, b!, base);
} else {
type = 'fin-body';
finCols = mkFins(r, r.analogous1(outer.h), outer);
bellyCols = mkBelly(r, r.complementary1(outer.h));
finCols = mkFins(r, r.analogous1(outer.h), outer, base);
bellyCols = mkBelly(r, r.complementary1(outer.h), base);
}
let miscCols = mkMisc(r, outerCols, finCols, bellyCols);
let miscCols = mkMisc(r, outerCols, finCols, bellyCols, base);
return merge(outerCols, sockCols, finCols, bellyCols, miscCols, type);
}
@ -229,9 +258,9 @@ export function colors(r: Rand = new Rand()): Scheme {
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),
l: l => r.darkFor(l),
c: c => r.brightFor(outer.l, c),
h: h => r.float(h + 12, h - 12),
})
}
@ -258,8 +287,10 @@ function mkCuffs(r: Rand, sock: Oklch): Oklch {
});
}
function mkFins(r: Rand, h: Hue, outer: Oklch): FinCols {
const [fin1Hue, fin2Hue, fin3Hue] = r.analogous(h, 3);
function mkFins(r: Rand, h: Hue, outer: Oklch, base?: KnownPalette): FinCols {
const baseFin1 = base?.fins;
const [fin1Hue, fin2Hue, fin3Hue] = r.analogous(baseFin1?.h ?? h, 3);
const direction: 'lighter' | 'darker' = r.choice(['lighter', 'darker']);
function ll(l: Luma): Luma {
@ -269,17 +300,18 @@ function mkFins(r: Rand, h: Hue, outer: Oklch): FinCols {
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 fins1 = baseFin1 ?? oklch(ll(outer.l), cc(outer.l, outer.c), fin1Hue!);
const fins2 = oklch(ll(fins1.l), cc(fins1.l, fins1.c), fin2Hue!);
const fins3 = oklch(ll(fins2.l), cc(fins2.l, fins2.c), fin3Hue!);
const lighter = fins1.l >= fins3.l ? fins1 : fins3;
const vitiligo4 = mkVitiligo(r, lighter);
return { fins1, fins2, fins3, vitiligo4 };
}
function mkBelly(r: Rand, h: Hue): BellyCols {
const [belly1Hue, belly2Hue] = r.analogous(h, 2);
const belly1 = new Oklch({
function mkBelly(r: Rand, h: Hue, base?: KnownPalette): BellyCols {
let baseBelly1 = base?.belly;
const [belly1Hue, belly2Hue] = r.analogous(baseBelly1?.h ?? h, 2);
const belly1 = baseBelly1 ?? new Oklch({
l: r.float(0.7, MAXL),
c: r.baseChroma(1),
h: belly1Hue!
@ -294,7 +326,8 @@ function mkBelly(r: Rand, h: Hue): BellyCols {
return { belly1, belly2, vitiligo2, vitiligo3 };
}
function mkMisc(r: Rand, o: OuterCols, f: FinCols, b: BellyCols): MiscCols {
function mkMisc(r: Rand, o: OuterCols, f: FinCols, b: BellyCols,
base?: KnownPalette): MiscCols {
const masks = new Oklch({
l: r.float(0.8, MAXL),
c: r.float(0.01, 0.06),
@ -302,7 +335,7 @@ function mkMisc(r: Rand, o: OuterCols, f: FinCols, b: BellyCols): MiscCols {
});
return {
masks,
eyes: new Oklch({
eyes: base?.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)
@ -365,6 +398,14 @@ export class Rgb {
}
return `#${h(this.r)}${h(this.g)}${h(this.b)}`
}
static validate(x: unknown): Rgb | undefined {
if (typeof x == 'object' && x != null && 'r' in x && 'g' in x && 'b' in x) {
const { r, g, b } = x;
if (typeof r == 'number' && typeof g == 'number' && typeof b == 'number')
return rgb(r, g, b);
}
}
}
export type Rgbs = Record<Layer, Rgb>;
@ -375,8 +416,8 @@ 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]!);
const pix = rgbBuf.getImageData(0, 0, 1, 1).data;
return rgb(pix[0]!, pix[1]!, pix[2]!);
}
export function toRgbs(col: Colors): Rgbs {
@ -390,3 +431,82 @@ export function toHex({r, g, b}: Rgb): string {
}
return `#${chan(r)}${chan(g)}${chan(b)}`;
}
export function oklch(l: number, c: number, h: number) {
return new Oklch(l, c, h);
}
export function rgb(r: number, g: number, b: number) {
return new Rgb(r, g, b);
}
export const KNOWN: Record<string, KnownPalette> = {
niss: {
outer: oklch(0.83, 0.201, 151),
belly: oklch(0.87, 0.082, 99),
fins: oklch(0.68, 0.178, 16),
eyes: oklch(0.73, 0.135, 242),
},
kesi: {
outer: oklch(0.86, 0.147, 147),
belly: oklch(0.96, 0.04, 108),
fins: oklch(0.94, 0.142, 102),
eyes: oklch(0.76, 0.115, 300),
},
60309: {
outer: oklch(0.84, 0.068, 212),
belly: oklch(0.56, 0.035, 233),
fins: oklch(0.55, 0.101, 268),
eyes: oklch(0.86, 0.146, 154),
},
'prickly pear': {
outer: oklch(0.64, 0.087, 316),
belly: oklch(0.88, 0.03, 88),
fins: oklch(0.6, 0.071, 142),
eyes: oklch(0.66, 0.091, 134),
},
'the goo': {
outer: oklch(0.92, 0.046, 354),
belly: oklch(0.83, 0.099, 354),
fins: oklch(0.74, 0.115, 354),
eyes: oklch(0.73, 0.149, 0),
},
lambda: {
outer: oklch(0.71, 0.154, 58),
belly: oklch(0.9, 0.05, 80),
fins: oklch(0.76, 0.16, 140),
eyes: oklch(0.82, 0.178, 141),
},
flussence: {
outer: oklch(0.77, 0.118, 133),
belly: oklch(0.71, 0.086, 253),
fins: oklch(0.58, 0.102, 254),
eyes: oklch(0.37, 0.107, 278),
},
serena: {
outer: oklch(0.69, 0.176, 349),
belly: oklch(0.92, 0.04, 350),
fins: oklch(0.74, 0.138, 319),
eyes: oklch(0.65, 0.206, 4),
},
pippin: {
outer: oklch(0.74, 0.08, 61),
belly: oklch(0.82, 0.062, 70),
fins: oklch(0.52, 0.09, 45),
eyes: oklch(0.74, 0.167, 136),
},
su: {
outer: oklch(0.29, 0.012, 219),
belly: oklch(0.89, 0.01, 256),
fins: oklch(0.53, 0.093, 20),
// eyes: oklch(0.53, 0.109, 254),
},
trans: {
outer: oklch(0.83, 0.065, 228),
belly: oklch(0.95, 0.021, 137),
fins: oklch(0.86, 0.069, 352),
// eyes: oklch(0.57, 0.158, 273),
},
};

View file

@ -0,0 +1,128 @@
import { Colors as Oklchs, Rgbs } from './color.js';
import * as Color from './color.js';
export class HistoryItem {
name: string;
oklch: Oklchs;
rgb: Rgbs;
constructor(name: string, oklch: Oklchs, rgb: Rgbs) {
this.oklch = oklch;
this.rgb = rgb;
this.name = name;
}
asHtml(): HTMLButtonElement {
const { lines: bg, outer, belly1: belly, fins1: fins } = this.rgb;
const content = `
<svg class=history-colors width=30 height=25 viewBox="-10 -10 140 120">
<rect x=-10 y=-10 width=140 height=120 fill="${bg.css()}" />
<path fill="${fins.css()}" d="M 60,0 h -57.73 v 100 z">
<title>fin colour: ${fins.css()}</title>
</path>
<path fill="${belly.css()}" d="M 70,0 h 40 l -57.73,100 h -40 z">
<title>belly colour: ${belly.css()}</title>
</path>
<path fill="${outer.css()}" d="M 120,0 v 100 h -57.73 z">
<title>outer body colour: ${outer.css()}</title>
</path>
<desc>
sample of the palette for ${this.name}.
fin colour: ${fins.css()}.
belly colour: ${belly.css()}.
outer body colour: ${outer.css()}.
</desc>
</svg>
<span class=history-name>${this.name}</span>
`;
let button = document.createElement('button');
button.className = 'history-item';
button.dataset.name = this.name;
button.innerHTML = content;
return button;
}
}
export class History {
items: string[];
constructor(items: string[] = []) { this.items = items; }
add(name: string): void { this.items.push(name); }
*iterNames(maxLength?: number | null): Iterable<string> {
let seen = new Set<string>;
let done = 0;
if (maxLength === undefined) maxLength = 100;
for (let i = this.items.length - 1; i >= 0; i--) {
if (maxLength !== null && done > maxLength) break;
const name = this.items[i]!;
if (!name || seen.has(name)) continue;
seen.add(name); done++;
yield name;
}
}
*iterItems(maxLength?: number | null): Iterable<HistoryItem> {
for (const name of this.iterNames(maxLength)) {
const oklch = Color.colors(new Color.Rand(name), Color.KNOWN[name]);
const rgbs = Color.toRgbs(oklch);
yield new HistoryItem(name, oklch, rgbs);
}
}
static validate(x: unknown): History | undefined {
if (!Array.isArray(x)) return;
if (!x.every(i => typeof i === 'string')) return;
return new History(x);
}
toJSON() { return this.items; }
save(persist = true) {
const storage = persist ? localStorage : sessionStorage;
storage.setItem('history', JSON.stringify(this));
}
// if the json was invalid, return it
// if no history exists just start a new one
static load(): History | string {
const json =
sessionStorage.getItem('history') ??
localStorage.getItem('history');
if (json != null) {
let h = History.validate(JSON.parse(json));
if (h) { h.prune(); return h; }
else return json;
} else {
return new History;
}
}
// if the json is invalid, discard it
static loadOrClear(): History {
const h = History.load();
return h instanceof History ? h : new History;
}
addSave(name: string, persist = true): void {
this.add(name);
this.save(persist);
}
prune(maxLength?: number | null) {
let keep = [];
for (let name of this.iterNames(maxLength)) keep.push(name);
this.items = keep.reverse();
}
pruneSave(maxLength?: number | null, persist = true) {
this.prune(maxLength);
this.save(persist);
}
}

View file

@ -1,6 +1,5 @@
import * as Color from './color.js';
type State = Color.Rand.State;
import { History } from './history.js';
async function loadBitmap(url: string): Promise<ImageBitmap> {
@ -177,46 +176,55 @@ function message(msg: string, error = false) {
}
function urlState(): State | undefined {
const str = document.location.hash;
if (str?.match(/^#\d+$/)) return parseInt(str.substring(1));
function urlState(): string | undefined {
let hash = document.location.hash?.substring(1);
if (hash != '' && hash !== undefined) return decodeURI(hash);
}
function updateUrl(state: State): void {
history.replaceState({}, '', `#${state}`);
function updateUrl(seed: string): void {
history.replaceState({}, '', `#${encodeURI(seed)}`);
}
type ApplyStateOpts =
{ side: Side, state: State, firstLoad: boolean, buf: Buffer, done: Done };
type ApplyStateOpts = {
seed: string,
side?: Side,
firstLoad?: boolean,
buf?: Buffer,
history?: History,
done?: Done,
};
async function
applyState(data: LayerData, opts: Partial<ApplyStateOpts>): Promise<State> {
let { side, state, firstLoad, buf, done } = opts;
applyState(data: LayerData, opts: ApplyStateOpts): Promise<string> {
let { side, seed, firstLoad, buf, history, done } = opts;
side ??= 'front';
firstLoad ??= false;
buf ??= new OffscreenCanvas(WIDTH, HEIGHT).getContext('2d')!;
done ??= () => {};
let r = new Color.Rand(state);
const initState = r.state;
let rand = new Color.Rand(seed);
const oklch = Color.colors(r);
const oklch = Color.colors(rand, Color.KNOWN[seed]);
const rgb = Color.toRgbs(oklch);
const newState = r.state;
const newSeed = rand.alphaNum();
await recolorLayers(data, rgb);
updateBg(oklch);
updateSvgs(oklch, rgb);
updateLabel(initState);
updateUrl(initState);
updateLabel(seed);
updateUrl(seed);
if (firstLoad) {
await instantUpdateImage(side, await ensureComposed(buf, data));
done();
} else {
await animateUpdateImage(buf, side, data, done ?? (() => {}));
await animateUpdateImage(buf, side, data, done);
}
return newState;
if (history) history.addSave(seed);
return newSeed;
}
type CanvasId = 'main' | 'aux';
@ -233,8 +241,16 @@ instantUpdateImage(side: Side, data: ComposedData) {
type Done = () => void;
const noAnim = matchMedia('(prefers-reduced-motion: reduce)');
async function
animateUpdateImage(buf: Buffer, side: Side, data: LayerData, done: Done) {
if (noAnim.matches) {
instantUpdateImage(side, await ensureComposed(buf, data));
done();
return;
}
const duration = 200;
const main = getCanvasCtx('main');
@ -258,6 +274,12 @@ animateUpdateImage(buf: Buffer, side: Side, data: LayerData, done: Done) {
async function
animateSwapImage(buf: Buffer, newSide: Side, data: ComposedData, done: Done) {
if (noAnim.matches) {
instantUpdateImage(newSide, data);
done();
return;
}
const duration = 400;
const main = getCanvasCtx('main');
@ -284,9 +306,9 @@ 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 updateLabel(seed: string) {
const stateLabel = document.getElementById('current-name');
if (stateLabel) stateLabel.innerHTML = seed;
}
function updateSvgs(oklch: Color.Colors, rgb: Color.Rgbs) {
@ -327,21 +349,56 @@ function updateSvgs(oklch: Color.Colors, rgb: Color.Rgbs) {
}
function showHistory(history: History, data: LayerData,
opts: Omit<ApplyStateOpts, 'seed' | 'history'>) {
const list = document.getElementById('history-items');
if (!list) return;
list.innerHTML = '';
let { side, firstLoad, buf, done } = opts;
for (const item of history.iterItems()) {
const elem = item.asHtml();
let allOpts = { side, firstLoad, buf, done, seed: item.name, history };
elem.addEventListener('click', () => {
applyState(data, allOpts);
});
list.appendChild(elem);
}
setTimeout(() => {
document.documentElement.dataset.state = 'history';
const elem = document.createElement('div');
elem.id = 'history-close-target';
elem.addEventListener('click', closeHistory);
document.body.appendChild(elem);
});
}
function closeHistory() {
document.getElementById('history-items')?.
scroll({top: 0, left: 0, behavior: 'smooth'});
let field = document.getElementById('history-close-target');
if (field) field.parentElement?.removeChild(field);
document.documentElement.dataset.state = 'ready';
}
async function setup() {
message('loading layers…');
let data = await loadData().catch(e => { message(e, true); throw e });
let history = History.loadOrClear();
let buf = new OffscreenCanvas(WIDTH, HEIGHT).getContext('2d')!;
let state = urlState();
let seed = urlState() ?? new Color.Rand().alphaNum();
let side: Side = 'front';
state = await applyState(data, { state, buf, firstLoad: true });
seed = await applyState(data, { seed, buf, history, firstLoad: true });
const reroll = document.getElementById('reroll')!;
const swap = document.getElementById('swap')!;
addListeners();
// these ones don't need to be toggled
document.getElementById('hideui')?.addEventListener('click', () => {
document.documentElement.dataset.state = 'fullquox';
@ -349,6 +406,24 @@ async function setup() {
document.getElementById('showui')?.addEventListener('click', () => {
document.documentElement.dataset.state = 'ready';
});
document.getElementById('history-button')?.addEventListener('click', () => {
// does this need the add/remove listeners dance
// actually does anything any more?
showHistory(history, data, { side, buf });
});
document.getElementById('close-history')?.addEventListener('click', closeHistory);
document.getElementById('current-name')?.addEventListener('focusout', async e => {
const space = String.raw`(\n|\s|<br>|&nbsp;)`;
const re = new RegExp(`^${space}+|${space}+$`, 'msgu');
let elem = e.target as HTMLElement;
let str = elem.innerText.replaceAll(re, '');
if (!str) str = new Color.Rand().alphaNum();
elem.innerText = str;
// todo allow images cos it's funny
seed = await applyState(data, { side, seed: str, buf, history });
});
document.documentElement.dataset.state = 'ready';
@ -359,15 +434,16 @@ async function setup() {
function updateFromUrl() {
run(async k => {
const newState = urlState();
if (newState) {
state = await applyState(data, { side, state: newState, buf, done: k });
const newSeed = urlState();
if (newSeed) {
const opts = { history, side, seed: newSeed, buf, done: k };
seed = await applyState(data, opts);
}
});
}
function runReroll() {
run(async k => {
state = await applyState(data, { side, state, buf, done: k });
seed = await applyState(data, { side, seed, buf, history, done: k });
});
}
function runSwap() {

View file

@ -1,45 +1,123 @@
// https://stackoverflow.com/a/424445 thanks my dude
// https://stackoverflow.com/questions/521295#47593316
export type State = number;
export type State = [number, number, number, 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);
}
export interface Randy {
state: State;
int(): number; // whole int32 range
int(x: number): number; // [0, x)
int(x: number, y: number): number; // [x, y)
float(): number; // [0, 1)
float(x: number): number; // [0, x)
float(x: number, y: number): number; // [x, y)
choice<A>(array: A[]): A;
boolean(): boolean;
}
export function fromHex(s: string): State {
const a = s.substring(0, 8);
const b = s.substring(8, 16);
const c = s.substring(16, 24);
const d = s.substring(24, 32);
return [h(a), h(b), h(c), h(d)];
function h(x: string) { return parseInt(x, 16); }
}
export function toHex([a, b, c, d]: State): string {
return `${h(a)}${h(b)}${h(c)}${h(d)}`;
function h(x: number) { return x.toString(16); }
}
const UINT_MAX = 4294967296;
export class Rand implements Randy {
#a: number; #b: number; #c: number; #d: number;
constructor();
constructor([a, b, c, d]: State);
constructor(str: string);
constructor(st?: State | string) {
const [a, b, c, d] =
st === '' ? s4() :
typeof st === 'string' ? Rand.stateFrom(st) :
st ?? s4();
this.#a = a; this.#b = b; this.#c = c; this.#d = d;
for (let i = 0; i < 20; ++i) this.#next();
function s() { return (Math.random() * 2**32) >>> 0; }
function s4() { return [s(), s(), s(), s()] as const; }
}
get state(): State { return [this.#a, this.#b, this.#c, this.#d]; }
get stateString() { return toHex(this.state); }
static stateFrom(str: string): State {
let h1 = 1779033703, h2 = 3144134277,
h3 = 1013904242, h4 = 2773480762;
for (let i = 0, k; i < str.length; i++) {
k = str.charCodeAt(i);
h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
}
h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
h1 ^= (h2 ^ h3 ^ h4), h2 ^= h1, h3 ^= h1, h4 ^= h1;
return [h1>>>0, h2>>>0, h3>>>0, h4>>>0];
}
int(): number; // whole int32 range
int(to: number): number; // [0, x)
int(from: number, to: number): number; // [x, y)
int(x?: number, y?: number): number {
return x === undefined ? this.#next() : Math.floor(this.#float(x, y));
if (x === undefined) return this.#next();
return Math.floor(y === undefined ? this.float(x) : 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); }
float(x?: number, y?: number): number {
const in01 = this.#next() / UINT_MAX;
if (x === undefined) return in01;
const [lo, hi] = y === undefined? [0, x] : [x, y];
return lo + in01 * (hi - lo);
}
choice<A>(array: A[]): A {
return array[this.int(0, array.length)]!;
return array[this.int(array.length)]!;
}
boolean(): boolean { return this.float() > 0.5; }
alphaNum(len?: number): string {
let res = "";
// [todo] is there a better way to make a string in js
if (len === undefined || len <= 0) len = 16; // idk
for (let i = 0; i < len; ++i)
res += this.choice(Array.from("abcdefghijklmnopqrstuvwxyz0123456789"));
return res;
}
#next(): number {
this.#a |= 0; this.#b |= 0; this.#c |= 0; this.#d |= 0;
let t = (this.#a + this.#b | 0) + this.#d | 0;
this.#d = this.#d + 1 | 0;
this.#a = this.#b ^ this.#b >>> 9;
this.#b = this.#c + (this.#c << 3) | 0;
this.#c = (this.#c << 21 | this.#c >>> 11);
this.#c = this.#c + t | 0;
return (t >>> 0);
}
}