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
This commit is contained in:
parent
9a8ced9f5b
commit
f06223df48
@ -6,6 +6,7 @@
|
|||||||
<file>checkbox-off-focused.svg</file>
|
<file>checkbox-off-focused.svg</file>
|
||||||
<file>checkbox-off.svg</file>
|
<file>checkbox-off.svg</file>
|
||||||
<file>checkbox.svg</file>
|
<file>checkbox.svg</file>
|
||||||
|
<file alias="icons/color-pick.svg">color-pick.svg</file>
|
||||||
<file>dash-placeholder.svg</file>
|
<file>dash-placeholder.svg</file>
|
||||||
<file>gnome-shell.css</file>
|
<file>gnome-shell.css</file>
|
||||||
<file>gnome-shell-high-contrast.css</file>
|
<file>gnome-shell-high-contrast.css</file>
|
||||||
|
94
data/theme/color-pick.svg
Normal file
94
data/theme/color-pick.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 3.7 KiB |
@ -1,7 +1,7 @@
|
|||||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||||
/* exported ScreenshotService */
|
/* 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 GrabHelper = imports.ui.grabHelper;
|
||||||
const Lightbox = imports.ui.lightbox;
|
const Lightbox = imports.ui.lightbox;
|
||||||
@ -259,15 +259,13 @@ var ScreenshotService = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async PickColorAsync(params, invocation) {
|
async PickColorAsync(params, invocation) {
|
||||||
let pickPixel = new PickPixel();
|
const screenshot = this._createScreenshot(invocation, false);
|
||||||
|
if (!screenshot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const pickPixel = new PickPixel(screenshot);
|
||||||
try {
|
try {
|
||||||
const coords = await pickPixel.pickAsync();
|
const color = await pickPixel.pickAsync();
|
||||||
|
|
||||||
let screenshot = this._createScreenshot(invocation, false);
|
|
||||||
if (!screenshot)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const [color] = await screenshot.pick_color(coords.x, coords.y);
|
|
||||||
const { red, green, blue } = color;
|
const { red, green, blue } = color;
|
||||||
const retval = GLib.Variant.new('(a{sv})', [{
|
const retval = GLib.Variant.new('(a{sv})', [{
|
||||||
color: GLib.Variant.new('(ddd)', [
|
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(
|
var PickPixel = GObject.registerClass(
|
||||||
class PickPixel extends St.Widget {
|
class PickPixel extends St.Widget {
|
||||||
_init() {
|
_init(screenshot) {
|
||||||
super._init({ visible: false, reactive: true });
|
super._init({ visible: false, reactive: true });
|
||||||
|
|
||||||
|
this._screenshot = screenshot;
|
||||||
|
|
||||||
this._result = null;
|
this._result = null;
|
||||||
|
this._color = null;
|
||||||
|
this._inPick = false;
|
||||||
|
|
||||||
Main.uiGroup.add_actor(this);
|
Main.uiGroup.add_actor(this);
|
||||||
|
|
||||||
@ -393,16 +524,44 @@ class PickPixel extends St.Widget {
|
|||||||
let constraint = new Clutter.BindConstraint({ source: global.stage,
|
let constraint = new Clutter.BindConstraint({ source: global.stage,
|
||||||
coordinate: Clutter.BindCoordinate.ALL });
|
coordinate: Clutter.BindCoordinate.ALL });
|
||||||
this.add_constraint(constraint);
|
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() {
|
async pickAsync() {
|
||||||
global.display.set_cursor(Meta.Cursor.CROSSHAIR);
|
global.display.set_cursor(Meta.Cursor.BLANK);
|
||||||
Main.uiGroup.set_child_above_sibling(this, null);
|
Main.uiGroup.set_child_above_sibling(this, null);
|
||||||
this.show();
|
this.show();
|
||||||
|
|
||||||
|
this._pickColor(...global.get_pointer());
|
||||||
|
|
||||||
await this._grabHelper.grabAsync({ actor: this });
|
await this._grabHelper.grabAsync({ actor: this });
|
||||||
|
|
||||||
global.display.set_cursor(Meta.Cursor.DEFAULT);
|
global.display.set_cursor(Meta.Cursor.DEFAULT);
|
||||||
|
this._previewCursor.destroy();
|
||||||
|
|
||||||
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
@ -412,10 +571,25 @@ class PickPixel extends St.Widget {
|
|||||||
return this._result;
|
return this._result;
|
||||||
}
|
}
|
||||||
|
|
||||||
vfunc_button_release_event(buttonEvent) {
|
async _pickColor(x, y) {
|
||||||
let { x, y } = buttonEvent;
|
if (this._inPick)
|
||||||
this._result = new Graphene.Point({ x, y });
|
return;
|
||||||
this._grabHelper.ungrab();
|
|
||||||
|
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;
|
return Clutter.EVENT_PROPAGATE;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user