refactor rainbow quox

This commit is contained in:
Rhiannon Morris 2024-12-03 14:50:37 +01:00
parent da06033eed
commit 3c5eeeae8e
2 changed files with 142 additions and 145 deletions

View file

@ -1,18 +1,4 @@
type Rand = () => number; const rand: () => number = Math.random; // [todo]
/*
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 max = Math.max;
const min = Math.min; const min = Math.min;
@ -38,11 +24,11 @@ const MAXH_WIDTH = 80;
// minimum distance between adjacent analogous colours // minimum distance between adjacent analogous colours
const MINH_SEP = 5; const MINH_SEP = 5;
// how far away from 180° a "complementary" colour can be // size of the wedge a "complementary" colour can be in
const MAXH_COMPL = 40; const MAXH_COMPL = 40;
// how far away from 120° a "triad" colour can be // size of the wedge a "triadic" colour can be in
const MAXH_TRIAD = 20; const MAXH_TRIAD = 25;
function randBetween(x: number, y: number): number { function randBetween(x: number, y: number): number {
const lo = min(x, y), hi = max(x, y); const lo = min(x, y), hi = max(x, y);
@ -54,8 +40,6 @@ function oneOf<A>(...xs: A[]): A {
} }
function isLight(l: number): boolean { return l >= MINL_LIGHT; }
function baseLuma(ld?: LD): number { function baseLuma(ld?: LD): number {
if (ld == 'light') { if (ld == 'light') {
return randBetween(MINL_LIGHT, MAXL); return randBetween(MINL_LIGHT, MAXL);
@ -88,6 +72,8 @@ function darkFor(baseL: number): number {
return randBetween(MINL, baseL); return randBetween(MINL, baseL);
} }
function isLight(l: number): boolean { return l >= MINL_LIGHT; }
function brightFor(l: number, baseC: number): number { function brightFor(l: number, baseC: number): number {
if (isLight(l)) { return randBetween(baseC, MAXC_LIGHT); } if (isLight(l)) { return randBetween(baseC, MAXC_LIGHT); }
else { return randBetween(baseC, MAXC_DARK); } else { return randBetween(baseC, MAXC_DARK); }
@ -99,138 +85,80 @@ function dullFor(l: number, baseC: number): number {
} }
type Numbers = number[]; function analogous1(baseH: number): number {
const size = randBetween(MINH_SEP, 2 * MINH_SEP);
function upto(end: number): Numbers { return rand() > 0.5 ? baseH + size : baseH - size;
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 { function analogous(baseH: number, count: number): number[] {
const minWidth = count * MINH_SEP; const minWidth = min(count * MINH_SEP, MAXH_WIDTH * 0.8);
const width = const width = randBetween(minWidth, MAXH_WIDTH);
MAXH_WIDTH < minWidth ? minWidth : randBetween(minWidth, MAXH_WIDTH);
const sep = width / (count - 1); const sep = width / (count - 1);
const start = baseH - (width / 2); const start = baseH - (width / 2);
let numbers = Array.from(upto(count).map(i => start + i * sep)); const numbers = Array.from({length: count}, (_u, i) => start + i * sep);
return rand() > 0.5 ? numbers : numbers.reverse(); return rand() > 0.5 ? numbers : numbers.reverse();
} }
function complementary(baseH: number, count: number): Numbers { function complementary1(baseH: number): number {
const angle = randBetween(180 - MAXH_COMPL, 180 + MAXH_COMPL); 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); return analogous(baseH + angle, count);
} }
function triad(baseH: number): [number, number] { function triad(baseH: number): [number, number] {
const angle = randBetween(120 - MAXH_TRIAD, 120 + MAXH_TRIAD); const angle = randBetween(120 - MAXH_TRIAD/2, 120 + MAXH_TRIAD/2);
return [baseH - angle, baseH + angle]; return [baseH - angle, baseH + angle];
} }
type Layer = type SchemeType = 'triad' | 'fin-belly' | 'fin-body';
'outer' | 'spines' | 'vitiligo1' |
'stripes' | 'cuffs' |
'fins1' | 'fins2' | 'fins3' | 'vitiligo4' |
'belly1' | 'vitiligo3' | 'belly2' | 'vitiligo2' |
'eyes' | 'masks' | 'claws' | 'lines';
type Colours = { [l in Layer]: Oklch }; 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<A extends string> = Record<A, Oklch>;
type OuterCols = ColsOf<OuterLayer>;
type SockCols = ColsOf<SockLayer>;
type FinCols = ColsOf<FinLayer>;
type BellyCols = ColsOf<BellyLayer>;
type MiscCols = ColsOf<MiscLayer>;
type Colours = ColsOf<Layer> & {type: SchemeType};
function colours(): Colours { function colours(): Colours {
let cols: Partial<Colours> = {}; const outer = baseOklch('dark');
let outerCols: OuterCols =
{ outer, spines: mkSpines(outer), vitiligo1: mkVitiligo(outer) };
cols.outer = baseOklch('dark'); // [todo] const stripes = mkStripes();
cols.spines = mkSpines(cols.outer); let sockCols: SockCols = { stripes, cuffs: mkCuffs(stripes) };
cols.vitiligo1 = mkVitiligo(cols.outer);
cols.stripes = mkStripes(); let finCols: FinCols, bellyCols: BellyCols, type: SchemeType;
cols.cuffs = mkCuffs(cols.stripes);
// fins, belly
const whichBody = rand(); const whichBody = rand();
if (whichBody > 0.85) { if (whichBody > 2/3) {
// triad type = 'triad';
const hs = triad(cols.outer.h); const [f, b] = triad(outer.h);
fins(hs[0]); belly(hs[1]); finCols = mkFins(f, outer); bellyCols = mkBelly(b);
} else if (whichBody > 0.4) { } else if (whichBody > 1/3) {
// fins like belly type = 'fin-belly';
const [f, b] = complementary(cols.outer.h, 2); const [f, b] = complementary(outer.h, 2);
fins(f!); belly(b!); finCols = mkFins(f!, outer); bellyCols = mkBelly(b!);
} else { } else {
// fins like outer type = 'fin-body';
fins(analogous(cols.outer.h, 3)[2]!); finCols = mkFins(analogous1(outer.h), outer);
belly(complementary(cols.outer.h, 3)[2]!); bellyCols = mkBelly(complementary1(outer.h));
} }
cols.eyes = { let miscCols = mkMisc(outerCols, finCols, bellyCols);
l: baseLuma('light'),
c: randBetween(0.28, MAXC_LIGHT),
h: oneOf(analogous, complementary)(cols.outer.h, 3)[2]!
};
cols.masks = { return merge(outerCols, sockCols, finCols, bellyCols, miscCols, type);
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 { function mkSpines(outer: Oklch): Oklch {
return { return {
@ -239,6 +167,14 @@ function mkSpines(outer: Oklch): Oklch {
}; };
} }
function mkVitiligo(outer: Oklch): Oklch {
return {
l: randBetween(max(outer.l, 0.85), MAXL),
c: randBetween(min(outer.c, 0.1), MINC_LIGHT),
h: randBetween(outer.h + 20, outer.h - 20)
};
}
function mkStripes(): Oklch { function mkStripes(): Oklch {
return { return {
l: randBetween(0.8, MAXL), l: randBetween(0.8, MAXL),
@ -249,42 +185,102 @@ function mkStripes(): Oklch {
function mkCuffs(sock: Oklch): Oklch { function mkCuffs(sock: Oklch): Oklch {
return { return {
l: sock.l * 0.7, l: randBetween(sock.l * 0.85, sock.l * 0.65),
c: randBetween(sock.c, MAXC_LIGHT), c: randBetween(sock.c, MAXC_LIGHT),
h: randBetween(sock.h + 8, sock.h - 8) 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) { function setColours(cols: Colours) {
for (const k in cols) { for (const k in cols) {
const c = cols[k as keyof Colours]; if (k == 'type') continue;
const c = cols[k as Exclude<keyof Colours, 'type'>];
for (const elem of Array.from(document.getElementsByClassName(k))) { for (const elem of Array.from(document.getElementsByClassName(k))) {
(elem as HTMLElement).style.background = `oklch(${c.l} ${c.c} ${c.h})`; (elem as HTMLElement).style.background = `oklch(${c.l} ${c.c} ${c.h})`;
} }
} }
document.documentElement.style.setProperty('--hue', `${cols.outer.h}deg`); document.documentElement.style.setProperty('--hue', `${cols.outer.h}`);
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const reroll = document.getElementById('reroll')!; document.getElementById('reroll')?.addEventListener('click', doReroll);
const swap = document.getElementById('swap')!; document.getElementById('swap')?.addEventListener('click', doSwap);
const pic = document.getElementById('pic')!;
reroll.addEventListener('click', doReroll);
swap.addEventListener('click', doSwap);
doReroll(); doReroll();
setTimeout(setTransition); setTimeout(setTransition);
function doReroll() { setColours(colours()); } function doReroll() { setColours(colours()); }
function doSwap() { pic.classList.toggle('back'); } function doSwap() {
document.getElementById('pic')?.classList.toggle('back');
}
function setTransition() { function setTransition() {
document.documentElement.style.setProperty('--transition', document.documentElement.style.setProperty(
'background 0.4s ease-in-out, color 0.4s ease-in-out'); '--transition',
'background 0.4s ease-in-out, color 0.4s ease-in-out'
);
} }
}); });
export {} export { };

View file

@ -16,7 +16,8 @@
} }
:root { :root {
--hue: 300deg; --hue: 300;
--c-hue: calc(180 + var(--hue));
min-height: 100vh; display: flex; min-height: 100vh; display: flex;
align-items: center; justify-content: center; align-items: center; justify-content: center;
@ -144,7 +145,7 @@ button {
font: 700 25pt var(--font); font: 700 25pt var(--font);
flex: 30%; flex: 30%;
background: oklch(0.5 0.2 var(--hue)); background: oklch(0.5 0.2 var(--hue));
color: oklch(0.95 0.075 calc(180deg + var(--hue))); color: oklch(0.95 0.075 var(--c-hue));
border: 3px solid oklch(0.2 0.05 var(--hue)); border: 3px solid oklch(0.2 0.05 var(--hue));
padding: 0.2em 0.5em; padding: 0.2em 0.5em;
filter: drop-shadow(0 0 10px oklch(0.4 0.2 var(--hue) / 0.45)); filter: drop-shadow(0 0 10px oklch(0.4 0.2 var(--hue) / 0.45));
@ -156,9 +157,9 @@ nav {
} }
nav a { nav a {
color: light-dark(oklch(0.4 0.15 calc(180deg + var(--hue))), color: light-dark(oklch(0.4 0.15 var(--c-hue)),
oklch(0.9 0.19 calc(180deg + var(--hue)))); oklch(0.9 0.19 var(--c-hue)));
text-decoration: 3px solid underline; text-decoration: 3px solid underline;
text-decoration-color: text-decoration-color:
oklch(0.6 0.1 calc(180deg + var(--hue))); oklch(0.6 0.1 var(--c-hue));
} }