ciecam02-ts
v0.1.0
Published
TypeScript port of ciecam - An implementation of CIECAM02
Downloads
1,329
Maintainers
Readme
ciecam02-ts
This library provides tools to work with the CIECAM02 color space.
It is a TypeScript port of @baskerville/ciecam02 with slight modifications (removed dependency, cleaned up code) and TypeScript declarations.
This hybrid package provides both CommonJS and ES module.
Install
// If you use yarn
yarn add ciecam02-ts
// If you use npm
npm install ciecam02-ts
Usage
API
Converters
cam(viewingConditions?, correlates?) -> {
fromXyz(XYZ) -> CAM,
toXyz(CAM) -> XYZ,
fillOut(correlates, inputs) -> outputs
}
ucs(name?="UCS") -> {
fromCam(CAM) -> UCS,
toCam(UCS) -> CAM,
distance(UCS1, UCS2) -> number
}
hq: {
fromHue(h) -> H,
toHue(H) -> h,
fromNotation(N) -> H,
toNotation(H) -> N
}
Default viewing conditions
{
whitePoint: illuminant.D65,
adaptingLuminance: 40,
backgroundLuminance: 20,
surroundType: "average",
discounting: false
}
Gamut helpers
gamut(xyz, cam, epsilon?=1e-6) -> {
contains(CAM) -> (boolean, RGB),
limit(camIn, camOut, prec?=1e-3) -> CAM,
spine(t) -> CAM
}
Misc helpers
cfs(str) -> correlates,
lerp(CAM1, CAM2, t) -> CAM
Example
See also this source file.
// The reference for understanding CIECAM02 is:
// http://www.springer.com/cda/content/document/cda_downloaddocument/9781441961891-c1.pdf
import * as ciebase from "ciebase-ts";
import { Vector3D } from "ciebase-ts";
import * as ciecam02 from "../lib";
import { IJchProps, IViewingConditions } from "../lib";
const { illuminant, rgb, workspace } = ciebase;
const { cfs, lerp, hq } = ciecam02;
const { min, max } = Math;
//
// Shared settings for the examples
//
const xyz = ciebase.xyz(workspace.sRGB, illuminant.D65);
const viewingConditions: IViewingConditions = {
adaptingLuminance: 40,
backgroundLuminance: 20,
discounting: false,
surroundType: "average",
whitePoint: illuminant.D65,
};
// By default, 7 correlates are returned when converting from XYZ to CAM.
// For the purpose of this example, we will limit ourselves to the JCh correlates.
// (J is the lightness, C the chroma and h the hue.)
const cam = ciecam02.cam(viewingConditions, cfs("JCh"));
const gamut = ciecam02.gamut(xyz, cam);
const ucs = ciecam02.ucs();
function hexToCam(hex: string) {
return cam.fromXyz(xyz.fromRgb(rgb.fromHex(hex)));
}
function camToHex(CAM: IJchProps) {
return rgb.toHex(xyz.toRgb(cam.toXyz(CAM)));
}
function crop(v: number) {
return max(0, min(1, v));
}
//
// Example 1
//
const example1 = () => {
const camSand = hexToCam("e0cda9");
// {J: 77.82, C: 16.99, h: 81.01}
const camOrange = { ...camSand, C: 90 };
// {J: 77.82, C: 90.00, h: 81.01}
const [isInside, rgbOrange] = gamut.contains(camOrange);
// [false, [1.09, 0.73, -0.7]]
if (!isInside) {
// The gamut.limit function interpolates between an inside and an outside point
// and return an inside point as close as possible to the boundary.
// (The gamut is the set of CAM values that maps to valid RGB coordinates.)
const camOrange1 = gamut.limit(camSand, camOrange);
// {J: 77.82, C: 55.23, h: 81.01}
// The alternative method is to simply crop the RGB coordinates
const camOrange2 = cam.fromXyz(xyz.fromRgb(rgbOrange.map(crop) as Vector3D));
// {J: 74.43, C: 67.60, h: 81.30}
// tslint:disable-next-line:no-console
console.log([camOrange1, camOrange2].map(camToHex)); // #ffc447 #ffb900
} else {
// tslint:disable-next-line:no-console
console.log(rgb.toHex(rgbOrange));
}
};
//
// Example 2
//
const example2 = () => {
function gradient(camStart: IJchProps, camEnd: IJchProps, steps = 3) {
const result = [];
for (let ε = 1 / (steps + 1), t = 0; steps > -2; t += ε, steps -= 1) {
const camBetween = lerp(camStart as any, camEnd as any, crop(t));
const hex = rgb.toHex(xyz.toRgb(cam.toXyz(camBetween as any) as Vector3D).map(crop) as Vector3D);
result.push(hex);
}
return result;
}
const hexCodes = gradient(
// camStart
hexToCam("17657d"),
// camEnd
hexToCam("fee7f0"),
// steps
8,
);
// tslint:disable-next-line:no-console
console.log(hexCodes);
};
//
// Example 3
//
const objectMap = <T, R>(
obj: { [key: string]: T },
fn: (key: string, value: T, index: number, o: { [key: string]: T }) => R,
) => {
const r: { [key: string]: R } = {};
Object.keys(obj).forEach((key, index) => {
r[key] = fn(key, obj[key], index, obj);
});
return r;
};
const example3 = () => {
function ucsLimit(camIn: IJchProps, camOut: IJchProps, prec = 1e-3) {
// UCS is based on the JMh correlates
let [ucsIn, ucsOut] = [camIn, camOut].map((v) => ucs.fromCam(cam.fillOut(cfs("JMh"), v)));
while (ucs.distance(ucsIn, ucsOut) > prec) {
const ucsMid = lerp(ucsIn as any, ucsOut as any, 0.5);
const [isInside] = gamut.contains(ucs.toCam(ucsMid as any) as any);
if (isInside) {
ucsIn = ucsMid as any;
} else {
ucsOut = ucsMid as any;
}
}
return cam.fillOut(objectMap(camIn as any, () => true), ucs.toCam(ucsIn as any) as any);
}
// The hue notation is a different writting of the hue quadrant,
// of the form a(p?b)? where a and b are in {R, Y, G, B} (a ≠ b)
// and p is in ]0, 100[. apb = b(100-p)a, ab = a50b.
function hue(N: string) {
return hq.toHue(hq.fromNotation(N));
}
const topChroma = max(...["f00", "0f0", "00f"].map((v) => hexToCam(v).C));
const camRed: IJchProps = { J: 60, C: topChroma + 1, h: hue("R") };
const camYellow: IJchProps = { J: 90, C: topChroma + 1, h: hue("Y") };
const camGreen: IJchProps = { J: 90, C: topChroma + 1, h: hue("G") };
const camBlue: IJchProps = { J: 70, C: topChroma + 1, h: hue("B") };
const hexCodes = [camRed, camYellow, camGreen, camBlue].map((CAM) => {
CAM = ucsLimit(gamut.spine(CAM.J / 100) as any, CAM);
return camToHex(CAM);
});
// tslint:disable-next-line:no-console
console.log(hexCodes);
};
//
// Main
//
const main = () => {
// tslint:disable-next-line:no-console
console.log("Example 1");
example1();
// tslint:disable-next-line:no-console
console.log("Example 2");
example2();
// tslint:disable-next-line:no-console
console.log("Example 3");
example3();
// tslint:disable-next-line:no-console
console.log("Done");
};
main();
License: MIT
See the LICENSE file.