[gnome-shell] screenshot: Add preview to color picker



commit f06223df4849c9ed6480c4a07bc9fc9727954458
Author: Florian Müllner <fmuellner gnome org>
Date:   Mon Jul 30 18:31:17 2018 +0200

    screenshot: Add preview to color picker
    
    With color picking implemented in the compositor, we
    can do better than letting the user pick a pixel with
    the crosshair cursor, and present them with a preview
    of the color that will be selected.
    
    Do this by replacing the cursor with a custom icon and
    apply a recoloring effect, where we replace a given color
    with the color of the currently hovered pixel (similar
    to a green screen).
    
    https://gitlab.gnome.org/GNOME/gnome-shell/issues/451

 data/gnome-shell-theme.gresource.xml |   1 +
 data/theme/color-pick.svg            |  94 ++++++++++++++++
 js/ui/screenshot.js                  | 204 ++++++++++++++++++++++++++++++++---
 3 files changed, 284 insertions(+), 15 deletions(-)
---
diff --git a/data/gnome-shell-theme.gresource.xml b/data/gnome-shell-theme.gresource.xml
index ac9c3fd5e1..d15c45c078 100644
--- a/data/gnome-shell-theme.gresource.xml
+++ b/data/gnome-shell-theme.gresource.xml
@@ -6,6 +6,7 @@
     <file>checkbox-off-focused.svg</file>
     <file>checkbox-off.svg</file>
     <file>checkbox.svg</file>
+    <file alias="icons/color-pick.svg">color-pick.svg</file>
     <file>dash-placeholder.svg</file>
     <file>gnome-shell.css</file>
     <file>gnome-shell-high-contrast.css</file>
diff --git a/data/theme/color-pick.svg b/data/theme/color-pick.svg
new file mode 100644
index 0000000000..d9af69023e
--- /dev/null
+++ b/data/theme/color-pick.svg
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="5.4116011mm"
+   height="5.1374583mm"
+   viewBox="0 0 5.4116011 5.1374583"
+   version="1.1"
+   id="svg5595"
+   inkscape:version="0.92.4 (unknown)"
+   sodipodi:docname="color-pick.svg">
+  <defs
+     id="defs5589">
+    <filter
+       inkscape:collect="always"
+       x="-0.10291173"
+       width="1.2058235"
+       y="-0.065432459"
+       height="1.1308649"
+       id="filter5601"
+       style="color-interpolation-filters:sRGB">
+      <feGaussianBlur
+         inkscape:collect="always"
+         stdDeviation="0.610872"
+         id="feGaussianBlur5603" />
+    </filter>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="15.839192"
+     inkscape:cx="39.387731"
+     inkscape:cy="12.554326"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1016"
+     inkscape:window-x="0"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0" />
+  <metadata
+     id="metadata5592">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-103.12753,-146.26461)">
+    <circle
+       r="8.4810486"
+       cy="9.82623"
+       cx="10.226647"
+       id="circle7584"
+       
style="color:#000000;display:inline;overflow:visible;opacity:0.6;vector-effect:none;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;filter:url(#filter5601)"
+       transform="matrix(0.26458333,0,0,0.26458333,103.12753,146.26461)" />
+    <path
+       
style="color:#000000;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.26399338;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
+       d="m 108.07728,148.64122 c 0,1.2393 -1.00465,2.24394 -2.24395,2.24394 -1.23929,0 -2.24716,-1.00465 
-2.25221,-2.24394 l -0.009,-2.24458 2.26136,6.4e-4 c 1.2393,3.4e-4 2.24395,1.00464 2.24395,2.24394 z"
+       id="path7523-7"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ssscss" />
+    <circle
+       
style="color:#000000;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#50dbb5;fill-opacity:1;stroke:none;stroke-width:0.36885914;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
+       id="path7482-1"
+       cx="105.83707"
+       cy="148.64352"
+       r="1.844296" />
+  </g>
+</svg>
diff --git a/js/ui/screenshot.js b/js/ui/screenshot.js
index 0fe5fdff01..7be9ba051a 100644
--- a/js/ui/screenshot.js
+++ b/js/ui/screenshot.js
@@ -1,7 +1,7 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported ScreenshotService */
 
-const { Clutter, Graphene, Gio, GObject, GLib, Meta, Shell, St } = imports.gi;
+const { Clutter, Gio, GObject, GLib, Meta, Shell, St } = imports.gi;
 
 const GrabHelper = imports.ui.grabHelper;
 const Lightbox = imports.ui.lightbox;
@@ -259,15 +259,13 @@ var ScreenshotService = class {
     }
 
     async PickColorAsync(params, invocation) {
-        let pickPixel = new PickPixel();
-        try {
-            const coords = await pickPixel.pickAsync();
-
-            let screenshot = this._createScreenshot(invocation, false);
-            if (!screenshot)
-                return;
+        const screenshot = this._createScreenshot(invocation, false);
+        if (!screenshot)
+            return;
 
-            const [color] = await screenshot.pick_color(coords.x, coords.y);
+        const pickPixel = new PickPixel(screenshot);
+        try {
+            const color = await pickPixel.pickAsync();
             const { red, green, blue } = color;
             const retval = GLib.Variant.new('(a{sv})', [{
                 color: GLib.Variant.new('(ddd)', [
@@ -379,12 +377,145 @@ class SelectArea extends St.Widget {
     }
 });
 
+var RecolorEffect = GObject.registerClass({
+    Properties: {
+        color: GObject.ParamSpec.boxed(
+            'color', 'color', 'replacement color',
+            GObject.ParamFlags.WRITABLE,
+            Clutter.Color.$gtype),
+        chroma: GObject.ParamSpec.boxed(
+            'chroma', 'chroma', 'color to replace',
+            GObject.ParamFlags.WRITABLE,
+            Clutter.Color.$gtype),
+        threshold: GObject.ParamSpec.float(
+            'threshold', 'threshold', 'threshold',
+            GObject.ParamFlags.WRITABLE,
+            0.0, 1.0, 0.0),
+        smoothing: GObject.ParamSpec.float(
+            'smoothing', 'smoothing', 'smoothing',
+            GObject.ParamFlags.WRITABLE,
+            0.0, 1.0, 0.0),
+    },
+}, class RecolorEffect extends Shell.GLSLEffect {
+    _init(params) {
+        this._color = new Clutter.Color();
+        this._chroma = new Clutter.Color();
+        this._threshold = 0;
+        this._smoothing = 0;
+
+        this._colorLocation = null;
+        this._chromaLocation = null;
+        this._thresholdLocation = null;
+        this._smoothingLocation = null;
+
+        super._init(params);
+
+        this._colorLocation = this.get_uniform_location('recolor_color');
+        this._chromaLocation = this.get_uniform_location('chroma_color');
+        this._thresholdLocation = this.get_uniform_location('threshold');
+        this._smoothingLocation = this.get_uniform_location('smoothing');
+
+        this._updateColorUniform(this._colorLocation, this._color);
+        this._updateColorUniform(this._chromaLocation, this._chroma);
+        this._updateFloatUniform(this._thresholdLocation, this._threshold);
+        this._updateFloatUniform(this._smoothingLocation, this._smoothing);
+    }
+
+    _updateColorUniform(location, color) {
+        if (!location)
+            return;
+
+        this.set_uniform_float(location,
+            3, [color.red / 255, color.green / 255, color.blue / 255]);
+        this.queue_repaint();
+    }
+
+    _updateFloatUniform(location, value) {
+        if (!location)
+            return;
+
+        this.set_uniform_float(location, 1, [value]);
+        this.queue_repaint();
+    }
+
+    set color(c) {
+        if (this._color.equal(c))
+            return;
+
+        this._color = c;
+        this.notify('color');
+
+        this._updateColorUniform(this._colorLocation, this._color);
+    }
+
+    set chroma(c) {
+        if (this._chroma.equal(c))
+            return;
+
+        this._chroma = c;
+        this.notify('chroma');
+
+        this._updateColorUniform(this._chromaLocation, this._chroma);
+    }
+
+    set threshold(value) {
+        if (this._threshold === value)
+            return;
+
+        this._threshold = value;
+        this.notify('threshold');
+
+        this._updateFloatUniform(this._thresholdLocation, this._threshold);
+    }
+
+    set smoothing(value) {
+        if (this._smoothing === value)
+            return;
+
+        this._smoothing = value;
+        this.notify('smoothing');
+
+        this._updateFloatUniform(this._smoothingLocation, this._smoothing);
+    }
+
+    vfunc_build_pipeline() {
+        // Conversion parameters from https://en.wikipedia.org/wiki/YCbCr
+        const decl = `
+            vec3 rgb2yCrCb(vec3 c) {                                \n
+                float y = 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;  \n
+                float cr = 0.7133 * (c.r - y);                      \n
+                float cb = 0.5643 * (c.b - y);                      \n
+                return vec3(y, cr, cb);                             \n
+            }                                                       \n
+                                                                    \n
+            uniform vec3 chroma_color;                              \n
+            uniform vec3 recolor_color;                             \n
+            uniform float threshold;                                \n
+            uniform float smoothing;                                \n`;
+        const src = `
+            vec3 mask = rgb2yCrCb(chroma_color.rgb);                \n
+            vec3 yCrCb = rgb2yCrCb(cogl_color_out.rgb);             \n
+            float blend =                                           \n
+              smoothstep(threshold,                                 \n
+                         threshold + smoothing,                     \n
+                         distance(yCrCb.gb, mask.gb));              \n
+            cogl_color_out.rgb =                                    \n
+              mix(recolor_color, cogl_color_out.rgb, blend);        \n`;
+
+        this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, decl, src, false);
+    }
+});
+
 var PickPixel = GObject.registerClass(
 class PickPixel extends St.Widget {
-    _init() {
+    _init(screenshot) {
         super._init({ visible: false, reactive: true });
 
+        this._screenshot = screenshot;
+
         this._result = null;
+        this._color = null;
+        this._inPick = false;
 
         Main.uiGroup.add_actor(this);
 
@@ -393,16 +524,44 @@ class PickPixel extends St.Widget {
         let constraint = new Clutter.BindConstraint({ source: global.stage,
                                                       coordinate: Clutter.BindCoordinate.ALL });
         this.add_constraint(constraint);
+
+        const action = new Clutter.ClickAction();
+        action.connect('clicked', async () => {
+            await this._pickColor(...action.get_coords());
+            this._result = this._color;
+            this._grabHelper.ungrab();
+        });
+        this.add_action(action);
+
+        this._recolorEffect = new RecolorEffect({
+            chroma: new Clutter.Color({
+                red: 80,
+                green: 219,
+                blue: 181,
+            }),
+            threshold: 0.04,
+            smoothing: 0.07,
+        });
+        this._previewCursor = new St.Icon({
+            icon_name: 'color-pick',
+            icon_size: Meta.prefs_get_cursor_size(),
+            effect: this._recolorEffect,
+            visible: false,
+        });
+        Main.uiGroup.add_actor(this._previewCursor);
     }
 
     async pickAsync() {
-        global.display.set_cursor(Meta.Cursor.CROSSHAIR);
+        global.display.set_cursor(Meta.Cursor.BLANK);
         Main.uiGroup.set_child_above_sibling(this, null);
         this.show();
 
+        this._pickColor(...global.get_pointer());
+
         await this._grabHelper.grabAsync({ actor: this });
 
         global.display.set_cursor(Meta.Cursor.DEFAULT);
+        this._previewCursor.destroy();
 
         GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
             this.destroy();
@@ -412,10 +571,25 @@ class PickPixel extends St.Widget {
         return this._result;
     }
 
-    vfunc_button_release_event(buttonEvent) {
-        let { x, y } = buttonEvent;
-        this._result = new Graphene.Point({ x, y });
-        this._grabHelper.ungrab();
+    async _pickColor(x, y) {
+        if (this._inPick)
+            return;
+
+        this._inPick = true;
+        this._previewCursor.set_position(x, y);
+        [this._color] = await this._screenshot.pick_color(x, y);
+        this._inPick = false;
+
+        if (!this._color)
+            return;
+
+        this._recolorEffect.color = this._color;
+        this._previewCursor.show();
+    }
+
+    vfunc_motion_event(motionEvent) {
+        const { x, y } = motionEvent;
+        this._pickColor(x, y);
         return Clutter.EVENT_PROPAGATE;
     }
 });


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]