Skip to content

Commit

Permalink
feat(color): split Lab & XYZ types into D50/D65
Browse files Browse the repository at this point in the history
- add LabD50/D65, add XYZD50/XYZD65
- rename factory fns (labD50/D65, xyzD50/D65)
- update/simplify ColorSpec in all color type defs
- add tools/limits.ts to sample RGB gamut limits, add to color defs
- add TypedColor.clamp(), update defColor()
- fix Oklab<>XYZ conversions (use D65 as default)
  • Loading branch information
postspectacular committed Jan 30, 2021
1 parent a7315c0 commit 29e1e74
Show file tree
Hide file tree
Showing 24 changed files with 313 additions and 128 deletions.
24 changes: 16 additions & 8 deletions packages/color/src/api.ts
Expand Up @@ -19,13 +19,15 @@ export type ColorMode =
| "hsi"
| "hsl"
| "hsv"
| "lab"
| "lab50"
| "lab65"
| "lch"
| "oklab"
| "rgb"
| "srgb"
| "xyy"
| "xyz"
| "xyz50"
| "xyz65"
| "ycc";

/**
Expand Down Expand Up @@ -55,17 +57,15 @@ export interface ChannelSpec {
* @defaultValue [0,1]
*/
range?: Range;
/**
* @defaultValue 0
*/
default?: number;
}

export interface ColorSpec<M extends ColorMode, K extends string> {
mode: M;
channels: Partial<Record<K, ChannelSpec>>;
channels?: Partial<Record<K, ChannelSpec>>;
order: readonly K[];
from: Partial<Record<ColorMode, ColorOp>> & { rgb: ColorOp };
from: Partial<Record<ColorMode, ColorOp | [ColorOp, ColorOp]>> & {
rgb: ColorOp;
};
}

export interface ColorFactory<T extends TypedColor<any>> {
Expand Down Expand Up @@ -132,6 +132,14 @@ export interface TypedColor<T> extends IColor, IDeref<Color>, IVector<T> {
* @param rnd
*/
randomize(rnd?: IRandom): this;
/**
* Clamps all color channels so that colors is inside RGB gamut.
*
* @remarks
* Note: This is not a 100% guarantee, due to each channel being clamped
* individually based on pre-determined limits.
*/
clamp(): this;
/**
* Copies `src` into this color's array.
*
Expand Down
14 changes: 9 additions & 5 deletions packages/color/src/color.ts
Expand Up @@ -3,34 +3,38 @@ import type {
Color,
ColorFactory,
ColorMode,
TypedColor,
ParsedColor,
TypedColor,
} from "./api";
import { hcy } from "./hcy/hcy";
import { hsi } from "./hsi/hsi";
import { hsl } from "./hsl/hsl";
import { hsv } from "./hsv/hsv";
import { lab } from "./lab/lab";
import { labD50 } from "./lab/lab50";
import { labD65 } from "./lab/lab65";
import { lch } from "./lch/lch";
import { oklab } from "./oklab/oklab";
import { rgb } from "./rgb/rgb";
import { srgb } from "./srgb/srgb";
import { xyy } from "./xyy/xyy";
import { xyz } from "./xyz/xyz";
import { xyzD50 } from "./xyz/xyz50";
import { xyzD65 } from "./xyz/xyz65";
import { ycc } from "./ycc/ycc";

const FACTORIES: Record<ColorMode, ColorFactory<any>> = {
hcy,
hsi,
hsl: hsl,
hsv: hsv,
lab,
lab50: labD50,
lab65: labD65,
lch,
oklab,
rgb,
srgb,
xyy,
xyz,
xyz50: xyzD50,
xyz65: xyzD65,
ycc,
};

Expand Down
37 changes: 35 additions & 2 deletions packages/color/src/convert.ts
@@ -1,11 +1,44 @@
import { assert } from "@thi.ng/api";
import { isArray } from "@thi.ng/checks";
import { unsupported } from "@thi.ng/errors";
import type { Color, ColorMode, ColorSpec, ReadonlyColor } from "./api";
import type {
Color,
ColorMode,
ColorOp,
ColorSpec,
ReadonlyColor,
} from "./api";

export const CONVERSIONS: Partial<
Record<ColorMode, ColorSpec<any, any>["from"]>
Record<
ColorMode,
Partial<Record<ColorMode, ColorOp>> & {
rgb: ColorOp | [ColorOp, ColorOp];
}
>
> = {};

/**
* Registers conversions for given {@link ColorSpec}. Called by
* {@link defColor}.
*
* @param spec
*
* @internal
*/
export const defConversions = <M extends ColorMode, K extends string>(
spec: ColorSpec<M, K>
) => {
for (let id in spec.from) {
const val = spec.from[<ColorMode>id];
if (isArray(val)) {
const [a, b] = val;
spec.from[<ColorMode>id] = (out, src) => b(out, a(out, src));
}
}
CONVERSIONS[spec.mode] = spec.from;
};

export const convert = <T extends Color>(
res: T | null,
src: ReadonlyColor,
Expand Down
3 changes: 2 additions & 1 deletion packages/color/src/css/css.ts
Expand Up @@ -13,7 +13,8 @@ import { srgbCss } from "../srgb/srgb-css";
const CSS_CONVERSIONS: Partial<Record<ColorMode, Fn<any, string>>> = {
hsl: hslCss,
hsv: hsvCss,
lab: labCss,
lab50: labCss,
lab65: labCss,
lch: lchCss,
rgb: rgbCss,
srgb: srgbCss,
Expand Down
1 change: 1 addition & 0 deletions packages/color/src/css/parse-css.ts
Expand Up @@ -72,6 +72,7 @@ export const parseCss = (src: string | IDeref<string>): IParsedColor => {
parseAlpha(d),
]);
case "lab":
return new ParsedColor("lab50", [
parsePercent(a, false),
parseNumber(b) * 0.01,
parseNumber(c) * 0.01,
Expand Down
46 changes: 27 additions & 19 deletions packages/color/src/defcolor.ts
Expand Up @@ -9,6 +9,7 @@ import { illegalArgs } from "@thi.ng/errors";
import { EPS } from "@thi.ng/math";
import type { IRandom } from "@thi.ng/random";
import {
clamp,
declareIndices,
eqDelta,
mapStridedBuffer,
Expand All @@ -27,7 +28,7 @@ import type {
ReadonlyColor,
TypedColor,
} from "./api";
import { CONVERSIONS, convert } from "./convert";
import { convert, defConversions } from "./convert";
import { parseCss } from "./css/parse-css";
import { int32Rgb } from "./int/int-rgba";
import { ensureArgs } from "./internal/ensure-args";
Expand All @@ -41,22 +42,25 @@ type $DefColor<M extends ColorMode, K extends string> = {
toJSON(): number[];
} & TypedColor<$DefColor<M, K>>;

const prepareSpec = (id: string, spec?: ChannelSpec): ChannelSpec => ({
...(id === "alpha" ? { range: [1, 1], default: 1 } : { range: [0, 1] }),
...spec,
});

export const defColor = <M extends ColorMode, K extends string>(
spec: ColorSpec<M, K>
) => {
const channels = spec.order;
const numChannels = channels.length;
channels.reduce(
(acc, id) => ((acc[id] = prepareSpec(id, spec.channels[id])), acc),
spec.channels
);
const min = channels.map((id) => spec.channels[id]!.range![0]);
const max = channels.map((id) => spec.channels[id]!.range![1]);
const channels: Partial<Record<K, ChannelSpec>> = spec.channels || {};
const order = spec.order;
const numChannels = order.length;
order.reduce((acc, id) => {
acc[id] = {
range: [0, 1],
...channels[id],
};
return acc;
}, channels);
const min = order.map((id) => channels[id]!.range![0]);
const max = order.map((id) => channels[id]!.range![1]);
// fix alpha channel for randomize()
const minR = set([], min);
const maxR = set([], max);
minR[numChannels - 1] = 1;

const $clazz = class implements TypedColor<$DefColor<any, any>> {
buf: Color;
Expand Down Expand Up @@ -104,8 +108,11 @@ export const defColor = <M extends ColorMode, K extends string>(
}

set(src: ReadonlyColor) {
set(this, src);
return this;
return <this>set(this, src);
}

clamp() {
return <this>clamp(null, this, min, max);
}

eqDelta(o: $DefColor<any, any>, eps = EPS): boolean {
Expand All @@ -117,11 +124,12 @@ export const defColor = <M extends ColorMode, K extends string>(
}

randomize(rnd?: IRandom): this {
return <any>randMinMax(this, min, max, rnd);
return <any>randMinMax(this, minR, maxR, rnd);
}
};
declareIndices($clazz.prototype, <any[]>channels);
CONVERSIONS[spec.mode] = spec.from;

declareIndices($clazz.prototype, <any[]>order);
defConversions(spec);

const fromColor = (src: ReadonlyColor, mode: ColorMode, xs: any[]): any => {
const res = new $clazz(...xs);
Expand Down
7 changes: 1 addition & 6 deletions packages/color/src/hcy/hcy.ts
Expand Up @@ -16,6 +16,7 @@ export declare class HCY implements TypedColor<HCY> {
readonly mode: "hcy";
readonly length: 4;
[Symbol.iterator](): Iterator<number, any, undefined>;
clamp(): this;
copy(): HCY;
copyView(): HCY;
deref(): Color;
Expand All @@ -28,12 +29,6 @@ export declare class HCY implements TypedColor<HCY> {

export const hcy = <ColorFactory<HCY>>defColor({
mode: "hcy",
channels: {
// h: {},
// c: {},
// y: {},
// alpha: { default: 1 },
},
order: <const>["h", "c", "y", "alpha"],
from: { rgb: rgbHcy },
});
7 changes: 1 addition & 6 deletions packages/color/src/hsi/hsi.ts
Expand Up @@ -16,6 +16,7 @@ export declare class HSI implements TypedColor<HSI> {
readonly mode: "hsi";
readonly length: 4;
[Symbol.iterator](): Iterator<number, any, undefined>;
clamp(): this;
copy(): HSI;
copyView(): HSI;
deref(): Color;
Expand All @@ -28,12 +29,6 @@ export declare class HSI implements TypedColor<HSI> {

export const hsi = <ColorFactory<HSI>>defColor({
mode: "hsi",
channels: {
// h: {},
// s: {},
// i: {},
// alpha: { default: 1 },
},
order: <const>["h", "s", "i", "alpha"],
from: { rgb: rgbHsi },
});
7 changes: 1 addition & 6 deletions packages/color/src/hsl/hsl.ts
Expand Up @@ -48,6 +48,7 @@ export declare class HSL implements TypedColor<HSL> {
readonly mode: "hsl";
readonly length: 4;
[Symbol.iterator](): Iterator<number, any, undefined>;
clamp(): this;
copy(): HSL;
copyView(): HSL;
deref(): Color;
Expand All @@ -60,12 +61,6 @@ export declare class HSL implements TypedColor<HSL> {

export const hsl = <ColorFactory<HSL>>defColor({
mode: "hsl",
channels: {
// h: {},
// s: {},
// l: {},
// alpha: { default: 1 },
},
order: <const>["h", "s", "l", "alpha"],
from: { rgb: rgbHsl, hsv: hsvHsl },
});
7 changes: 1 addition & 6 deletions packages/color/src/hsv/hsv.ts
Expand Up @@ -48,6 +48,7 @@ export declare class HSV implements TypedColor<HSV> {
readonly mode: "hsv";
readonly length: 4;
[Symbol.iterator](): Iterator<number, any, undefined>;
clamp(): this;
copy(): HSV;
copyView(): HSV;
deref(): Color;
Expand All @@ -60,12 +61,6 @@ export declare class HSV implements TypedColor<HSV> {

export const hsv = <ColorFactory<HSV>>defColor({
mode: "hsv",
channels: {
// h: {},
// s: {},
// v: {},
// alpha: { default: 1 },
},
order: <const>["h", "s", "v", "alpha"],
from: { rgb: rgbHsv, hsl: hslHsv },
});
6 changes: 4 additions & 2 deletions packages/color/src/index.ts
Expand Up @@ -34,7 +34,8 @@ export * from "./lab/lab-css";
export * from "./lab/lab-lch";
export * from "./lab/lab-rgb";
export * from "./lab/lab-xyz";
export * from "./lab/lab";
export * from "./lab/lab50";
export * from "./lab/lab65";

export * from "./lch/lch-css";
export * from "./lch/lch";
Expand Down Expand Up @@ -90,7 +91,8 @@ export * from "./xyz/xyz-oklab";
export * from "./xyz/xyz-rgb";
export * from "./xyz/xyz-xyy";
export * from "./xyz/xyz-xyz";
export * from "./xyz/xyz";
export * from "./xyz/xyz50";
export * from "./xyz/xyz65";

export * from "./ycc/ycc-rgb";
export * from "./ycc/ycc";

0 comments on commit 29e1e74

Please sign in to comment.