const rand: () => number = 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.1; const MINC_DARK = 0.12; const MAXC_DARK = 0.175; // 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; // size of the wedge a "complementary" colour can be in const MAXH_COMPL = 40; // size of the wedge a "triadic" colour can be in const MAXH_TRIAD = 25; function randBetween(x: number, y: number): number { const lo = min(x, y), hi = max(x, y); return lo + rand() * (hi - lo); } function oneOf(...xs: A[]): A { return xs[Math.floor(rand() * xs.length)]!; } 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 isLight(l: number): boolean { return l >= MINL_LIGHT; } 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); } } function analogous1(baseH: number): number { const size = randBetween(MINH_SEP, 2 * MINH_SEP); return rand() > 0.5 ? baseH + size : baseH - size; } function analogous(baseH: number, count: number): number[] { const minWidth = min(count * MINH_SEP, MAXH_WIDTH * 0.8); const width = randBetween(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 rand() > 0.5 ? numbers : numbers.reverse(); } function complementary1(baseH: number): number { return analogous1((baseH + 180) % 360); } function complementary(baseH: number, count: number): number[] { const angle = randBetween(180 - MAXH_COMPL/2, 180 + MAXH_COMPL/2); return analogous(baseH + angle, count); } function triad(baseH: number): [number, number] { const angle = randBetween(120 - MAXH_TRIAD/2, 120 + MAXH_TRIAD/2); return [baseH - angle, baseH + angle]; } type SchemeType = 'triad' | 'fin-belly' | 'fin-body'; type OuterLayer = 'outer' | 'spines' | 'vitiligo1'; type SockLayer = 'stripes' | 'cuffs'; type FinLayer = 'fins1' | 'fins2' | 'fins3' | 'vitiligo4'; type BellyLayer = 'belly1' | 'vitiligo3' | 'belly2' | 'vitiligo2'; type MiscLayer = 'eyes' | 'masks' | 'claws' | 'lines'; type Layer = OuterLayer | SockLayer | FinLayer | BellyLayer | MiscLayer; type ColsOf = Record; type OuterCols = ColsOf; type SockCols = ColsOf; type FinCols = ColsOf; type BellyCols = ColsOf; type MiscCols = ColsOf; type Colours = ColsOf & {type: SchemeType}; function colours(): Colours { const outer = baseOklch('dark'); let outerCols: OuterCols = { outer, spines: mkSpines(outer), vitiligo1: mkVitiligo(outer) }; const stripes = mkStripes(); let sockCols: SockCols = { stripes, cuffs: mkCuffs(stripes) }; let finCols: FinCols, bellyCols: BellyCols, type: SchemeType; const whichBody = rand(); if (whichBody > 2/3) { type = 'triad'; const [f, b] = triad(outer.h); finCols = mkFins(f, outer); bellyCols = mkBelly(b); } else if (whichBody > 1/3) { type = 'fin-belly'; const [f, b] = complementary(outer.h, 2); finCols = mkFins(f!, outer); bellyCols = mkBelly(b!); } else { type = 'fin-body'; finCols = mkFins(analogous1(outer.h), outer); bellyCols = mkBelly(complementary1(outer.h)); } let miscCols = mkMisc(outerCols, finCols, bellyCols); return merge(outerCols, sockCols, finCols, bellyCols, miscCols, type); } 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 mkVitiligo(outer: Oklch): Oklch { return { l: randBetween(max(outer.l, 0.94), 0.985), // exception to MAXL c: randBetween(min(outer.c, 0.1), MINC_LIGHT), h: outer.h }; } 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: randBetween(sock.l * 0.85, sock.l * 0.65), c: randBetween(sock.c, MAXC_LIGHT), h: randBetween(sock.h + 8, sock.h - 8) }; } function mkFins(h: number, outer: Oklch): FinCols { const [fin1Hue, fin2Hue, fin3Hue] = analogous(h, 3); const [ll, cc] = oneOf([lightFor, dullFor], [darkFor, brightFor]); const fins1 = { l: ll(outer.l), c: cc(outer.l, outer.c), h: fin1Hue! }; const fins2 = { l: ll(fins1.l), c: cc(fins1.l, fins1.c), h: fin2Hue! }; const fins3 = { l: ll(fins2.l), c: cc(fins2.l, fins2.c), h: fin3Hue! }; const vitiligo4 = mkVitiligo(fins1); return { fins1, fins2, fins3, vitiligo4 }; } function mkBelly(h: number): BellyCols { const [belly1Hue, belly2Hue] = analogous(h, 2); const belly1 = { l: randBetween(0.7, MAXL), c: baseChroma(1), h: belly1Hue! }; const belly2 = { l: min(MAXL, belly1.l * 1.1), c: belly1.c * 0.9, h: belly2Hue! }; const vitiligo3 = mkVitiligo(belly1); const vitiligo2 = mkVitiligo(belly2); return { belly1, belly2, vitiligo2, vitiligo3 }; } function mkMisc(o: OuterCols, f: FinCols, b: BellyCols): MiscCols { const masks = { l: randBetween(0.8, MAXL), c: randBetween(0.01, 0.06), h: analogous1(oneOf(o.outer, b.belly1, f.fins1).h) }; return { masks, eyes: { l: baseLuma('light'), c: randBetween(0.28, MAXC_LIGHT), h: oneOf(analogous1, complementary1)(o.outer.h) }, claws: { l: min(MAXL, masks.l + randBetween(0, 0.1)), c: randBetween(0.01, 0.06), h: analogous1(masks.h) }, lines: { l: randBetween(0.01, 0.06), c: baseChroma(0), h: 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): Colours { return { outer, spines, vitiligo1, stripes, cuffs, fins1, fins2, fins3, vitiligo4, belly1, vitiligo3, belly2, vitiligo2, eyes, masks, claws, lines, type }; } function setColours(cols: Colours) { for (const k in cols) { if (k == 'type') continue; const c = cols[k as Exclude]; 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}`); } document.addEventListener('DOMContentLoaded', function() { document.getElementById('reroll')?.addEventListener('click', doReroll); document.getElementById('swap')?.addEventListener('click', doSwap); doReroll(); setTimeout(setTransition); function doReroll() { setColours(colours()); } function doSwap() { document.getElementById('pic')?.classList.toggle('back'); } function setTransition() { document.documentElement.style.setProperty( '--transition', 'background 0.4s ease-in-out, color 0.4s ease-in-out' ); } }); export { };