[gnome] Update extensions

This commit is contained in:
2024-10-31 10:19:02 -04:00
parent 851ec4a772
commit 6d1e6d1132
238 changed files with 8705 additions and 4185 deletions

View File

@ -1,114 +0,0 @@
uniform sampler2D tex;
uniform float sigma;
uniform int dir;
uniform float brightness;
uniform float corner_radius;
uniform float width;
uniform float height;
float circleBounds(vec2 p, vec2 center, float clip_radius) {
vec2 delta = p - center;
float dist_squared = dot(delta, delta);
float outer_radius = clip_radius + 0.5;
if (dist_squared >= (outer_radius * outer_radius))
return 0.0;
float inner_radius = clip_radius - 0.5;
if (dist_squared <= (inner_radius * inner_radius))
return 1.0;
return outer_radius - sqrt(dist_squared);
}
vec4 shapeCorner(vec4 pixel, vec2 p, vec2 center, float clip_radius) {
float alpha = circleBounds(p, center, clip_radius);
return vec4(pixel.rgb * alpha, min(alpha, pixel.a));
}
vec4 getTexture(vec2 uv) {
if (uv.x < 2. / width)
uv.x = 2. / width;
if (uv.y < 2. / height)
uv.y = 2. / height;
if (uv.x > 1. - 3. / width)
uv.x = 1. - 3. / width;
if (uv.y > 1. - 3. / height)
uv.y = 1. - 3. / height;
return texture2D(tex, uv);
}
void main(void) {
vec2 uv = cogl_tex_coord_in[0].xy;
vec2 pos = uv * vec2(width, height);
vec2 direction = vec2(dir, (1.0 - dir));
float pixel_step;
if (dir == 0)
pixel_step = 1.0 / height;
else
pixel_step = 1.0 / width;
vec3 gauss_coefficient;
gauss_coefficient.x = 1.0 / (sqrt(2.0 * 3.14159265) * sigma);
gauss_coefficient.y = exp(-0.5 / (sigma * sigma));
gauss_coefficient.z = gauss_coefficient.y * gauss_coefficient.y;
float gauss_coefficient_total = gauss_coefficient.x;
vec4 ret = getTexture(uv) * gauss_coefficient.x;
gauss_coefficient.xy *= gauss_coefficient.yz;
int n_steps = int(ceil(1.5 * sigma)) * 2;
for (int i = 1; i <= n_steps; i += 2) {
float coefficient_subtotal = gauss_coefficient.x;
gauss_coefficient.xy *= gauss_coefficient.yz;
coefficient_subtotal += gauss_coefficient.x;
float gauss_ratio = gauss_coefficient.x / coefficient_subtotal;
float foffset = float(i) + gauss_ratio;
vec2 offset = direction * foffset * pixel_step;
ret += getTexture(uv + offset) * coefficient_subtotal;
ret += getTexture(uv - offset) * coefficient_subtotal;
gauss_coefficient_total += 2.0 * coefficient_subtotal;
gauss_coefficient.xy *= gauss_coefficient.yz;
}
vec4 outColor = ret / gauss_coefficient_total;
// apply brightness and rounding on the second pass (dir==0 comes last)
if (dir == 0) {
// left side
if (pos.x < corner_radius) {
// top left corner
if (pos.y < corner_radius) {
outColor = shapeCorner(outColor, pos, vec2(corner_radius + 2., corner_radius + 2.), corner_radius);
// bottom left corner
} else if (pos.y > height - corner_radius) {
outColor = shapeCorner(outColor, pos, vec2(corner_radius + 2., height - corner_radius - 1.), corner_radius);
}
// right side
} else if (pos.x > width - corner_radius) {
// top right corner
if (pos.y < corner_radius) {
outColor = shapeCorner(outColor, pos, vec2(width - corner_radius - 1., corner_radius + 2.), corner_radius);
// bottom right corner
} else if (pos.y > height - corner_radius) {
outColor = shapeCorner(outColor, pos, vec2(width - corner_radius - 1., height - corner_radius - 1.), corner_radius);
}
}
outColor.rgb *= brightness;
}
cogl_color_out = outColor;
}

View File

@ -1,236 +0,0 @@
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Clutter from 'gi://Clutter';
import Shell from 'gi://Shell';
const SHADER_PATH = GLib.filename_from_uri(GLib.uri_resolve_relative(import.meta.url, 'blur_effect.glsl', GLib.UriFlags.NONE))[0];
const get_shader_source = _ => {
try {
return Shell.get_file_contents_utf8_sync(SHADER_PATH);
} catch (e) {
console.warn(`[Blur my Shell] error loading shader from ${SHADER_PATH}: ${e}`);
return null;
}
};
export const BlurEffect = new GObject.registerClass({
GTypeName: "BlurEffect",
Properties: {
'radius': GObject.ParamSpec.double(
`radius`,
`Radius`,
`Blur radius`,
GObject.ParamFlags.READWRITE,
0.0, 2000.0,
200.0,
),
'brightness': GObject.ParamSpec.double(
`brightness`,
`Brightness`,
`Blur brightness`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.6,
),
'width': GObject.ParamSpec.double(
`width`,
`Width`,
`Width`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
),
'height': GObject.ParamSpec.double(
`height`,
`Height`,
`Height`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
),
'corner_radius': GObject.ParamSpec.double(
`corner_radius`,
`Corner Radius`,
`Corner Radius`,
GObject.ParamFlags.READWRITE,
0, Number.MAX_SAFE_INTEGER,
0,
),
'direction': GObject.ParamSpec.int(
`direction`,
`Direction`,
`Direction`,
GObject.ParamFlags.READWRITE,
0, 1,
0,
),
'chained_effect': GObject.ParamSpec.object(
`chained_effect`,
`Chained Effect`,
`Chained Effect`,
GObject.ParamFlags.READABLE,
GObject.Object,
),
}
}, class BlurEffect extends Clutter.ShaderEffect {
constructor(params, settings) {
super(params);
this._sigma = null;
this._brightness = null;
this._width = null;
this._height = null;
this._corner_radius = null;
this._static = true;
this._settings = settings;
this._direction = 0;
this._chained_effect = null;
if (params.sigma)
this.sigma = params.sigma;
if (params.brightness)
this.brightness = params.brightness;
if (params.width)
this.width = params.width;
if (params.height)
this.height = params.height;
if (params.corner_radius)
this.corner_radius = params.corner_radius;
if (params.direction)
this.direction = params.direction;
// set shader source
this._source = get_shader_source();
if (this._source)
this.set_shader_source(this._source);
}
get radius() {
return this._radius;
}
set radius(value) {
if (this._radius !== value) {
this._radius = value;
// like Clutter, we use the assumption radius = 2*sigma
this.set_uniform_value('sigma', parseFloat(this._radius / 2 - 1e-6));
if (this._chained_effect) {
this._chained_effect.radius = value;
}
}
}
get brightness() {
return this._brightness;
}
set brightness(value) {
if (this._brightness !== value) {
this._brightness = value;
this.set_uniform_value('brightness', parseFloat(this._brightness - 1e-6));
if (this._chained_effect) {
this._chained_effect.brightness = value;
}
}
}
get width() {
return this._width;
}
set width(value) {
if (this._width !== value) {
this._width = value;
this.set_uniform_value('width', parseFloat(this._width + 3.0 - 1e-6));
if (this._chained_effect) {
this._chained_effect.width = value;
}
}
}
get height() {
return this._height;
}
set height(value) {
if (this._height !== value) {
this._height = value;
this.set_uniform_value('height', parseFloat(this._height + 3.0 - 1e-6));
if (this._chained_effect) {
this._chained_effect.height = value;
}
}
}
get corner_radius() {
return this._corner_radius;
}
set corner_radius(value) {
if (this._corner_radius !== value) {
this._corner_radius = value;
this.set_uniform_value('corner_radius', parseFloat(this._corner_radius - 1e-6));
if (this._chained_effect) {
this._chained_effect.corner_radius = value;
}
}
}
get direction() {
return this._direction;
}
set direction(value) {
if (this._direction !== value) {
this._direction = value;
}
}
get chained_effect() {
return this._chained_effect;
}
vfunc_set_actor(actor) {
super.vfunc_set_actor(actor);
if (this._direction == 0) {
this._chained_effect = new BlurEffect({
radius: this.radius,
brightness: this.brightness,
width: this.width,
height: this.height,
corner_radius: this.corner_radius,
direction: 1
});
if (actor !== null)
actor.add_effect(this._chained_effect);
}
}
vfunc_paint_target(paint_node = null, paint_context = null) {
this.set_uniform_value("tex", 0);
this.set_uniform_value("dir", this._direction);
if (paint_node && paint_context)
super.vfunc_paint_target(paint_node, paint_context);
else if (paint_node)
super.vfunc_paint_target(paint_node);
else
super.vfunc_paint_target();
}
});

View File

@ -0,0 +1,140 @@
import GObject from 'gi://GObject';
import * as utils from '../conveniences/utils.js';
const Shell = await utils.import_in_shell_only('gi://Shell');
const Clutter = await utils.import_in_shell_only('gi://Clutter');
const SHADER_FILENAME = 'color.glsl';
const DEFAULT_PARAMS = {
color: [0.0, 0.0, 0.0, 0.0]
};
export const ColorEffect = utils.IS_IN_PREFERENCES ?
{ default_params: DEFAULT_PARAMS } :
new GObject.registerClass({
GTypeName: "ColorEffect",
Properties: {
'red': GObject.ParamSpec.double(
`red`,
`Red`,
`Red value in shader`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.0,
),
'green': GObject.ParamSpec.double(
`green`,
`Green`,
`Green value in shader`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.0,
),
'blue': GObject.ParamSpec.double(
`blue`,
`Blue`,
`Blue value in shader`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.0,
),
'blend': GObject.ParamSpec.double(
`blend`,
`Blend`,
`Amount of blending between the colors`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.0,
),
}
}, class ColorEffect extends Clutter.ShaderEffect {
constructor(params) {
// initialize without color as a parameter
const { color, ...parent_params } = params;
super(parent_params);
this._red = null;
this._green = null;
this._blue = null;
this._blend = null;
// set shader source
this._source = utils.get_shader_source(Shell, SHADER_FILENAME, import.meta.url);
if (this._source)
this.set_shader_source(this._source);
// set shader color
this.color = 'color' in params ? color : this.constructor.default_params.color;
}
static get default_params() {
return DEFAULT_PARAMS;
}
get red() {
return this._red;
}
set red(value) {
if (this._red !== value) {
this._red = value;
this.set_uniform_value('red', parseFloat(this._red - 1e-6));
}
}
get green() {
return this._green;
}
set green(value) {
if (this._green !== value) {
this._green = value;
this.set_uniform_value('green', parseFloat(this._green - 1e-6));
}
}
get blue() {
return this._blue;
}
set blue(value) {
if (this._blue !== value) {
this._blue = value;
this.set_uniform_value('blue', parseFloat(this._blue - 1e-6));
}
}
get blend() {
return this._blend;
}
set blend(value) {
if (this._blend !== value) {
this._blend = value;
this.set_uniform_value('blend', parseFloat(this._blend - 1e-6));
this.set_enabled(this.blend > 0);
}
}
set color(rgba) {
let [r, g, b, a] = rgba;
this.red = r;
this.green = g;
this.blue = b;
this.blend = a;
}
get color() {
return [this.red, this.green, this.blue, this.blend];
}
/// False set function, only cares about the color. Too hard to change.
set(params) {
this.color = params.color;
}
});

View File

@ -1,181 +0,0 @@
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Clutter from 'gi://Clutter';
import Shell from 'gi://Shell';
const SHADER_PATH = GLib.filename_from_uri(GLib.uri_resolve_relative(import.meta.url, 'color_effect.glsl', GLib.UriFlags.NONE))[0];
const get_shader_source = _ => {
try {
return Shell.get_file_contents_utf8_sync(SHADER_PATH);
} catch (e) {
console.warn(`[Blur my Shell] error loading shader from ${SHADER_PATH}: ${e}`);
return null;
}
};
/// New Clutter Shader Effect that simply mixes a color in, the class applies
/// the GLSL shader programmed into vfunc_get_static_shader_source and applies
/// it to an Actor.
///
/// Clutter Shader Source Code:
/// https://github.com/GNOME/clutter/blob/master/clutter/clutter-shader-effect.c
///
/// GJS Doc:
/// https://gjs-docs.gnome.org/clutter10~10_api/clutter.shadereffect
export const ColorEffect = new GObject.registerClass({
GTypeName: "ColorEffect",
Properties: {
'red': GObject.ParamSpec.double(
`red`,
`Red`,
`Red value in shader`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.4,
),
'green': GObject.ParamSpec.double(
`green`,
`Green`,
`Green value in shader`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.4,
),
'blue': GObject.ParamSpec.double(
`blue`,
`Blue`,
`Blue value in shader`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.4,
),
'blend': GObject.ParamSpec.double(
`blend`,
`Blend`,
`Amount of blending between the colors`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.4,
),
}
}, class ColorShader extends Clutter.ShaderEffect {
constructor(params, settings) {
// initialize without color as a parameter
let _color = params.color;
delete params.color;
super(params);
this._red = null;
this._green = null;
this._blue = null;
this._blend = null;
this._static = true;
this._settings = settings;
// set shader source
this._source = get_shader_source();
if (this._source)
this.set_shader_source(this._source);
// set shader color
if (_color)
this.color = _color;
this.update_enabled();
}
get red() {
return this._red;
}
set red(value) {
if (this._red !== value) {
this._red = value;
this.set_uniform_value('red', parseFloat(this._red - 1e-6));
}
}
get green() {
return this._green;
}
set green(value) {
if (this._green !== value) {
this._green = value;
this.set_uniform_value('green', parseFloat(this._green - 1e-6));
}
}
get blue() {
return this._blue;
}
set blue(value) {
if (this._blue !== value) {
this._blue = value;
this.set_uniform_value('blue', parseFloat(this._blue - 1e-6));
}
}
get blend() {
return this._blend;
}
set blend(value) {
if (this._blend !== value) {
this._blend = value;
this.set_uniform_value('blend', parseFloat(this._blend - 1e-6));
}
this.update_enabled();
}
set color(rgba) {
let [r, g, b, a] = rgba;
this.red = r;
this.green = g;
this.blue = b;
this.blend = a;
}
get color() {
return [this.red, this.green, this.blue, this.blend];
}
/// False set function, only cares about the color. Too hard to change.
set(params) {
this.color = params.color;
}
update_enabled() {
// don't anything if this._settings is undefined (when calling super)
if (this._settings === undefined)
return;
this.set_enabled(
this.blend > 0 &&
this._settings.COLOR_AND_NOISE &&
this._static
);
}
vfunc_paint_target(paint_node = null, paint_context = null) {
this.set_uniform_value("tex", 0);
if (paint_node && paint_context)
super.vfunc_paint_target(paint_node, paint_context);
else if (paint_node)
super.vfunc_paint_target(paint_node);
else
super.vfunc_paint_target();
}
});

View File

@ -0,0 +1,93 @@
// Heavily based on https://github.com/yilozt/rounded-window-corners
// which is itself based on upstream Mutter code
uniform sampler2D tex;
uniform float radius;
uniform float width;
uniform float height;
uniform bool corners_top;
uniform bool corners_bottom;
uniform float clip_x0;
uniform float clip_y0;
uniform float clip_width;
uniform float clip_height;
float circle_bounds(vec2 p, vec2 center, float clip_radius) {
vec2 delta = p - center;
float dist_squared = dot(delta, delta);
float outer_radius = clip_radius + 0.5;
if (dist_squared >= (outer_radius * outer_radius))
return 0.0;
float inner_radius = clip_radius - 0.5;
if (dist_squared <= (inner_radius * inner_radius))
return 1.0;
return outer_radius - sqrt(dist_squared);
}
vec4 getTexture(vec2 uv) {
if (uv.x < 2. / width)
uv.x = 2. / width;
if (uv.y < 2. / height)
uv.y = 2. / height;
if (uv.x > 1. - 3. / width)
uv.x = 1. - 3. / width;
if (uv.y > 1. - 3. / height)
uv.y = 1. - 3. / height;
return texture2D(tex, uv);
}
float rounded_rect_coverage(vec2 p, vec4 bounds, float clip_radius) {
// Outside the bounds
if (p.x < bounds.x || p.x > bounds.z || p.y < bounds.y || p.y > bounds.w) {
return 0.;
}
vec2 center;
float center_left = bounds.x + clip_radius;
float center_right = bounds.z - clip_radius;
if (p.x < center_left)
center.x = center_left + 2.;
else if (p.x > center_right)
center.x = center_right - 1.;
else
return 1.0;
float center_top = bounds.y + clip_radius;
float center_bottom = bounds.w - clip_radius;
if (corners_top && p.y < center_top)
center.y = center_top + 2.;
else if (corners_bottom && p.y > center_bottom)
center.y = center_bottom - 1.;
else
return 1.0;
return circle_bounds(p, center, clip_radius);
}
void main(void) {
vec2 uv = cogl_tex_coord_in[0].xy;
vec2 pos = uv * vec2(width, height);
vec4 c = getTexture(uv);
vec4 bounds;
if (clip_width < 0. || clip_height < 0.) {
bounds = vec4(clip_x0, clip_y0, clip_x0 + width, clip_y0 + height);
} else {
bounds = vec4(clip_x0, clip_y0, clip_x0 + clip_width, clip_y0 + clip_height);
}
float alpha = rounded_rect_coverage(pos, bounds, radius);
cogl_color_out = vec4(c.rgb * alpha, min(alpha, c.a));
}

View File

@ -0,0 +1,211 @@
import GObject from 'gi://GObject';
import * as utils from '../conveniences/utils.js';
const St = await utils.import_in_shell_only('gi://St');
const Shell = await utils.import_in_shell_only('gi://Shell');
const Clutter = await utils.import_in_shell_only('gi://Clutter');
const SHADER_FILENAME = 'corner.glsl';
const DEFAULT_PARAMS = {
radius: 12, width: 0, height: 0,
corners_top: true, corners_bottom: true,
clip: [0, 0, -1, -1]
};
export const CornerEffect = utils.IS_IN_PREFERENCES ?
{ default_params: DEFAULT_PARAMS } :
new GObject.registerClass({
GTypeName: "CornerEffect",
Properties: {
'radius': GObject.ParamSpec.double(
`radius`,
`Corner Radius`,
`Corner Radius`,
GObject.ParamFlags.READWRITE,
0, Number.MAX_SAFE_INTEGER,
12,
),
'width': GObject.ParamSpec.double(
`width`,
`Width`,
`Width`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
),
'height': GObject.ParamSpec.double(
`height`,
`Height`,
`Height`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
),
'corners_top': GObject.ParamSpec.boolean(
`corners_top`,
`Round top corners`,
`Round top corners`,
GObject.ParamFlags.READWRITE,
true,
),
'corners_bottom': GObject.ParamSpec.boolean(
`corners_bottom`,
`Round bottom corners`,
`Round bottom corners`,
GObject.ParamFlags.READWRITE,
true,
),
// FIXME this works but it logs an error, because I'm not a double...
// I don't want to fiddle with GVariants again
'clip': GObject.ParamSpec.double(
`clip`,
`Clip`,
`Clip`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
),
}
}, class CornerEffect extends Clutter.ShaderEffect {
constructor(params) {
super(params);
this._clip_x0 = null;
this._clip_y0 = null;
this._clip_width = null;
this._clip_height = null;
utils.setup_params(this, params);
// set shader source
this._source = utils.get_shader_source(Shell, SHADER_FILENAME, import.meta.url);
if (this._source)
this.set_shader_source(this._source);
const theme_context = St.ThemeContext.get_for_stage(global.stage);
theme_context.connectObject('notify::scale-factor', _ => this.update_radius(), this);
}
static get default_params() {
return DEFAULT_PARAMS;
}
get radius() {
return this._radius;
}
set radius(value) {
if (this._radius !== value) {
this._radius = value;
this.update_radius();
}
}
update_radius() {
const theme_context = St.ThemeContext.get_for_stage(global.stage);
let radius = Math.min(
this.radius * theme_context.scale_factor,
this.width / 2, this.height / 2
);
if (this._clip_width >= 0 || this._clip_height >= 0)
radius = Math.min(radius, this._clip_width / 2, this._clip_height / 2);
this.set_uniform_value('radius', parseFloat(radius - 1e-6));
}
get width() {
return this._width;
}
set width(value) {
if (this._width !== value) {
this._width = value;
this.set_uniform_value('width', parseFloat(this._width + 3.0 - 1e-6));
this.update_radius();
}
}
get height() {
return this._height;
}
set height(value) {
if (this._height !== value) {
this._height = value;
this.set_uniform_value('height', parseFloat(this._height + 3.0 - 1e-6));
this.update_radius();
}
}
get corners_top() {
return this._corners_top;
}
set corners_top(value) {
if (this._corners_top !== value) {
this._corners_top = value;
this.set_uniform_value('corners_top', this._corners_top ? 1 : 0);
}
}
get corners_bottom() {
return this._corners_bottom;
}
set corners_bottom(value) {
if (this._corners_bottom !== value) {
this._corners_bottom = value;
this.set_uniform_value('corners_bottom', this._corners_bottom ? 1 : 0);
}
}
get clip() {
return [this._clip_x0, this._clip_y0, this._clip_width, this._clip_height];
}
set clip(value) {
[this._clip_x0, this._clip_y0, this._clip_width, this._clip_height] = value;
this.set_uniform_value('clip_x0', parseFloat(this._clip_x0 - 1e-6));
this.set_uniform_value('clip_y0', parseFloat(this._clip_y0 - 1e-6));
this.set_uniform_value('clip_width', parseFloat(this._clip_width + 3 - 1e-6));
this.set_uniform_value('clip_height', parseFloat(this._clip_height + 3 - 1e-6));
this.update_radius();
}
vfunc_set_actor(actor) {
if (this._actor_connection_size_id) {
let old_actor = this.get_actor();
old_actor?.disconnect(this._actor_connection_size_id);
}
if (this._actor_connection_clip_rect_id) {
let old_actor = this.get_actor();
old_actor?.disconnect(this._actor_connection_clip_rect_id);
}
if (actor) {
this.width = actor.width;
this.height = actor.height;
this._actor_connection_size_id = actor.connect('notify::size', _ => {
this.width = actor.width;
this.height = actor.height;
});
this.clip = actor.has_clip ? actor.get_clip() : [0, 0, -10, -10];
this._actor_connection_clip_rect_id = actor.connect('notify::clip-rect', _ => {
this.clip = actor.has_clip ? actor.get_clip() : [0, 0, -10, -10];
});
}
else {
this._actor_connection_size_id = null;
this._actor_connection_clip_rect_id = null;
}
super.vfunc_set_actor(actor);
}
});

View File

@ -0,0 +1,71 @@
uniform sampler2D tex;
uniform int operation;
uniform float width;
uniform float height;
#define CORRECTION 2.25
#define SIZE_ADDITION 3
vec4 get_texture_at_position(vec2 position) {
vec2 raw_position = position + vec2(CORRECTION, CORRECTION);
vec2 raw_uv = raw_position / vec2(width + SIZE_ADDITION, height + SIZE_ADDITION);
return texture2D(tex, raw_uv);
}
vec4 try_get_texture_at_position(vec2 position, inout int count) {
if (any(greaterThanEqual(position, vec2(width, height))) ||
any(lessThan(position, vec2(0, 0)))) {
return vec4(0);
} else {
count++;
return get_texture_at_position(position);
}
}
ivec2 get_corrected_position() {
vec2 raw_uv = cogl_tex_coord0_in.st;
vec2 raw_position = raw_uv * vec2(width + SIZE_ADDITION, height + SIZE_ADDITION);
return ivec2(raw_position - vec2(CORRECTION, CORRECTION));
}
void main() {
ivec2 corrected_position = get_corrected_position();
// 1-step derivative
if (operation == 0) {
vec4 color = vec4(0);
int c = 0;
color += try_get_texture_at_position(corrected_position + vec2(0, 1), c);
color -= try_get_texture_at_position(corrected_position + vec2(0, 0), c);
color += try_get_texture_at_position(corrected_position + vec2(1, 0), c);
color -= try_get_texture_at_position(corrected_position + vec2(0, 0), c);
if (c < 4) {
color = vec4(0);
}
cogl_color_out = vec4(color.xyz, 1);
} else
// 2-step derivative
if (operation == 1) {
vec4 color = vec4(0);
int c = 0;
color += try_get_texture_at_position(corrected_position + vec2(0, 1), c);
color -= try_get_texture_at_position(corrected_position + vec2(0, -1), c);
color += try_get_texture_at_position(corrected_position + vec2(1, 0), c);
color -= try_get_texture_at_position(corrected_position + vec2(-1, 0), c);
if (c < 4) {
color = vec4(0);
}
cogl_color_out = vec4(color.xyz / 2, 1);
} else
// laplacian
if (operation == 2) {
vec4 color = vec4(0);
color = -4 * get_texture_at_position(corrected_position);
color += get_texture_at_position(corrected_position + vec2(0, 1));
color += get_texture_at_position(corrected_position + vec2(0, -1));
color += get_texture_at_position(corrected_position + vec2(1, 0));
color += get_texture_at_position(corrected_position + vec2(-1, 0));
cogl_color_out = vec4(color.xyz, 1);
}
}

View File

@ -0,0 +1,120 @@
import GObject from 'gi://GObject';
import * as utils from '../conveniences/utils.js';
const Shell = await utils.import_in_shell_only('gi://Shell');
const Clutter = await utils.import_in_shell_only('gi://Clutter');
const SHADER_FILENAME = 'derivative.glsl';
const DEFAULT_PARAMS = {
operation: 0, width: 0, height: 0
};
export const DerivativeEffect = utils.IS_IN_PREFERENCES ?
{ default_params: DEFAULT_PARAMS } :
new GObject.registerClass({
GTypeName: "DerivativeEffect",
Properties: {
'operation': GObject.ParamSpec.int(
`operation`,
`Operation`,
`Operation`,
GObject.ParamFlags.READWRITE,
0, 2,
0,
),
'width': GObject.ParamSpec.double(
`width`,
`Width`,
`Width`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
),
'height': GObject.ParamSpec.double(
`height`,
`Height`,
`Height`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
)
}
}, class DerivativeEffect extends Clutter.ShaderEffect {
constructor(params) {
super(params);
utils.setup_params(this, params);
// set shader source
this._source = utils.get_shader_source(Shell, SHADER_FILENAME, import.meta.url);
if (this._source)
this.set_shader_source(this._source);
}
static get default_params() {
return DEFAULT_PARAMS;
}
get operation() {
return this._operation;
}
set operation(value) {
if (this._operation !== value) {
this._operation = value;
this.set_uniform_value('operation', this._operation);
}
}
get width() {
return this._width;
}
set width(value) {
if (this._width !== value) {
this._width = value;
this.set_uniform_value('width', parseFloat(this._width - 1e-6));
}
}
get height() {
return this._height;
}
set height(value) {
if (this._height !== value) {
this._height = value;
this.set_uniform_value('height', parseFloat(this._height - 1e-6));
}
}
vfunc_set_actor(actor) {
if (this._actor_connection_size_id) {
let old_actor = this.get_actor();
old_actor?.disconnect(this._actor_connection_size_id);
}
if (actor) {
this.width = actor.width;
this.height = actor.height;
this._actor_connection_size_id = actor.connect('notify::size', _ => {
this.width = actor.width;
this.height = actor.height;
});
}
else
this._actor_connection_size_id = null;
super.vfunc_set_actor(actor);
}
vfunc_paint_target(paint_node, paint_context) {
// force setting nearest-neighbour texture filtering
this.get_pipeline().set_layer_filters(0, 9728, 9728);
super.vfunc_paint_target(paint_node, paint_context);
}
});

View File

@ -0,0 +1,76 @@
uniform sampler2D tex;
uniform int divider;
uniform float width;
uniform float height;
uniform int downsampling_mode;
#define CORRECTION 2.25
#define SIZE_ADDITION 3
vec4 get_texture_at_position(vec2 position) {
vec2 raw_position = position + vec2(CORRECTION, CORRECTION);
vec2 raw_uv = raw_position / vec2(width + SIZE_ADDITION, height + SIZE_ADDITION);
return texture2D(tex, raw_uv);
}
vec2 get_corrected_position() {
vec2 raw_uv = cogl_tex_coord0_in.st;
vec2 raw_position = raw_uv * vec2(width + SIZE_ADDITION, height + SIZE_ADDITION);
return raw_position - vec2(CORRECTION, CORRECTION);
}
void main() {
ivec2 corrected_position = ivec2(get_corrected_position());
vec2 multiplied_position = corrected_position * divider;
if (any(greaterThan(multiplied_position, vec2(width, height)))) {
discard;
}
// mode 0: boxcar downsampling
if (downsampling_mode == 0) {
vec4 color = vec4(0.);
int count = 0;
for (int i = 0; i < divider; i++) {
for (int j = 0; j < divider; j++) {
vec2 lookup_position = multiplied_position + vec2(i, j);
if (all(greaterThanEqual(lookup_position, vec2(0, 0))) &&
all(lessThan(lookup_position, vec2(width, height)))) {
color += get_texture_at_position(lookup_position);
count += 1;
}
}
}
cogl_color_out = color / count;
} else
// mode 1: triangular downsampling
if (downsampling_mode == 1) {
vec4 color = vec4(0.);
int count = 0;
int force = 1;
for (int i = 0; i < divider; i++) {
for (int j = 0; j < divider; j++) {
vec2 lookup_position = multiplied_position + vec2(i, j);
if (all(greaterThanEqual(lookup_position, vec2(0, 0))) &&
all(lessThan(lookup_position, vec2(width, height)))) {
force = 1 + divider - int(abs(divider - i - j));
color += get_texture_at_position(lookup_position) * force;
count += force;
}
}
}
cogl_color_out = color / count;
} else
// mode 2: Dirac downsampling
if (downsampling_mode == 2) {
vec2 lookup_position = min(multiplied_position + vec2(divider, divider) / 2, vec2(width - 1, height - 1));
cogl_color_out = get_texture_at_position(lookup_position);
}
}

View File

@ -0,0 +1,140 @@
import GObject from 'gi://GObject';
import * as utils from '../conveniences/utils.js';
const Shell = await utils.import_in_shell_only('gi://Shell');
const Clutter = await utils.import_in_shell_only('gi://Clutter');
const SHADER_FILENAME = 'downscale.glsl';
const DEFAULT_PARAMS = {
divider: 8, downsampling_mode: 0, width: 0, height: 0
};
export const DownscaleEffect = utils.IS_IN_PREFERENCES ?
{ default_params: DEFAULT_PARAMS } :
new GObject.registerClass({
GTypeName: "DownscaleEffect",
Properties: {
'divider': GObject.ParamSpec.int(
`divider`,
`Divider`,
`Divider`,
GObject.ParamFlags.READWRITE,
0, 64,
8,
),
'downsampling_mode': GObject.ParamSpec.int(
`downsampling_mode`,
`Downsampling mode`,
`Downsampling mode`,
GObject.ParamFlags.READWRITE,
0, 2,
0,
),
'width': GObject.ParamSpec.double(
`width`,
`Width`,
`Width`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
),
'height': GObject.ParamSpec.double(
`height`,
`Height`,
`Height`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
)
}
}, class DownscaleEffect extends Clutter.ShaderEffect {
constructor(params) {
super(params);
utils.setup_params(this, params);
// set shader source
this._source = utils.get_shader_source(Shell, SHADER_FILENAME, import.meta.url);
if (this._source)
this.set_shader_source(this._source);
}
static get default_params() {
return DEFAULT_PARAMS;
}
get divider() {
return this._divider;
}
set divider(value) {
if (this._divider !== value) {
this._divider = value;
this.set_uniform_value('divider', this._divider);
}
}
get downsampling_mode() {
return this._downsampling_mode;
}
set downsampling_mode(value) {
if (this._downsampling_mode !== value) {
this._downsampling_mode = value;
this.set_uniform_value('downsampling_mode', this._downsampling_mode);
}
}
get width() {
return this._width;
}
set width(value) {
if (this._width !== value) {
this._width = value;
this.set_uniform_value('width', parseFloat(this._width - 1e-6));
}
}
get height() {
return this._height;
}
set height(value) {
if (this._height !== value) {
this._height = value;
this.set_uniform_value('height', parseFloat(this._height - 1e-6));
}
}
vfunc_set_actor(actor) {
if (this._actor_connection_size_id) {
let old_actor = this.get_actor();
old_actor?.disconnect(this._actor_connection_size_id);
}
if (actor) {
this.width = actor.width;
this.height = actor.height;
this._actor_connection_size_id = actor.connect('notify::size', _ => {
this.width = actor.width;
this.height = actor.height;
});
}
else
this._actor_connection_size_id = null;
super.vfunc_set_actor(actor);
}
vfunc_paint_target(paint_node, paint_context) {
// force setting nearest-neighbour texture filtering
this.get_pipeline().set_layer_filters(0, 9728, 9728);
super.vfunc_paint_target(paint_node, paint_context);
}
});

View File

@ -0,0 +1,332 @@
import { NativeDynamicBlurEffect } from '../effects/native_dynamic_gaussian_blur.js';
import { NativeStaticBlurEffect } from '../effects/native_static_gaussian_blur.js';
import { GaussianBlurEffect } from '../effects/gaussian_blur.js';
import { MonteCarloBlurEffect } from '../effects/monte_carlo_blur.js';
import { ColorEffect } from '../effects/color.js';
import { NoiseEffect } from '../effects/noise.js';
import { CornerEffect } from '../effects/corner.js';
import { DownscaleEffect } from './downscale.js';
import { UpscaleEffect } from './upscale.js';
import { PixelizeEffect } from './pixelize.js';
import { DerivativeEffect } from './derivative.js';
import { RgbToHslEffect } from './rgb_to_hsl.js';
import { HslToRgbEffect } from './hsl_to_rgb.js';
// We do in this way because I've not found another way to store our preferences in a dictionnary
// while calling `gettext` on it while in preferences. Not so pretty, but works.
export function get_effects_groups(_ = _ => "") {
return {
blur_effects: {
name: _("Blur effects"),
contains: [
"native_static_gaussian_blur",
"gaussian_blur",
"monte_carlo_blur"
]
},
texture_effects: {
name: _("Texture effects"),
contains: [
"downscale",
"upscale",
"pixelize",
"derivative",
"noise",
"color",
"rgb_to_hsl",
"hsl_to_rgb"
]
},
shape_effects: {
name: _("Shape effects"),
contains: [
"corner"
]
}
};
};
export function get_supported_effects(_ = () => "") {
return {
native_dynamic_gaussian_blur: {
class: NativeDynamicBlurEffect
},
native_static_gaussian_blur: {
class: NativeStaticBlurEffect,
name: _("Native gaussian blur"),
description: _("An optimized blur effect that smoothly blends pixels within a given radius."),
is_advanced: false,
editable_params: {
unscaled_radius: {
name: _("Radius"),
description: _("The intensity of the blur effect."),
type: "float",
min: 0.,
max: 100.,
increment: 1.0,
big_increment: 10.,
digits: 0
},
brightness: {
name: _("Brightness"),
description: _("The brightness of the blur effect, a high value might make the text harder to read."),
type: "float",
min: 0.,
max: 1.,
increment: 0.01,
big_increment: 0.1,
digits: 2
},
}
},
gaussian_blur: {
class: GaussianBlurEffect,
name: _("Gaussian blur (advanced effect)"),
description: _("A blur effect that smoothly blends pixels within a given radius. This effect is more precise, but way less optimized."),
is_advanced: true,
editable_params: {
radius: {
name: _("Radius"),
description: _("The intensity of the blur effect. The bigger it is, the slower it will be."),
type: "float",
min: 0.,
max: 100.,
increment: .1,
big_increment: 10.,
digits: 1
},
brightness: {
name: _("Brightness"),
description: _("The brightness of the blur effect, a high value might make the text harder to read."),
type: "float",
min: 0.,
max: 1.,
increment: 0.01,
big_increment: 0.1,
digits: 2
},
}
},
monte_carlo_blur: {
class: MonteCarloBlurEffect,
name: _("Monte Carlo blur"),
description: _("A blur effect that mimics a random walk, by picking pixels further and further away from its origin and mixing them all together."),
is_advanced: false,
editable_params: {
radius: {
name: _("Radius"),
description: _("The maximum travel distance for each step in the random walk. A higher value will make the blur more randomized."),
type: "float",
min: 0.,
max: 10.,
increment: 0.01,
big_increment: 0.1,
digits: 2
},
iterations: {
name: _("Iterations"),
description: _("The number of iterations. The more there are, the smoother the blur is."),
type: "integer",
min: 0,
max: 50,
increment: 1
},
brightness: {
name: _("Brightness"),
description: _("The brightness of the blur effect, a high value might make the text harder to read."),
type: "float",
min: 0.,
max: 1.,
increment: 0.01,
big_increment: 0.1,
digits: 2
},
use_base_pixel: {
name: _("Use base pixel"),
description: _("Whether or not the original pixel is counted for the blur. If it is, the image will be more legible."),
type: "boolean"
}
}
},
color: {
class: ColorEffect,
name: _("Color"),
description: _("An effect that blends a color into the pipeline."),
is_advanced: false,
// TODO make this RGB + blend
editable_params: {
color: {
name: _("Color"),
description: _("The color to blend in. The blending amount is controled by the opacity of the color."),
type: "rgba"
}
}
},
pixelize: {
class: PixelizeEffect,
name: _("Pixelize"),
description: _("An effect that pixelizes the image."),
is_advanced: false,
editable_params: {
factor: {
name: _("Factor"),
description: _("How much to scale down the image."),
type: "integer",
min: 1,
max: 50,
increment: 1
},
downsampling_mode: {
name: _("Downsampling mode"),
description: _("The downsampling method that is used."),
type: "dropdown",
options: [
_("Boxcar"),
_("Triangular"),
_("Dirac")
]
}
}
},
downscale: {
class: DownscaleEffect,
name: _("Downscale (advanced effect)"),
description: _("An effect that downscales the image and put it on the top-left corner."),
is_advanced: true,
editable_params: {
divider: {
name: _("Factor"),
description: _("How much to scale down the image."),
type: "integer",
min: 1,
max: 50,
increment: 1
},
downsampling_mode: {
name: _("Downsampling mode"),
description: _("The downsampling method that is used."),
type: "dropdown",
options: [
_("Boxcar"),
_("Triangular"),
_("Dirac")
]
}
}
},
upscale: {
class: UpscaleEffect,
name: _("Upscale (advanced effect)"),
description: _("An effect that upscales the image from the top-left corner."),
is_advanced: true,
editable_params: {
factor: {
name: _("Factor"),
description: _("How much to scale up the image."),
type: "integer",
min: 1,
max: 50,
increment: 1
}
}
},
derivative: {
class: DerivativeEffect,
name: _("Derivative"),
description: _("Apply a spatial derivative, or a laplacian."),
is_advanced: false,
editable_params: {
operation: {
name: _("Operation"),
description: _("The mathematical operation to apply."),
type: "dropdown",
options: [
_("1-step derivative"),
_("2-step derivative"),
_("Laplacian")
]
}
}
},
noise: {
class: NoiseEffect,
name: _("Noise"),
description: _("An effect that adds a random noise. Prefer the Monte Carlo blur for a more organic effect if needed."),
is_advanced: false,
editable_params: {
noise: {
name: _("Noise"),
description: _("The amount of noise to add."),
type: "float",
min: 0.,
max: 1.,
increment: 0.01,
big_increment: 0.1,
digits: 2
},
lightness: {
name: _("Lightness"),
description: _("The luminosity of the noise. A setting of '1.0' will make the effect transparent."),
type: "float",
min: 0.,
max: 2.,
increment: 0.01,
big_increment: 0.1,
digits: 2
}
}
},
rgb_to_hsl: {
class: RgbToHslEffect,
name: _("RGB to HSL (advanced effect)"),
description: _("Converts the image from RGBA colorspace to HSLA."),
is_advanced: true,
editable_params: {}
},
hsl_to_rgb: {
class: HslToRgbEffect,
name: _("HSL to RGB (advanced effect)"),
description: _("Converts the image from HSLA colorspace to RGBA."),
is_advanced: true,
editable_params: {}
},
corner: {
class: CornerEffect,
name: _("Corner"),
description: _("An effect that draws corners. Add it last not to have the other effects perturb the corners."),
is_advanced: false,
editable_params: {
radius: {
name: _("Radius"),
description: _("The radius of the corner. GNOME apps use a radius of 12 px by default."),
type: "integer",
min: 0,
max: 50,
increment: 1,
},
corners_top: {
name: _("Top corners"),
description: _("Whether or not to round the top corners."),
type: "boolean"
},
corners_bottom: {
name: _("Bottom corners"),
description: _("Whether or not to round the bottom corners."),
type: "boolean"
}
}
}
};
};

View File

@ -0,0 +1,70 @@
uniform sampler2D tex;
uniform float sigma;
uniform int dir;
uniform float brightness;
uniform float width;
uniform float height;
vec4 getTexture(vec2 uv) {
if (uv.x < 3. / width)
uv.x = 3. / width;
if (uv.y < 3. / height)
uv.y = 3. / height;
if (uv.x > 1. - 3. / width)
uv.x = 1. - 3. / width;
if (uv.y > 1. - 3. / height)
uv.y = 1. - 3. / height;
return texture2D(tex, uv);
}
void main(void) {
vec2 uv = cogl_tex_coord_in[0].xy;
vec2 direction = vec2(dir, (1.0 - dir));
float pixel_step;
if (dir == 0)
pixel_step = 1.0 / height;
else
pixel_step = 1.0 / width;
vec3 gauss_coefficient;
gauss_coefficient.x = 1.0 / (sqrt(2.0 * 3.14159265) * sigma);
gauss_coefficient.y = exp(-0.5 / (sigma * sigma));
gauss_coefficient.z = gauss_coefficient.y * gauss_coefficient.y;
float gauss_coefficient_total = gauss_coefficient.x;
vec4 ret = getTexture(uv) * gauss_coefficient.x;
gauss_coefficient.xy *= gauss_coefficient.yz;
int n_steps = int(ceil(1.5 * sigma)) * 2;
for (int i = 1; i <= n_steps; i += 2) {
float coefficient_subtotal = gauss_coefficient.x;
gauss_coefficient.xy *= gauss_coefficient.yz;
coefficient_subtotal += gauss_coefficient.x;
float gauss_ratio = gauss_coefficient.x / coefficient_subtotal;
float foffset = float(i) + gauss_ratio;
vec2 offset = direction * foffset * pixel_step;
ret += getTexture(uv + offset) * coefficient_subtotal;
ret += getTexture(uv - offset) * coefficient_subtotal;
gauss_coefficient_total += 2.0 * coefficient_subtotal;
gauss_coefficient.xy *= gauss_coefficient.yz;
}
vec4 outColor = ret / gauss_coefficient_total;
// apply brightness on the second pass (dir==0 comes last)
if (dir == 0) {
outColor.rgb *= brightness;
}
cogl_color_out = outColor;
}

View File

@ -0,0 +1,213 @@
import GObject from 'gi://GObject';
import * as utils from '../conveniences/utils.js';
const St = await utils.import_in_shell_only('gi://St');
const Shell = await utils.import_in_shell_only('gi://Shell');
const Clutter = await utils.import_in_shell_only('gi://Clutter');
const SHADER_FILENAME = 'gaussian_blur.glsl';
const DEFAULT_PARAMS = {
radius: 30, brightness: .6,
width: 0, height: 0, direction: 0, chained_effect: null
};
export const GaussianBlurEffect = utils.IS_IN_PREFERENCES ?
{ default_params: DEFAULT_PARAMS } :
new GObject.registerClass({
GTypeName: "GaussianBlurEffect",
Properties: {
'radius': GObject.ParamSpec.double(
`radius`,
`Radius`,
`Blur radius`,
GObject.ParamFlags.READWRITE,
0.0, 2000.0,
30.0,
),
'brightness': GObject.ParamSpec.double(
`brightness`,
`Brightness`,
`Blur brightness`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.6,
),
'width': GObject.ParamSpec.double(
`width`,
`Width`,
`Width`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
),
'height': GObject.ParamSpec.double(
`height`,
`Height`,
`Height`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
),
'direction': GObject.ParamSpec.int(
`direction`,
`Direction`,
`Direction`,
GObject.ParamFlags.READWRITE,
0, 1,
0,
),
'chained_effect': GObject.ParamSpec.object(
`chained_effect`,
`Chained Effect`,
`Chained Effect`,
GObject.ParamFlags.READWRITE,
GObject.Object,
),
}
}, class GaussianBlurEffect extends Clutter.ShaderEffect {
constructor(params) {
super(params);
utils.setup_params(this, params);
// set shader source
this._source = utils.get_shader_source(Shell, SHADER_FILENAME, import.meta.url);
if (this._source)
this.set_shader_source(this._source);
const theme_context = St.ThemeContext.get_for_stage(global.stage);
theme_context.connectObject(
'notify::scale-factor', _ =>
this.set_uniform_value('sigma',
parseFloat(this.radius * theme_context.scale_factor / 2 - 1e-6)
),
this
);
}
static get default_params() {
return DEFAULT_PARAMS;
}
get radius() {
return this._radius;
}
set radius(value) {
if (this._radius !== value) {
this._radius = value;
const scale_factor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
// like Clutter, we use the assumption radius = 2*sigma
this.set_uniform_value('sigma', parseFloat(this._radius * scale_factor / 2 - 1e-6));
this.set_enabled(this.radius > 0.);
if (this.chained_effect)
this.chained_effect.radius = value;
}
}
get brightness() {
return this._brightness;
}
set brightness(value) {
if (this._brightness !== value) {
this._brightness = value;
this.set_uniform_value('brightness', parseFloat(this._brightness - 1e-6));
if (this.chained_effect)
this.chained_effect.brightness = value;
}
}
get width() {
return this._width;
}
set width(value) {
if (this._width !== value) {
this._width = value;
this.set_uniform_value('width', parseFloat(this._width + 3.0 - 1e-6));
if (this.chained_effect)
this.chained_effect.width = value;
}
}
get height() {
return this._height;
}
set height(value) {
if (this._height !== value) {
this._height = value;
this.set_uniform_value('height', parseFloat(this._height + 3.0 - 1e-6));
if (this.chained_effect)
this.chained_effect.height = value;
}
}
get direction() {
return this._direction;
}
set direction(value) {
if (this._direction !== value)
this._direction = value;
}
get chained_effect() {
return this._chained_effect;
}
set chained_effect(value) {
this._chained_effect = value;
}
vfunc_set_actor(actor) {
if (this._actor_connection_size_id) {
let old_actor = this.get_actor();
old_actor?.disconnect(this._actor_connection_size_id);
}
if (actor) {
this.width = actor.width;
this.height = actor.height;
this._actor_connection_size_id = actor.connect('notify::size', _ => {
this.width = actor.width;
this.height = actor.height;
});
}
else
this._actor_connection_size_id = null;
super.vfunc_set_actor(actor);
if (this.direction == 0) {
if (this.chained_effect)
this.chained_effect.get_actor()?.remove_effect(this.chained_effect);
else
this.chained_effect = new GaussianBlurEffect({
radius: this.radius,
brightness: this.brightness,
width: this.width,
height: this.height,
direction: 1
});
if (actor !== null)
actor.add_effect(this.chained_effect);
}
}
vfunc_paint_target(paint_node, paint_context) {
this.set_uniform_value("dir", this.direction);
super.vfunc_paint_target(paint_node, paint_context);
}
});

View File

@ -0,0 +1,14 @@
uniform sampler2D tex;
vec3 hsl_to_rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main(void) {
vec2 uv = cogl_tex_coord_in[0].xy;
vec4 hsla = texture2D(tex, uv);
vec4 rgba = vec4(hsl_to_rgb(hsla.xyz), hsla.w);
cogl_color_out = rgba;
}

View File

@ -0,0 +1,31 @@
import GObject from 'gi://GObject';
import * as utils from '../conveniences/utils.js';
const Shell = await utils.import_in_shell_only('gi://Shell');
const Clutter = await utils.import_in_shell_only('gi://Clutter');
const SHADER_FILENAME = 'hsl_to_rgb.glsl';
const DEFAULT_PARAMS = {};
export const HslToRgbEffect = utils.IS_IN_PREFERENCES ?
{ default_params: DEFAULT_PARAMS } :
new GObject.registerClass({
GTypeName: "HslToRgbEffect",
Properties: {}
}, class HslToRgbEffect extends Clutter.ShaderEffect {
constructor(params) {
super(params);
utils.setup_params(this, params);
// set shader source
this._source = utils.get_shader_source(Shell, SHADER_FILENAME, import.meta.url);
if (this._source)
this.set_shader_source(this._source);
}
static get default_params() {
return DEFAULT_PARAMS;
}
});

View File

@ -0,0 +1,44 @@
uniform sampler2D tex;
uniform float radius;
uniform int iterations;
uniform float brightness;
uniform float width;
uniform float height;
uniform bool use_base_pixel;
float srand(vec2 a) {
return sin(dot(a, vec2(1233.224, 1743.335)));
}
float rand(inout float r) {
r = fract(3712.65 * r + 0.61432);
return (r - 0.5) * 2.0;
}
void main() {
vec2 uv = cogl_tex_coord0_in.st;
vec2 p = 16 * radius / vec2(width, height);
float r = srand(uv);
vec2 rv;
int count = 0;
vec4 c = vec4(0.);
for (int i = 0; i < iterations; i++) {
rv.x = rand(r);
rv.y = rand(r);
vec2 new_uv = uv + rv * p;
if (new_uv.x > 2. / width && new_uv.y > 2. / height && new_uv.x < 1. - 3. / width && new_uv.y < 1. - 3. / height) {
c += texture2D(tex, new_uv);
count += 1;
}
}
if (count == 0 || use_base_pixel) {
c += texture2D(tex, uv);
count += 1;
}
c.xyz *= brightness;
cogl_color_out = c / count;
}

View File

@ -0,0 +1,187 @@
import GObject from 'gi://GObject';
import * as utils from '../conveniences/utils.js';
const St = await utils.import_in_shell_only('gi://St');
const Shell = await utils.import_in_shell_only('gi://Shell');
const Clutter = await utils.import_in_shell_only('gi://Clutter');
const SHADER_FILENAME = 'monte_carlo_blur.glsl';
const DEFAULT_PARAMS = {
radius: 2., iterations: 5, brightness: .6,
width: 0, height: 0, use_base_pixel: true
};
export const MonteCarloBlurEffect = utils.IS_IN_PREFERENCES ?
{ default_params: DEFAULT_PARAMS } :
new GObject.registerClass({
GTypeName: "MonteCarloBlurEffect",
Properties: {
'radius': GObject.ParamSpec.double(
`radius`,
`Radius`,
`Blur radius`,
GObject.ParamFlags.READWRITE,
0.0, 2000.0,
2.0,
),
'iterations': GObject.ParamSpec.int(
`iterations`,
`Iterations`,
`Blur iterations`,
GObject.ParamFlags.READWRITE,
0, 64,
5,
),
'brightness': GObject.ParamSpec.double(
`brightness`,
`Brightness`,
`Blur brightness`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.6,
),
'width': GObject.ParamSpec.double(
`width`,
`Width`,
`Width`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
),
'height': GObject.ParamSpec.double(
`height`,
`Height`,
`Height`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
),
'use_base_pixel': GObject.ParamSpec.boolean(
`use_base_pixel`,
`Use base pixel`,
`Use base pixel`,
GObject.ParamFlags.READWRITE,
true,
),
}
}, class MonteCarloBlurEffect extends Clutter.ShaderEffect {
constructor(params) {
super(params);
utils.setup_params(this, params);
// set shader source
this._source = utils.get_shader_source(Shell, SHADER_FILENAME, import.meta.url);
if (this._source)
this.set_shader_source(this._source);
const theme_context = St.ThemeContext.get_for_stage(global.stage);
theme_context.connectObject(
'notify::scale-factor',
_ => this.set_uniform_value('radius',
parseFloat(this._radius * theme_context.scale_factor - 1e-6)
),
this
);
}
static get default_params() {
return DEFAULT_PARAMS;
}
get radius() {
return this._radius;
}
set radius(value) {
if (this._radius !== value) {
this._radius = value;
const scale_factor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
this.set_uniform_value('radius', parseFloat(this._radius * scale_factor - 1e-6));
this.set_enabled(this.radius > 0. && this.iterations > 0);
}
}
get iterations() {
return this._iterations;
}
set iterations(value) {
if (this._iterations !== value) {
this._iterations = value;
this.set_uniform_value('iterations', this._iterations);
this.set_enabled(this.radius > 0. && this.iterations > 0);
}
}
get brightness() {
return this._brightness;
}
set brightness(value) {
if (this._brightness !== value) {
this._brightness = value;
this.set_uniform_value('brightness', parseFloat(this._brightness - 1e-6));
}
}
get width() {
return this._width;
}
set width(value) {
if (this._width !== value) {
this._width = value;
this.set_uniform_value('width', parseFloat(this._width + 3.0 - 1e-6));
}
}
get height() {
return this._height;
}
set height(value) {
if (this._height !== value) {
this._height = value;
this.set_uniform_value('height', parseFloat(this._height + 3.0 - 1e-6));
}
}
get use_base_pixel() {
return this._use_base_pixel;
}
set use_base_pixel(value) {
if (this._use_base_pixel !== value) {
this._use_base_pixel = value;
this.set_uniform_value('use_base_pixel', this._use_base_pixel ? 1 : 0);
}
}
vfunc_set_actor(actor) {
if (this._actor_connection_size_id) {
let old_actor = this.get_actor();
old_actor?.disconnect(this._actor_connection_size_id);
}
if (actor) {
this.width = actor.width;
this.height = actor.height;
this._actor_connection_size_id = actor.connect('notify::size', _ => {
this.width = actor.width;
this.height = actor.height;
});
}
else
this._actor_connection_size_id = null;
super.vfunc_set_actor(actor);
}
});

View File

@ -0,0 +1,43 @@
import GObject from 'gi://GObject';
import * as utils from '../conveniences/utils.js';
const St = await utils.import_in_shell_only('gi://St');
const Shell = await utils.import_in_shell_only('gi://Shell');
const DEFAULT_PARAMS = {
unscaled_radius: 30, brightness: 0.6
};
export const NativeDynamicBlurEffect = utils.IS_IN_PREFERENCES ?
{ default_params: DEFAULT_PARAMS } :
new GObject.registerClass({
GTypeName: "NativeDynamicBlurEffect"
}, class NativeDynamicBlurEffect extends Shell.BlurEffect {
constructor(params) {
const { unscaled_radius, brightness, ...parent_params } = params;
super({ ...parent_params, mode: Shell.BlurMode.BACKGROUND });
this._theme_context = St.ThemeContext.get_for_stage(global.stage);
this._theme_context.connectObject(
'notify::scale-factor',
_ => this.radius = this.unscaled_radius * this._theme_context.scale_factor,
this
);
utils.setup_params(this, params);
}
static get default_params() {
return DEFAULT_PARAMS;
}
get unscaled_radius() {
return this._unscaled_radius;
}
set unscaled_radius(value) {
this._unscaled_radius = value;
this.radius = value * this._theme_context.scale_factor;
}
});

View File

@ -0,0 +1,43 @@
import GObject from 'gi://GObject';
import * as utils from '../conveniences/utils.js';
const St = await utils.import_in_shell_only('gi://St');
const Shell = await utils.import_in_shell_only('gi://Shell');
const DEFAULT_PARAMS = {
unscaled_radius: 30, brightness: 0.6
};
export const NativeStaticBlurEffect = utils.IS_IN_PREFERENCES ?
{ default_params: DEFAULT_PARAMS } :
new GObject.registerClass({
GTypeName: "NativeStaticBlurEffect"
}, class NativeStaticBlurEffect extends Shell.BlurEffect {
constructor(params) {
const { unscaled_radius, brightness, ...parent_params } = params;
super({ ...parent_params, mode: Shell.BlurMode.ACTOR });
this._theme_context = St.ThemeContext.get_for_stage(global.stage);
this._theme_context.connectObject(
'notify::scale-factor',
_ => this.radius = this.unscaled_radius * this._theme_context.scale_factor,
this
);
utils.setup_params(this, params);
}
static get default_params() {
return DEFAULT_PARAMS;
}
get unscaled_radius() {
return this._unscaled_radius;
}
set unscaled_radius(value) {
this._unscaled_radius = value;
this.radius = value * this._theme_context.scale_factor;
}
});

View File

@ -0,0 +1,76 @@
import GObject from 'gi://GObject';
import * as utils from '../conveniences/utils.js';
const Shell = await utils.import_in_shell_only('gi://Shell');
const Clutter = await utils.import_in_shell_only('gi://Clutter');
const SHADER_FILENAME = 'noise.glsl';
const DEFAULT_PARAMS = {
noise: 0.4, lightness: 0.4
};
export const NoiseEffect = utils.IS_IN_PREFERENCES ?
{ default_params: DEFAULT_PARAMS } :
new GObject.registerClass({
GTypeName: "NoiseEffect",
Properties: {
'noise': GObject.ParamSpec.double(
`noise`,
`Noise`,
`Amount of noise integrated with the image`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.4,
),
'lightness': GObject.ParamSpec.double(
`lightness`,
`Lightness`,
`Lightness of the grey used for the noise`,
GObject.ParamFlags.READWRITE,
0.0, 2.0,
0.4,
),
}
}, class NoiseEffect extends Clutter.ShaderEffect {
constructor(params) {
super(params);
utils.setup_params(this, params);
// set shader source
this._source = utils.get_shader_source(Shell, SHADER_FILENAME, import.meta.url);
if (this._source)
this.set_shader_source(this._source);
}
static get default_params() {
return DEFAULT_PARAMS;
}
get noise() {
return this._noise;
}
set noise(value) {
if (this._noise !== value) {
this._noise = value;
this.set_uniform_value('noise', parseFloat(this._noise - 1e-6));
this.set_enabled(this.noise > 0. && this.lightness != 1);
}
}
get lightness() {
return this._lightness;
}
set lightness(value) {
if (this._lightness !== value) {
this._lightness = value;
this.set_uniform_value('lightness', parseFloat(this._lightness - 1e-6));
this.set_enabled(this.noise > 0. && this.lightness != 1);
}
}
});

View File

@ -1,109 +0,0 @@
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Clutter from 'gi://Clutter';
import Shell from 'gi://Shell';
const SHADER_PATH = GLib.filename_from_uri(GLib.uri_resolve_relative(import.meta.url, 'noise_effect.glsl', GLib.UriFlags.NONE))[0];
const get_shader_source = _ => {
try {
return Shell.get_file_contents_utf8_sync(SHADER_PATH);
} catch (e) {
console.warn(`[Blur my Shell] error loading shader from ${SHADER_PATH}: ${e}`);
return null;
}
};
export const NoiseEffect = new GObject.registerClass({
GTypeName: "NoiseEffect",
Properties: {
'noise': GObject.ParamSpec.double(
`noise`,
`Noise`,
`Amount of noise integrated with the image`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.4,
),
'lightness': GObject.ParamSpec.double(
`lightness`,
`Lightness`,
`Lightness of the grey used for the noise`,
GObject.ParamFlags.READWRITE,
0.0, 2.0,
0.4,
),
}
}, class NoiseShader extends Clutter.ShaderEffect {
constructor(params, settings) {
super(params);
this._noise = null;
this._lightness = null;
this._static = true;
this._settings = settings;
if (params.noise)
this.noise = params.noise;
if (params.lightness)
this.lightness = params.lightness;
// set shader source
this._source = get_shader_source();
if (this._source)
this.set_shader_source(this._source);
this.update_enabled();
}
get noise() {
return this._noise;
}
set noise(value) {
if (this._noise !== value) {
this._noise = value;
this.set_uniform_value('noise', parseFloat(this._noise - 1e-6));
}
this.update_enabled();
}
get lightness() {
return this._lightness;
}
set lightness(value) {
if (this._lightness !== value) {
this._lightness = value;
this.set_uniform_value('lightness', parseFloat(this._lightness - 1e-6));
}
}
update_enabled() {
// don't anything if this._settings is undefined (when calling super)
if (this._settings === undefined)
return;
this.set_enabled(
this.noise > 0 &&
this._settings.COLOR_AND_NOISE &&
this._static
);
}
vfunc_paint_target(paint_node = null, paint_context = null) {
this.set_uniform_value("tex", 0);
if (paint_node && paint_context)
super.vfunc_paint_target(paint_node, paint_context);
else if (paint_node)
super.vfunc_paint_target(paint_node);
else
super.vfunc_paint_target();
}
});

View File

@ -1,89 +0,0 @@
import GObject from 'gi://GObject';
import Clutter from 'gi://Clutter';
export const PaintSignals = class PaintSignals {
constructor(connections) {
this.buffer = [];
this.connections = connections;
}
connect(actor, blur_effect) {
let paint_effect = new EmitPaintSignal();
let infos = {
actor: actor,
paint_effect: paint_effect
};
let counter = 0;
actor.add_effect(paint_effect);
this.connections.connect(paint_effect, 'update-blur', () => {
try {
// checking if blur_effect.queue_repaint() has been recently called
if (counter === 0) {
counter = 2;
blur_effect.queue_repaint();
}
else counter--;
} catch (e) { }
});
// remove the actor from buffer when it is destroyed
if (
actor.connect &&
(
!(actor instanceof GObject.Object) ||
GObject.signal_lookup('destroy', actor)
)
)
this.connections.connect(actor, 'destroy', () => {
this.buffer.forEach(infos => {
if (infos.actor === actor) {
// remove from buffer
let index = this.buffer.indexOf(infos);
this.buffer.splice(index, 1);
}
});
});
this.buffer.push(infos);
}
disconnect_all_for_actor(actor) {
this.buffer.forEach(infos => {
if (infos.actor === actor) {
this.connections.disconnect_all_for(infos.paint_effect);
infos.actor.remove_effect(infos.paint_effect);
// remove from buffer
let index = this.buffer.indexOf(infos);
this.buffer.splice(index, 1);
}
});
}
disconnect_all() {
this.buffer.forEach(infos => {
this.connections.disconnect_all_for(infos.paint_effect);
infos.actor.remove_effect(infos.paint_effect);
});
this.buffer = [];
}
};
export const EmitPaintSignal = GObject.registerClass({
GTypeName: 'EmitPaintSignal',
Signals: {
'update-blur': {
param_types: []
},
}
},
class EmitPaintSignal extends Clutter.Effect {
vfunc_paint(node, paint_context, paint_flags) {
this.emit("update-blur");
super.vfunc_paint(node, paint_context, paint_flags);
}
}
);

View File

@ -0,0 +1,88 @@
import GObject from 'gi://GObject';
import * as utils from '../conveniences/utils.js';
const Clutter = await utils.import_in_shell_only('gi://Clutter');
import { UpscaleEffect } from './upscale.js';
import { DownscaleEffect } from './downscale.js';
const DEFAULT_PARAMS = {
factor: 8, downsampling_mode: 0
};
export const PixelizeEffect = utils.IS_IN_PREFERENCES ?
{ default_params: DEFAULT_PARAMS } :
new GObject.registerClass({
GTypeName: "PixelizeEffect",
Properties: {
'factor': GObject.ParamSpec.int(
`factor`,
`Factor`,
`Factor`,
GObject.ParamFlags.READWRITE,
0, 64,
8,
),
'downsampling_mode': GObject.ParamSpec.int(
`downsampling_mode`,
`Downsampling mode`,
`Downsampling mode`,
GObject.ParamFlags.READWRITE,
0, 2,
0,
)
}
}, class PixelizeEffect extends Clutter.Effect {
constructor(params) {
super();
this.upscale_effect = new UpscaleEffect({});
this.downscale_effect = new DownscaleEffect({});
utils.setup_params(this, params);
}
static get default_params() {
return DEFAULT_PARAMS;
}
get factor() {
// should be the same as `this.downscale_effect.divider`
return this.upscale_effect.factor;
}
set factor(value) {
this.upscale_effect.factor = value;
this.downscale_effect.divider = value;
}
get downsampling_mode() {
return this.downscale_effect.downsampling_mode;
}
set downsampling_mode(value) {
this.downscale_effect.downsampling_mode = value;
}
vfunc_set_actor(actor) {
// deattach effects from old actor
this.upscale_effect?.actor?.remove_effect(this.upscale_effect);
this.downscale_effect?.actor?.remove_effect(this.downscale_effect);
// attach effects to new actor
if (actor) {
if (this.upscale_effect)
actor.add_effect(this.upscale_effect);
if (this.downscale_effect)
actor.add_effect(this.downscale_effect);
}
super.vfunc_set_actor(actor);
}
vfunc_set_enabled(is_enabled) {
this.upscale_effect?.set_enabled(is_enabled);
this.downscale_effect?.set_enabled(is_enabled);
super.vfunc_set_enabled(is_enabled);
}
});

View File

@ -0,0 +1,18 @@
uniform sampler2D tex;
vec3 rgb_to_hsl(vec3 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
void main(void) {
vec2 uv = cogl_tex_coord_in[0].xy;
vec4 rgba = texture2D(tex, uv);
vec4 hsla = vec4(rgb_to_hsl(rgba.xyz), rgba.w);
cogl_color_out = hsla;
}

View File

@ -0,0 +1,31 @@
import GObject from 'gi://GObject';
import * as utils from '../conveniences/utils.js';
const Shell = await utils.import_in_shell_only('gi://Shell');
const Clutter = await utils.import_in_shell_only('gi://Clutter');
const SHADER_FILENAME = 'rgb_to_hsl.glsl';
const DEFAULT_PARAMS = {};
export const RgbToHslEffect = utils.IS_IN_PREFERENCES ?
{ default_params: DEFAULT_PARAMS } :
new GObject.registerClass({
GTypeName: "RgbToHslEffect",
Properties: {}
}, class RgbToHslEffect extends Clutter.ShaderEffect {
constructor(params) {
super(params);
utils.setup_params(this, params);
// set shader source
this._source = utils.get_shader_source(Shell, SHADER_FILENAME, import.meta.url);
if (this._source)
this.set_shader_source(this._source);
}
static get default_params() {
return DEFAULT_PARAMS;
}
});

View File

@ -0,0 +1,57 @@
uniform sampler2D tex;
uniform int factor;
uniform float width;
uniform float height;
#define CORRECTION 2.25
#define SIZE_ADDITION 3
vec4 get_texture_at_position(vec2 position) {
vec2 raw_position = position + vec2(CORRECTION, CORRECTION);
vec2 raw_uv = raw_position / vec2(width + SIZE_ADDITION, height + SIZE_ADDITION);
return texture2D(tex, raw_uv);
}
ivec2 get_corrected_position() {
vec2 raw_uv = cogl_tex_coord0_in.st;
vec2 raw_position = raw_uv * vec2(width + SIZE_ADDITION, height + SIZE_ADDITION);
return ivec2(raw_position - vec2(CORRECTION, CORRECTION));
}
void main() {
ivec2 corrected_position = get_corrected_position();
vec2 adjusted_position = corrected_position / factor;
cogl_color_out = get_texture_at_position(adjusted_position);
// round
if (distance(corrected_position, (floor(adjusted_position) + 0.5) * factor) < factor / 2.5) {
//cogl_color_out = get_texture_at_position(adjusted_position);
} else {
//cogl_color_out = vec4(0, 0, 0, 1);
}
// square
if (mod(corrected_position.x, factor) >= 2 && mod(corrected_position.y, factor) >= 2) {
//cogl_color_out = get_texture_at_position(adjusted_position);
} else {
//cogl_color_out = vec4(0, 0, 0, 1);
}
// local mix
vec4 color = vec4(0);
int count = 0;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
vec2 lookup_position = adjusted_position + vec2(i, j);
if (all(greaterThanEqual(lookup_position, vec2(0, 0))) &&
all(lessThan(lookup_position, vec2(width, height) / factor))) {
color += get_texture_at_position(lookup_position);
count += 1;
}
}
}
//cogl_color_out = color / count;
}

View File

@ -0,0 +1,120 @@
import GObject from 'gi://GObject';
import * as utils from '../conveniences/utils.js';
const Shell = await utils.import_in_shell_only('gi://Shell');
const Clutter = await utils.import_in_shell_only('gi://Clutter');
const SHADER_FILENAME = 'upscale.glsl';
const DEFAULT_PARAMS = {
factor: 8, width: 0, height: 0
};
export const UpscaleEffect = utils.IS_IN_PREFERENCES ?
{ default_params: DEFAULT_PARAMS } :
new GObject.registerClass({
GTypeName: "UpscaleEffect",
Properties: {
'factor': GObject.ParamSpec.int(
`factor`,
`Factor`,
`Factor`,
GObject.ParamFlags.READWRITE,
0, 64,
8,
),
'width': GObject.ParamSpec.double(
`width`,
`Width`,
`Width`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
),
'height': GObject.ParamSpec.double(
`height`,
`Height`,
`Height`,
GObject.ParamFlags.READWRITE,
0.0, Number.MAX_SAFE_INTEGER,
0.0,
)
}
}, class UpscaleEffect extends Clutter.ShaderEffect {
constructor(params) {
super(params);
utils.setup_params(this, params);
// set shader source
this._source = utils.get_shader_source(Shell, SHADER_FILENAME, import.meta.url);
if (this._source)
this.set_shader_source(this._source);
}
static get default_params() {
return DEFAULT_PARAMS;
}
get factor() {
return this._factor;
}
set factor(value) {
if (this._factor !== value) {
this._factor = value;
this.set_uniform_value('factor', this._factor);
}
}
get width() {
return this._width;
}
set width(value) {
if (this._width !== value) {
this._width = value;
this.set_uniform_value('width', parseFloat(this._width - 1e-6));
}
}
get height() {
return this._height;
}
set height(value) {
if (this._height !== value) {
this._height = value;
this.set_uniform_value('height', parseFloat(this._height - 1e-6));
}
}
vfunc_set_actor(actor) {
if (this._actor_connection_size_id) {
let old_actor = this.get_actor();
old_actor?.disconnect(this._actor_connection_size_id);
}
if (actor) {
this.width = actor.width;
this.height = actor.height;
this._actor_connection_size_id = actor.connect('notify::size', _ => {
this.width = actor.width;
this.height = actor.height;
});
}
else
this._actor_connection_size_id = null;
super.vfunc_set_actor(actor);
}
vfunc_paint_target(paint_node, paint_context) {
// force setting nearest-neighbour texture filtering
this.get_pipeline().set_layer_filters(0, 9728, 9728);
super.vfunc_paint_target(paint_node, paint_context);
}
});