import { Oklch, Oklab, Srgb as Rgb, AnyColor, normDeg } from './conv.js'; import * as Conv from './conv.js'; export { Oklch, Oklab, Rgb, AnyColor }; export type CSSFormat = 'oklch' | 'oklab' | 'rgb' | 'hex'; export type ChannelMapper = [A] | ((x: A) => A); export type ColorMapper = { type: C['type'] } & { [k in Exclude]?: ChannelMapper }; export function apply(f: ChannelMapper | 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: Rgb; constructor(c: AnyColor) { 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) { return `${normDeg(θ).toFixed(0)}deg`; } } with(maps: ColorMapper): Color; with(maps: ColorMapper): Color; with(maps: ColorMapper): Color { switch (maps.type) { case 'oklch': { const { l, c, h } = this.oklch; const { l: ll, c: cc, h: hh } = maps as ColorMapper; 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; 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;