Skip to content

Commit 791f845

Browse files
authored
ColorManagement: Add Display P3 transforms (#25520)
* ColorManagement: Add Display P3 transforms. * Clean up * Update color.getStyle(), add non-passing unit test. * Clean up test * Clean up * Clean up * Fix math and tests.
1 parent 1ae6d2d commit 791f845

File tree

5 files changed

+161
-45
lines changed

5 files changed

+161
-45
lines changed

src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export const ObjectSpaceNormalMap = 1;
150150
export const NoColorSpace = '';
151151
export const SRGBColorSpace = 'srgb';
152152
export const LinearSRGBColorSpace = 'srgb-linear';
153+
export const DisplayP3ColorSpace = 'display-p3';
153154

154155
export const ZeroStencilOp = 0;
155156
export const KeepStencilOp = 7680;

src/extras/ImageUtils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createElementNS } from '../utils.js';
2-
import { SRGBToLinear } from '../math/Color.js';
2+
import { SRGBToLinear } from '../math/ColorManagement.js';
33

44
let _canvas;
55

src/math/Color.js

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ const _colorKeywords = { 'aliceblue': 0xF0F8FF, 'antiquewhite': 0xFAEBD7, 'aqua'
2727
'springgreen': 0x00FF7F, 'steelblue': 0x4682B4, 'tan': 0xD2B48C, 'teal': 0x008080, 'thistle': 0xD8BFD8, 'tomato': 0xFF6347, 'turquoise': 0x40E0D0,
2828
'violet': 0xEE82EE, 'wheat': 0xF5DEB3, 'white': 0xFFFFFF, 'whitesmoke': 0xF5F5F5, 'yellow': 0xFFFF00, 'yellowgreen': 0x9ACD32 };
2929

30-
const _rgb = { r: 0, g: 0, b: 0 };
3130
const _hslA = { h: 0, s: 0, l: 0 };
3231
const _hslB = { h: 0, s: 0, l: 0 };
3332

@@ -42,16 +41,6 @@ function hue2rgb( p, q, t ) {
4241

4342
}
4443

45-
function toComponents( source, target ) {
46-
47-
target.r = source.r;
48-
target.g = source.g;
49-
target.b = source.b;
50-
51-
return target;
52-
53-
}
54-
5544
class Color {
5645

5746
constructor( r, g, b ) {
@@ -363,9 +352,9 @@ class Color {
363352

364353
getHex( colorSpace = SRGBColorSpace ) {
365354

366-
ColorManagement.fromWorkingColorSpace( toComponents( this, _rgb ), colorSpace );
355+
ColorManagement.fromWorkingColorSpace( _color.copy( this ), colorSpace );
367356

368-
return clamp( _rgb.r * 255, 0, 255 ) << 16 ^ clamp( _rgb.g * 255, 0, 255 ) << 8 ^ clamp( _rgb.b * 255, 0, 255 ) << 0;
357+
return clamp( _color.r * 255, 0, 255 ) << 16 ^ clamp( _color.g * 255, 0, 255 ) << 8 ^ clamp( _color.b * 255, 0, 255 ) << 0;
369358

370359
}
371360

@@ -379,9 +368,9 @@ class Color {
379368

380369
// h,s,l ranges are in 0.0 - 1.0
381370

382-
ColorManagement.fromWorkingColorSpace( toComponents( this, _rgb ), colorSpace );
371+
ColorManagement.fromWorkingColorSpace( _color.copy( this ), colorSpace );
383372

384-
const r = _rgb.r, g = _rgb.g, b = _rgb.b;
373+
const r = _color.r, g = _color.g, b = _color.b;
385374

386375
const max = Math.max( r, g, b );
387376
const min = Math.min( r, g, b );
@@ -422,28 +411,30 @@ class Color {
422411

423412
getRGB( target, colorSpace = ColorManagement.workingColorSpace ) {
424413

425-
ColorManagement.fromWorkingColorSpace( toComponents( this, _rgb ), colorSpace );
414+
ColorManagement.fromWorkingColorSpace( _color.copy( this ), colorSpace );
426415

427-
target.r = _rgb.r;
428-
target.g = _rgb.g;
429-
target.b = _rgb.b;
416+
target.r = _color.r;
417+
target.g = _color.g;
418+
target.b = _color.b;
430419

431420
return target;
432421

433422
}
434423

435424
getStyle( colorSpace = SRGBColorSpace ) {
436425

437-
ColorManagement.fromWorkingColorSpace( toComponents( this, _rgb ), colorSpace );
426+
ColorManagement.fromWorkingColorSpace( _color.copy( this ), colorSpace );
427+
428+
const r = _color.r, g = _color.g, b = _color.b;
438429

439430
if ( colorSpace !== SRGBColorSpace ) {
440431

441432
// Requires CSS Color Module Level 4 (https://www.w3.org/TR/css-color-4/).
442-
return `color(${ colorSpace } ${ _rgb.r } ${ _rgb.g } ${ _rgb.b })`;
433+
return `color(${ colorSpace } ${ r.toFixed( 3 ) } ${ g.toFixed( 3 ) } ${ b.toFixed( 3 ) })`;
443434

444435
}
445436

446-
return `rgb(${( _rgb.r * 255 ) | 0},${( _rgb.g * 255 ) | 0},${( _rgb.b * 255 ) | 0})`;
437+
return `rgb(${( r * 255 ) | 0},${( g * 255 ) | 0},${( b * 255 ) | 0})`;
447438

448439
}
449440

@@ -606,6 +597,8 @@ class Color {
606597

607598
}
608599

600+
const _color = new Color();
601+
609602
Color.NAMES = _colorKeywords;
610603

611-
export { Color, SRGBToLinear };
604+
export { Color };

src/math/ColorManagement.js

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { SRGBColorSpace, LinearSRGBColorSpace } from '../constants.js';
1+
import { SRGBColorSpace, LinearSRGBColorSpace, DisplayP3ColorSpace, } from '../constants.js';
2+
import { Matrix3 } from './Matrix3.js';
3+
import { Vector3 } from './Vector3.js';
24

35
export function SRGBToLinear( c ) {
46

@@ -12,10 +14,78 @@ export function LinearToSRGB( c ) {
1214

1315
}
1416

15-
// RGB-to-RGB transforms, defined as `FN[InputColorSpace][OutputColorSpace] → conversionFn`.
16-
const FN = {
17-
[ SRGBColorSpace ]: { [ LinearSRGBColorSpace ]: SRGBToLinear },
18-
[ LinearSRGBColorSpace ]: { [ SRGBColorSpace ]: LinearToSRGB },
17+
18+
/**
19+
* Matrices for sRGB and Display P3, based on the W3C specifications
20+
* for sRGB and Display P3, and the ICC specification for the D50
21+
* connection space.
22+
*
23+
* Reference:
24+
* - http://www.russellcottrell.com/photo/matrixCalculator.htm
25+
*/
26+
27+
const SRGB_TO_DISPLAY_P3 = new Matrix3().multiplyMatrices(
28+
// XYZ to Display P3
29+
new Matrix3().set(
30+
2.4039840, - 0.9899069, - 0.3976415,
31+
- 0.8422229, 1.7988437, 0.0160354,
32+
0.0482059, - 0.0974068, 1.2740049,
33+
),
34+
// sRGB to XYZ
35+
new Matrix3().set(
36+
0.4360413, 0.3851129, 0.1430458,
37+
0.2224845, 0.7169051, 0.0606104,
38+
0.0139202, 0.0970672, 0.7139126,
39+
),
40+
);
41+
42+
const DISPLAY_P3_TO_SRGB = new Matrix3().multiplyMatrices(
43+
// XYZ to sRGB
44+
new Matrix3().set(
45+
3.1341864, - 1.6172090, - 0.4906941,
46+
- 0.9787485, 1.9161301, 0.0334334,
47+
0.0719639, - 0.2289939, 1.4057537,
48+
),
49+
// Display P3 to XYZ
50+
new Matrix3().set(
51+
0.5151187, 0.2919778, 0.1571035,
52+
0.2411892, 0.6922441, 0.0665668,
53+
- 0.0010505, 0.0418791, 0.7840713,
54+
),
55+
);
56+
57+
const _vector = new Vector3();
58+
59+
function DisplayP3ToLinearSRGB( color ) {
60+
61+
color.convertSRGBToLinear();
62+
63+
_vector.set( color.r, color.g, color.b ).applyMatrix3( DISPLAY_P3_TO_SRGB );
64+
65+
return color.setRGB( _vector.x, _vector.y, _vector.z );
66+
67+
}
68+
69+
function LinearSRGBToDisplayP3( color ) {
70+
71+
_vector.set( color.r, color.g, color.b ).applyMatrix3( SRGB_TO_DISPLAY_P3 );
72+
73+
return color.setRGB( _vector.x, _vector.y, _vector.z ).convertLinearToSRGB();
74+
75+
}
76+
77+
// Conversions from <source> to Linear-sRGB reference space.
78+
const TO_LINEAR = {
79+
[ LinearSRGBColorSpace ]: ( color ) => color,
80+
[ SRGBColorSpace ]: ( color ) => color.convertSRGBToLinear(),
81+
[ DisplayP3ColorSpace ]: DisplayP3ToLinearSRGB,
82+
};
83+
84+
// Conversions to <target> from Linear-sRGB reference space.
85+
const FROM_LINEAR = {
86+
[ LinearSRGBColorSpace ]: ( color ) => color,
87+
[ SRGBColorSpace ]: ( color ) => color.convertLinearToSRGB(),
88+
[ DisplayP3ColorSpace ]: LinearSRGBToDisplayP3,
1989
};
2090

2191
export const ColorManagement = {
@@ -58,19 +128,16 @@ export const ColorManagement = {
58128

59129
}
60130

61-
if ( FN[ sourceColorSpace ] && FN[ sourceColorSpace ][ targetColorSpace ] !== undefined ) {
62-
63-
const fn = FN[ sourceColorSpace ][ targetColorSpace ];
131+
const sourceToLinear = TO_LINEAR[ sourceColorSpace ];
132+
const targetFromLinear = FROM_LINEAR[ targetColorSpace ];
64133

65-
color.r = fn( color.r );
66-
color.g = fn( color.g );
67-
color.b = fn( color.b );
134+
if ( sourceToLinear === undefined || targetFromLinear === undefined ) {
68135

69-
return color;
136+
throw new Error( `Unsupported color space conversion, "${ sourceColorSpace }" to "${ targetColorSpace }".` );
70137

71138
}
72139

73-
throw new Error( 'Unsupported color space conversion.' );
140+
return targetFromLinear( sourceToLinear( color ) );
74141

75142
},
76143

test/unit/src/math/Color.tests.js

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
/* global QUnit */
22

33
import { Color } from '../../../../src/math/Color.js';
4+
import { ColorManagement } from '../../../../src/math/ColorManagement.js';
45
import { eps } from '../../utils/math-constants.js';
56
import { CONSOLE_LEVEL } from '../../utils/console-wrapper.js';
7+
import { DisplayP3ColorSpace, SRGBColorSpace } from '../../../../src/constants.js';
68

79
export default QUnit.module( 'Maths', () => {
810

911
QUnit.module( 'Color', () => {
1012

13+
const colorManagementEnabled = ColorManagement.enabled;
14+
15+
QUnit.testDone( () => {
16+
17+
ColorManagement.enabled = colorManagementEnabled;
18+
19+
} );
20+
1121
// INSTANCING
1222
QUnit.test( 'Instancing', ( assert ) => {
1323

@@ -84,11 +94,33 @@ export default QUnit.module( 'Maths', () => {
8494

8595
QUnit.test( 'setRGB', ( assert ) => {
8696

97+
ColorManagement.enabled = true;
98+
8799
const c = new Color();
100+
88101
c.setRGB( 0.3, 0.5, 0.7 );
89-
assert.ok( c.r == 0.3, 'Red: ' + c.r );
90-
assert.ok( c.g == 0.5, 'Green: ' + c.g );
91-
assert.ok( c.b == 0.7, 'Blue: ' + c.b );
102+
103+
assert.equal( c.r, 0.3, 'Red: ' + c.r + ' (srgb-linear)' );
104+
assert.equal( c.g, 0.5, 'Green: ' + c.g + ' (srgb-linear)' );
105+
assert.equal( c.b, 0.7, 'Blue: ' + c.b + ' (srgb-linear)' );
106+
107+
c.setRGB( 0.3, 0.5, 0.7, SRGBColorSpace );
108+
109+
assert.equal( c.r.toFixed( 3 ), 0.073, 'Red: ' + c.r + ' (srgb)' );
110+
assert.equal( c.g.toFixed( 3 ), 0.214, 'Green: ' + c.g + ' (srgb)' );
111+
assert.equal( c.b.toFixed( 3 ), 0.448, 'Blue: ' + c.b + ' (srgb)' );
112+
113+
c.setRGB( 0.614, 0.731, 0.843, DisplayP3ColorSpace );
114+
115+
assert.numEqual( c.r.toFixed( 2 ), 0.3, 'Red: ' + c.r + ' (display-p3, in gamut)' );
116+
assert.numEqual( c.g.toFixed( 2 ), 0.5, 'Green: ' + c.g + ' (display-p3, in gamut)' );
117+
assert.numEqual( c.b.toFixed( 2 ), 0.7, 'Blue: ' + c.b + ' (display-p3, in gamut)' );
118+
119+
c.setRGB( 1.0, 0.5, 0.0, DisplayP3ColorSpace );
120+
121+
assert.numEqual( c.r.toFixed( 3 ), 1.179, 'Red: ' + c.r + ' (display-p3, out of gamut)' );
122+
assert.numEqual( c.g.toFixed( 3 ), 0.181, 'Green: ' + c.g + ' (display-p3, out of gamut)' );
123+
assert.numEqual( c.b.toFixed( 3 ), - 0.036, 'Blue: ' + c.b + ' (display-p3, out of gamut)' );
92124

93125
} );
94126

@@ -251,18 +283,41 @@ export default QUnit.module( 'Maths', () => {
251283

252284
} );
253285

254-
QUnit.todo( 'getRGB', ( assert ) => {
286+
QUnit.test( 'getRGB', ( assert ) => {
255287

256-
// getRGB( target, colorSpace = ColorManagement.workingColorSpace )
257-
assert.ok( false, 'everything\'s gonna be alright' );
288+
ColorManagement.enabled = true;
289+
290+
const c = new Color( 'plum' );
291+
const t = { r: 0, g: 0, b: 0 };
292+
293+
c.getRGB( t );
294+
295+
assert.equal( t.r.toFixed( 3 ), 0.723, 'r (srgb-linear)' );
296+
assert.equal( t.g.toFixed( 3 ), 0.352, 'g (srgb-linear)' );
297+
assert.equal( t.b.toFixed( 3 ), 0.723, 'b (srgb-linear)' );
298+
299+
c.getRGB( t, SRGBColorSpace );
300+
301+
assert.equal( t.r.toFixed( 3 ), ( 221 / 255 ).toFixed( 3 ), 'r (srgb)' );
302+
assert.equal( t.g.toFixed( 3 ), ( 160 / 255 ).toFixed( 3 ), 'g (srgb)' );
303+
assert.equal( t.b.toFixed( 3 ), ( 221 / 255 ).toFixed( 3 ), 'b (srgb)' );
304+
305+
c.getRGB( t, DisplayP3ColorSpace );
306+
307+
assert.equal( t.r.toFixed( 3 ), 0.831, 'r (display-p3)' );
308+
assert.equal( t.g.toFixed( 3 ), 0.637, 'g (display-p3)' );
309+
assert.equal( t.b.toFixed( 3 ), 0.852, 'b (display-p3)' );
258310

259311
} );
260312

261313
QUnit.test( 'getStyle', ( assert ) => {
262314

315+
ColorManagement.enabled = true;
316+
263317
const c = new Color( 'plum' );
264-
const res = c.getStyle();
265-
assert.ok( res == 'rgb(221,160,221)', 'style: ' + res );
318+
319+
assert.equal( c.getStyle(), 'rgb(221,160,221)', 'style: srgb' );
320+
assert.equal( c.getStyle( DisplayP3ColorSpace ), 'color(display-p3 0.831 0.637 0.852)', 'style: display-p3' );
266321

267322
} );
268323

0 commit comments

Comments
 (0)