136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
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;
|