rainbow quox

This commit is contained in:
Rhiannon Morris 2024-12-03 03:40:16 +01:00
parent 6681d2eefb
commit da06033eed
45 changed files with 646 additions and 4 deletions

290
rainbow-quox/colour.ts Normal file
View file

@ -0,0 +1,290 @@
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 {}