diff --git a/docs/api.md b/docs/api.md index 56eec372..5fbdb7c6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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` | ICtCp color space | `modeItp` `jab` | Jzazbz color space | `modeJab` `jch` | Jzazbz in cylindrical form | `modeJch` `lab` | CIELAB color space (D50 Illuminant) | `modeLab` diff --git a/docs/color-spaces.md b/docs/color-spaces.md index cc53d1e8..d7d37263 100644 --- a/docs/color-spaces.md +++ b/docs/color-spaces.md @@ -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` + +ICtCp (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. diff --git a/docs/guides/tree-shaking.md b/docs/guides/tree-shaking.md index ec1f750c..e58bc5dc 100644 --- a/docs/guides/tree-shaking.md +++ b/docs/guides/tree-shaking.md @@ -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'; diff --git a/src/bootstrap/all.js b/src/bootstrap/all.js index acf1fc5a..ec3e5d32 100644 --- a/src/bootstrap/all.js +++ b/src/bootstrap/all.js @@ -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'; @@ -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); diff --git a/src/index-fn.js b/src/index-fn.js index c09f8b9b..35f3f41d 100644 --- a/src/index-fn.js +++ b/src/index-fn.js @@ -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'; @@ -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'; @@ -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'; diff --git a/src/index.js b/src/index.js index 0de325d7..83ca714b 100644 --- a/src/index.js +++ b/src/index.js @@ -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'; @@ -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'; @@ -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'; @@ -235,6 +238,7 @@ export { modeHsl, modeHsv, modeHwb, + modeItp, modeJab, modeJch, modeLab, @@ -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); diff --git a/src/itp/constants.js b/src/itp/constants.js new file mode 100644 index 00000000..d20d1191 --- /dev/null +++ b/src/itp/constants.js @@ -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; diff --git a/src/itp/convertItpToXyz65.js b/src/itp/convertItpToXyz65.js new file mode 100644 index 00000000..ba3e5ebb --- /dev/null +++ b/src/itp/convertItpToXyz65.js @@ -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; diff --git a/src/itp/convertXyz65ToItp.js b/src/itp/convertXyz65ToItp.js new file mode 100644 index 00000000..39eed1ca --- /dev/null +++ b/src/itp/convertXyz65ToItp.js @@ -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; diff --git a/src/itp/definition.js b/src/itp/definition.js new file mode 100644 index 00000000..9bd7d9e7 --- /dev/null +++ b/src/itp/definition.js @@ -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; diff --git a/test/api.test.js b/test/api.test.js index 0b6f2a4a..b04a0ef8 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -27,6 +27,7 @@ const API_FULL = [ 'convertHslToRgb', 'convertHsvToRgb', 'convertHwbToRgb', + 'convertItpToXyz65', 'convertJabToJch', 'convertJabToRgb', 'convertJabToXyz65', @@ -73,6 +74,7 @@ const API_FULL = [ 'convertXyz50ToRgb', 'convertXyz50ToXyz65', 'convertXyz65ToA98', + 'convertXyz65ToItp', 'convertXyz65ToJab', 'convertXyz65ToLab65', 'convertXyz65ToP3', @@ -125,6 +127,7 @@ const API_FULL = [ 'hsl', 'hsv', 'hwb', + 'itp', 'inGamut', 'interpolate', 'interpolateWith', @@ -161,6 +164,7 @@ const API_FULL = [ 'modeHsl', 'modeHsv', 'modeHwb', + 'modeItp', 'modeJab', 'modeJch', 'modeLab', @@ -253,6 +257,7 @@ const API_ALL = [ 'hsl', 'hsv', 'hwb', + 'itp', 'jab', 'jch', 'lab', @@ -294,6 +299,7 @@ const API_FN = [ 'convertHslToRgb', 'convertHsvToRgb', 'convertHwbToRgb', + 'convertItpToXyz65', 'convertJabToJch', 'convertJabToRgb', 'convertJabToXyz65', @@ -340,6 +346,7 @@ const API_FN = [ 'convertXyz50ToRgb', 'convertXyz50ToXyz65', 'convertXyz65ToA98', + 'convertXyz65ToItp', 'convertXyz65ToJab', 'convertXyz65ToLab65', 'convertXyz65ToP3', @@ -412,6 +419,7 @@ const API_FN = [ 'modeHsl', 'modeHsv', 'modeHwb', + 'modeItp', 'modeJab', 'modeJch', 'modeLab', diff --git a/test/itp.test.js b/test/itp.test.js new file mode 100644 index 00000000..7a2958aa --- /dev/null +++ b/test/itp.test.js @@ -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(); +}); diff --git a/tools/ranges.js b/tools/ranges.js index fc706f34..f420c593 100644 --- a/tools/ranges.js +++ b/tools/ranges.js @@ -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( @@ -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));