Prototype colorblind correction filters (#1595)

This commit is contained in:
Leo 2023-06-07 03:03:28 +09:00 committed by GitHub
parent d408d394db
commit 3bf28b0ddf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 528 additions and 0 deletions

View File

@ -22,5 +22,7 @@
</gresource>
<gresource prefix="/io/elementary/desktop/gala">
<file compressed="true">gala.css</file>
<file compressed="true">shaders/colorblindness-correction.vert</file>
<file compressed="true">shaders/monochrome.vert</file>
</gresource>
</gresources>

View File

@ -394,4 +394,38 @@
<description>The action that corresponds to performing a horizontal swipe gesture with four fingers</description>
</key>
</schema>
<enum id="GalaColorblindnessFilterType">
<value nick="none" value="0"/>
<value nick="protanopia" value="1"/>
<value nick="protanopia-high-contrast" value="2"/>
<value nick="deuteranopia" value="3"/>
<value nick="deuteranopia-high-contrast" value="4"/>
<value nick="tritanopia" value="5"/>
</enum>
<schema path="/io/elementary/desktop/wm/accessibility/" id="io.elementary.desktop.wm.accessibility">
<key enum="GalaColorblindnessFilterType" name="colorblindness-correction-filter">
<default>"none"</default>
<summary>Colorblind correction filter.</summary>
<description></description>
</key>
<key type="d" name="colorblindness-correction-filter-strength">
<range min="0.0" max="1.0"/>
<default>1.0</default>
<summary>The strength of colorblindness correction filter.</summary>
<description></description>
</key>
<key type="b" name="enable-monochrome-filter">
<default>false</default>
<summary>Enable monochrome filter.</summary>
<description></description>
</key>
<key type="d" name="monochrome-filter-strength">
<range min="0.0" max="1.0"/>
<default>1.0</default>
<summary>The strength of monochrome filter.</summary>
<description></description>
</key>
</schema>
</schemalist>

View File

@ -0,0 +1,90 @@
/*
* Copyright 2022-2023 GdH <G-dH@github.com>
* Copyright 2023 elementary, Inc. <https://elementary.io>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
uniform sampler2D tex;
uniform int COLORBLIND_MODE;
uniform float STRENGTH;
void main() {
vec4 c = texture2D(tex, cogl_tex_coord0_in.xy);
// RGB to LMS matrix
float L = (17.8824f * c.r) + (43.5161f * c.g) + (4.11935f * c.b);
float M = (3.45565f * c.r) + (27.1554f * c.g) + (3.86714f * c.b);
float S = (0.0299566f * c.r) + (0.184309f * c.g) + (1.46709f * c.b);
float l, m, s;
// Remove invisible colors
if ( COLORBLIND_MODE == 1 || COLORBLIND_MODE == 2) { // Protanopia - reds are greatly reduced
l = 0.0f * L + 2.02344f * M + -2.52581f * S;
m = 0.0f * L + 1.0f * M + 0.0f * S;
s = 0.0f * L + 0.0f * M + 1.0f * S;
} else if ( COLORBLIND_MODE == 3 || COLORBLIND_MODE == 4) { // Deuteranopia - greens are greatly reduced
l = 1.0f * L + 0.0f * M + 0.0f * S;
m = 0.494207f * L + 0.0f * M + 1.24827f * S;
s = 0.0f * L + 0.0f * M + 1.0f * S;
} else if ( COLORBLIND_MODE == 5 ) { // Tritanopia - blues are greatly reduced (1 of 10 000)
l = 1.0f * L + 0.0f * M + 0.0f * S;
m = 0.0f * L + 1.0f * M + 0.0f * S;
// GdH - trinatopia vector calculated by me, all public sources were off
s = -0.012491378299329402f * L + 0.07203451899279534f * M + 0.0f * S;
}
// LMS to RGB matrix conversion
vec4 error;
error.r = (0.0809444479f * l) + (-0.130504409f * m) + (0.116721066f * s);
error.g = (-0.0102485335f * l) + (0.0540193266f * m) + (-0.113614708f * s);
error.b = (-0.000365296938f * l) + (-0.00412161469f * m) + (0.693511405f * s);
// The error is what they see
// ratio between original and error colors allows adjusting filter for weaker forms of dichromacy
error = error * STRENGTH + c * (1.0 - STRENGTH);
error.a = 1.0;
// Isolate invisible colors to color vision deficiency (calculate error matrix)
error = (c - error);
// Shift colors
vec4 correction;
// protanopia / protanomaly corrections
if ( COLORBLIND_MODE == 1 ) {
//(kwin effect values)
correction.r = error.r * 0.56667 + error.g * 0.43333 + error.b * 0.00000;
correction.g = error.r * 0.55833 + error.g * 0.44267 + error.b * 0.00000;
correction.b = error.r * 0.00000 + error.g * 0.24167 + error.b * 0.75833;
// tries to mimic Android, GdH
//correction.r = error.r * -0.5 + error.g * -0.3 + error.b * 0.0;
//correction.g = error.r * 0.2 + error.g * 0.0 + error.b * 0.0;
//correction.b = error.r * 0.2 + error.g * 1.0 + error.b * 1.0;
// protanopia / protanomaly high contrast G-R corrections
} else if ( COLORBLIND_MODE == 2 ) {
correction.r = error.r * 2.56667 + error.g * 0.43333 + error.b * 0.00000;
correction.g = error.r * 1.55833 + error.g * 0.44267 + error.b * 0.00000;
correction.b = error.r * 0.00000 + error.g * 0.24167 + error.b * 0.75833;
// deuteranopia / deuteranomaly corrections (tries to mimic Android, GdH)
} else if ( COLORBLIND_MODE == 3 ) {
correction.r = error.r * -0.7 + error.g * 0.0 + error.b * 0.0;
correction.g = error.r * 0.5 + error.g * 1.0 + error.b * 0.0;
correction.b = error.r * -0.3 + error.g * 0.0 + error.b * 1.0;
// deuteranopia / deuteranomaly high contrast R-G corrections
} else if ( COLORBLIND_MODE == 4 ) {
correction.r = error.r * -1.5 + error.g * 1.5 + error.b * 0.0;
correction.g = error.r * -1.5 + error.g * 1.5 + error.b * 0.0;
correction.b = error.r * 1.5 + error.g * 0.0 + error.b * 0.0;
// tritanopia / tritanomaly corrections (GdH)
} else if ( COLORBLIND_MODE == 5 ) {
correction.r = error.r * 0.3 + error.g * 0.5 + error.b * 0.4;
correction.g = error.r * 0.5 + error.g * 0.7 + error.b * 0.3;
correction.b = error.r * 0.0 + error.g * 0.0 + error.b * 1.0;
}
// Add compensation to original values
correction = c + correction;
correction.a = c.a;
cogl_color_out = correction;
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2023 elementary, Inc. <https://elementary.io>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
uniform sampler2D tex;
uniform float STRENGTH;
void main() {
vec2 uv = cogl_tex_coord0_in.xy;
vec4 sample = texture2D (tex, uv);
vec3 luminance = vec3 (0.2126, 0.7512, 0.0722);
float gray = luminance.r * sample.r + luminance.g * sample.g + luminance.b * sample.b;
cogl_color_out = vec4 (
sample.r * (1.0 - STRENGTH) + gray * STRENGTH,
sample.g * (1.0 - STRENGTH) + gray * STRENGTH,
sample.b * (1.0 - STRENGTH) + gray * STRENGTH,
sample.a
) ;
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2023 elementary, Inc. <https://elementary.io>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
public class Gala.ColorblindnessCorrectionEffect : Clutter.ShaderEffect {
public const string EFFECT_NAME = "colorblindness-correction-filter";
private int _mode;
public int mode {
get { return _mode; }
construct set {
_mode = value;
set_uniform_value ("COLORBLIND_MODE", _mode);
}
}
private double _strength;
public double strength {
get { return _strength; }
construct set {
_strength = value;
set_uniform_value ("STRENGTH", value);
queue_repaint ();
}
}
/*
* Used for fading in and out the effect, since you can't add transitions to effects.
*/
public Clutter.Actor? transition_actor { get; set; default = null; }
public ColorblindnessCorrectionEffect (int mode, double strength) {
Object (
shader_type: Clutter.ShaderType.FRAGMENT_SHADER,
mode: mode,
strength: strength
);
try {
var bytes = GLib.resources_lookup_data ("/io/elementary/desktop/gala/shaders/colorblindness-correction.vert", GLib.ResourceLookupFlags.NONE);
set_shader_source ((string) bytes.get_data ());
} catch (Error e) {
critical ("Unable to load colorblindness-correction.vert: %s", e.message);
}
}
}

View File

@ -0,0 +1,293 @@
/*
* Copyright 2023 elementary, Inc. <https://elementary.io>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
public class Gala.FilterManager : Object {
private const string TRANSITION_NAME = "strength-transition";
private const int TRANSITION_DURATION = 500;
private static FilterManager instance;
private static GLib.Settings settings;
public WindowManager wm { get; construct; }
public static void init (WindowManager wm) {
if (instance != null) {
return;
}
instance = new FilterManager (wm);
}
private FilterManager (WindowManager wm) {
Object (wm: wm);
}
static construct {
settings = new GLib.Settings ("io.elementary.desktop.wm.accessibility");
}
construct {
settings.changed["colorblindness-correction-filter"].connect (update_colorblindness_filter);
settings.changed["colorblindness-correction-filter-strength"].connect (update_colorblindness_strength);
settings.changed["enable-monochrome-filter"].connect (update_monochrome_filter);
settings.changed["monochrome-filter-strength"].connect (update_monochrome_strength);
load_filters ();
}
private void load_filters () {
load_colorblindness_filter ();
load_monochrome_filter ();
}
private void load_colorblindness_filter () {
// When gala launches and there is an effect active, it shouldn't be faded in
var filter_variant = settings.get_enum ("colorblindness-correction-filter");
var strength = settings.get_double ("colorblindness-correction-filter-strength");
if (filter_variant != 0 && strength > 0.0) {
wm.stage.add_effect_with_name (
ColorblindnessCorrectionEffect.EFFECT_NAME,
new ColorblindnessCorrectionEffect (filter_variant, strength)
);
}
}
private void update_colorblindness_filter () {
var filter_variant = settings.get_enum ("colorblindness-correction-filter");
var strength = settings.get_double ("colorblindness-correction-filter-strength");
// Fade out applied effects
foreach (unowned var _effect in wm.stage.get_effects ()) {
if (_effect is ColorblindnessCorrectionEffect) {
unowned var effect = (ColorblindnessCorrectionEffect) _effect;
if (effect.mode == filter_variant) {
continue;
}
// Since you can't add a transition to an effect
// add it to a transition actor and bind one of its properties to the effect
// stop transition (if there is one in progress)
if (effect.transition_actor != null) {
effect.transition_actor.destroy ();
}
// create a new transition
var transition = new Clutter.PropertyTransition ("scale_x") {
duration = TRANSITION_DURATION,
progress_mode = Clutter.AnimationMode.LINEAR,
remove_on_complete = true
};
transition.set_from_value (effect.strength);
transition.set_to_value (0.0);
// create a transition actor and bind its `scale_x` to effect's `strength`
effect.transition_actor = new Clutter.Actor () {
visible = false
};
wm.ui_group.add_child (effect.transition_actor);
effect.transition_actor.bind_property ("scale_x", effect, "strength");
transition.completed.connect (() => {
effect.transition_actor.destroy ();
wm.stage.remove_effect (effect);
});
effect.transition_actor.add_transition (TRANSITION_NAME, transition);
}
}
// Apply a new filter
if (filter_variant == 0 || strength == 0.0) {
return;
}
var new_effect = new ColorblindnessCorrectionEffect (filter_variant, 0.0);
wm.stage.add_effect_with_name (ColorblindnessCorrectionEffect.EFFECT_NAME, new_effect);
// Transition new effect in the same way
var new_transition = new Clutter.PropertyTransition ("scale_x") {
duration = TRANSITION_DURATION,
progress_mode = Clutter.AnimationMode.LINEAR,
remove_on_complete = true
};
new_transition.set_from_value (0.0);
new_transition.set_to_value (strength);
new_effect.transition_actor = new Clutter.Actor () {
visible = false
};
wm.ui_group.add_child (new_effect.transition_actor);
new_effect.transition_actor.bind_property ("scale_x", new_effect, "strength");
new_transition.completed.connect (() => {
new_effect.transition_actor.destroy ();
});
new_effect.transition_actor.add_transition (TRANSITION_NAME, new_transition);
}
private void update_colorblindness_strength () {
var filter_variant = settings.get_enum ("colorblindness-correction-filter");
var strength = settings.get_double ("colorblindness-correction-filter-strength");
foreach (unowned var _effect in wm.stage.get_effects ()) {
if (_effect is ColorblindnessCorrectionEffect) {
unowned var effect = (ColorblindnessCorrectionEffect) _effect;
if (effect.mode != filter_variant) {
continue;
}
// stop transition (if there is one in progress)
if (effect.transition_actor != null) {
effect.transition_actor.destroy ();
}
// create a new transition
var transition = new Clutter.PropertyTransition ("scale_x") {
duration = TRANSITION_DURATION,
progress_mode = Clutter.AnimationMode.LINEAR,
remove_on_complete = true
};
transition.set_from_value (effect.strength);
transition.set_to_value (strength);
// create a transition actor and bind its `scale_x` to effect's `strength`
effect.transition_actor = new Clutter.Actor () {
visible = false
};
wm.ui_group.add_child (effect.transition_actor);
effect.transition_actor.bind_property ("scale_x", effect, "strength");
transition.completed.connect (() => {
effect.transition_actor.destroy ();
});
effect.transition_actor.add_transition (TRANSITION_NAME, transition);
return;
}
}
}
private void load_monochrome_filter () {
// When gala launches and there is an effect active, it shouldn't be faded in
var enable = settings.get_boolean ("enable-monochrome-filter");
var strength = settings.get_double ("monochrome-filter-strength");
if (enable && strength > 0.0) {
wm.stage.add_effect_with_name (
MonochromeEffect.EFFECT_NAME,
new MonochromeEffect (strength)
);
}
}
private void update_monochrome_filter () {
var enabled = settings.get_boolean ("enable-monochrome-filter");
var strength = settings.get_double ("monochrome-filter-strength");
unowned var effect = (MonochromeEffect) wm.stage.get_effect (MonochromeEffect.EFFECT_NAME);
if ((effect != null) == enabled) {
return;
}
// Fade out applied effects
if (effect != null) {
// Since you can't add a transition to an effect
// add it to a transition actor and bind one of its properties to the effect
// stop transition (if there is one in progress)
if (effect.transition_actor != null) {
effect.transition_actor.destroy ();
}
// create a new transition
var transition = new Clutter.PropertyTransition ("scale_x") {
duration = TRANSITION_DURATION,
progress_mode = Clutter.AnimationMode.LINEAR,
remove_on_complete = true
};
transition.set_from_value (effect.strength);
transition.set_to_value (0.0);
// create a transition actor and bind its `scale_x` to effect's `strength`
effect.transition_actor = new Clutter.Actor () {
visible = false
};
wm.ui_group.add_child (effect.transition_actor);
effect.transition_actor.bind_property ("scale_x", effect, "strength");
transition.completed.connect (() => {
effect.transition_actor.destroy ();
wm.stage.remove_effect (effect);
});
effect.transition_actor.add_transition (TRANSITION_NAME, transition);
}
// Apply a new filter
if (!enabled || strength == 0.0) {
return;
}
var new_effect = new MonochromeEffect (0.0);
wm.stage.add_effect_with_name (MonochromeEffect.EFFECT_NAME, new_effect);
// Transition new effect in the same way
var new_transition = new Clutter.PropertyTransition ("scale_x") {
duration = TRANSITION_DURATION,
progress_mode = Clutter.AnimationMode.LINEAR,
remove_on_complete = true
};
new_transition.set_from_value (0.0);
new_transition.set_to_value (strength);
new_effect.transition_actor = new Clutter.Actor () {
visible = false
};
wm.ui_group.add_child (new_effect.transition_actor);
new_effect.transition_actor.bind_property ("scale_x", new_effect, "strength");
new_transition.completed.connect (() => {
new_effect.transition_actor.destroy ();
});
new_effect.transition_actor.add_transition (TRANSITION_NAME, new_transition);
}
private void update_monochrome_strength () {
var strength = settings.get_double ("monochrome-filter-strength");
unowned var effect = (MonochromeEffect) wm.stage.get_effect (MonochromeEffect.EFFECT_NAME);
// stop transition (if there is one in progress)
if (effect.transition_actor != null) {
effect.transition_actor.destroy ();
}
// create a new transition
var transition = new Clutter.PropertyTransition ("scale_x") {
duration = TRANSITION_DURATION,
progress_mode = Clutter.AnimationMode.LINEAR,
remove_on_complete = true
};
transition.set_from_value (effect.strength);
transition.set_to_value (strength);
// create a transition actor and bind its `scale_x` to effect's `strength`
effect.transition_actor = new Clutter.Actor () {
visible = false
};
wm.ui_group.add_child (effect.transition_actor);
effect.transition_actor.bind_property ("scale_x", effect, "strength");
transition.completed.connect (() => {
effect.transition_actor.destroy ();
});
effect.transition_actor.add_transition (TRANSITION_NAME, transition);
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2023 elementary, Inc. <https://elementary.io>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
public class Gala.MonochromeEffect : Clutter.ShaderEffect {
public const string EFFECT_NAME = "monochrome-filter";
private double _strength;
public double strength {
get { return _strength; }
construct set {
_strength = value;
set_uniform_value ("STRENGTH", value);
queue_repaint ();
}
}
/*
* Used for fading in and out the effect, since you can't add transitions to effects.
*/
public Clutter.Actor? transition_actor { get; set; default = null; }
public MonochromeEffect (double strength) {
Object (
shader_type: Clutter.ShaderType.FRAGMENT_SHADER,
strength: strength
);
try {
var bytes = GLib.resources_lookup_data ("/io/elementary/desktop/gala/shaders/monochrome.vert", GLib.ResourceLookupFlags.NONE);
set_shader_source ((string) bytes.get_data ());
} catch (Error e) {
critical ("Unable to load monochrome.vert: %s", e.message);
}
}
}

View File

@ -242,6 +242,8 @@ namespace Gala {
stage.remove_child (top_window_group);
ui_group.add_child (top_window_group);
FilterManager.init (this);
/*keybindings*/
var keybinding_settings = new GLib.Settings (Config.SCHEMA + ".keybindings");

View File

@ -29,6 +29,9 @@ gala_bin_sources = files(
'Background/BackgroundManager.vala',
'Background/BackgroundSource.vala',
'Background/SystemBackground.vala',
'ColorFilters/ColorblindnessCorrectionEffect.vala',
'ColorFilters/FilterManager.vala',
'ColorFilters/MonochromeEffect.vala',
'Gestures/Gesture.vala',
'Gestures/GestureSettings.vala',
'Gestures/GestureTracker.vala',