rainbow quox
This commit is contained in:
parent
6681d2eefb
commit
da06033eed
45 changed files with 646 additions and 4 deletions
290
rainbow-quox/colour.ts
Normal file
290
rainbow-quox/colour.ts
Normal 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 {}
|
Loading…
Add table
Add a link
Reference in a new issue