Parcourir la source

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.
Don McCurdy il y a 2 ans
Parent
commit
791f845b9f
5 fichiers modifiés avec 161 ajouts et 45 suppressions
  1. 1 0
      src/constants.js
  2. 1 1
      src/extras/ImageUtils.js
  3. 16 23
      src/math/Color.js
  4. 80 13
      src/math/ColorManagement.js
  5. 63 8
      test/unit/src/math/Color.tests.js

+ 1 - 0
src/constants.js

@@ -150,6 +150,7 @@ export const ObjectSpaceNormalMap = 1;
 export const NoColorSpace = '';
 export const SRGBColorSpace = 'srgb';
 export const LinearSRGBColorSpace = 'srgb-linear';
+export const DisplayP3ColorSpace = 'display-p3';
 
 export const ZeroStencilOp = 0;
 export const KeepStencilOp = 7680;

+ 1 - 1
src/extras/ImageUtils.js

@@ -1,5 +1,5 @@
 import { createElementNS } from '../utils.js';
-import { SRGBToLinear } from '../math/Color.js';
+import { SRGBToLinear } from '../math/ColorManagement.js';
 
 let _canvas;
 

+ 16 - 23
src/math/Color.js

@@ -27,7 +27,6 @@ const _colorKeywords = { 'aliceblue': 0xF0F8FF, 'antiquewhite': 0xFAEBD7, 'aqua'
 	'springgreen': 0x00FF7F, 'steelblue': 0x4682B4, 'tan': 0xD2B48C, 'teal': 0x008080, 'thistle': 0xD8BFD8, 'tomato': 0xFF6347, 'turquoise': 0x40E0D0,
 	'violet': 0xEE82EE, 'wheat': 0xF5DEB3, 'white': 0xFFFFFF, 'whitesmoke': 0xF5F5F5, 'yellow': 0xFFFF00, 'yellowgreen': 0x9ACD32 };
 
-const _rgb = { r: 0, g: 0, b: 0 };
 const _hslA = { h: 0, s: 0, l: 0 };
 const _hslB = { h: 0, s: 0, l: 0 };
 
@@ -42,16 +41,6 @@ function hue2rgb( p, q, t ) {
 
 }
 
-function toComponents( source, target ) {
-
-	target.r = source.r;
-	target.g = source.g;
-	target.b = source.b;
-
-	return target;
-
-}
-
 class Color {
 
 	constructor( r, g, b ) {
@@ -363,9 +352,9 @@ class Color {
 
 	getHex( colorSpace = SRGBColorSpace ) {
 
-		ColorManagement.fromWorkingColorSpace( toComponents( this, _rgb ), colorSpace );
+		ColorManagement.fromWorkingColorSpace( _color.copy( this ), colorSpace );
 
-		return clamp( _rgb.r * 255, 0, 255 ) << 16 ^ clamp( _rgb.g * 255, 0, 255 ) << 8 ^ clamp( _rgb.b * 255, 0, 255 ) << 0;
+		return clamp( _color.r * 255, 0, 255 ) << 16 ^ clamp( _color.g * 255, 0, 255 ) << 8 ^ clamp( _color.b * 255, 0, 255 ) << 0;
 
 	}
 
@@ -379,9 +368,9 @@ class Color {
 
 		// h,s,l ranges are in 0.0 - 1.0
 
-		ColorManagement.fromWorkingColorSpace( toComponents( this, _rgb ), colorSpace );
+		ColorManagement.fromWorkingColorSpace( _color.copy( this ), colorSpace );
 
-		const r = _rgb.r, g = _rgb.g, b = _rgb.b;
+		const r = _color.r, g = _color.g, b = _color.b;
 
 		const max = Math.max( r, g, b );
 		const min = Math.min( r, g, b );
@@ -422,11 +411,11 @@ class Color {
 
 	getRGB( target, colorSpace = ColorManagement.workingColorSpace ) {
 
-		ColorManagement.fromWorkingColorSpace( toComponents( this, _rgb ), colorSpace );
+		ColorManagement.fromWorkingColorSpace( _color.copy( this ), colorSpace );
 
-		target.r = _rgb.r;
-		target.g = _rgb.g;
-		target.b = _rgb.b;
+		target.r = _color.r;
+		target.g = _color.g;
+		target.b = _color.b;
 
 		return target;
 
@@ -434,16 +423,18 @@ class Color {
 
 	getStyle( colorSpace = SRGBColorSpace ) {
 
-		ColorManagement.fromWorkingColorSpace( toComponents( this, _rgb ), colorSpace );
+		ColorManagement.fromWorkingColorSpace( _color.copy( this ), colorSpace );
+
+		const r = _color.r, g = _color.g, b = _color.b;
 
 		if ( colorSpace !== SRGBColorSpace ) {
 
 			// Requires CSS Color Module Level 4 (https://www.w3.org/TR/css-color-4/).
-			return `color(${ colorSpace } ${ _rgb.r } ${ _rgb.g } ${ _rgb.b })`;
+			return `color(${ colorSpace } ${ r.toFixed( 3 ) } ${ g.toFixed( 3 ) } ${ b.toFixed( 3 ) })`;
 
 		}
 
-		return `rgb(${( _rgb.r * 255 ) | 0},${( _rgb.g * 255 ) | 0},${( _rgb.b * 255 ) | 0})`;
+		return `rgb(${( r * 255 ) | 0},${( g * 255 ) | 0},${( b * 255 ) | 0})`;
 
 	}
 
@@ -606,6 +597,8 @@ class Color {
 
 }
 
+const _color = new Color();
+
 Color.NAMES = _colorKeywords;
 
-export { Color, SRGBToLinear };
+export { Color };

+ 80 - 13
src/math/ColorManagement.js

@@ -1,4 +1,6 @@
-import { SRGBColorSpace, LinearSRGBColorSpace } from '../constants.js';
+import { SRGBColorSpace, LinearSRGBColorSpace, DisplayP3ColorSpace, } from '../constants.js';
+import { Matrix3 } from './Matrix3.js';
+import { Vector3 } from './Vector3.js';
 
 export function SRGBToLinear( c ) {
 
@@ -12,10 +14,78 @@ export function LinearToSRGB( c ) {
 
 }
 
-// RGB-to-RGB transforms, defined as `FN[InputColorSpace][OutputColorSpace] → conversionFn`.
-const FN = {
-	[ SRGBColorSpace ]: { [ LinearSRGBColorSpace ]: SRGBToLinear },
-	[ LinearSRGBColorSpace ]: { [ SRGBColorSpace ]: LinearToSRGB },
+
+/**
+ * Matrices for sRGB and Display P3, based on the W3C specifications
+ * for sRGB and Display P3, and the ICC specification for the D50
+ * connection space.
+ *
+ * Reference:
+ * - http://www.russellcottrell.com/photo/matrixCalculator.htm
+ */
+
+const SRGB_TO_DISPLAY_P3 = new Matrix3().multiplyMatrices(
+	// XYZ to Display P3
+	new Matrix3().set(
+		2.4039840, - 0.9899069, - 0.3976415,
+		- 0.8422229, 1.7988437, 0.0160354,
+		0.0482059, - 0.0974068, 1.2740049,
+	),
+	// sRGB to XYZ
+	new Matrix3().set(
+		0.4360413, 0.3851129, 0.1430458,
+		0.2224845, 0.7169051, 0.0606104,
+		0.0139202, 0.0970672, 0.7139126,
+	),
+);
+
+const DISPLAY_P3_TO_SRGB = new Matrix3().multiplyMatrices(
+	// XYZ to sRGB
+	new Matrix3().set(
+		3.1341864, - 1.6172090, - 0.4906941,
+		- 0.9787485, 1.9161301, 0.0334334,
+		0.0719639, - 0.2289939, 1.4057537,
+	),
+	// Display P3 to XYZ
+	new Matrix3().set(
+		0.5151187, 0.2919778, 0.1571035,
+		0.2411892, 0.6922441, 0.0665668,
+		- 0.0010505, 0.0418791, 0.7840713,
+	),
+);
+
+const _vector = new Vector3();
+
+function DisplayP3ToLinearSRGB( color ) {
+
+	color.convertSRGBToLinear();
+
+	_vector.set( color.r, color.g, color.b ).applyMatrix3( DISPLAY_P3_TO_SRGB );
+
+	return color.setRGB( _vector.x, _vector.y, _vector.z );
+
+}
+
+function LinearSRGBToDisplayP3( color ) {
+
+	_vector.set( color.r, color.g, color.b ).applyMatrix3( SRGB_TO_DISPLAY_P3 );
+
+	return color.setRGB( _vector.x, _vector.y, _vector.z ).convertLinearToSRGB();
+
+}
+
+// Conversions from <source> to Linear-sRGB reference space.
+const TO_LINEAR = {
+	[ LinearSRGBColorSpace ]: ( color ) => color,
+	[ SRGBColorSpace ]: ( color ) => color.convertSRGBToLinear(),
+	[ DisplayP3ColorSpace ]: DisplayP3ToLinearSRGB,
+};
+
+// Conversions to <target> from Linear-sRGB reference space.
+const FROM_LINEAR = {
+	[ LinearSRGBColorSpace ]: ( color ) => color,
+	[ SRGBColorSpace ]: ( color ) => color.convertLinearToSRGB(),
+	[ DisplayP3ColorSpace ]: LinearSRGBToDisplayP3,
 };
 
 export const ColorManagement = {
@@ -58,19 +128,16 @@ export const ColorManagement = {
 
 		}
 
-		if ( FN[ sourceColorSpace ] && FN[ sourceColorSpace ][ targetColorSpace ] !== undefined ) {
-
-			const fn = FN[ sourceColorSpace ][ targetColorSpace ];
+		const sourceToLinear = TO_LINEAR[ sourceColorSpace ];
+		const targetFromLinear = FROM_LINEAR[ targetColorSpace ];
 
-			color.r = fn( color.r );
-			color.g = fn( color.g );
-			color.b = fn( color.b );
+		if ( sourceToLinear === undefined || targetFromLinear === undefined ) {
 
-			return color;
+			throw new Error( `Unsupported color space conversion, "${ sourceColorSpace }" to "${ targetColorSpace }".` );
 
 		}
 
-		throw new Error( 'Unsupported color space conversion.' );
+		return targetFromLinear( sourceToLinear( color ) );
 
 	},
 

+ 63 - 8
test/unit/src/math/Color.tests.js

@@ -1,13 +1,23 @@
 /* global QUnit */
 
 import { Color } from '../../../../src/math/Color.js';
+import { ColorManagement } from '../../../../src/math/ColorManagement.js';
 import { eps } from '../../utils/math-constants.js';
 import { CONSOLE_LEVEL } from '../../utils/console-wrapper.js';
+import { DisplayP3ColorSpace, SRGBColorSpace } from '../../../../src/constants.js';
 
 export default QUnit.module( 'Maths', () => {
 
 	QUnit.module( 'Color', () => {
 
+		const colorManagementEnabled = ColorManagement.enabled;
+
+		QUnit.testDone( () => {
+
+			ColorManagement.enabled = colorManagementEnabled;
+
+		} );
+
 		// INSTANCING
 		QUnit.test( 'Instancing', ( assert ) => {
 
@@ -84,11 +94,33 @@ export default QUnit.module( 'Maths', () => {
 
 		QUnit.test( 'setRGB', ( assert ) => {
 
+			ColorManagement.enabled = true;
+
 			const c = new Color();
+
 			c.setRGB( 0.3, 0.5, 0.7 );
-			assert.ok( c.r == 0.3, 'Red: ' + c.r );
-			assert.ok( c.g == 0.5, 'Green: ' + c.g );
-			assert.ok( c.b == 0.7, 'Blue: ' + c.b );
+
+			assert.equal( c.r, 0.3, 'Red: ' + c.r + ' (srgb-linear)' );
+			assert.equal( c.g, 0.5, 'Green: ' + c.g + ' (srgb-linear)' );
+			assert.equal( c.b, 0.7, 'Blue: ' + c.b + ' (srgb-linear)' );
+
+			c.setRGB( 0.3, 0.5, 0.7, SRGBColorSpace );
+
+			assert.equal( c.r.toFixed( 3 ), 0.073, 'Red: ' + c.r + ' (srgb)' );
+			assert.equal( c.g.toFixed( 3 ), 0.214, 'Green: ' + c.g + ' (srgb)' );
+			assert.equal( c.b.toFixed( 3 ), 0.448, 'Blue: ' + c.b + ' (srgb)' );
+
+			c.setRGB( 0.614, 0.731, 0.843, DisplayP3ColorSpace );
+
+			assert.numEqual( c.r.toFixed( 2 ), 0.3, 'Red: ' + c.r + ' (display-p3, in gamut)' );
+			assert.numEqual( c.g.toFixed( 2 ), 0.5, 'Green: ' + c.g + ' (display-p3, in gamut)' );
+			assert.numEqual( c.b.toFixed( 2 ), 0.7, 'Blue: ' + c.b + ' (display-p3, in gamut)' );
+
+			c.setRGB( 1.0, 0.5, 0.0, DisplayP3ColorSpace );
+
+			assert.numEqual( c.r.toFixed( 3 ), 1.179, 'Red: ' + c.r + ' (display-p3, out of gamut)' );
+			assert.numEqual( c.g.toFixed( 3 ), 0.181, 'Green: ' + c.g + ' (display-p3, out of gamut)' );
+			assert.numEqual( c.b.toFixed( 3 ), - 0.036, 'Blue: ' + c.b + ' (display-p3, out of gamut)' );
 
 		} );
 
@@ -251,18 +283,41 @@ export default QUnit.module( 'Maths', () => {
 
 		} );
 
-		QUnit.todo( 'getRGB', ( assert ) => {
+		QUnit.test( 'getRGB', ( assert ) => {
 
-			// getRGB( target, colorSpace = ColorManagement.workingColorSpace )
-			assert.ok( false, 'everything\'s gonna be alright' );
+			ColorManagement.enabled = true;
+
+			const c = new Color( 'plum' );
+			const t = { r: 0, g: 0, b: 0 };
+
+			c.getRGB( t );
+
+			assert.equal( t.r.toFixed( 3 ), 0.723, 'r (srgb-linear)' );
+			assert.equal( t.g.toFixed( 3 ), 0.352, 'g (srgb-linear)' );
+			assert.equal( t.b.toFixed( 3 ), 0.723, 'b (srgb-linear)' );
+
+			c.getRGB( t, SRGBColorSpace );
+
+			assert.equal( t.r.toFixed( 3 ), ( 221 / 255 ).toFixed( 3 ), 'r (srgb)' );
+			assert.equal( t.g.toFixed( 3 ), ( 160 / 255 ).toFixed( 3 ), 'g (srgb)' );
+			assert.equal( t.b.toFixed( 3 ), ( 221 / 255 ).toFixed( 3 ), 'b (srgb)' );
+
+			c.getRGB( t, DisplayP3ColorSpace );
+
+			assert.equal( t.r.toFixed( 3 ), 0.831, 'r (display-p3)' );
+			assert.equal( t.g.toFixed( 3 ), 0.637, 'g (display-p3)' );
+			assert.equal( t.b.toFixed( 3 ), 0.852, 'b (display-p3)' );
 
 		} );
 
 		QUnit.test( 'getStyle', ( assert ) => {
 
+			ColorManagement.enabled = true;
+
 			const c = new Color( 'plum' );
-			const res = c.getStyle();
-			assert.ok( res == 'rgb(221,160,221)', 'style: ' + res );
+
+			assert.equal( c.getStyle(), 'rgb(221,160,221)', 'style: srgb' );
+			assert.equal( c.getStyle( DisplayP3ColorSpace ), 'color(display-p3 0.831 0.637 0.852)', 'style: display-p3' );
 
 		} );