Web Inspector: Display color picker for p3 colors
authornvasilyev@apple.com <nvasilyev@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 7 Nov 2019 02:53:40 +0000 (02:53 +0000)
committernvasilyev@apple.com <nvasilyev@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 7 Nov 2019 02:53:40 +0000 (02:53 +0000)
https://bugs.webkit.org/show_bug.cgi?id=203436
<rdar://problem/56635062>

Reviewed by Brian Burg.

Source/WebInspectorUI:

For p3 colors, display ColorSquare with display-p3 gamut.
Continue showing ColorSquare with sRGB gamut by default.

* UserInterface/Base/Setting.js:
* UserInterface/Models/Color.js:
(WI.Color):
(WI.Color.rgb2hsv):
(WI.Color.hsv2rgb.fraction):
(WI.Color.hsv2rgb):
(WI.Color.prototype.get hsla):
(WI.Color.prototype.isKeyword):

* UserInterface/Views/ColorPicker.css:
(.color-picker > .hue):
(@media (color-gamut: p3)):
(.color-picker.gamut-p3 > .hue):

* UserInterface/Views/ColorPicker.js:
(WI.ColorPicker.prototype._updateColor):
(WI.ColorPicker.prototype._updateOpacitySlider):
(WI.ColorPicker.prototype._handleFormatChange):
Introduce `gamut` parameter. Previously, the only available `gamut` was sRGB.

* UserInterface/Views/ColorSquare.css:
(.color-square > .crosshair):
Update the crosshair style to look better for both light and dark backgrounds.

* UserInterface/Views/ColorSquare.js:
(WI.ColorSquare):
(WI.ColorSquare.prototype.get tintedColor):
(WI.ColorSquare.prototype.set tintedColor):
(WI.ColorSquare.prototype._setCrosshairPosition):
(WI.ColorSquare.prototype._updateBaseColor):
(WI.ColorSquare.prototype._updateCrosshairBackground):

* UserInterface/Views/InlineSwatch.js:
Make p3 color picker a preview (e.g. STP-only) feature.

LayoutTests:

Add tests for WI.Color.rgb2hsv and WI.Color.hsv2rgb.

* inspector/model/color-expected.txt:
* inspector/model/color.html:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@252168 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/inspector/model/color-expected.txt
LayoutTests/inspector/model/color.html
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/UserInterface/Base/Setting.js
Source/WebInspectorUI/UserInterface/Models/Color.js
Source/WebInspectorUI/UserInterface/Views/ColorPicker.css
Source/WebInspectorUI/UserInterface/Views/ColorPicker.js
Source/WebInspectorUI/UserInterface/Views/ColorSquare.css
Source/WebInspectorUI/UserInterface/Views/ColorSquare.js
Source/WebInspectorUI/UserInterface/Views/InlineSwatch.js

index 94c0542..6e44e0b 100644 (file)
@@ -1,3 +1,16 @@
+2019-11-06  Nikita Vasilyev  <nvasilyev@apple.com>
+
+        Web Inspector: Display color picker for p3 colors
+        https://bugs.webkit.org/show_bug.cgi?id=203436
+        <rdar://problem/56635062>
+
+        Reviewed by Brian Burg.
+
+        Add tests for WI.Color.rgb2hsv and WI.Color.hsv2rgb.
+
+        * inspector/model/color-expected.txt:
+        * inspector/model/color.html:
+
 2019-11-06  Justin Fan  <justin_fan@apple.com>
 
         REGRESSION: r252121 introduced new webgl/ failures
index cb702a5..7b9832b 100644 (file)
@@ -483,6 +483,25 @@ PASS: Should convert [-1,-1,-1] to [0,0,0].
 PASS: Should convert [361,101,50] to [360,100,25].
 PASS: Should convert [361,101,101] to [360,100,50].
 
+-- Running test case: WI.Color.rgb2hsv
+PASS: Should convert [0,0,0] to [0,0,0].
+PASS: Should convert [1,1,1] to [0,0,100].
+PASS: Should convert [1,0,0] to [0,100,100].
+PASS: Should convert [0,1,0] to [120,100,100].
+PASS: Should convert [0,0,1] to [240,100,100].
+PASS: Should convert [-1,-1,-1] to [0,0,0].
+PASS: Should convert [1.1,1.1,1.1] to [0,0,100].
+
+-- Running test case: WI.Color.hsv2rgb
+PASS: Should convert [42,100,0] to [0,0,0].
+PASS: Should convert [42,50,100] to [1,0.8500000000000001,0.5].
+PASS: Should convert [42,100,50] to [0.5,0.3500000000000001,0].
+PASS: Should convert [42,50,50] to [0.5,0.42500000000000004,0.25].
+PASS: Should convert [42,100,100] to [1,0.7000000000000002,0].
+PASS: Should convert [-1,-1,-1] to [0,0,0].
+PASS: Should convert [361,101,50] to [0.5,0,0].
+PASS: Should convert [361,101,101] to [1,0,0].
+
 -- Running test case: WI.Color.cmyk2rgb
 PASS: Should convert [0,0,0,1] to [0,0,0].
 PASS: Should convert [1,0,0,0] to [0,255,255].
index 7bfec26..de5510f 100644 (file)
@@ -542,6 +542,43 @@ function test()
     });
 
     suite.addTestCase({
+        name: "WI.Color.rgb2hsv",
+        description: "Test conversion from RGB to HSV.",
+        test() {
+            testColorConversion(WI.Color.rgb2hsv, [0, 0, 0], [0, 0, 0]);
+            testColorConversion(WI.Color.rgb2hsv, [1, 1, 1], [0, 0, 100]);
+            testColorConversion(WI.Color.rgb2hsv, [1, 0, 0], [0, 100, 100]);
+            testColorConversion(WI.Color.rgb2hsv, [0, 1, 0], [120, 100, 100]);
+            testColorConversion(WI.Color.rgb2hsv, [0, 0, 1], [240, 100, 100]);
+
+            // Out-of-bounds.
+            testColorConversion(WI.Color.rgb2hsv, [-1, -1, -1], [0, 0, 0]);
+            testColorConversion(WI.Color.rgb2hsv, [1.1, 1.1, 1.1], [0, 0, 100]);
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "WI.Color.hsv2rgb",
+        description: "Test conversion from HSV to RGB.",
+        test() {
+            testColorConversion(WI.Color.hsv2rgb, [42, 100, 0], [0, 0, 0]);
+            testColorConversion(WI.Color.hsv2rgb, [42, 50, 100], [1, 0.8500000000000001, 0.5]);
+            testColorConversion(WI.Color.hsv2rgb, [42, 100, 50], [0.5, 0.3500000000000001, 0]);
+            testColorConversion(WI.Color.hsv2rgb, [42, 50, 50], [0.5, 0.42500000000000004, 0.25]);
+            testColorConversion(WI.Color.hsv2rgb, [42, 100, 100], [1, 0.7000000000000002, 0]);
+
+            // Out-of-bounds.
+            testColorConversion(WI.Color.hsv2rgb, [-1, -1, -1], [0, 0, 0]);
+            testColorConversion(WI.Color.hsv2rgb, [361, 101, 50], [0.5, 0, 0]);
+            testColorConversion(WI.Color.hsv2rgb, [361, 101, 101], [1, 0, 0]);
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
         name: "WI.Color.cmyk2rgb",
         description: "Test conversion from CMYK to RGB.",
         test() {
index c8a07b2..c2cecb1 100644 (file)
@@ -1,3 +1,49 @@
+2019-11-06  Nikita Vasilyev  <nvasilyev@apple.com>
+
+        Web Inspector: Display color picker for p3 colors
+        https://bugs.webkit.org/show_bug.cgi?id=203436
+        <rdar://problem/56635062>
+
+        Reviewed by Brian Burg.
+
+        For p3 colors, display ColorSquare with display-p3 gamut.
+        Continue showing ColorSquare with sRGB gamut by default.
+
+        * UserInterface/Base/Setting.js:
+        * UserInterface/Models/Color.js:
+        (WI.Color):
+        (WI.Color.rgb2hsv):
+        (WI.Color.hsv2rgb.fraction):
+        (WI.Color.hsv2rgb):
+        (WI.Color.prototype.get hsla):
+        (WI.Color.prototype.isKeyword):
+
+        * UserInterface/Views/ColorPicker.css:
+        (.color-picker > .hue):
+        (@media (color-gamut: p3)):
+        (.color-picker.gamut-p3 > .hue):
+
+        * UserInterface/Views/ColorPicker.js:
+        (WI.ColorPicker.prototype._updateColor):
+        (WI.ColorPicker.prototype._updateOpacitySlider):
+        (WI.ColorPicker.prototype._handleFormatChange):
+        Introduce `gamut` parameter. Previously, the only available `gamut` was sRGB.
+
+        * UserInterface/Views/ColorSquare.css:
+        (.color-square > .crosshair):
+        Update the crosshair style to look better for both light and dark backgrounds.
+
+        * UserInterface/Views/ColorSquare.js:
+        (WI.ColorSquare):
+        (WI.ColorSquare.prototype.get tintedColor):
+        (WI.ColorSquare.prototype.set tintedColor):
+        (WI.ColorSquare.prototype._setCrosshairPosition):
+        (WI.ColorSquare.prototype._updateBaseColor):
+        (WI.ColorSquare.prototype._updateCrosshairBackground):
+
+        * UserInterface/Views/InlineSwatch.js:
+        Make p3 color picker a preview (e.g. STP-only) feature.
+
 2019-11-05  Ross Kirsling  <ross.kirsling@sony.com>
 
         Web Inspector: Layers: enable tab by default
index 183ddfb..97c9db8 100644 (file)
@@ -211,7 +211,9 @@ WI.settings = {
     debugLayoutDirection: new WI.Setting("debug-layout-direction-override", "system"),
 };
 
-WI.previewFeatures = [];
+WI.previewFeatures = [
+    "p3-gamut-color-picker" // FIXME: <https://webkit.org/b/203931> Web Inspector: Enable p3 color picker by default
+];
 
 WI.isTechnologyPreviewBuild = function()
 {
index cc6a877..6ac9b7f 100644 (file)
@@ -32,7 +32,9 @@ WI.Color = class Color
     constructor(format, components, gamut)
     {
         this.format = format;
-        this.gamut = gamut || "srgb";
+
+        console.assert(gamut === undefined || Object.values(WI.Color.Gamut).includes(gamut));
+        this.gamut = gamut || WI.Color.Gamut.SRGB;
 
         if (components.length === 3)
             components.push(1);
@@ -178,7 +180,7 @@ WI.Color = class Color
                 return null;
 
             let gamut = components[0].toLowerCase();
-            if (gamut !== "srgb" && gamut !== "display-p3")
+            if (!Object.values(WI.Color.Gamut).includes(gamut))
                 return null;
 
             let alpha = 1;
@@ -298,6 +300,54 @@ WI.Color = class Color
         return [h, saturation * 100, l * 100];
     }
 
+    // https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
+    static rgb2hsv(r, g, b)
+    {
+        r = Number.constrain(r, 0, 1);
+        g = Number.constrain(g, 0, 1);
+        b = Number.constrain(b, 0, 1);
+
+        let max = Math.max(r, g, b);
+        let min = Math.min(r, g, b);
+        let h = 0;
+        let delta = max - min;
+        let s = max === 0 ? 0 : delta / max;
+        let v = max;
+
+        if (max === min)
+            h = 0; // Grayscale.
+        else {
+            switch (max) {
+            case r:
+                h = ((g - b) / delta) + ((g < b) ? 6 : 0);
+                break;
+            case g:
+                h = ((b - r) / delta) + 2;
+                break;
+            case b:
+                h = ((r - g) / delta) + 4;
+                break;
+            }
+            h /= 6;
+        }
+
+        return [h * 360, s * 100, v * 100];
+    }
+
+    // https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative
+    static hsv2rgb(h, s, v)
+    {
+        h = Number.constrain(h, 0, 360);
+        s = Number.constrain(s, 0, 100) / 100;
+        v = Number.constrain(v, 0, 100) / 100;
+
+        function fraction(n) {
+            let k = (n + (h / 60)) % 6;
+            return v - (v * s * Math.max(Math.min(k, 4 - k, 1), 0));
+        }
+        return [fraction(5), fraction(3), fraction(1)];
+    }
+
     static cmyk2rgb(c, m, y, k)
     {
         c = Number.constrain(c, 0, 1);
@@ -399,8 +449,19 @@ WI.Color = class Color
 
     get hsla()
     {
-        if (!this._hsla)
-            this._hsla = this._rgbaToHSLA(this.rgba);
+        if (!this._hsla) {
+            let rgba = this.rgba;
+            if (this.format === WI.Color.Format.ColorFunction) {
+                rgba = [
+                    rgba[0] * 255,
+                    rgba[1] * 255,
+                    rgba[2] * 255,
+                    rgba[3],
+                ];
+            }
+            this._hsla = this._rgbaToHSLA(rgba);
+        }
+
         return this._hsla;
     }
 
@@ -461,7 +522,7 @@ WI.Color = class Color
         if (this.keyword)
             return true;
 
-        if (this.gamut !== "srgb")
+        if (this.gamut !== WI.Color.Gamut.SRGB)
             return false;
 
         if (!this.simple)
@@ -665,6 +726,11 @@ WI.Color.Format = {
     ColorFunction: "color-format-color-function",
 };
 
+WI.Color.Gamut = {
+    SRGB: "srgb",
+    DisplayP3: "display-p3",
+};
+
 WI.Color.FunctionNames = new Set([
     "rgb",
     "rgba",
index f2c7fc6..e198b18 100644 (file)
 }
 
 .color-picker > .hue {
-    background-image: linear-gradient(to right, red 0%, yellow 16.6%, lime 33.2%, aqua 50%, blue 66.6%, fuchsia 83.2%, red 100%);
+    background-image: linear-gradient(to right,
+        red 0%,
+        yellow 16.6%,
+        lime 33.2%,
+        cyan 50%,
+        blue 66.6%,
+        fuchsia 83.2%,
+        red 100%
+    );
+}
+
+@media (color-gamut: p3) {
+    .color-picker.gamut-p3 > .hue {
+        background-image: linear-gradient(to right,
+            color(display-p3 1 0 0) 0%,
+            color(display-p3 1 1 0) 16.6%,
+            color(display-p3 0 1 0) 33.2%,
+            color(display-p3 0 1 1) 50%,
+            color(display-p3 0 0 1) 66.6%,
+            color(display-p3 1 0 1) 83.2%,
+            color(display-p3 1 0 0) 100%
+        );
+    }
 }
 
 body[dir=ltr] .color-picker > .hue {
index 102a706..818a666 100644 (file)
@@ -60,6 +60,7 @@ WI.ColorPicker = class ColorPicker extends WI.Object
             return {containerElement, numberInputElement};
         };
 
+        // FIXME: <https://webkit.org/b/203928> Web Inspector: Show RGBA input fields for p3 color picker
         this._colorInputs = new Map([
             ["R", createColorInput("R", {max: 255})],
             ["G", createColorInput("G", {max: 255})],
@@ -167,6 +168,7 @@ WI.ColorPicker = class ColorPicker extends WI.Object
         let opacity = Math.round(this._opacity * 100) / 100;
 
         let format = this._color.format;
+        let gamut = this._color.gamut;
         let components = null;
         if (format === WI.Color.Format.HSL || format === WI.Color.Format.HSLA) {
             components = this._colorSquare.tintedColor.hsl.concat(opacity);
@@ -180,7 +182,7 @@ WI.ColorPicker = class ColorPicker extends WI.Object
 
         let formatChanged = this._color.format === format;
 
-        this._color = new WI.Color(format, components);
+        this._color = new WI.Color(format, components, gamut);
 
         this._showColorComponentInputs();
 
@@ -193,8 +195,10 @@ WI.ColorPicker = class ColorPicker extends WI.Object
     _updateOpacitySlider()
     {
         let rgb = this._colorSquare.tintedColor.rgb;
-        let opaque = new WI.Color(WI.Color.Format.RGBA, rgb.concat(1)).toString();
-        let transparent = new WI.Color(WI.Color.Format.RGBA, rgb.concat(0)).toString();
+        let gamut = this._colorSquare.tintedColor.gamut;
+        let format = gamut === WI.Color.Gamut.DisplayP3 ? WI.Color.Format.ColorFunction : WI.Color.Format.RGBA;
+        let opaque = new WI.Color(format, rgb.concat(1), gamut).toString();
+        let transparent = new WI.Color(format, rgb.concat(0), gamut).toString();
         this._opacitySlider.element.style.setProperty("background-image", "linear-gradient(90deg, " + transparent + ", " + opaque + "), " + this._opacityPattern);
     }
 
@@ -210,6 +214,8 @@ WI.ColorPicker = class ColorPicker extends WI.Object
             && this._color.format !== WI.Color.Format.HSL
             && this._color.format !== WI.Color.Format.HSLA);
 
+        this._element.classList.toggle("gamut-p3", this._color.gamut === WI.Color.Gamut.DisplayP3);
+
         this.dispatchEventToListeners(WI.ColorPicker.Event.FormatChanged);
     }
 
index a7ca705..4796c26 100644 (file)
 
 .color-square > .crosshair {
     position: absolute;
-    top: calc(-1 * var(--crosshair-size) / 2);
-    left: calc(-1 * var(--crosshair-size) / 2);
+    top: calc(-1 * (var(--crosshair-size) + var(--border-width)) / 2);
+    left: calc(-1 * (var(--crosshair-size) + var(--border-width)) / 2);
     width: var(--crosshair-size);
     height: var(--crosshair-size);
-    background-color: white;
-    border: 0.5px solid black;
+    border: var(--border-width) solid white;
+    box-shadow: 0 0 2px black;
     border-radius: 3px;
     pointer-events: none;
 
+    --border-width: 1px;
     --crosshair-size: 7px;
 }
index 2cea10f..cbc46fb 100644 (file)
@@ -32,6 +32,7 @@ WI.ColorSquare = class ColorSquare
         this._hue = 0;
         this._x = 0;
         this._y = 0;
+        this._gamut = null;
         this._crosshairPosition = null;
 
         this._element = document.createElement("div");
@@ -80,6 +81,12 @@ WI.ColorSquare = class ColorSquare
     get tintedColor()
     {
         if (this._crosshairPosition) {
+            if (this._gamut === WI.Color.Gamut.DisplayP3) {
+                let rgb = WI.Color.hsv2rgb(this._hue, this._saturation, this._brightness);
+                rgb = rgb.map(((x) => Math.roundTo(x, 0.001)));
+                return new WI.Color(WI.Color.Format.ColorFunction, rgb, this._gamut);
+            }
+
             let hsl = WI.Color.hsv2hsl(this._hue, this._saturation, this._brightness);
             return new WI.Color(WI.Color.Format.HSL, hsl);
         }
@@ -90,26 +97,31 @@ WI.ColorSquare = class ColorSquare
     set tintedColor(tintedColor)
     {
         console.assert(tintedColor instanceof WI.Color);
-        let hsl = tintedColor.hsl;
-        let saturation = Number.constrain(hsl[1], 0, 100);
-        let x = saturation / 100 * this._dimension;
 
-        let lightness = hsl[2];
+        this._gamut = tintedColor.gamut;
 
-        // The color picker is HSB-based. (HSB is also known as HSV.)
-        // Derive lightness by using HSB to HSL equation.
-        let y = 2 * lightness / (200 - saturation);
-        y = -1 * (y - 1) * this._dimension;
+        if (tintedColor.format === WI.Color.Format.ColorFunction) {
+            // CSS color function only supports RGB. It doesn't support HSL.
+            let hsv = WI.Color.rgb2hsv(...tintedColor.rgb);
+            let x = hsv[1] / 100 * this._dimension;
+            let y = (1 - (hsv[2] / 100)) * this._dimension;
+            this._setCrosshairPosition(new WI.Point(x, y));
+        } else {
+            let hsl = tintedColor.hsl;
+            let saturation = Number.constrain(hsl[1], 0, 100);
+            let x = saturation / 100 * this._dimension;
 
-        this._setCrosshairPosition(new WI.Point(x, y));
-    }
+            let lightness = hsl[2];
 
-    get rawColor()
-    {
-        if (this._crosshairPosition)
-            return new WI.Color(WI.Color.Format.HSL, [this._hue, this._saturation, 50]);
+            // The color picker is HSV-based. (HSV is also known as HSB.)
+            // Derive lightness by using HSV to HSL equation.
+            let y = 2 * lightness / (200 - saturation);
+            y = -1 * (y - 1) * this._dimension;
 
-        return new WI.Color(WI.Color.Format.HSLA, WI.Color.Keywords.transparent);
+            this._setCrosshairPosition(new WI.Point(x, y));
+        }
+
+        this._updateBaseColor();
     }
 
     // Protected
@@ -182,10 +194,23 @@ WI.ColorSquare = class ColorSquare
         this._x = Number.constrain(Math.round(point.x), 0, this._dimension);
         this._y = Number.constrain(Math.round(point.y), 0, this._dimension);
         this._crosshairElement.style.setProperty("transform", `translate(${this._x}px, ${this._y}px)`);
+
+        this._updateCrosshairBackground();
     }
 
     _updateBaseColor()
     {
-        this._element.style.backgroundColor = `hsl(${this._hue}, 100%, 50%)`;
+        if (this._gamut === WI.Color.Gamut.DisplayP3) {
+            let [r, g, b] = WI.Color.hsl2rgb(this._hue, 100, 50);
+            this._element.style.backgroundColor = `color(display-p3 ${r / 255} ${g / 255} ${b / 255})`;
+        } else
+            this._element.style.backgroundColor = `hsl(${this._hue}, 100%, 50%)`;
+
+        this._updateCrosshairBackground();
+    }
+
+    _updateCrosshairBackground()
+    {
+        this._crosshairElement.style.backgroundColor = this.tintedColor.toString();
     }
 };
index c20a692..fff2311 100644 (file)
@@ -159,6 +159,7 @@ WI.InlineSwatch = class InlineSwatch extends WI.Object
         if (event.shiftKey && value) {
             if (this._type === WI.InlineSwatch.Type.Color) {
                 let nextFormat = value.nextFormat();
+                // FIXME: <https://webkit.org/b/203534> Provide UI to convert between sRGB and p3 color spaces
                 console.assert(nextFormat);
                 if (nextFormat) {
                     value.format = nextFormat;
@@ -178,6 +179,9 @@ WI.InlineSwatch = class InlineSwatch extends WI.Object
         if (this._valueEditor)
             return;
 
+        if (!WI.arePreviewFeaturesEnabled() && value.format === WI.Color.Format.ColorFunction)
+            return;
+
         if (!value)
             value = this._fallbackValue();