Skip to content

Commit

Permalink
Add support for ICtCp color space
Browse files Browse the repository at this point in the history
  • Loading branch information
devgru committed Jan 6, 2024
1 parent d58dd0a commit 88c4ec0
Show file tree
Hide file tree
Showing 13 changed files with 248 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1547,6 +1547,7 @@ Mode | Color space | Definition object
`hsl` | HSL color space | `modeHsl`
`hsv` | HSV color space | `modeHsv`
`hwb` | HWB color space | `modeHwb`
`itp` | IC<sub>t</sub>C<sub>p</sub> color space | `modeItp`
`jab` | J<sub>z</sub>a<sub>z</sub>b<sub>z</sub> color space | `modeJab`
`jch` | J<sub>z</sub>a<sub>z</sub>b<sub>z</sub> in cylindrical form | `modeJch`
`lab` | CIELAB color space (D50 Illuminant) | `modeLab`
Expand Down
16 changes: 15 additions & 1 deletion docs/color-spaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,21 @@ It has the default _Chroma from Luma_ adjustment applied (effectively Y is subtr
| ------- | ------------- | ----------- |
| `x` | `[-0.0154, 0.0281]`| Cyan-red component |
| `y` | `[0, 0.8453]`| Luma |
| `b` | `[ -0.2778, 0.3880 ]`| Blue-yellow component |
| `b` | `[ -0.2778, 0.3880 ]`| Blue-yellow component |

Does not have gamut limits.

#### `itp`

IC<sub>t</sub>C<sub>p</sub> (or ITP) color space, as defined in ITU-R Recommendation BT.2100.

| Channel | Range | Description |
|---------|-------------------|-----------------------|
| `i` | `[0, 0.581]`| Intensity |
| `t` | `[-0.282, 0.278]`| Blue-yellow component |
| `p` | `[-0.162, 0.279]`| Green–red component |

Serialized as `color(--ictcp i t p)`, with the `none` keyword for any missing color channel. An explicit `alpha < 1` is included as ` / alpha`.

Does not have gamut limits.

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/tree-shaking.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ interpolate(['red', 'green'], 'lch');

Bootstrap all the color spaces available in Culori.

It provides the following named exports: `a98`, `cubehelix`, `dlab`, `dlch`, `hsi`, `hsl`, `hsv`, `hwb`, `jab`, `jch`, `lab`, `lab65`, `lch`, `lch65`, `lchuv`, `lrgb`, `luv`, `okhsl`, `okhsv`, `oklab`, `oklch`, `p3`, `prophoto`, `rec2020`, `rgb`, `xyb`, `xyz50`, `xyz65`, and `yiq`.
It provides the following named exports: `a98`, `cubehelix`, `dlab`, `dlch`, `hsi`, `hsl`, `hsv`, `hwb`, `itp`, `jab`, `jch`, `lab`, `lab65`, `lch`, `lch65`, `lchuv`, `lrgb`, `luv`, `okhsl`, `okhsv`, `oklab`, `oklch`, `p3`, `prophoto`, `rec2020`, `rgb`, `xyb`, `xyz50`, `xyz65`, and `yiq`.

```js
import 'culori/all';
Expand Down
2 changes: 2 additions & 0 deletions src/bootstrap/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import modeHsi from '../hsi/definition.js';
import modeHsl from '../hsl/definition.js';
import modeHsv from '../hsv/definition.js';
import modeHwb from '../hwb/definition.js';
import modeItp from '../itp/definition.js';
import modeJab from '../jab/definition.js';
import modeJch from '../jch/definition.js';
import modeLab from '../lab/definition.js';
Expand Down Expand Up @@ -38,6 +39,7 @@ export const hsi = useMode(modeHsi);
export const hsl = useMode(modeHsl);
export const hsv = useMode(modeHsv);
export const hwb = useMode(modeHwb);
export const itp = useMode(modeItp);
export const jab = useMode(modeJab);
export const jch = useMode(modeJch);
export const lab = useMode(modeLab);
Expand Down
3 changes: 3 additions & 0 deletions src/index-fn.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { default as modeHsi } from './hsi/definition.js';
export { default as modeHsl } from './hsl/definition.js';
export { default as modeHsv } from './hsv/definition.js';
export { default as modeHwb } from './hwb/definition.js';
export { default as modeItp } from './itp/definition.js';
export { default as modeJab } from './jab/definition.js';
export { default as modeJch } from './jch/definition.js';
export { default as modeLab } from './lab/definition.js';
Expand Down Expand Up @@ -171,6 +172,7 @@ export { default as convertHsiToRgb } from './hsi/convertHsiToRgb.js';
export { default as convertHslToRgb } from './hsl/convertHslToRgb.js';
export { default as convertHsvToRgb } from './hsv/convertHsvToRgb.js';
export { default as convertHwbToRgb } from './hwb/convertHwbToRgb.js';
export { default as convertItpToXyz65 } from './itp/convertItpToXyz65.js';
export { default as convertJabToJch } from './jch/convertJabToJch.js';
export { default as convertJabToRgb } from './jab/convertJabToRgb.js';
export { default as convertJabToXyz65 } from './jab/convertJabToXyz65.js';
Expand Down Expand Up @@ -212,6 +214,7 @@ export { default as convertRgbToYiq } from './yiq/convertRgbToYiq.js';
export { default as convertRgbToXyb } from './xyb/convertRgbToXyb.js';
export { default as convertXybToRgb } from './xyb/convertXybToRgb.js';
export { default as convertXyz65ToA98 } from './a98/convertXyz65ToA98.js';
export { default as convertXyz65ToItp } from './itp/convertXyz65ToItp.js';
export { default as convertXyz65ToJab } from './jab/convertXyz65ToJab.js';
export { default as convertXyz65ToLab65 } from './lab65/convertXyz65ToLab65.js';
export { default as convertXyz65ToP3 } from './p3/convertXyz65ToP3.js';
Expand Down
5 changes: 5 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import modeHsi from './hsi/definition.js';
import modeHsl from './hsl/definition.js';
import modeHsv from './hsv/definition.js';
import modeHwb from './hwb/definition.js';
import modeItp from './itp/definition.js';
import modeJab from './jab/definition.js';
import modeJch from './jch/definition.js';
import modeLab from './lab/definition.js';
Expand Down Expand Up @@ -172,6 +173,7 @@ export { default as convertHsiToRgb } from './hsi/convertHsiToRgb.js';
export { default as convertHslToRgb } from './hsl/convertHslToRgb.js';
export { default as convertHsvToRgb } from './hsv/convertHsvToRgb.js';
export { default as convertHwbToRgb } from './hwb/convertHwbToRgb.js';
export { default as convertItpToXyz65 } from './itp/convertItpToXyz65.js';
export { default as convertJabToJch } from './jch/convertJabToJch.js';
export { default as convertJabToRgb } from './jab/convertJabToRgb.js';
export { default as convertJabToXyz65 } from './jab/convertJabToXyz65.js';
Expand Down Expand Up @@ -218,6 +220,7 @@ export { default as convertXyz50ToProphoto } from './prophoto/convertXyz50ToProp
export { default as convertXyz50ToRgb } from './xyz50/convertXyz50ToRgb.js';
export { default as convertXyz50ToXyz65 } from './xyz65/convertXyz50ToXyz65.js';
export { default as convertXyz65ToA98 } from './a98/convertXyz65ToA98.js';
export { default as convertXyz65ToItp } from './itp/convertXyz65ToItp.js';
export { default as convertXyz65ToJab } from './jab/convertXyz65ToJab.js';
export { default as convertXyz65ToLab65 } from './lab65/convertXyz65ToLab65.js';
export { default as convertXyz65ToP3 } from './p3/convertXyz65ToP3.js';
Expand All @@ -235,6 +238,7 @@ export {
modeHsl,
modeHsv,
modeHwb,
modeItp,
modeJab,
modeJch,
modeLab,
Expand Down Expand Up @@ -266,6 +270,7 @@ export const hsi = useMode(modeHsi);
export const hsl = useMode(modeHsl);
export const hsv = useMode(modeHsv);
export const hwb = useMode(modeHwb);
export const itp = useMode(modeItp);
export const jab = useMode(modeJab);
export const jch = useMode(modeJch);
export const lab = useMode(modeLab);
Expand Down
16 changes: 16 additions & 0 deletions src/itp/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// PQ Constants
// https://en.wikipedia.org/wiki/High-dynamic-range_video#Perceptual_quantizer
export const M1 = 2610 / 16384;
export const M2 = 2523 / 32;
export const IM1 = 16384 / 2610;
export const IM2 = 32 / 2523;
export const C1 = 3424 / 4096;
export const C2 = 2413 / 128;
export const C3 = 2392 / 128;

// Maximum luminance in PQ is 10,000 cd/m^2
// Relative XYZ has Y=1 for media white
// BT.2048 says media white Y=203 at PQ 58
//
// This is confirmed here: https://www.itu.int/dms_pub/itu-r/opb/rep/R-REP-BT.2408-3-2019-PDF-E.pdf
export const YW = 203;
30 changes: 30 additions & 0 deletions src/itp/convertItpToXyz65.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { YW } from './constants.js';
import { pq_st2084_eotf } from './utils.js';

const convertItpToXyz65 = ({ i, t, p, alpha }) => {
const [l, m, s] = [
i + 0.008609037037932761 * t + 0.11102962500302593 * p,
i - 0.00860903703793275 * t - 0.11102962500302599 * p,
i + 0.5600313357106791 * t - 0.32062717498731885 * p
].map(pq_st2084_eotf);
const [x, y, z] = [
2.0701522183894223 * l -
1.3263473389671556 * m +
0.20665104762940512 * s,
0.36473852097480713 * l +
0.6805660249472276 * m -
0.04530454592203474 * s,
-0.04974720753581203 * l -
0.04926096669661379 * m +
1.1880659249923042 * s
].map(c => Math.max(c / YW, 0));

const res = { mode: 'xyz65', x, y, z };
if (alpha !== undefined) {
res.alpha = alpha;
}

return res;
};

export default convertItpToXyz65;
30 changes: 30 additions & 0 deletions src/itp/convertXyz65ToItp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { YW } from './constants.js';
import { pq_st2084_oetf } from './utils.js';

const convertXyz65ToItp = ({ x, y, z, alpha }) => {
const [absX, absY, absZ] = [x, y, z].map(c => Math.max(c * YW, 0));
const [l, m, s] = [
0.3592832590121218 * absX +
0.6976051147779497 * absY -
0.0358915932320289 * absZ,
-0.1920808463704992 * absX +
1.1004767970374318 * absY +
0.07537486585191187 * absZ,
0.007079784460747716 * absX +
0.07483966621863658 * absY +
0.8433265453898765 * absZ
].map(pq_st2084_oetf);

const i = 0.5 * l + 0.5 * m;
const t = 1.61376953125 * l + -3.323486328125 * m + 1.709716796875 * s;
const p = 4.378173828125 * l + -4.24560546875 * m + -0.132568359375 * s;

const res = { mode: 'itp', i, t, p };
if (alpha !== undefined) {
res.alpha = alpha;
}

return res;
};

export default convertXyz65ToItp;
44 changes: 44 additions & 0 deletions src/itp/definition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { interpolatorLinear } from '../interpolate/linear.js';
import { fixupAlpha } from '../fixup/alpha.js';
import convertItpToXyz65 from './convertItpToXyz65.js';
import convertXyz65ToItp from './convertXyz65ToItp.js';
import { convertRgbToXyz65, convertXyz65ToRgb } from '../index.js';

/*
ICtCp (or ITP) color space, as defined in ITU-R Recommendation BT.2100.
ICtCp is drafted to be supported in CSS within
[CSS Color HDR Module Level 1](https://drafts.csswg.org/css-color-hdr/#ICtCp) spec.
*/

const definition = {
mode: 'itp',
channels: ['i', 't', 'p', 'alpha'],
parse: ['--ictcp'],
serialize: '--ictcp',

toMode: {
xyz65: convertItpToXyz65,
rgb: color => convertXyz65ToRgb(convertItpToXyz65(color))
},

fromMode: {
xyz65: convertXyz65ToItp,
rgb: color => convertXyz65ToItp(convertRgbToXyz65(color))
},

ranges: {
i: [0, 0.581],
t: [-0.369, 0.272],
p: [-0.164, 0.331]
},

interpolate: {
i: interpolatorLinear,
t: interpolatorLinear,
p: interpolatorLinear,
alpha: { use: interpolatorLinear, fixup: fixupAlpha }
}
};

export default definition;
17 changes: 17 additions & 0 deletions src/itp/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { C1, C2, C3, IM1, IM2, M1, M2 } from './constants.js';

function npow(base, exp) {
return Math.sign(base) * Math.pow(Math.abs(base), exp);
}

export function pq_st2084_oetf(c) {
const powC = npow(c / 10000, M1);
const r = (C1 + C2 * powC) / (1 + C3 * powC);
return npow(r, M2);
}

export function pq_st2084_eotf(c) {
const powC = npow(c, IM2);
const r = (powC - C1) / (C2 - C3 * powC);
return 10000 * npow(r, IM1);
}
8 changes: 8 additions & 0 deletions test/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const API_FULL = [
'convertHslToRgb',
'convertHsvToRgb',
'convertHwbToRgb',
'convertItpToXyz65',
'convertJabToJch',
'convertJabToRgb',
'convertJabToXyz65',
Expand Down Expand Up @@ -73,6 +74,7 @@ const API_FULL = [
'convertXyz50ToRgb',
'convertXyz50ToXyz65',
'convertXyz65ToA98',
'convertXyz65ToItp',
'convertXyz65ToJab',
'convertXyz65ToLab65',
'convertXyz65ToP3',
Expand Down Expand Up @@ -125,6 +127,7 @@ const API_FULL = [
'hsl',
'hsv',
'hwb',
'itp',
'inGamut',
'interpolate',
'interpolateWith',
Expand Down Expand Up @@ -161,6 +164,7 @@ const API_FULL = [
'modeHsl',
'modeHsv',
'modeHwb',
'modeItp',
'modeJab',
'modeJch',
'modeLab',
Expand Down Expand Up @@ -253,6 +257,7 @@ const API_ALL = [
'hsl',
'hsv',
'hwb',
'itp',
'jab',
'jch',
'lab',
Expand Down Expand Up @@ -294,6 +299,7 @@ const API_FN = [
'convertHslToRgb',
'convertHsvToRgb',
'convertHwbToRgb',
'convertItpToXyz65',
'convertJabToJch',
'convertJabToRgb',
'convertJabToXyz65',
Expand Down Expand Up @@ -340,6 +346,7 @@ const API_FN = [
'convertXyz50ToRgb',
'convertXyz50ToXyz65',
'convertXyz65ToA98',
'convertXyz65ToItp',
'convertXyz65ToJab',
'convertXyz65ToLab65',
'convertXyz65ToP3',
Expand Down Expand Up @@ -412,6 +419,7 @@ const API_FN = [
'modeHsl',
'modeHsv',
'modeHwb',
'modeItp',
'modeJab',
'modeJch',
'modeLab',
Expand Down
Loading

0 comments on commit 88c4ec0

Please sign in to comment.