rainbow quox canvas stuff
This commit is contained in:
parent
d16ea49d62
commit
d0099fbf19
80 changed files with 497 additions and 415 deletions
|
@ -1,16 +1,131 @@
|
|||
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<A>(...xs: A[]): A {
|
||||
return xs[Math.floor(rand() * xs.length)]!;
|
||||
}
|
||||
|
||||
const max = Math.max;
|
||||
const min = Math.min;
|
||||
|
||||
export type Oklch = { l: number, c: number, h: number };
|
||||
|
||||
export function oklch(col: Oklch, alpha: number = 1): string {
|
||||
return `oklch(${col.l} ${col.c} ${col.h} / ${alpha})`;
|
||||
}
|
||||
|
||||
type LD = 'light' | 'dark';
|
||||
|
||||
namespace Oklch {
|
||||
export type Channel = 'l' | 'c' | 'h';
|
||||
export type Channels = Record<Channel, number>;
|
||||
export type ChannelMap = (x: number) => number;
|
||||
export type ChannelMaps = Record<Channel, ChannelMap>;
|
||||
}
|
||||
|
||||
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<Record<Oklch.Channel, number | Oklch.ChannelMap>>): 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;
|
||||
|
@ -34,89 +149,6 @@ const MAXH_COMPL = 40;
|
|||
// size of the wedge a "triadic" color can be in
|
||||
const MAXH_TRIAD = 25;
|
||||
|
||||
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 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 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); }
|
||||
}
|
||||
|
||||
function dullFor(l: number, baseC: number): number {
|
||||
if (isLight(l)) { return randBetween(baseC, MINC_LIGHT); }
|
||||
else { return randBetween(baseC, MINC_DARK); }
|
||||
}
|
||||
|
||||
|
||||
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): 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();
|
||||
}
|
||||
|
||||
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/2, 120 + MAXH_TRIAD/2);
|
||||
return [baseH - angle, baseH + angle];
|
||||
}
|
||||
|
||||
export type SchemeType = 'triad' | 'fin-belly' | 'fin-body';
|
||||
|
||||
export type OuterLayer = 'outer' | 'spines' | 'vitiligo1';
|
||||
|
@ -148,7 +180,7 @@ export function makeColorInfo<A>(f: (l: Layer) => A): Record<Layer, A> {
|
|||
|
||||
|
||||
export function colors(): Scheme {
|
||||
const outer = baseOklch('dark');
|
||||
const outer = new Oklch('dark');
|
||||
let outerCols: OuterCols =
|
||||
{ outer, spines: mkSpines(outer), vitiligo1: mkVitiligo(outer) };
|
||||
|
||||
|
@ -159,16 +191,16 @@ export function colors(): Scheme {
|
|||
const whichBody = rand();
|
||||
if (whichBody > 2/3) {
|
||||
type = 'triad';
|
||||
const [f, b] = triad(outer.h);
|
||||
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] = complementary(outer.h, 2);
|
||||
const [f, b] = Oklch.complementary(outer.h, 2);
|
||||
finCols = mkFins(f!, outer); bellyCols = mkBelly(b!);
|
||||
} else {
|
||||
type = 'fin-body';
|
||||
finCols = mkFins(analogous1(outer.h), outer);
|
||||
bellyCols = mkBelly(complementary1(outer.h));
|
||||
finCols = mkFins(Oklch.analogous1(outer.h), outer);
|
||||
bellyCols = mkBelly(Oklch.complementary1(outer.h));
|
||||
}
|
||||
|
||||
let miscCols = mkMisc(outerCols, finCols, bellyCols);
|
||||
|
@ -178,80 +210,88 @@ export function colors(): Scheme {
|
|||
|
||||
|
||||
function mkSpines(outer: Oklch): Oklch {
|
||||
return {
|
||||
l: outer.l * 0.8, c: outer.c * 1.1,
|
||||
h: randBetween(outer.h + 12, outer.h - 12)
|
||||
};
|
||||
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 {
|
||||
l: randBetween(max(outer.l, 0.94), 0.985), // exception to MAXL
|
||||
c: randBetween(min(outer.c, 0.1), MINC_LIGHT),
|
||||
h: outer.h
|
||||
};
|
||||
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 {
|
||||
return new Oklch({
|
||||
l: randBetween(0.8, MAXL),
|
||||
c: randBetween(MINC_LIGHT, MAXC_LIGHT),
|
||||
h: rand() * 360
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function mkCuffs(sock: Oklch): Oklch {
|
||||
return {
|
||||
l: randBetween(sock.l * 0.85, sock.l * 0.65),
|
||||
c: randBetween(sock.c, MAXC_LIGHT),
|
||||
h: randBetween(sock.h + 8, sock.h - 8)
|
||||
};
|
||||
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] = 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 [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] = 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 [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 = {
|
||||
const masks = new Oklch({
|
||||
l: randBetween(0.8, MAXL),
|
||||
c: randBetween(0.01, 0.06),
|
||||
h: analogous1(oneOf(o.outer, b.belly1, f.fins1).h)
|
||||
};
|
||||
h: Oklch.analogous1(oneOf(o.outer, b.belly1, f.fins1).h)
|
||||
});
|
||||
return {
|
||||
masks,
|
||||
eyes: {
|
||||
l: baseLuma('light'),
|
||||
eyes: new Oklch({
|
||||
l: Oklch.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)),
|
||||
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: analogous1(masks.h)
|
||||
},
|
||||
lines: {
|
||||
h: Oklch.analogous1,
|
||||
}),
|
||||
lines: new Oklch({
|
||||
l: randBetween(0.01, 0.06),
|
||||
c: baseChroma(0),
|
||||
h: analogous1(o.outer.h)
|
||||
}
|
||||
c: Oklch.baseChroma(0),
|
||||
h: Oklch.analogous1(o.outer.h)
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue