Web Inspector: Provide UI to convert between sRGB and p3 color spaces
https://bugs.webkit.org/show_bug.cgi?id=203534
<rdar://problem/56688523>

Reviewed by Devin Rousso.

Source/WebInspectorUI:

Add context menus:
- "Convert to sRGB" and "Clamp to sRGB" for p3 colors, such as `color(display-p3 0 1 0)`.
- "Convert to Display-P3" for sRGB colors, such as `rgb(0, 255, 0)`.

Shift-clicking the color swatch of sRGB colors now goes through the color function syntax as well.
Shift-clicking the color swatch of Display-P3 colors converts the color to sRGB when it can be lossless.
When the convertion cannot be lossless, Web Inspector beeps.

* Localizations/en.lproj/localizedStrings.js:
* UserInterface/Base/Utilities.js:
* UserInterface/Models/Color.js:
(WI.Color):
Introduce `_normalizedRGB` property, which stores rgb values from 0 to 1.
Previously, `_rgba` stored values from 0 to 1 for color function format, and from 0 to 255 otherwise.
That required format checks before every `rgb` value access and resulted in silent errors when
the values were in the wrong format.

Store alpha as a separate property to simplify format conversion. Previously, alpha was duplicated between `_rgba` and `_hsla`.

(WI.Color.displayP3toSRGB):
(WI.Color.srgbToDisplayP3): Added.
(WI.Color.prototype.nextFormat):
(WI.Color.prototype.get rgb):
(WI.Color.prototype.get hsl):
(WI.Color.prototype.get normalizedRGB):
(WI.Color.prototype.get rgba):
(WI.Color.prototype.get hsla):
(WI.Color.prototype.get normalizedRGBA):
(WI.Color.prototype.get gamut):
(WI.Color.prototype.set gamut):
(WI.Color.prototype.copy):
(WI.Color.prototype.isKeyword):
(WI.Color.prototype.isOutsideSRGB): Added.
(WI.Color.prototype.canBeSerializedAsShortHEX):
(WI.Color.prototype._toKeywordString):
(WI.Color.prototype._toShortHEXString):
(WI.Color.prototype._toHEXString):
(WI.Color.prototype._toShortHEXAlphaString):
(WI.Color.prototype._toHEXAlphaString):
(WI.Color.prototype._toRGBString):
(WI.Color.prototype._toRGBAString):
(WI.Color.prototype._toFunctionString):
Limit the values to 4 decimals.

(WI.Color.prototype._toHSLString):
(WI.Color.prototype._toHSLAString):
(WI.Color.prototype._hslToRGB):
* UserInterface/Views/ColorPicker.js:
(WI.ColorPicker.prototype._updateColor):
(WI.ColorPicker.prototype._updateOpacitySlider):
* UserInterface/Views/ColorSquare.css:
(.color-square > .svg-root):
(.color-square > .svg-root > .srgb-edge):
(.color-square > .srgb-label):
(.color-square > .srgb-label:hover):
(.color-square > .srgb-label:hover + .svg-root > .srgb-edge):
(@media (-webkit-device-pixel-ratio: 1)):
(.color-square > .srgb-edge):
* UserInterface/Views/ColorSquare.js:
(WI.ColorSquare.prototype.set tintedColor):
(WI.ColorSquare.prototype._drawSRGBOutline):
(WI.ColorSquare):

* UserInterface/Views/InlineSwatch.js:
(WI.InlineSwatch):
(WI.InlineSwatch.prototype._updateSwatch):
(WI.InlineSwatch.prototype._allowShiftClickColor): Added.
(WI.InlineSwatch.prototype._handleContextMenuEvent):

LayoutTests:

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


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@253018 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 1186d2d..8bbec85 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,14 @@
+2019-12-02  Nikita Vasilyev  <nvasilyev@apple.com>
+
+        Web Inspector: Provide UI to convert between sRGB and p3 color spaces
+        https://bugs.webkit.org/show_bug.cgi?id=203534
+        <rdar://problem/56688523>
+
+        Reviewed by Devin Rousso.
+
+        * inspector/model/color-expected.txt:
+        * inspector/model/color.html:
+
 2019-12-02  Said Abou-Hallawa  <sabouhallawa@apple.com>
 
         Crash when animating an enum attribute for multiple instances of an SVG element
diff --git a/LayoutTests/inspector/model/color-expected.txt b/LayoutTests/inspector/model/color-expected.txt
index 89267b9..93cf93a 100644
--- a/LayoutTests/inspector/model/color-expected.txt
+++ b/LayoutTests/inspector/model/color-expected.txt
@@ -396,12 +396,12 @@
 PASS: '#11223345' should not be serializable as a short Hex
 PASS: 'color(display-p3 0 1 0.5)' color should have a default alpha of 1
 PASS: color function is not a keyword
-PASS: 'color(display-p3 0 1 0.5)' has rgb of [0, 1, 0.5]
-PASS: 'color(display-p3 0 1 0.5)' has rgba of [0, 1, 0.5, 1]
+PASS: 'color(display-p3 0 1 0.5)' has normalized RGB of [0, 1, 0.5]
+PASS: 'color(display-p3 0 1 0.5)' has normalized RGBA of [0, 1, 0.5, 1]
 PASS: 'color(display-p3 0 1 0.5 / 0.3)' should have an alpha of 0.3
 PASS: color function is not a keyword
-PASS: 'color(display-p3 0 1 0.5 / 0.3)' has rgb of [0, 1, 0.5]
-PASS: 'color(display-p3 0 1 0.5 / 0.3)' has rgba of [0, 1, 0.5, 0.3]
+PASS: 'color(display-p3 0 1 0.5 / 0.3)' has normalized RGB of [0, 1, 0.5]
+PASS: 'color(display-p3 0 1 0.5 / 0.3)' has normalized RGBA of [0, 1, 0.5, 0.3]
 
 -- Running test case: WI.Color from components
 Check components for color 'rgb(255, 0, 0)'.
@@ -520,10 +520,26 @@
 
 -- Running test case: WI.Color.displayP3toSRGB
 PASS: Should convert [0,0,0] to [0,0,0].
-PASS: Should convert [1,1,1] to [0.9999300658875597,1.0000101408119602,1.0001054916006267].
-PASS: Should convert [1,0,0] to [1.0929902391315327,-0.5433883521407902,-0.2537782522695441].
-PASS: Should convert [0,1,0] to [-2.9057643199691516,1.0182759798644396,-1.0162215225472337].
-PASS: Should convert [0,0,1] to [2.3902361583783006e-7,-2.0487738563140788e-7,1.0421313171983129].
-PASS: Should convert [2,0,0] to [1.0929902391315327,-0.5433883521407902,-0.2537782522695441].
+PASS: Should convert [1,1,1] to [0.9999,1,1.0001].
+PASS: Should convert [1,0,0] to [1.093,-0.5434,-0.2538].
+PASS: Should convert [0,1,0] to [-2.9058,1.0183,-1.0162].
+PASS: Should convert [0,0,1] to [0,0,1.0421].
+PASS: Should convert [2,0,0] to [1.093,-0.5434,-0.2538].
 PASS: Should convert [-1,0,0] to [0,0,0].
 
+-- Running test case: WI.Color.srgbToDisplayP3
+PASS: Should convert [0,0,0] to [0,0,0].
+PASS: Should convert [1,1,1] to [1.0001,1,0.9999].
+PASS: Should convert [1,0,0] to [0.9176,0.2003,0.1386].
+PASS: Should convert [0,1,0] to [0.4584,0.9853,0.2983].
+PASS: Should convert [0,0,1] to [0,0,0.9595].
+PASS: Should convert [2,0,0] to [0.9176,0.2003,0.1386].
+PASS: Should convert [-1,0,0] to [0,0,0].
+
+-- Running test case: WI.Color.isOutsideSRGB
+"color(display-p3 0 0 0)" is inside sRGB.
+"color(display-p3 1 1 1)" is inside sRGB.
+"color(display-p3 0.04 0.14 0.016)" is inside sRGB.
+"color(display-p3 1 0 0)" is outside sRGB.
+"color(display-p3 0.93 0.353 0.353)" is outside sRGB.
+
diff --git a/LayoutTests/inspector/model/color.html b/LayoutTests/inspector/model/color.html
index 1dd4b6d..040d2f7 100644
--- a/LayoutTests/inspector/model/color.html
+++ b/LayoutTests/inspector/model/color.html
@@ -323,14 +323,14 @@
             color = WI.Color.fromString("color(display-p3 0 1 0.5)");
             InspectorTest.expectThat(color.alpha === 1, "'color(display-p3 0 1 0.5)' color should have a default alpha of 1");
             InspectorTest.expectThat(color.isKeyword() === false, "color function is not a keyword");
-            InspectorTest.expectShallowEqual(color.rgb, [0, 1, 0.5], "'color(display-p3 0 1 0.5)' has rgb of [0, 1, 0.5]");
-            InspectorTest.expectShallowEqual(color.rgba, [0, 1, 0.5, 1], "'color(display-p3 0 1 0.5)' has rgba of [0, 1, 0.5, 1]");
+            InspectorTest.expectShallowEqual(color.normalizedRGB, [0, 1, 0.5], "'color(display-p3 0 1 0.5)' has normalized RGB of [0, 1, 0.5]");
+            InspectorTest.expectShallowEqual(color.normalizedRGBA, [0, 1, 0.5, 1], "'color(display-p3 0 1 0.5)' has normalized RGBA of [0, 1, 0.5, 1]");
 
             color = WI.Color.fromString("color(display-p3 0 1 0.5 / 0.3)");
             InspectorTest.expectThat(color.alpha === 0.3, "'color(display-p3 0 1 0.5 / 0.3)' should have an alpha of 0.3");
             InspectorTest.expectThat(color.isKeyword() === false, "color function is not a keyword");
-            InspectorTest.expectShallowEqual(color.rgb, [0, 1, 0.5], "'color(display-p3 0 1 0.5 / 0.3)' has rgb of [0, 1, 0.5]");
-            InspectorTest.expectShallowEqual(color.rgba, [0, 1, 0.5, 0.3], "'color(display-p3 0 1 0.5 / 0.3)' has rgba of [0, 1, 0.5, 0.3]");
+            InspectorTest.expectShallowEqual(color.normalizedRGB, [0, 1, 0.5], "'color(display-p3 0 1 0.5 / 0.3)' has normalized RGB of [0, 1, 0.5]");
+            InspectorTest.expectShallowEqual(color.normalizedRGBA, [0, 1, 0.5, 0.3], "'color(display-p3 0 1 0.5 / 0.3)' has normalized RGBA of [0, 1, 0.5, 0.3]");
 
             return true;
         }
@@ -402,6 +402,7 @@
             // All with alpha.
             test("transparent", [
                 WI.Color.Format.RGBA,
+                WI.Color.Format.ColorFunction,
                 WI.Color.Format.HSLA,
                 WI.Color.Format.Keyword,
                 WI.Color.Format.ShortHEXAlpha,
@@ -411,6 +412,7 @@
             // All without alpha.
             test("red", [
                 WI.Color.Format.RGB,
+                WI.Color.Format.ColorFunction,
                 WI.Color.Format.HSL,
                 WI.Color.Format.Keyword,
                 WI.Color.Format.ShortHEX,
@@ -420,6 +422,7 @@
             // No short hex or keyword.
             test("rgb(100, 150, 200)", [
                 WI.Color.Format.RGB,
+                WI.Color.Format.ColorFunction,
                 WI.Color.Format.HSL,
                 WI.Color.Format.HEX,
             ]);
@@ -427,6 +430,7 @@
             // No short hex alpha or keyword.
             test("rgba(100, 150, 200, 0.5)", [
                 WI.Color.Format.RGBA,
+                WI.Color.Format.ColorFunction,
                 WI.Color.Format.HSLA,
                 WI.Color.Format.HEXAlpha,
             ]);
@@ -617,19 +621,57 @@
         description: "Test conversion from display-P3 gamut to sRGB.",
         test() {
             testColorConversion(WI.Color.displayP3toSRGB, [0, 0, 0], [0, 0, 0]);
-            testColorConversion(WI.Color.displayP3toSRGB, [1, 1, 1], [0.9999300658875597, 1.0000101408119602, 1.0001054916006267]);
-            testColorConversion(WI.Color.displayP3toSRGB, [1, 0, 0], [1.0929902391315327, -0.5433883521407902, -0.2537782522695441]);
-            testColorConversion(WI.Color.displayP3toSRGB, [0, 1, 0], [-2.9057643199691516, 1.0182759798644396, -1.0162215225472337]);
-            testColorConversion(WI.Color.displayP3toSRGB, [0, 0, 1], [2.3902361583783006e-7, -2.0487738563140788e-7, 1.0421313171983129]);
+            testColorConversion(WI.Color.displayP3toSRGB, [1, 1, 1], [0.9999, 1, 1.0001]);
+            testColorConversion(WI.Color.displayP3toSRGB, [1, 0, 0], [1.093, -0.5434, -0.2538]);
+            testColorConversion(WI.Color.displayP3toSRGB, [0, 1, 0], [-2.9058, 1.0183, -1.0162]);
+            testColorConversion(WI.Color.displayP3toSRGB, [0, 0, 1], [0, 0, 1.0421]);
 
             // Out-of-bounds inputs.
-            testColorConversion(WI.Color.displayP3toSRGB, [2, 0, 0], [1.0929902391315327, -0.5433883521407902, -0.2537782522695441]);
+            testColorConversion(WI.Color.displayP3toSRGB, [2, 0, 0], [1.093, -0.5434, -0.2538]);
             testColorConversion(WI.Color.displayP3toSRGB, [-1, 0, 0], [0, 0, 0]);
 
             return true;
         }
     });
 
+    suite.addTestCase({
+        name: "WI.Color.srgbToDisplayP3",
+        description: "Test conversion from sRGB gamut to display-P3.",
+        test() {
+            testColorConversion(WI.Color.srgbToDisplayP3, [0, 0, 0], [0, 0, 0]);
+            testColorConversion(WI.Color.srgbToDisplayP3, [1, 1, 1], [1.0001, 1, 0.9999]);
+            testColorConversion(WI.Color.srgbToDisplayP3, [1, 0, 0], [0.9176, 0.2003, 0.1386]);
+            testColorConversion(WI.Color.srgbToDisplayP3, [0, 1, 0], [0.4584, 0.9853, 0.2983]);
+            testColorConversion(WI.Color.srgbToDisplayP3, [0, 0, 1], [0, 0, 0.9595]);
+
+            // Out-of-bounds inputs.
+            testColorConversion(WI.Color.srgbToDisplayP3, [2, 0, 0], [0.9176, 0.2003, 0.1386]);
+            testColorConversion(WI.Color.srgbToDisplayP3, [-1, 0, 0], [0, 0, 0]);
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "WI.Color.isOutsideSRGB",
+        description: "Test conversion from sRGB gamut to display-P3.",
+        test() {
+            function test(string) {
+                let color = WI.Color.fromString(string);
+                let result = color.isOutsideSRGB();
+                InspectorTest.log(`"${string}" is ${result ? "outside" : "inside"} sRGB.`);
+            }
+
+            test("color(display-p3 0 0 0)");
+            test("color(display-p3 1 1 1)");
+            test("color(display-p3 0.04 0.14 0.016)"); // Barely inside sRGB. Values chosen to test rounding behavior.
+            test("color(display-p3 1 0 0)");
+            test("color(display-p3 0.93 0.353 0.353)"); // Barely outside sRGB.
+
+            return true;
+        }
+    });
+
     suite.runTestCasesAndFinish();
 }
 </script>
diff --git a/Source/WebInspectorUI/ChangeLog b/Source/WebInspectorUI/ChangeLog
index 9b37f79..5e1da34 100644
--- a/Source/WebInspectorUI/ChangeLog
+++ b/Source/WebInspectorUI/ChangeLog
@@ -1,3 +1,80 @@
+2019-12-02  Nikita Vasilyev  <nvasilyev@apple.com>
+
+        Web Inspector: Provide UI to convert between sRGB and p3 color spaces
+        https://bugs.webkit.org/show_bug.cgi?id=203534
+        <rdar://problem/56688523>
+
+        Reviewed by Devin Rousso.
+
+        Add context menus:
+        - "Convert to sRGB" and "Clamp to sRGB" for p3 colors, such as `color(display-p3 0 1 0)`.
+        - "Convert to Display-P3" for sRGB colors, such as `rgb(0, 255, 0)`.
+
+        Shift-clicking the color swatch of sRGB colors now goes through the color function syntax as well.
+        Shift-clicking the color swatch of Display-P3 colors converts the color to sRGB when it can be lossless.
+        When the convertion cannot be lossless, Web Inspector beeps.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        * UserInterface/Base/Utilities.js:
+        * UserInterface/Models/Color.js:
+        (WI.Color):
+        Introduce `_normalizedRGB` property, which stores rgb values from 0 to 1.
+        Previously, `_rgba` stored values from 0 to 1 for color function format, and from 0 to 255 otherwise.
+        That required format checks before every `rgb` value access and resulted in silent errors when
+        the values were in the wrong format.
+
+        Store alpha as a separate property to simplify format conversion. Previously, alpha was duplicated between `_rgba` and `_hsla`.
+
+        (WI.Color.displayP3toSRGB):
+        (WI.Color.srgbToDisplayP3): Added.
+        (WI.Color.prototype.nextFormat):
+        (WI.Color.prototype.get rgb):
+        (WI.Color.prototype.get hsl):
+        (WI.Color.prototype.get normalizedRGB):
+        (WI.Color.prototype.get rgba):
+        (WI.Color.prototype.get hsla):
+        (WI.Color.prototype.get normalizedRGBA):
+        (WI.Color.prototype.get gamut):
+        (WI.Color.prototype.set gamut):
+        (WI.Color.prototype.copy):
+        (WI.Color.prototype.isKeyword):
+        (WI.Color.prototype.isOutsideSRGB): Added.
+        (WI.Color.prototype.canBeSerializedAsShortHEX):
+        (WI.Color.prototype._toKeywordString):
+        (WI.Color.prototype._toShortHEXString):
+        (WI.Color.prototype._toHEXString):
+        (WI.Color.prototype._toShortHEXAlphaString):
+        (WI.Color.prototype._toHEXAlphaString):
+        (WI.Color.prototype._toRGBString):
+        (WI.Color.prototype._toRGBAString):
+        (WI.Color.prototype._toFunctionString):
+        Limit the values to 4 decimals.
+
+        (WI.Color.prototype._toHSLString):
+        (WI.Color.prototype._toHSLAString):
+        (WI.Color.prototype._hslToRGB):
+        * UserInterface/Views/ColorPicker.js:
+        (WI.ColorPicker.prototype._updateColor):
+        (WI.ColorPicker.prototype._updateOpacitySlider):
+        * UserInterface/Views/ColorSquare.css:
+        (.color-square > .svg-root):
+        (.color-square > .svg-root > .srgb-edge):
+        (.color-square > .srgb-label):
+        (.color-square > .srgb-label:hover):
+        (.color-square > .srgb-label:hover + .svg-root > .srgb-edge):
+        (@media (-webkit-device-pixel-ratio: 1)):
+        (.color-square > .srgb-edge):
+        * UserInterface/Views/ColorSquare.js:
+        (WI.ColorSquare.prototype.set tintedColor):
+        (WI.ColorSquare.prototype._drawSRGBOutline):
+        (WI.ColorSquare):
+
+        * UserInterface/Views/InlineSwatch.js:
+        (WI.InlineSwatch):
+        (WI.InlineSwatch.prototype._updateSwatch):
+        (WI.InlineSwatch.prototype._allowShiftClickColor): Added.
+        (WI.InlineSwatch.prototype._handleContextMenuEvent):
+
 2019-12-02  Devin Rousso  <drousso@apple.com>
 
         Web Inspector: Console: the saved result value is still shown after page reload
diff --git a/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js b/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
index 9c49ad7..d3ca407 100644
--- a/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
+++ b/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
@@ -240,6 +240,7 @@
 localizedStrings["Child added to "] = "Child added to ";
 localizedStrings["Children"] = "Children";
 localizedStrings["Cipher"] = "Cipher";
+localizedStrings["Clamp to sRGB"] = "Clamp to sRGB";
 localizedStrings["Classes"] = "Classes";
 localizedStrings["Clear Filters"] = "Clear Filters";
 localizedStrings["Clear Log"] = "Clear Log";
@@ -254,6 +255,7 @@
 localizedStrings["Click to close this tab"] = "Click to close this tab";
 localizedStrings["Click to create a Local Override from this content"] = "Click to create a Local Override from this content";
 localizedStrings["Click to import a file and create a Local Override\nShift-click to create a Local Override from this content"] = "Click to import a file and create a Local Override\nShift-click to create a Local Override from this content";
+localizedStrings["Click to select a color"] = "Click to select a color";
 localizedStrings["Click to select a color\nShift-click to switch color formats"] = "Click to select a color\nShift-click to switch color formats";
 localizedStrings["Click to view variable value\nShift-click to replace variable with value"] = "Click to view variable value\nShift-click to replace variable with value";
 localizedStrings["Clickable"] = "Clickable";
@@ -304,6 +306,8 @@
 localizedStrings["Continue to Here"] = "Continue to Here";
 localizedStrings["Continue without automatically stopping"] = "Continue without automatically stopping";
 localizedStrings["Controls"] = "Controls";
+localizedStrings["Convert to Display-P3"] = "Convert to Display-P3";
+localizedStrings["Convert to sRGB"] = "Convert to sRGB";
 localizedStrings["Cookies"] = "Cookies";
 localizedStrings["Copy"] = "Copy";
 localizedStrings["Copy Action"] = "Copy Action";
@@ -407,6 +411,8 @@
 localizedStrings["Dynamically calculated for the parent element"] = "Dynamically calculated for the parent element";
 localizedStrings["Dynamically calculated for the selected element"] = "Dynamically calculated for the selected element";
 localizedStrings["Dynamically calculated for the selected element and did not match"] = "Dynamically calculated for the selected element and did not match";
+/* Label for a guide within the color picker */
+localizedStrings["Edge of sRGB color space"] = "Edge of sRGB color space";
 localizedStrings["Edit"] = "Edit";
 localizedStrings["Edit Breakpoint\u2026"] = "Edit Breakpoint\u2026";
 localizedStrings["Edit Local Override\u2026"] = "Edit Local Override\u2026";
@@ -540,6 +546,7 @@
 localizedStrings["Forced Layout"] = "Forced Layout";
 /* A context menu item to force (override) a DOM node's pseudo-classes */
 localizedStrings["Forced Pseudo-Classes"] = "Forced Pseudo-Classes";
+localizedStrings["Format: Color Function"] = "Format: Color Function";
 localizedStrings["Format: HSL"] = "Format: HSL";
 localizedStrings["Format: HSLA"] = "Format: HSLA";
 localizedStrings["Format: Hex"] = "Format: Hex";
@@ -1175,8 +1182,6 @@
 localizedStrings["This is what the result of a test that threw an error with no data looks like."] = "This is what the result of a test that threw an error with no data looks like.";
 localizedStrings["This is what the result of a warning test with no data looks like."] = "This is what the result of a warning test with no data looks like.";
 localizedStrings["This is what the result of an unsupported test with no data looks like."] = "This is what the result of an unsupported test with no data looks like.";
-/* Label for a guide within the color picker */
-localizedStrings["This line marks the edge of sRGB color space"] = "This line marks the edge of sRGB color space";
 localizedStrings["This object is a root"] = "This object is a root";
 localizedStrings["This object is referenced by internal objects"] = "This object is referenced by internal objects";
 localizedStrings["This test will pass with a variety of accessibility information about the <body> element."] = "This test will pass with a variety of accessibility information about the <body> element.";
diff --git a/Source/WebInspectorUI/UserInterface/Base/Utilities.js b/Source/WebInspectorUI/UserInterface/Base/Utilities.js
index f88c562..0459622 100644
--- a/Source/WebInspectorUI/UserInterface/Base/Utilities.js
+++ b/Source/WebInspectorUI/UserInterface/Base/Utilities.js
@@ -1211,6 +1211,25 @@
     }
 });
 
+// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Matrix_math_for_the_web#Multiplying_a_matrix_and_a_point
+Object.defineProperty(Math, "multiplyMatrixByVector",
+{
+    value(matrix, vector)
+    {
+        let height = matrix.length;
+        let width = matrix[0].length;
+        console.assert(width === vector.length);
+
+        let result = Array(width).fill(0);
+        for (let i = 0; i < width; ++i) {
+            for (let rowIndex = 0; rowIndex < height; ++rowIndex)
+                result[i] += vector[rowIndex] * matrix[i][rowIndex];
+        }
+
+        return result;
+    }
+});
+
 Object.defineProperty(Number, "constrain",
 {
     value(num, min, max)
diff --git a/Source/WebInspectorUI/UserInterface/Models/Color.js b/Source/WebInspectorUI/UserInterface/Models/Color.js
index bb5e4d8..fbb3320 100644
--- a/Source/WebInspectorUI/UserInterface/Models/Color.js
+++ b/Source/WebInspectorUI/UserInterface/Models/Color.js
@@ -34,15 +34,21 @@
         this.format = format;
 
         console.assert(gamut === undefined || Object.values(WI.Color.Gamut).includes(gamut));
-        this.gamut = gamut || WI.Color.Gamut.SRGB;
+        this._gamut = gamut || WI.Color.Gamut.SRGB;
 
-        if (components.length === 3)
-            components.push(1);
+        console.assert(components.length === 3 || components.length === 4, components);
+        this.alpha = components.length === 4 ? components[3] : 1;
+
+        this._rgb = null;
+        this._normalizedRGB = null;
+        this._hsl = null;
 
         if (format === WI.Color.Format.HSL || format === WI.Color.Format.HSLA)
-            this._hsla = components;
+            this._hsl = components.slice(0, 3);
+        else if (format === WI.Color.Format.ColorFunction)
+            this._normalizedRGB = components.slice(0, 3);
         else
-            this._rgba = components;
+            this._rgb = components.slice(0, 3);
 
         this.valid = !components.some(isNaN);
     }
@@ -357,42 +363,60 @@
 
         let linearP3 = WI.Color._toLinearLight([r, g, b]);
 
-        // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Matrix_math_for_the_web#Multiplying_a_matrix_and_a_point
-        function multiplyMatrixByVector(matrix, vector) {
-            let height = matrix.length;
-            let width = matrix[0].length;
-            console.assert(width === vector.length);
-
-            let result = Array(width).fill(0);
-            for (let i = 0; i < width; i++)
-                for (let rowIndex = 0; rowIndex < height; rowIndex++)
-                    result[i] += vector[rowIndex] * matrix[i][rowIndex];
-
-            return result;
-        }
-
         // Convert an array of linear-light display-p3 values to CIE XYZ
         // using D65 (no chromatic adaptation).
         // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
-        let matrix = [
+        const rgbToXYZMatrix = [
             [0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
             [0.2289745640697488, 0.6917385218365064,  0.079286914093745],
-            [0.0000000000000000, 0.04511338185890264, 1.043944368900976]
+            [0.0000000000000000, 0.04511338185890264, 1.043944368900976],
         ];
-        let xyz = multiplyMatrixByVector(matrix, linearP3);
+        let xyz = Math.multiplyMatrixByVector(rgbToXYZMatrix, linearP3);
 
         // Convert XYZ to linear-light sRGB.
-        matrix = [
+        const xyzToLinearSRGBMatrix = [
             [ 3.2404542, -1.5371385, -0.4985314],
             [-0.9692660,  1.8760108,  0.0415560],
-            [ 0.0556434, -0.2040259,  1.0572252]
+            [ 0.0556434, -0.2040259,  1.0572252],
         ];
-        let linearSRGB = multiplyMatrixByVector(matrix, xyz);
+        let linearSRGB = Math.multiplyMatrixByVector(xyzToLinearSRGBMatrix, xyz);
 
-        return WI.Color._gammaCorrect(linearSRGB);
+        let srgb = WI.Color._gammaCorrect(linearSRGB);
+        return srgb.map((x) => x.maxDecimals(4));
+    }
+
+    // https://www.w3.org/TR/css-color-4/#color-conversion-code
+    static srgbToDisplayP3(r, g, b)
+    {
+        r = Number.constrain(r, 0, 1);
+        g = Number.constrain(g, 0, 1);
+        b = Number.constrain(b, 0, 1);
+
+        let linearSRGB = WI.Color._toLinearLight([r, g, b]);
+
+        // Convert an array of linear-light sRGB values to CIE XYZ
+        // using sRGB's own white, D65 (no chromatic adaptation)
+        // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
+        const linearSRGBtoXYZMatrix = [
+            [0.4124564,  0.3575761,  0.1804375],
+            [0.2126729,  0.7151522,  0.0721750],
+            [0.0193339,  0.1191920,  0.9503041],
+        ];
+        let xyz = Math.multiplyMatrixByVector(linearSRGBtoXYZMatrix, linearSRGB);
+
+        const xyzToLinearP3Matrix = [
+            [ 2.493496911941425,   -0.9313836179191239, -0.40271078445071684],
+            [-0.8294889695615747,   1.7626640603183463,  0.023624685841943577],
+            [ 0.03584583024378447, -0.07617238926804182, 0.9568845240076872],
+        ];
+        let linearP3 = Math.multiplyMatrixByVector(xyzToLinearP3Matrix, xyz);
+
+        let p3 = WI.Color._gammaCorrect(linearP3);
+        return p3.map((x) => x.maxDecimals(4));
     }
 
     // Convert gamma-corrected sRGB or Display-P3 to linear light form.
+    // https://www.w3.org/TR/css-color-4/#color-conversion-code
     static _toLinearLight(rgb)
     {
         return rgb.map(function(value) {
@@ -405,6 +429,7 @@
 
     // Convert linear-light sRGB or Display-P3 to gamma corrected form.
     // Inverse of `toLinearLight`.
+    // https://www.w3.org/TR/css-color-4/#color-conversion-code
     static _gammaCorrect(rgb)
     {
         return rgb.map(function(value) {
@@ -456,7 +481,12 @@
 
         case WI.Color.Format.RGB:
         case WI.Color.Format.RGBA:
-            return this.simple ? WI.Color.Format.HSL : WI.Color.Format.HSLA;
+            return WI.Color.Format.ColorFunction;
+
+        case WI.Color.Format.ColorFunction:
+            if (this.simple)
+                return WI.Color.Format.HSL;
+            return WI.Color.Format.HSLA;
 
         case WI.Color.Format.HSL:
         case WI.Color.Format.HSLA:
@@ -483,11 +513,6 @@
         }
     }
 
-    get alpha()
-    {
-        return this._rgba ? this._rgba[3] : this._hsla[3];
-    }
-
     get simple()
     {
         return this.alpha === 1;
@@ -495,41 +520,67 @@
 
     get rgb()
     {
-        let rgb = this.rgba.slice();
-        rgb.pop();
-        return rgb;
+        if (!this._rgb) {
+            if (this._hsl)
+                this._rgb = WI.Color.hsl2rgb(...this._hsl);
+            else if (this._normalizedRGB)
+                this._rgb = this._normalizedRGB.map((component) => WI.Color._eightBitChannel(component * 255));
+        }
+        return this._rgb;
     }
 
     get hsl()
     {
-        let hsl = this.hsla.slice();
-        hsl.pop();
-        return hsl;
+        if (!this._hsl)
+            this._hsl = WI.Color.rgb2hsl(...this.rgb);
+        return this._hsl;
+    }
+
+    get normalizedRGB()
+    {
+        if (!this._normalizedRGB)
+            this._normalizedRGB = this.rgb.map((component) => component / 255);
+        return this._normalizedRGB;
     }
 
     get rgba()
     {
-        if (!this._rgba)
-            this._rgba = this._hslaToRGBA(this._hsla);
-        return this._rgba;
+        return [...this.rgb, this.alpha];
     }
 
     get hsla()
     {
-        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.hsl, this.alpha];
+    }
+
+    get normalizedRGBA()
+    {
+        return [...this.normalizedRGB, this.alpha];
+    }
+
+    get gamut()
+    {
+        return this._gamut;
+    }
+
+    set gamut(gamut)
+    {
+        console.assert(gamut !== this._gamut);
+
+        if (this._gamut === WI.Color.Gamut.DisplayP3 && gamut === WI.Color.Gamut.SRGB) {
+            this._normalizedRGB = WI.Color.displayP3toSRGB(...this.normalizedRGB).map((x) => Number.constrain(x, 0, 1));
+            this._hsl = null;
+            this._rgb = null;
+        } else if (this._gamut === WI.Color.Gamut.SRGB && gamut === WI.Color.Gamut.DisplayP3) {
+            this._normalizedRGB = WI.Color.srgbToDisplayP3(...this.normalizedRGB);
+            this._hsl = null;
+            this._rgb = null;
+
+            // Display-P3 is only available with the color function syntax.
+            this.format = WI.Color.Format.ColorFunction;
         }
 
-        return this._hsla;
+        this._gamut = gamut;
     }
 
     copy()
@@ -542,12 +593,15 @@
         case WI.Color.Format.ShortHEXAlpha:
         case WI.Color.Format.Keyword:
         case WI.Color.Format.RGBA:
-        case WI.Color.Format.ColorFunction:
-            return new WI.Color(this.format, this.rgba, this.gamut);
+            return new WI.Color(this.format, this.rgba, this._gamut);
         case WI.Color.Format.HSL:
         case WI.Color.Format.HSLA:
-            return new WI.Color(this.format, this.hsla, this.gamut);
+            return new WI.Color(this.format, this.hsla, this._gamut);
+        case WI.Color.Format.ColorFunction:
+            return new WI.Color(this.format, this.normalizedRGBA, this._gamut);
         }
+
+        console.error("Invalid color format: " + this.format);
     }
 
     toString(format)
@@ -589,34 +643,50 @@
         if (this.keyword)
             return true;
 
-        if (this.gamut !== WI.Color.Gamut.SRGB)
+        if (this._gamut !== WI.Color.Gamut.SRGB)
             return false;
 
         if (!this.simple)
-            return Array.shallowEqual(this._rgba, [0, 0, 0, 0]) || Array.shallowEqual(this._hsla, [0, 0, 0, 0]);
+            return Array.shallowEqual(this.rgba, [0, 0, 0, 0]);
 
-        let rgb = (this._rgba && this._rgba.slice(0, 3)) || WI.Color.hsl2rgb(...this._hsla);
-        return Object.keys(WI.Color.Keywords).some(key => Array.shallowEqual(WI.Color.Keywords[key], rgb));
+        return Object.keys(WI.Color.Keywords).some(key => Array.shallowEqual(WI.Color.Keywords[key], this.rgb));
+    }
+
+    isOutsideSRGB()
+    {
+        if (this._gamut !== WI.Color.Gamut.DisplayP3)
+            return false;
+
+        let rgb = WI.Color.displayP3toSRGB(...this.normalizedRGB);
+
+        // displayP3toSRGB(1, 1, 1) produces [0.9999, 1, 1.0001], which aren't pure white color values.
+        // However, `color(sRGB 0.9999 1 1.0001)` looks exactly the same as color `color(sRGB 1 1 1)`
+        // because sRGB is only 8bit per channel. The values get rounded. For example,
+        // `rgb(255, 254.51, 255)` looks exactly the same as `rgb(255, 255, 255)`.
+        //
+        // Consider a color to be within sRGB even if it's actually outside of sRGB by less than half a bit.
+        const epsilon = (1 / 255) / 2;
+        return rgb.some((x) => x <= -epsilon || x >= 1 + epsilon);
     }
 
     canBeSerializedAsShortHEX()
     {
-        let rgba = this.rgba || this._hslaToRGBA(this._hsla);
+        let rgb = this.rgb;
 
-        let r = this._componentToHexValue(rgba[0]);
+        let r = this._componentToHexValue(rgb[0]);
         if (r[0] !== r[1])
             return false;
 
-        let g = this._componentToHexValue(rgba[1]);
+        let g = this._componentToHexValue(rgb[1]);
         if (g[0] !== g[1])
             return false;
 
-        let b = this._componentToHexValue(rgba[2]);
+        let b = this._componentToHexValue(rgb[2]);
         if (b[0] !== b[1])
             return false;
 
         if (!this.simple) {
-            let a = this._componentToHexValue(Math.round(rgba[3] * 255));
+            let a = this._componentToHexValue(Math.round(this.alpha * 255));
             if (a[0] !== a[1])
                 return false;
         }
@@ -638,7 +708,7 @@
 
         let rgba = this.rgba;
         if (!this.simple) {
-            if (rgba[0] === 0 && rgba[1] === 0 && rgba[2] === 0 && rgba[3] === 0)
+            if (Array.shallowEqual(rgba, [0, 0, 0, 0]))
                 return "transparent";
             return this._toRGBAString();
         }
@@ -661,15 +731,10 @@
         if (!this.simple)
             return this._toRGBAString();
 
-        let rgba = this.rgba;
-        let r = this._componentToHexValue(rgba[0]);
-        let g = this._componentToHexValue(rgba[1]);
-        let b = this._componentToHexValue(rgba[2]);
-
+        let [r, g, b] = this.rgb.map(this._componentToHexValue);
         if (r[0] === r[1] && g[0] === g[1] && b[0] === b[1])
             return "#" + r[0] + g[0] + b[0];
-        else
-            return "#" + r + g + b;
+        return "#" + r + g + b;
     }
 
     _toHEXString()
@@ -677,36 +742,23 @@
         if (!this.simple)
             return this._toRGBAString();
 
-        let rgba = this.rgba;
-        let r = this._componentToHexValue(rgba[0]);
-        let g = this._componentToHexValue(rgba[1]);
-        let b = this._componentToHexValue(rgba[2]);
-
+        let [r, g, b] = this.rgb.map(this._componentToHexValue);
         return "#" + r + g + b;
     }
 
     _toShortHEXAlphaString()
     {
-        let rgba = this.rgba;
-        let r = this._componentToHexValue(rgba[0]);
-        let g = this._componentToHexValue(rgba[1]);
-        let b = this._componentToHexValue(rgba[2]);
-        let a = this._componentToHexValue(Math.round(rgba[3] * 255));
-
+        let [r, g, b] = this.rgb.map(this._componentToHexValue);
+        let a = this._componentToHexValue(Math.round(this.alpha * 255));
         if (r[0] === r[1] && g[0] === g[1] && b[0] === b[1] && a[0] === a[1])
             return "#" + r[0] + g[0] + b[0] + a[0];
-        else
-            return "#" + r + g + b + a;
+        return "#" + r + g + b + a;
     }
 
     _toHEXAlphaString()
     {
-        let rgba = this.rgba;
-        let r = this._componentToHexValue(rgba[0]);
-        let g = this._componentToHexValue(rgba[1]);
-        let b = this._componentToHexValue(rgba[2]);
-        let a = this._componentToHexValue(Math.round(rgba[3] * 255));
-
+        let [r, g, b] = this.rgb.map(this._componentToHexValue);
+        let a = this._componentToHexValue(Math.round(this.alpha * 255));
         return "#" + r + g + b + a;
     }
 
@@ -715,26 +767,22 @@
         if (!this.simple)
             return this._toRGBAString();
 
-        let r = WI.Color._eightBitChannel(Math.round(this.rgba[0]));
-        let g = WI.Color._eightBitChannel(Math.round(this.rgba[1]));
-        let b = WI.Color._eightBitChannel(Math.round(this.rgba[2]));
+        let [r, g, b] = this.rgb.map(WI.Color._eightBitChannel);
         return `rgb(${r}, ${g}, ${b})`;
     }
 
     _toRGBAString()
     {
-        let r = WI.Color._eightBitChannel(Math.round(this.rgba[0]));
-        let g = WI.Color._eightBitChannel(Math.round(this.rgba[1]));
-        let b = WI.Color._eightBitChannel(Math.round(this.rgba[2]));
+        let [r, g, b] = this.rgb.map(WI.Color._eightBitChannel);
         return `rgba(${r}, ${g}, ${b}, ${this.alpha})`;
     }
 
     _toFunctionString()
     {
-        let [r, g, b, alpha] = this.rgba;
-        if (alpha === 1)
-            return `color(${this.gamut} ${r} ${g} ${b})`;
-        return `color(${this.gamut} ${r} ${g} ${b} / ${alpha})`;
+        let [r, g, b] = this.normalizedRGB.map((x) => x.maxDecimals(4));
+        if (this.alpha === 1)
+            return `color(${this._gamut} ${r} ${g} ${b})`;
+        return `color(${this._gamut} ${r} ${g} ${b} / ${this.alpha})`;
     }
 
     _toHSLString()
@@ -742,17 +790,13 @@
         if (!this.simple)
             return this._toHSLAString();
 
-        let h = this.hsla[0].maxDecimals(2);
-        let s = this.hsla[1].maxDecimals(2);
-        let l = this.hsla[2].maxDecimals(2);
+        let [h, s, l] = this.hsl.map((x) => x.maxDecimals(2));
         return `hsl(${h}, ${s}%, ${l}%)`;
     }
 
     _toHSLAString()
     {
-        let h = this.hsla[0].maxDecimals(2);
-        let s = this.hsla[1].maxDecimals(2);
-        let l = this.hsla[2].maxDecimals(2);
+        let [h, s, l] = this.hsl.map((x) => x.maxDecimals(2));
         return `hsla(${h}, ${s}%, ${l}%, ${this.alpha})`;
     }
 
@@ -763,20 +807,6 @@
             hex = "0" + hex;
         return hex;
     }
-
-    _rgbaToHSLA(rgba)
-    {
-        let hsla = WI.Color.rgb2hsl(...rgba);
-        hsla.push(rgba[3]);
-        return hsla;
-    }
-
-    _hslaToRGBA(hsla)
-    {
-        let rgba = WI.Color.hsl2rgb(...hsla);
-        rgba.push(hsla[3]);
-        return rgba;
-    }
 };
 
 WI.Color.Format = {
diff --git a/Source/WebInspectorUI/UserInterface/Views/ColorPicker.js b/Source/WebInspectorUI/UserInterface/Views/ColorPicker.js
index 818a666..54a79d9 100644
--- a/Source/WebInspectorUI/UserInterface/Views/ColorPicker.js
+++ b/Source/WebInspectorUI/UserInterface/Views/ColorPicker.js
@@ -174,7 +174,9 @@
             components = this._colorSquare.tintedColor.hsl.concat(opacity);
             if (opacity !== 1)
                 format = WI.Color.Format.HSLA;
-        } else {
+        } else if (format === WI.Color.Format.ColorFunction)
+            components = this._colorSquare.tintedColor.normalizedRGB.concat(opacity);
+        else {
             components = this._colorSquare.tintedColor.rgb.concat(opacity);
             if (opacity !== 1 && format === WI.Color.Format.RGB)
                 format = WI.Color.Format.RGBA;
@@ -194,8 +196,10 @@
 
     _updateOpacitySlider()
     {
-        let rgb = this._colorSquare.tintedColor.rgb;
-        let gamut = this._colorSquare.tintedColor.gamut;
+        let color = this._colorSquare.tintedColor;
+
+        let rgb = color.format === WI.Color.Format.ColorFunction ? color.normalizedRGB : color.rgb;
+        let gamut = color.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();
diff --git a/Source/WebInspectorUI/UserInterface/Views/ColorSquare.css b/Source/WebInspectorUI/UserInterface/Views/ColorSquare.css
index 683b2da..1056a7d 100644
--- a/Source/WebInspectorUI/UserInterface/Views/ColorSquare.css
+++ b/Source/WebInspectorUI/UserInterface/Views/ColorSquare.css
@@ -61,7 +61,7 @@
     --crosshair-size: 7px;
 }
 
-.color-square .svg-root {
+.color-square > .svg-root {
     position: absolute;
     top: 0;
     left: 0;
@@ -70,30 +70,30 @@
     pointer-events: none;
 }
 
-.color-square .srgb-edge {
+.color-square > .svg-root > .srgb-edge {
     fill: none;
     stroke: white;
     stroke-width: 0.5px;
     stroke-opacity: var(--stroke-opacity);
 }
 
-.color-square .srgb-label {
+.color-square > .srgb-label {
     position: absolute;
-    padding-right: 3px;
+    padding-right: 5px;
     color: hsla(0, 0%, 100%, var(--stroke-opacity));
     font-size: 10px;
 }
 
-.color-square .srgb-label:hover {
+.color-square > .srgb-label:hover {
     color: white;
 }
 
-.color-square .srgb-label:hover + .svg-root > .srgb-edge {
+.color-square > .srgb-label:hover + .svg-root > .srgb-edge {
     stroke-width: 1px;
 }
 
 @media (-webkit-device-pixel-ratio: 1) {
-    .color-square .srgb-edge {
+    .color-square > .srgb-edge {
         stroke-width: 1px;
         stroke-opacity: var(--stroke-opacity) / 2;
     }
diff --git a/Source/WebInspectorUI/UserInterface/Views/ColorSquare.js b/Source/WebInspectorUI/UserInterface/Views/ColorSquare.js
index bb0fd87..7d87937 100644
--- a/Source/WebInspectorUI/UserInterface/Views/ColorSquare.js
+++ b/Source/WebInspectorUI/UserInterface/Views/ColorSquare.js
@@ -106,7 +106,7 @@
 
         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 hsv = WI.Color.rgb2hsv(...tintedColor.normalizedRGB);
             let x = hsv[1] / 100 * this._dimension;
             let y = (1 - (hsv[2] / 100)) * this._dimension;
             this._setCrosshairPosition(new WI.Point(x, y));
@@ -229,12 +229,11 @@
             this._srgbLabelElement = this._element.appendChild(document.createElement("span"));
             this._srgbLabelElement.className = "srgb-label";
             this._srgbLabelElement.textContent = WI.unlocalizedString("sRGB");
-            this._srgbLabelElement.title = WI.UIString("This line marks the edge of sRGB color space", "Label for a guide within the color picker");
+            this._srgbLabelElement.title = WI.UIString("Edge of sRGB color space", "Label for a guide within the color picker");
 
             const svgNamespace = "http://www.w3.org/2000/svg";
-            this._svgElement = document.createElementNS(svgNamespace, "svg");
+            this._svgElement = this._element.appendChild(document.createElementNS(svgNamespace, "svg"));
             this._svgElement.classList.add("svg-root");
-            this._element.append(this._svgElement);
 
             this._polylineElement = this._svgElement.appendChild(document.createElementNS(svgNamespace, "polyline"));
             this._polylineElement.classList.add("srgb-edge");
@@ -244,7 +243,7 @@
         let step = 1 / window.devicePixelRatio;
         let x = 0;
         for (let y = 0; y < this._dimension; y += step) {
-            let value = 100 - (y / this._dimension) * 100;
+            let value = 100 - ((y / this._dimension) * 100);
 
             // Optimization: instead of starting from x = 0, we can benefit from the fact that the next point
             // always has x >= of the current x. This minimizes processing time over 100 times.
@@ -252,7 +251,7 @@
                 let saturation = x / this._dimension * 100;
                 let rgb = WI.Color.hsv2rgb(this._hue, saturation, value);
                 let srgb = WI.Color.displayP3toSRGB(rgb[0], rgb[1], rgb[2]);
-                if (srgb.some((value) => value > 1 || value < 0)) {
+                if (srgb.some((value) => value < 0 || value > 1)) {
                     // The point is outside of sRGB.
                     points.push({x, y});
                     break;
@@ -274,10 +273,10 @@
         this._srgbLabelElement.style.right = `${this._dimension - points.lastValue.x}px`;
 
         this._polylineElement.points.clear();
-        for (let point of points) {
+        for (let {x, y} of points) {
             let svgPoint = this._svgElement.createSVGPoint();
-            svgPoint.x = point.x;
-            svgPoint.y = point.y;
+            svgPoint.x = x;
+            svgPoint.y = y;
             this._polylineElement.points.appendItem(svgPoint);
         }
     }
diff --git a/Source/WebInspectorUI/UserInterface/Views/InlineSwatch.js b/Source/WebInspectorUI/UserInterface/Views/InlineSwatch.js
index fff2311..0ea7d89 100644
--- a/Source/WebInspectorUI/UserInterface/Views/InlineSwatch.js
+++ b/Source/WebInspectorUI/UserInterface/Views/InlineSwatch.js
@@ -46,7 +46,7 @@
         else {
             switch (this._type) {
             case WI.InlineSwatch.Type.Color:
-                this._swatchElement.title = WI.UIString("Click to select a color\nShift-click to switch color formats");
+                // Handled later by _updateSwatch.
                 break;
             case WI.InlineSwatch.Type.Gradient:
                 this._swatchElement.title = WI.UIString("Edit custom gradient");
@@ -77,6 +77,7 @@
 
         this._value = value || this._fallbackValue();
         this._valueEditor = null;
+        this._readOnly = readOnly;
 
         this._updateSwatch();
     }
@@ -146,10 +147,22 @@
         else if (this._type === WI.InlineSwatch.Type.Image)
             this._swatchInnerElement.style.setProperty("background-image", `url(${value.src})`);
 
+        if (this._type === WI.InlineSwatch.Type.Color) {
+            if (this._allowShiftClickColor())
+                this._swatchElement.title = WI.UIString("Click to select a color\nShift-click to switch color formats");
+            else
+                this._swatchElement.title = WI.UIString("Click to select a color");
+        }
+
         if (!dontFireEvents)
             this.dispatchEventToListeners(WI.InlineSwatch.Event.ValueChanged, {value});
     }
 
+    _allowShiftClickColor()
+    {
+        return !this._readOnly && !this.value.isOutsideSRGB();
+    }
+
     _swatchElementClicked(event)
     {
         event.stop();
@@ -158,8 +171,12 @@
 
         if (event.shiftKey && value) {
             if (this._type === WI.InlineSwatch.Type.Color) {
+                if (!this._allowShiftClickColor()) {
+                    InspectorFrontendHost.beep();
+                    return;
+                }
+
                 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;
@@ -313,42 +330,69 @@
             return;
 
         let contextMenu = WI.ContextMenu.createFromEvent(event);
+        let isColorOutsideSRGB = value.isOutsideSRGB();
 
-        if (value.isKeyword() && value.format !== WI.Color.Format.Keyword) {
-            contextMenu.appendItem(WI.UIString("Format: Keyword"), () => {
-                value.format = WI.Color.Format.Keyword;
+        if (!isColorOutsideSRGB) {
+            if (value.isKeyword() && value.format !== WI.Color.Format.Keyword) {
+                contextMenu.appendItem(WI.UIString("Format: Keyword"), () => {
+                    value.format = WI.Color.Format.Keyword;
+                    this._updateSwatch();
+                });
+            }
+
+            let hexInfo = this._getNextValidHEXFormat();
+            if (hexInfo) {
+                contextMenu.appendItem(hexInfo.title, () => {
+                    value.format = hexInfo.format;
+                    this._updateSwatch();
+                });
+            }
+
+            if (value.simple && value.format !== WI.Color.Format.HSL) {
+                contextMenu.appendItem(WI.UIString("Format: HSL"), () => {
+                    value.format = WI.Color.Format.HSL;
+                    this._updateSwatch();
+                });
+            } else if (value.format !== WI.Color.Format.HSLA) {
+                contextMenu.appendItem(WI.UIString("Format: HSLA"), () => {
+                    value.format = WI.Color.Format.HSLA;
+                    this._updateSwatch();
+                });
+            }
+
+            if (value.simple && value.format !== WI.Color.Format.RGB) {
+                contextMenu.appendItem(WI.UIString("Format: RGB"), () => {
+                    value.format = WI.Color.Format.RGB;
+                    this._updateSwatch();
+                });
+            } else if (value.format !== WI.Color.Format.RGBA) {
+                contextMenu.appendItem(WI.UIString("Format: RGBA"), () => {
+                    value.format = WI.Color.Format.RGBA;
+                    this._updateSwatch();
+                });
+            }
+
+            if (value.format !== WI.Color.Format.ColorFunction) {
+                contextMenu.appendItem(WI.UIString("Format: Color Function"), () => {
+                    value.format = WI.Color.Format.ColorFunction;
+                    this._updateSwatch();
+                });
+            }
+
+            contextMenu.appendSeparator();
+        }
+
+        if (value.gamut !== WI.Color.Gamut.DisplayP3) {
+            contextMenu.appendItem(WI.UIString("Convert to Display-P3"), () => {
+                value.gamut = WI.Color.Gamut.DisplayP3;
                 this._updateSwatch();
             });
         }
 
-        let hexInfo = this._getNextValidHEXFormat();
-        if (hexInfo) {
-            contextMenu.appendItem(hexInfo.title, () => {
-                value.format = hexInfo.format;
-                this._updateSwatch();
-            });
-        }
-
-        if (value.simple && value.format !== WI.Color.Format.HSL) {
-            contextMenu.appendItem(WI.UIString("Format: HSL"), () => {
-                value.format = WI.Color.Format.HSL;
-                this._updateSwatch();
-            });
-        } else if (value.format !== WI.Color.Format.HSLA) {
-            contextMenu.appendItem(WI.UIString("Format: HSLA"), () => {
-                value.format = WI.Color.Format.HSLA;
-                this._updateSwatch();
-            });
-        }
-
-        if (value.simple && value.format !== WI.Color.Format.RGB) {
-            contextMenu.appendItem(WI.UIString("Format: RGB"), () => {
-                value.format = WI.Color.Format.RGB;
-                this._updateSwatch();
-            });
-        } else if (value.format !== WI.Color.Format.RGBA) {
-            contextMenu.appendItem(WI.UIString("Format: RGBA"), () => {
-                value.format = WI.Color.Format.RGBA;
+        if (value.gamut !== WI.Color.Gamut.SRGB) {
+            let label = isColorOutsideSRGB ? WI.UIString("Clamp to sRGB") : WI.UIString("Convert to sRGB");
+            contextMenu.appendItem(label, () => {
+                value.gamut = WI.Color.Gamut.SRGB;
                 this._updateSwatch();
             });
         }