290 lines
7.3 KiB
TypeScript
290 lines
7.3 KiB
TypeScript
type Rand = () => number;
|
|
|
|
|
|
/*
|
|
let randomData = new Uint32Array(100);
|
|
let next: number = 0;
|
|
function reset() { self.crypto.getRandomValues(randomData); next = 0; }
|
|
function rand() {
|
|
if (next >= 100) { reset(); }
|
|
return randomData[next++] / 4_294_967_295; // u32 max
|
|
}
|
|
reset();
|
|
*/
|
|
|
|
const rand: Rand = Math.random; // [todo]
|
|
|
|
const max = Math.max;
|
|
const min = Math.min;
|
|
|
|
type Oklch = { l: number, c: number, h: number };
|
|
|
|
type LD = 'light' | 'dark';
|
|
|
|
const MAXL = 0.9;
|
|
const MINL = 0.4;
|
|
const MINL_LIGHT = 0.7;
|
|
const MAXL_DARK = 0.65;
|
|
|
|
const MINC_LIGHT = 0.08;
|
|
const MAXC_LIGHT = 0.125;
|
|
const MINC_DARK = 0.12;
|
|
const MAXC_DARK = 0.2;
|
|
|
|
// max spread for a sequence of analogous colours. unless that would put them
|
|
// too close together
|
|
const MAXH_WIDTH = 80;
|
|
|
|
// minimum distance between adjacent analogous colours
|
|
const MINH_SEP = 5;
|
|
|
|
// how far away from 180° a "complementary" colour can be
|
|
const MAXH_COMPL = 40;
|
|
|
|
// how far away from 120° a "triad" colour can be
|
|
const MAXH_TRIAD = 20;
|
|
|
|
function randBetween(x: number, y: number): number {
|
|
const lo = min(x, y), hi = max(x, y);
|
|
return lo + rand() * (hi - lo);
|
|
}
|
|
|
|
function oneOf<A>(...xs: A[]): A {
|
|
return xs[Math.floor(rand() * xs.length)]!;
|
|
}
|
|
|
|
|
|
function isLight(l: number): boolean { return l >= MINL_LIGHT; }
|
|
|
|
function baseLuma(ld?: LD): number {
|
|
if (ld == 'light') {
|
|
return randBetween(MINL_LIGHT, MAXL);
|
|
} else if (ld == 'dark') {
|
|
return randBetween(MINL, MAXL_DARK);
|
|
} else {
|
|
return randBetween(MINL, MAXL);
|
|
}
|
|
}
|
|
|
|
function baseChroma(l: number): number {
|
|
if (l >= MINL_LIGHT) {
|
|
return randBetween(MINC_LIGHT, MAXC_LIGHT);
|
|
} else {
|
|
return randBetween(MINC_DARK, MAXC_DARK);
|
|
}
|
|
}
|
|
|
|
function baseHue(): number { return rand() * 360; }
|
|
|
|
function baseOklch(ld?: LD): Oklch {
|
|
const l = baseLuma(ld);
|
|
return {l, c: baseChroma(l), h: baseHue()};
|
|
}
|
|
|
|
function lightFor(baseL: number): number {
|
|
return randBetween(baseL, MAXL);
|
|
}
|
|
function darkFor(baseL: number): number {
|
|
return randBetween(MINL, baseL);
|
|
}
|
|
|
|
function brightFor(l: number, baseC: number): number {
|
|
if (isLight(l)) { return randBetween(baseC, MAXC_LIGHT); }
|
|
else { return randBetween(baseC, MAXC_DARK); }
|
|
}
|
|
|
|
function dullFor(l: number, baseC: number): number {
|
|
if (isLight(l)) { return randBetween(baseC, MINC_LIGHT); }
|
|
else { return randBetween(baseC, MINC_DARK); }
|
|
}
|
|
|
|
|
|
type Numbers = number[];
|
|
|
|
function upto(end: number): Numbers {
|
|
function* go(next: number): Iterable<number> {
|
|
if (next < end) { yield next; yield* go(next+1) }
|
|
}
|
|
return Array.from(go(0));
|
|
}
|
|
|
|
function analogous(baseH: number, count: number): Numbers {
|
|
const minWidth = count * MINH_SEP;
|
|
const width =
|
|
MAXH_WIDTH < minWidth ? minWidth : randBetween(minWidth, MAXH_WIDTH);
|
|
const sep = width / (count - 1);
|
|
const start = baseH - (width / 2);
|
|
let numbers = Array.from(upto(count).map(i => start + i * sep));
|
|
return rand() > 0.5 ? numbers : numbers.reverse();
|
|
}
|
|
|
|
function complementary(baseH: number, count: number): Numbers {
|
|
const angle = randBetween(180 - MAXH_COMPL, 180 + MAXH_COMPL);
|
|
return analogous(baseH + angle, count);
|
|
}
|
|
|
|
function triad(baseH: number): [number, number] {
|
|
const angle = randBetween(120 - MAXH_TRIAD, 120 + MAXH_TRIAD);
|
|
return [baseH - angle, baseH + angle];
|
|
}
|
|
|
|
type Layer =
|
|
'outer' | 'spines' | 'vitiligo1' |
|
|
'stripes' | 'cuffs' |
|
|
'fins1' | 'fins2' | 'fins3' | 'vitiligo4' |
|
|
'belly1' | 'vitiligo3' | 'belly2' | 'vitiligo2' |
|
|
'eyes' | 'masks' | 'claws' | 'lines';
|
|
|
|
type Colours = { [l in Layer]: Oklch };
|
|
|
|
function colours(): Colours {
|
|
let cols: Partial<Colours> = {};
|
|
|
|
cols.outer = baseOklch('dark'); // [todo]
|
|
cols.spines = mkSpines(cols.outer);
|
|
cols.vitiligo1 = mkVitiligo(cols.outer);
|
|
|
|
cols.stripes = mkStripes();
|
|
cols.cuffs = mkCuffs(cols.stripes);
|
|
|
|
// fins, belly
|
|
const whichBody = rand();
|
|
if (whichBody > 0.85) {
|
|
// triad
|
|
const hs = triad(cols.outer.h);
|
|
fins(hs[0]); belly(hs[1]);
|
|
} else if (whichBody > 0.4) {
|
|
// fins like belly
|
|
const [f, b] = complementary(cols.outer.h, 2);
|
|
fins(f!); belly(b!);
|
|
} else {
|
|
// fins like outer
|
|
fins(analogous(cols.outer.h, 3)[2]!);
|
|
belly(complementary(cols.outer.h, 3)[2]!);
|
|
}
|
|
|
|
cols.eyes = {
|
|
l: baseLuma('light'),
|
|
c: randBetween(0.28, MAXC_LIGHT),
|
|
h: oneOf(analogous, complementary)(cols.outer.h, 3)[2]!
|
|
};
|
|
|
|
cols.masks = {
|
|
l: randBetween(0.8, MAXL),
|
|
c: randBetween(0.01, 0.06),
|
|
h: analogous(oneOf(cols.outer, cols.belly1, cols.fins1)!.h, 3)[2]!
|
|
};
|
|
cols.claws = {
|
|
l: min(MAXL, cols.masks!.l + randBetween(0.1, -0.1)),
|
|
c: randBetween(0.01, 0.06),
|
|
h: analogous(cols.masks!.h, 3)[2]!
|
|
};
|
|
|
|
cols.lines = {
|
|
l: randBetween(0.01, 0.06),
|
|
c: baseChroma(0),
|
|
h: analogous(cols.outer!.h, 3)[2]!
|
|
}
|
|
|
|
return cols as Colours;
|
|
|
|
|
|
function fins(h: number) {
|
|
const [fin1Hue, fin2Hue, fin3Hue] = analogous(h, 3);
|
|
const d = direction();
|
|
cols.fins1 = doDirection(cols.outer!, fin1Hue!, d);
|
|
cols.fins2 = doDirection(cols.fins1, fin2Hue!, d);
|
|
cols.fins3 = doDirection(cols.fins2, fin3Hue!, d);
|
|
cols.vitiligo4 = mkVitiligo(cols.fins1);
|
|
}
|
|
|
|
function belly(h: number) {
|
|
const [belly1Hue, belly2Hue] = analogous(h, 2);
|
|
cols.belly1 = {
|
|
l: randBetween(0.7, MAXL),
|
|
c: baseChroma(1),
|
|
h: belly1Hue!
|
|
};
|
|
cols.belly2 = {
|
|
l: min(MAXL, cols.belly1!.l * 1.1),
|
|
c: cols.belly1!.c * 0.9,
|
|
h: belly2Hue!
|
|
};
|
|
cols.vitiligo3 = mkVitiligo(cols.belly1); // oops sorry
|
|
cols.vitiligo2 = mkVitiligo(cols.belly2);
|
|
}
|
|
|
|
type LFun = (l: number) => number;
|
|
type CFun = (l: number, c: number) => number;
|
|
function direction(): [LFun, CFun] {
|
|
return oneOf([lightFor, dullFor], [darkFor, brightFor]);
|
|
}
|
|
function doDirection(col: Oklch, h: number, [ll, cc]: [LFun, CFun]) {
|
|
return { l: ll(col.l), c: cc(col.l, col.c), h };
|
|
}
|
|
}
|
|
|
|
function mkVitiligo(outer: Oklch): Oklch {
|
|
return {
|
|
l: randBetween(max(outer.l, 0.8), MAXL),
|
|
c: randBetween(min(outer.c, 0.1), MINC_LIGHT),
|
|
h: randBetween(outer.h + 20, outer.h - 20)
|
|
};
|
|
}
|
|
|
|
function mkSpines(outer: Oklch): Oklch {
|
|
return {
|
|
l: outer.l * 0.8, c: outer.c * 1.1,
|
|
h: randBetween(outer.h + 12, outer.h - 12)
|
|
};
|
|
}
|
|
|
|
function mkStripes(): Oklch {
|
|
return {
|
|
l: randBetween(0.8, MAXL),
|
|
c: randBetween(MINC_LIGHT, MAXC_LIGHT),
|
|
h: rand() * 360
|
|
};
|
|
}
|
|
|
|
function mkCuffs(sock: Oklch): Oklch {
|
|
return {
|
|
l: sock.l * 0.7,
|
|
c: randBetween(sock.c, MAXC_LIGHT),
|
|
h: randBetween(sock.h + 8, sock.h - 8)
|
|
};
|
|
}
|
|
|
|
|
|
function setColours(cols: Colours) {
|
|
for (const k in cols) {
|
|
const c = cols[k as keyof Colours];
|
|
for (const elem of Array.from(document.getElementsByClassName(k))) {
|
|
(elem as HTMLElement).style.background = `oklch(${c.l} ${c.c} ${c.h})`;
|
|
}
|
|
}
|
|
document.documentElement.style.setProperty('--hue', `${cols.outer.h}deg`);
|
|
}
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const reroll = document.getElementById('reroll')!;
|
|
const swap = document.getElementById('swap')!;
|
|
const pic = document.getElementById('pic')!;
|
|
|
|
reroll.addEventListener('click', doReroll);
|
|
swap.addEventListener('click', doSwap);
|
|
|
|
doReroll();
|
|
setTimeout(setTransition);
|
|
|
|
function doReroll() { setColours(colours()); }
|
|
function doSwap() { pic.classList.toggle('back'); }
|
|
function setTransition() {
|
|
document.documentElement.style.setProperty('--transition',
|
|
'background 0.4s ease-in-out, color 0.4s ease-in-out');
|
|
}
|
|
});
|
|
|
|
|
|
export {}
|