yummy.cricket/rainbow-quox/script/color/def.ts

136 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Oklch, Oklab, Srgb, ColorType, ColorData } from './conv.js';
import * as Conv from './conv.js';
export { Oklch, Oklab, Srgb, ColorType, ColorData };
export type CSSFormat = 'oklch' | 'oklab' | 'rgb' | 'hex';
export type ChannelMapper<A> = [A] | ((x: A) => A);
export type ColorMapper<C extends { type: string }> =
{ type: C['type'] } &
{ [k in Exclude<keyof C, 'type'>]?: ChannelMapper<C[k]> };
export function apply<A>(f: ChannelMapper<A> | undefined, x: A): A {
if (typeof f == 'undefined') return x;
else if (typeof f == 'function') return f(x);
else return f[0];
}
export class Color {
readonly oklch: Oklch;
readonly oklab: Oklab;
readonly srgb: Srgb;
constructor(c: ColorData) {
this.oklch = Conv.toOklch(c);
this.oklab = Conv.toOklab(c);
this.srgb = Conv.toSrgb(c);
}
get luma() { return this.oklch.l; }
get chroma() { return this.oklch.c; }
get hue() { return this.oklch.h; }
get labA() { return this.oklab.a; }
get labB() { return this.oklab.b; }
get red() { return Math.floor(255 * this.srgb.r); }
get green() { return Math.floor(255 * this.srgb.g); }
get blue() { return Math.floor(255 * this.srgb.b); }
css(format: CSSFormat = 'hex', α = 1): string {
switch (format) {
case 'oklch': {
const { l, c, h } = this.oklch;
return `oklch(${pc(l)} ${pc(c, 0.4)} ${deg(h)} / ${pc(α)})`;
}
case 'oklab': {
const { l, a, b } = this.oklab;
return `oklab(${pc(l)} ${pc(a)} ${pc(b)} / ${pc(α)})`;
}
case 'rgb': {
const { r, g, b } = this.srgb;
return `rgb(${pc(r)} ${pc(g)} ${pc(b)} / ${pc(α)})`;
}
case 'hex': {
const { r, g, b } = this.srgb;
return α == 1 ? `#${hex(r)}${hex(g)}${hex(b)}` :
`#${hex(r)}${hex(g)}${hex(b)}${hex(α)}`;
}
}
function hex(c: number) {
const d = Math.min(255, Math.max(0, Math.floor(255 * c)));
const str = d.toString(16);
return str.length == 1 ? `0${str}` : str;
}
function pc(c: number, max = 1) {
c *= 100 / max;
return `${c.toFixed(0)}%`;
}
function deg(θ: number) {
θ %= 360; if (θ < 0) θ += 360;
return `${θ.toFixed(0)}deg`;
}
}
with(maps: ColorMapper<Oklch>): Color;
with(maps: ColorMapper<Srgb>): Color;
with(maps: ColorMapper<Oklch | Srgb>): Color {
switch (maps.type) {
case 'oklch': {
const { l, c, h } = this.oklch;
const { l: ll, c: cc, h: hh } = maps as ColorMapper<Oklch>;
return oklch(apply(ll, l), apply(cc, c), apply(hh, h));
}
case 'srgb': {
const { r, g, b } = this.srgb;
const { r: rr, g: gg, b: bb } = maps as ColorMapper<Srgb>;
return rgb(apply(rr, r), apply(gg, g), apply(bb, b));
}
}
}
static validate(x: unknown): Color | undefined {
if (typeof x == 'object' && x != null) {
if ('l' in x && 'c' in x && 'h' in x) {
const { l, c, h } = x;
if (num(l) && num(c) && num(h)) return oklch(l, c, h);
} else if ('l' in x && 'a' in x && 'b' in x) {
const { l, a, b } = x;
if (num(l) && num(a) && num(b)) return oklab(l, a, b);
} else if ('r' in x && 'g' in x && 'b' in x) {
const { r, g, b } = x;
if (num(r) && num(g) && num(b)) return rgb(r, g, b);
}
}
function num(x: unknown): x is number { return typeof x == 'number'; }
}
toJSON(): unknown { const { l, c, h } = this.oklch; return { l, c, h }; }
}
export function oklch(l: number, c: number, h: number): Color {
return new Color({ type: 'oklch', l, c, h });
}
export function oklab(l: number, a: number, b: number): Color {
return new Color({ type: 'oklab', l, a, b });
}
export function rgb(r: number, g: number, b: number,
style: 'int' | 'float' = 'int'): Color {
return style == 'int' ?
new Color({ type: 'srgb', r: r / 255, g: g / 255, b: b / 255 }) :
new Color({ type: 'srgb', r, g, b });
}
export type Luma = number;
export type Chroma = number;
export type Hue = number;
export type Alpha = number;
export type HueDistance = number;