const rand: () => number = Math.random; // [todo] 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)]!; } const max = Math.max; const min = Math.min; type LD = 'light' | 'dark'; namespace Oklch { export type Channel = 'l' | 'c' | 'h'; export type Channels = Record; export type ChannelMap = (x: number) => number; export type ChannelMaps = Record; } export class Oklch { static lightFor(baseL: number): number { return randBetween(baseL, MAXL); } static darkFor(baseL: number): number { return randBetween(MINL, baseL); } static isLight(l: number): boolean { return l >= MINL_LIGHT; } static brightFor(l: number, baseC: number): number { if (Oklch.isLight(l)) { return randBetween(baseC, MAXC_LIGHT); } else { return randBetween(baseC, MAXC_DARK); } } static dullFor(l: number, baseC: number): number { if (Oklch.isLight(l)) { return randBetween(baseC, MINC_LIGHT); } else { return randBetween(baseC, MINC_DARK); } } static analogous1(baseH: number): number { const size = randBetween(MINH_SEP, 2 * MINH_SEP); return rand() > 0.5 ? baseH + size : baseH - size; } static 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(); } static complementary1(baseH: number): number { return Oklch.analogous1((baseH + 180) % 360); } static complementary(baseH: number, count: number): number[] { const angle = randBetween(180 - MAXH_COMPL/2, 180 + MAXH_COMPL/2); return Oklch.analogous(baseH + angle, count); } static triad(baseH: number): [number, number] { const angle = randBetween(120 - MAXH_TRIAD/2, 120 + MAXH_TRIAD/2); return [baseH - angle, baseH + angle]; } readonly l: number; readonly c: number; readonly h: number; static 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); } } static baseChroma(l: number): number { if (l >= MINL_LIGHT) { return randBetween(MINC_LIGHT, MAXC_LIGHT); } else { return randBetween(MINC_DARK, MAXC_DARK); } } static baseHue(): number { return rand() * 360; } constructor(); constructor(ld: LD); constructor(cs: Oklch.Channels); constructor(l: number, c: number, h: number); constructor(lcsld?: number | Oklch.Channels | LD, cc?: number, hh?: number) { if (typeof lcsld == 'string' || lcsld == undefined) { this.l = Oklch.baseLuma(lcsld as LD | undefined); this.c = Oklch.baseChroma(this.l); this.h = Oklch.baseHue(); } else if (cc == undefined && hh == undefined) { const {l, c, h} = lcsld as Oklch.Channels; this.l = l; this.c = c; this.h = h; } else { this.l = lcsld as number; this.c = cc!; this.h = hh!; } } css(alpha: number = 1): string { return `oklch(${this.l} ${this.c} ${this.h} / ${alpha})`; } with(lch: Partial>): Oklch { function call(comp: undefined | number | Oklch.ChannelMap, x: number) { if (comp == undefined) { return x; } else if (typeof comp == 'function') { return comp(x); } else { return comp as number; } } return new Oklch({ l: call(lch.l, this.l), c: call(lch.c, this.c), h: call(lch.h, this.h), }); } } 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 colors. unless that would put them // too close together const MAXH_WIDTH = 80; // minimum distance between adjacent analogous colors const MINH_SEP = 5; // size of the wedge a "complementary" color can be in const MAXH_COMPL = 40; // size of the wedge a "triadic" color can be in const MAXH_TRIAD = 25; export type SchemeType = 'triad' | 'fin-belly' | 'fin-body'; export type OuterLayer = 'outer' | 'spines' | 'vitiligo1'; export type SockLayer = 'stripes' | 'cuffs'; export type FinLayer = 'fins1' | 'fins2' | 'fins3' | 'vitiligo4'; export type BellyLayer = 'belly1' | 'vitiligo3' | 'belly2' | 'vitiligo2'; export type MiscLayer = 'eyes' | 'masks' | 'claws' | 'lines'; export type Layer = OuterLayer | SockLayer | FinLayer | BellyLayer | MiscLayer; export type ColsOf = Record; export type OuterCols = ColsOf; export type SockCols = ColsOf; export type FinCols = ColsOf; export type BellyCols = ColsOf; export type MiscCols = ColsOf; export type Colors = ColsOf; export type Scheme = Colors & {type: SchemeType}; export const allLayers: Layer[] = ['outer', 'spines', 'stripes', 'cuffs', 'fins1', 'fins2', 'fins3', 'belly1', 'belly2', 'masks', 'claws', 'vitiligo1', 'vitiligo2', 'vitiligo3', 'vitiligo4', 'eyes', 'lines']; export function makeColorInfo(f: (l: Layer) => A): Record { return Object.fromEntries(allLayers.map(l => [l, f(l)])) as Record; } export function colors(): Scheme { const outer = new Oklch('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] = Oklch.triad(outer.h); finCols = mkFins(f, outer); bellyCols = mkBelly(b); } else if (whichBody > 1/3) { type = 'fin-belly'; const [f, b] = Oklch.complementary(outer.h, 2); finCols = mkFins(f!, outer); bellyCols = mkBelly(b!); } else { type = 'fin-body'; finCols = mkFins(Oklch.analogous1(outer.h), outer); bellyCols = mkBelly(Oklch.complementary1(outer.h)); } let miscCols = mkMisc(outerCols, finCols, bellyCols); return merge(outerCols, sockCols, finCols, bellyCols, miscCols, type); } function mkSpines(outer: Oklch): Oklch { return outer.with({ l: x => x * 0.8, c: x => x * 1.1, h: x => randBetween(x + 12, x - 12), }) } function mkVitiligo(outer: Oklch): Oklch { return outer.with({ l: x => randBetween(max(x, 0.94), 0.985), // exception to MAXL c: x => randBetween(min(x, 0.1), MINC_LIGHT), }); } function mkStripes(): Oklch { return new Oklch({ l: randBetween(0.8, MAXL), c: randBetween(MINC_LIGHT, MAXC_LIGHT), h: rand() * 360 }); } function mkCuffs(sock: Oklch): Oklch { return sock.with({ l: x => randBetween(x * 0.85, x * 0.65), c: x => randBetween(x, MAXC_LIGHT), h: x => randBetween(x + 8, x - 8), }); } function mkFins(h: number, outer: Oklch): FinCols { const [fin1Hue, fin2Hue, fin3Hue] = Oklch.analogous(h, 3); const [ll, cc] = oneOf( [Oklch.lightFor, Oklch.dullFor], [Oklch.darkFor, Oklch.brightFor]); const fins1 = new Oklch(ll(outer.l), cc(outer.l, outer.c), fin1Hue!); const fins2 = new Oklch(ll(fins1.l), cc(fins1.l, fins1.c), fin2Hue!); const fins3 = new Oklch(ll(fins2.l), cc(fins2.l, fins2.c), fin3Hue!); const vitiligo4 = mkVitiligo(fins1); return { fins1, fins2, fins3, vitiligo4 }; } function mkBelly(h: number): BellyCols { const [belly1Hue, belly2Hue] = Oklch.analogous(h, 2); const belly1 = new Oklch({ l: randBetween(0.7, MAXL), c: Oklch.baseChroma(1), h: belly1Hue! }); const belly2 = belly1.with({ l: x => min(MAXL, x * 1.1), c: x => x * 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 = new Oklch({ l: randBetween(0.8, MAXL), c: randBetween(0.01, 0.06), h: Oklch.analogous1(oneOf(o.outer, b.belly1, f.fins1).h) }); return { masks, eyes: new Oklch({ l: Oklch.baseLuma('light'), c: randBetween(0.28, MAXC_LIGHT), h: oneOf(Oklch.analogous1, Oklch.complementary1)(o.outer.h) }), claws: masks.with({ l: x => min(MAXL, x + randBetween(0, 0.1)), c: randBetween(0.01, 0.06), h: Oklch.analogous1, }), lines: new Oklch({ l: randBetween(0.01, 0.06), c: Oklch.baseChroma(0), h: Oklch.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): Scheme { return { outer, spines, vitiligo1, stripes, cuffs, fins1, fins2, fins3, vitiligo4, belly1, vitiligo3, belly2, vitiligo2, eyes, masks, claws, lines, type }; }