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;
/*
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 rand: () => number = Math.random; // [todo]
const max = Math.max;
const min = Math.min;
@ -38,11 +24,11 @@ const MAXH_WIDTH = 80;
// minimum distance between adjacent analogous colours
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;
// how far away from 120° a "triad" colour can be
const MAXH_TRIAD = 20;
// 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);
@ -54,8 +40,6 @@ function oneOf<A>(...xs: A[]): A {
}
function isLight(l: number): boolean { return l >= MINL_LIGHT; }
function baseLuma(ld?: LD): number {
if (ld == 'light') {
return randBetween(MINL_LIGHT, MAXL);
@ -78,7 +62,7 @@ function baseHue(): number { return rand() * 360; }
function baseOklch(ld?: LD): Oklch {
const l = baseLuma(ld);
return {l, c: baseChroma(l), h: baseHue()};
return { l, c: baseChroma(l), h: baseHue() };
}
function lightFor(baseL: number): number {
@ -88,6 +72,8 @@ 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); }
@ -99,138 +85,80 @@ function dullFor(l: number, baseC: number): number {
}
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 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): Numbers {
const minWidth = count * MINH_SEP;
const width =
MAXH_WIDTH < minWidth ? minWidth : randBetween(minWidth, MAXH_WIDTH);
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);
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();
}
function complementary(baseH: number, count: number): Numbers {
const angle = randBetween(180 - MAXH_COMPL, 180 + MAXH_COMPL);
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, 120 + MAXH_TRIAD);
const angle = randBetween(120 - MAXH_TRIAD/2, 120 + MAXH_TRIAD/2);
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 SchemeType = 'triad' | 'fin-belly' | 'fin-body';
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 {
let cols: Partial<Colours> = {};
const outer = baseOklch('dark');
let outerCols: OuterCols =
{ outer, spines: mkSpines(outer), vitiligo1: mkVitiligo(outer) };
cols.outer = baseOklch('dark'); // [todo]
cols.spines = mkSpines(cols.outer);
cols.vitiligo1 = mkVitiligo(cols.outer);
const stripes = mkStripes();
let sockCols: SockCols = { stripes, cuffs: mkCuffs(stripes) };
cols.stripes = mkStripes();
cols.cuffs = mkCuffs(cols.stripes);
// fins, belly
let finCols: FinCols, bellyCols: BellyCols, type: SchemeType;
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!);
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 {
// fins like outer
fins(analogous(cols.outer.h, 3)[2]!);
belly(complementary(cols.outer.h, 3)[2]!);
type = 'fin-body';
finCols = mkFins(analogous1(outer.h), outer);
bellyCols = mkBelly(complementary1(outer.h));
}
cols.eyes = {
l: baseLuma('light'),
c: randBetween(0.28, MAXC_LIGHT),
h: oneOf(analogous, complementary)(cols.outer.h, 3)[2]!
};
let miscCols = mkMisc(outerCols, finCols, bellyCols);
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 };
}
return merge(outerCols, sockCols, finCols, bellyCols, miscCols, type);
}
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 {
@ -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 {
return {
l: randBetween(0.8, MAXL),
@ -249,42 +185,102 @@ function mkStripes(): Oklch {
function mkCuffs(sock: Oklch): Oklch {
return {
l: sock.l * 0.7,
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) {
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))) {
(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() {
const reroll = document.getElementById('reroll')!;
const swap = document.getElementById('swap')!;
const pic = document.getElementById('pic')!;
reroll.addEventListener('click', doReroll);
swap.addEventListener('click', doSwap);
document.getElementById('reroll')?.addEventListener('click', doReroll);
document.getElementById('swap')?.addEventListener('click', doSwap);
doReroll();
setTimeout(setTransition);
function doReroll() { setColours(colours()); }
function doSwap() { pic.classList.toggle('back'); }
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');
document.documentElement.style.setProperty(
'--transition',
'background 0.4s ease-in-out, color 0.4s ease-in-out'
);
}
});
export {}
export { };

View file

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