Skip to content

Add support for ICtCp color space #216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
27 changes: 27 additions & 0 deletions src/itp/convertItpToXyz65.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { YW } from './constants.js';
import { transferPqDecode } from '../hdr/transfer.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(transferPqDecode);

const [x, y, z] = [
2.0701522183894219 * l -
1.3263473389671556 * m +
0.2066510476294051 * s,
0.3647385209748074 * l + 0.680566024947227 * m - 0.0453045459220346 * s,
-0.049747207535812 * l - 0.0492609666966138 * 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 { transferPqEncode } from '../hdr/transfer.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.3592832590121217 * absX +
0.6976051147779502 * absY -
0.0358915932320289 * absZ,
-0.1920808463704995 * absX +
1.1004767970374323 * absY +
0.0753748658519118 * absZ,
0.0070797844607477 * absX +
0.0748396662186366 * absY +
0.8433265453898765 * absZ
].map(transferPqEncode);

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;
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
76 changes: 76 additions & 0 deletions test/itp.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import tape from 'tape';
import { formatCss, formatHex, itp } from '../src/index.js';

tape('itp', t => {
t.deepEqual(
itp('white'),
{
mode: 'itp',
i: 0.5806888810416109,
t: 1.1102230246251565e-16,
p: 2.914335439641036e-16
},
'white'
);
t.deepEqual(
itp('black'),
{
mode: 'itp',
i: 7.309559025783966e-7,
t: -2.117582368135751e-22,
p: 1.3234889800848443e-23
},
'black'
);
t.deepEqual(
itp('red'),
{
mode: 'itp',
i: 0.4278802843622844,
t: -0.11570435976969046,
p: 0.27872894737532694
},
'red'
);
t.end();
});

tape('color(--ictcp)', t => {
t.deepEqual(itp('color(--ictcp 1 0 0 / 0.25)'), {
mode: 'itp',
i: 1,
t: 0,
p: 0,
alpha: 0.25
});
t.deepEqual(itp('color(--ictcp 0% 50% 0.5 / 25%)'), {
mode: 'itp',
i: 0,
t: 0.5,
p: 0.5,
alpha: 0.25
});
t.end();
});

tape('formatCss', t => {
t.equal(
formatCss('color(--ictcp 0% 50% 0.5 / 25%)'),
'color(--ictcp 0 0.5 0.5 / 0.25)'
);
t.end();
});

tape('formatCss', t => {
t.equal(
formatHex({
mode: 'itp',
i: 0.4278802843622844,
t: -0.11570435976969046,
p: 0.27872894737532694
}),
'#ff0000',
'red'
);
t.end();
});
4 changes: 2 additions & 2 deletions tools/ranges.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { converter, getMode } from '../src/index.js';
for a particular color space, by converting lots of
RGB colors to that space.
*/
let ranges = (mode, step = 0.01) => {
let ranges = (mode, step = 1 / 128) => {
let conv = converter(mode);
let chs = getMode(mode).channels;
let res = chs.reduce(
Expand All @@ -32,4 +32,4 @@ let ranges = (mode, step = 0.01) => {
return res;
};

console.log(ranges(process.argv[2], 0.0025));
console.log(ranges(process.argv[2], 1 / 512));