♻️ Add base classes for the effects and shaders

This commit is contained in:
Simon Schneegans
2022-05-13 20:39:45 +02:00
parent 1ccc34f766
commit cb4b87811e
17 changed files with 955 additions and 1349 deletions
+9 -9
View File
@@ -128,7 +128,7 @@ var SimpleFade = class SimpleFade {
// ---------------------------------------------------------------------------- metadata // ---------------------------------------------------------------------------- metadata
// The effect is available on all GNOME Shell versions supported by this extension. // The effect is available on all GNOME Shell versions supported by this extension.
static getMinShellVersion() { getMinShellVersion() {
return [3, 36]; return [3, 36];
} }
@@ -136,13 +136,13 @@ var SimpleFade = class SimpleFade {
// required. It should match the prefix of the settings keys which store whether the // required. It should match the prefix of the settings keys which store whether the
// effect is enabled currently (e.g. '*-close-effect'), and its animation time // effect is enabled currently (e.g. '*-close-effect'), and its animation time
// (e.g. '*-animation-time'). // (e.g. '*-animation-time').
static getNick() { getNick() {
return 'simple-fade'; return 'simple-fade';
} }
// This will be shown in the sidebar of the preferences dialog as well as in the // This will be shown in the sidebar of the preferences dialog as well as in the
// drop-down menus where the user can choose the effect. // drop-down menus where the user can choose the effect.
static getLabel() { getLabel() {
return _('Simple Fade Effect'); return _('Simple Fade Effect');
} }
@@ -151,7 +151,7 @@ var SimpleFade = class SimpleFade {
// This is called by the preferences dialog. It loads the settings page for this effect, // This is called by the preferences dialog. It loads the settings page for this effect,
// binds all properties to the settings and appends the page to the main stack of the // binds all properties to the settings and appends the page to the main stack of the
// preferences dialog. // preferences dialog.
static getPreferences(dialog) { getPreferences(dialog) {
// Empty for now... Code is added here later in the tutorial! // Empty for now... Code is added here later in the tutorial!
return null; return null;
} }
@@ -161,7 +161,7 @@ var SimpleFade = class SimpleFade {
// This is called from extension.js whenever a window is opened or closed with this // This is called from extension.js whenever a window is opened or closed with this
// effect. It returns an instance of the shader class, trying to reuse previously // effect. It returns an instance of the shader class, trying to reuse previously
// created shaders. // created shaders.
static getShader(actor, settings, forOpening) { getShader(actor, settings, forOpening) {
let shader; let shader;
if (freeShaders.length == 0) { if (freeShaders.length == 0) {
@@ -170,7 +170,7 @@ var SimpleFade = class SimpleFade {
shader = freeShaders.pop(); shader = freeShaders.pop();
} }
shader.setUniforms(actor, settings, forOpening); shader.updateAnimation(actor, settings, forOpening);
return shader; return shader;
} }
@@ -178,7 +178,7 @@ var SimpleFade = class SimpleFade {
// The getActorScale() is called from extension.js to adjust the actor's size during the // The getActorScale() is called from extension.js to adjust the actor's size during the
// animation. This is useful if the effect requires drawing something beyond the usual // animation. This is useful if the effect requires drawing something beyond the usual
// bounds of the actor. This only works for GNOME 3.38+. // bounds of the actor. This only works for GNOME 3.38+.
static getActorScale(settings) { getActorScale(settings) {
return {x: 1.0, y: 1.0}; return {x: 1.0, y: 1.0};
} }
} }
@@ -203,9 +203,9 @@ if (utils.isInShellProcess()) {
this._uFadeWidth = this.get_uniform_location('uFadeWidth'); this._uFadeWidth = this.get_uniform_location('uFadeWidth');
} }
// This is called each time the effect is used. This can be used to retrieve the // This is called each time the shader is used. This can be used to retrieve the
// configuration from the settings and update all uniforms accordingly. // configuration from the settings and update all uniforms accordingly.
setUniforms(actor, settings, forOpening) { updateAnimation(actor, settings, forOpening) {
this.set_uniform_float(this._uForOpening, 1, [forOpening]); this.set_uniform_float(this._uForOpening, 1, [forOpening]);
this.set_uniform_float(this._uFadeWidth, 1, [settings.get_double('simple-fade-width')]); this.set_uniform_float(this._uFadeWidth, 1, [settings.get_double('simple-fade-width')]);
} }
+22 -29
View File
@@ -33,17 +33,17 @@ const utils = Me.imports.src.utils;
// New effects must be registered here and in prefs.js. // New effects must be registered here and in prefs.js.
const ALL_EFFECTS = [ const ALL_EFFECTS = [
Me.imports.src.Apparition.Apparition, new Me.imports.src.Apparition.Apparition(),
Me.imports.src.BrokenGlass.BrokenGlass, new Me.imports.src.BrokenGlass.BrokenGlass(),
Me.imports.src.EnergizeA.EnergizeA, new Me.imports.src.EnergizeA.EnergizeA(),
Me.imports.src.EnergizeB.EnergizeB, new Me.imports.src.EnergizeB.EnergizeB(),
Me.imports.src.Fire.Fire, new Me.imports.src.Fire.Fire(),
Me.imports.src.Hexagon.Hexagon, new Me.imports.src.Hexagon.Hexagon(),
Me.imports.src.Matrix.Matrix, new Me.imports.src.Matrix.Matrix(),
Me.imports.src.SnapOfDisintegration.SnapOfDisintegration, new Me.imports.src.SnapOfDisintegration.SnapOfDisintegration(),
Me.imports.src.TRexAttack.TRexAttack, new Me.imports.src.TRexAttack.TRexAttack(),
Me.imports.src.TVEffect.TVEffect, new Me.imports.src.TVEffect.TVEffect(),
Me.imports.src.Wisps.Wisps, new Me.imports.src.Wisps.Wisps(),
]; ];
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
@@ -222,7 +222,7 @@ class Extension {
const shader = actor.get_effect('burn-my-windows-effect'); const shader = actor.get_effect('burn-my-windows-effect');
if (shader) { if (shader) {
actor.remove_effect(shader); actor.remove_effect(shader);
shader.free(); shader.endAnimation();
} }
} }
@@ -306,7 +306,7 @@ class Extension {
disable() { disable() {
// Free all effect resources. // Free all effect resources.
ALL_EFFECTS.forEach(Effect => Effect.cleanUp()); ALL_EFFECTS.forEach(effect => effect.cleanUp());
// Unregister our resources. // Unregister our resources.
Gio.resources_unregister(this._resources); Gio.resources_unregister(this._resources);
@@ -371,7 +371,7 @@ class Extension {
const oldShader = actor.get_effect('burn-my-windows-effect'); const oldShader = actor.get_effect('burn-my-windows-effect');
if (oldShader) { if (oldShader) {
actor.remove_effect(oldShader); actor.remove_effect(oldShader);
oldShader.free(); oldShader.endAnimation();
} }
// ------------------------------------------------------------------ choose an effect // ------------------------------------------------------------------ choose an effect
@@ -381,9 +381,7 @@ class Extension {
// First we check if an effect is to be previewed. // First we check if an effect is to be previewed.
if (previewNick != '') { if (previewNick != '') {
effect = ALL_EFFECTS.find(Effect => { effect = ALL_EFFECTS.find(effect => effect.getNick() == previewNick);
return Effect.getNick() == previewNick;
});
// Only preview the effect once. // Only preview the effect once.
this._settings.set_string(action + '-preview-effect', ''); this._settings.set_string(action + '-preview-effect', '');
@@ -393,8 +391,8 @@ class Extension {
else { else {
// Therefore, we first create a list of all currently enabled effects. // Therefore, we first create a list of all currently enabled effects.
const enabled = ALL_EFFECTS.filter(Effect => { const enabled = ALL_EFFECTS.filter(effect => {
return this._settings.get_boolean(`${Effect.getNick()}-${action}-effect`); return this._settings.get_boolean(`${effect.getNick()}-${action}-effect`);
}); });
// And then choose a random effect. // And then choose a random effect.
@@ -490,17 +488,12 @@ class Extension {
actor.add_effect_with_name('burn-my-windows-effect', shader); actor.add_effect_with_name('burn-my-windows-effect', shader);
shader.beginAnimation(actor, this._settings, forOpening);
// Update uniforms at each frame. // Update uniforms at each frame.
transition.connect('new-frame', (t) => { transition.connect('new-frame', (t) => {
shader.set_uniform_float(shader.get_uniform_location('uForOpening'), 1, shader.updateAnimation(testMode ? 0.5 : t.get_progress(),
[forOpening]); testMode ? duration / 2 : 0.001 * t.get_elapsed_time());
shader.set_uniform_float(shader.get_uniform_location('uProgress'), 1,
[testMode ? 0.5 : t.get_progress()]);
shader.set_uniform_float(
shader.get_uniform_location('uTime'), 1,
[testMode ? duration / 2 : 0.001 * t.get_elapsed_time()]);
shader.set_uniform_float(shader.get_uniform_location('uSize'), 2,
[actor.width, actor.height]);
}); });
// Remove the effect if the animation finished or was interrupted. // Remove the effect if the animation finished or was interrupted.
@@ -509,7 +502,7 @@ class Extension {
const oldShader = actor.get_effect('burn-my-windows-effect'); const oldShader = actor.get_effect('burn-my-windows-effect');
if (oldShader) { if (oldShader) {
actor.remove_effect(oldShader); actor.remove_effect(oldShader);
oldShader.free(); oldShader.endAnimation();
} }
}); });
} }
+36 -36
View File
@@ -32,17 +32,17 @@ const utils = Me.imports.src.utils;
// New effects must be registered here and in extension.js. // New effects must be registered here and in extension.js.
const ALL_EFFECTS = [ const ALL_EFFECTS = [
Me.imports.src.Apparition.Apparition, new Me.imports.src.Apparition.Apparition(),
Me.imports.src.BrokenGlass.BrokenGlass, new Me.imports.src.BrokenGlass.BrokenGlass(),
Me.imports.src.EnergizeA.EnergizeA, new Me.imports.src.EnergizeA.EnergizeA(),
Me.imports.src.EnergizeB.EnergizeB, new Me.imports.src.EnergizeB.EnergizeB(),
Me.imports.src.Fire.Fire, new Me.imports.src.Fire.Fire(),
Me.imports.src.Hexagon.Hexagon, new Me.imports.src.Hexagon.Hexagon(),
Me.imports.src.Matrix.Matrix, new Me.imports.src.Matrix.Matrix(),
Me.imports.src.SnapOfDisintegration.SnapOfDisintegration, new Me.imports.src.SnapOfDisintegration.SnapOfDisintegration(),
Me.imports.src.TRexAttack.TRexAttack, new Me.imports.src.TRexAttack.TRexAttack(),
Me.imports.src.TVEffect.TVEffect, new Me.imports.src.TVEffect.TVEffect(),
Me.imports.src.Wisps.Wisps, new Me.imports.src.Wisps.Wisps(),
]; ];
// This template widget class is defined at the bottom of this file. // This template widget class is defined at the bottom of this file.
@@ -113,24 +113,24 @@ var PreferencesDialog = class PreferencesDialog {
const group = new Adw.PreferencesGroup({title: _('Effect Options')}); const group = new Adw.PreferencesGroup({title: _('Effect Options')});
this.gtkBoxAppend(this._widget, group); this.gtkBoxAppend(this._widget, group);
ALL_EFFECTS.forEach(Effect => { ALL_EFFECTS.forEach(effect => {
const [minMajor, minMinor] = Effect.getMinShellVersion(); const [minMajor, minMinor] = effect.getMinShellVersion();
if (utils.shellVersionIsAtLeast(minMajor, minMinor)) { if (utils.shellVersionIsAtLeast(minMajor, minMinor)) {
const row = new Adw.ActionRow({title: Effect.getLabel(), activatable: true}); const row = new Adw.ActionRow({title: effect.getLabel(), activatable: true});
row.add_suffix(new Gtk.Image({icon_name: 'go-next-symbolic'})); row.add_suffix(new Gtk.Image({icon_name: 'go-next-symbolic'}));
// Open a subpage with the effect's settings. // Open a subpage with the effect's settings.
row.connect('activated', () => { row.connect('activated', () => {
const page = new BurnMyWindowsEffectPage(Effect, this); const page = new BurnMyWindowsEffectPage(effect, this);
page.valign = Gtk.Align.CENTER; page.valign = Gtk.Align.CENTER;
page.margin_top = 10; page.margin_top = 10;
page.margin_bottom = 10; page.margin_bottom = 10;
page.margin_start = 10; page.margin_start = 10;
page.margin_end = 10; page.margin_end = 10;
// Add the Effect's preferences (if any). // Add the effect's preferences (if any).
const preferences = Effect.getPreferences(this); const preferences = effect.getPreferences(this);
if (preferences) { if (preferences) {
this.gtkBoxAppend(page, preferences); this.gtkBoxAppend(page, preferences);
} }
@@ -186,23 +186,23 @@ var PreferencesDialog = class PreferencesDialog {
this.gtkBoxAppend(this._widget, stack); this.gtkBoxAppend(this._widget, stack);
// Add all other effect pages. // Add all other effect pages.
ALL_EFFECTS.forEach(Effect => { ALL_EFFECTS.forEach(effect => {
const [minMajor, minMinor] = Effect.getMinShellVersion(); const [minMajor, minMinor] = effect.getMinShellVersion();
if (utils.shellVersionIsAtLeast(minMajor, minMinor)) { if (utils.shellVersionIsAtLeast(minMajor, minMinor)) {
const page = new BurnMyWindowsEffectPage(Effect, this); const page = new BurnMyWindowsEffectPage(effect, this);
page.margin_start = 60; page.margin_start = 60;
page.margin_end = 60; page.margin_end = 60;
page.margin_top = 60; page.margin_top = 60;
page.margin_bottom = 60; page.margin_bottom = 60;
// Add the Effect's preferences (if any). // Add the effect's preferences (if any).
const preferences = Effect.getPreferences(this); const preferences = effect.getPreferences(this);
if (preferences) { if (preferences) {
this.gtkBoxAppend(page, preferences); this.gtkBoxAppend(page, preferences);
} }
stack.add_titled(page, Effect.getNick(), Effect.getLabel()); stack.add_titled(page, effect.getNick(), effect.getLabel());
} }
}); });
} }
@@ -302,11 +302,11 @@ var PreferencesDialog = class PreferencesDialog {
const group = Gio.SimpleActionGroup.new(); const group = Gio.SimpleActionGroup.new();
window.insert_action_group('open-effects', group); window.insert_action_group('open-effects', group);
ALL_EFFECTS.forEach(Effect => { ALL_EFFECTS.forEach(effect => {
const [minMajor, minMinor] = Effect.getMinShellVersion(); const [minMajor, minMinor] = effect.getMinShellVersion();
if (utils.shellVersionIsAtLeast(minMajor, minMinor)) { if (utils.shellVersionIsAtLeast(minMajor, minMinor)) {
const nick = Effect.getNick(); const nick = effect.getNick();
const label = Effect.getLabel(); const label = effect.getLabel();
const actionName = nick + '-open-effect'; const actionName = nick + '-open-effect';
const fullName = 'open-effects.' + actionName; const fullName = 'open-effects.' + actionName;
@@ -324,11 +324,11 @@ var PreferencesDialog = class PreferencesDialog {
const group = Gio.SimpleActionGroup.new(); const group = Gio.SimpleActionGroup.new();
window.insert_action_group('close-effects', group); window.insert_action_group('close-effects', group);
ALL_EFFECTS.forEach(Effect => { ALL_EFFECTS.forEach(effect => {
const [minMajor, minMinor] = Effect.getMinShellVersion(); const [minMajor, minMinor] = effect.getMinShellVersion();
if (utils.shellVersionIsAtLeast(minMajor, minMinor)) { if (utils.shellVersionIsAtLeast(minMajor, minMinor)) {
const nick = Effect.getNick(); const nick = effect.getNick();
const label = Effect.getLabel(); const label = effect.getLabel();
const actionName = nick + '-close-effect'; const actionName = nick + '-close-effect';
const fullName = 'close-effects.' + actionName; const fullName = 'close-effects.' + actionName;
@@ -490,17 +490,17 @@ var PreferencesDialog = class PreferencesDialog {
InternalChildren: ['label', 'button'], InternalChildren: ['label', 'button'],
}, },
class BurnMyWindowsEffectPage extends Gtk.Box { // ------------------------------ class BurnMyWindowsEffectPage extends Gtk.Box { // ------------------------------
_init(Effect, dialog) { _init(effect, dialog) {
super._init(); super._init();
// Set the effect's name as label. // Set the effect's name as label.
this._label.label = Effect.getLabel(); this._label.label = effect.getLabel();
// Open the preview window once the preview button is clicked. // Open the preview window once the preview button is clicked.
this._button.connect('clicked', () => { this._button.connect('clicked', () => {
// Set the to-be-previewed effect. // Set the to-be-previewed effect.
dialog.getSettings().set_string('open-preview-effect', Effect.getNick()); dialog.getSettings().set_string('open-preview-effect', effect.getNick());
dialog.getSettings().set_string('close-preview-effect', Effect.getNick()); dialog.getSettings().set_string('close-preview-effect', effect.getNick());
// Make sure that the window.show() firther below "sees" this change. // Make sure that the window.show() firther below "sees" this change.
Gio.Settings.sync(); Gio.Settings.sync();
@@ -508,7 +508,7 @@ var PreferencesDialog = class PreferencesDialog {
// Create the preview-window. // Create the preview-window.
const window = new Gtk.Window({ const window = new Gtk.Window({
// Translators: %s will be replaced by the effect's name. // Translators: %s will be replaced by the effect's name.
title: _('Preview for %s').replace('%s', Effect.getLabel()), title: _('Preview for %s').replace('%s', effect.getLabel()),
default_width: 800, default_width: 800,
default_height: 450, default_height: 450,
modal: true, modal: true,
+35 -81
View File
@@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext;
const ExtensionUtils = imports.misc.extensionUtils; const ExtensionUtils = imports.misc.extensionUtils;
const Me = imports.misc.extensionUtils.getCurrentExtension(); const Me = imports.misc.extensionUtils.getCurrentExtension();
const utils = Me.imports.src.utils; const utils = Me.imports.src.utils;
const Effect = Me.imports.src.Effect.Effect;
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// This effect hides the actor by violently sucking it into the void of magic. // // This effect hides the actor by violently sucking it into the void of magic. //
@@ -27,24 +28,16 @@ const utils = Me.imports.src.utils;
// the center. // // the center. //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect is registered further down in this file. When this // The effect class can be used to get some metadata (like the effect's name or supported
// effect is used for the first time, an instance of this shader class is created. Once // GNOME Shell versions), to initialize the respective page of the settings dialog, as
// the effect is finished, the shader will be stored in the freeShaders array and will // well as to create the actual shader for the effect.
// then be reused if a new shader is requested. ShaderClass which will be used whenever var Apparition = class Apparition extends Effect {
// this effect is used.
let ShaderClass = null;
let freeShaders = [];
// The effect class is completely static. It can be used to get some metadata (like the
// effect's name or supported GNOME Shell versions), to initialize the respective page of
// the settings dialog, as well as to create the actual shader for the effect.
var Apparition = class Apparition {
// ---------------------------------------------------------------------------- metadata // ---------------------------------------------------------------------------- metadata
// The effect is not available on GNOME Shell 3.36 as it requires scaling of the window // The effect is not available on GNOME Shell 3.36 as it requires scaling of the window
// actor. // actor.
static getMinShellVersion() { getMinShellVersion() {
return [3, 38]; return [3, 38];
} }
@@ -52,22 +45,21 @@ var Apparition = class Apparition {
// required. It should match the prefix of the settings keys which store whether the // required. It should match the prefix of the settings keys which store whether the
// effect is enabled currently (e.g. '*-close-effect'), and its animation time // effect is enabled currently (e.g. '*-close-effect'), and its animation time
// (e.g. '*-animation-time'). // (e.g. '*-animation-time').
static getNick() { getNick() {
return 'apparition'; return 'apparition';
} }
// This will be shown in the sidebar of the preferences dialog as well as in the // This will be shown in the sidebar of the preferences dialog as well as in the
// drop-down menus where the user can choose the effect. // drop-down menus where the user can choose the effect.
static getLabel() { getLabel() {
return _('Apparition'); return _('Apparition');
} }
// -------------------------------------------------------------------- API for prefs.js // -------------------------------------------------------------------- API for prefs.js
// This is called by the preferences dialog. It loads the settings page for this effect, // This is called by the preferences dialog. It loads the settings page for this effect,
// binds all properties to the settings and appends the page to the main stack of the // and binds all properties to the settings.
// preferences dialog. getPreferences(dialog) {
static getPreferences(dialog) {
// Add the settings page to the builder. // Add the settings page to the builder.
dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/Apparition.ui`); dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/Apparition.ui`);
@@ -85,53 +77,28 @@ var Apparition = class Apparition {
// ---------------------------------------------------------------- API for extension.js // ---------------------------------------------------------------- API for extension.js
// This is called from extension.js whenever a window is opened or closed with this
// effect. It returns an instance of the shader class, trying to reuse previously
// created shaders.
static getShader(actor, settings, forOpening) {
let shader;
if (freeShaders.length == 0) {
shader = new ShaderClass();
} else {
shader = freeShaders.pop();
}
shader.setUniforms(actor, settings, forOpening);
return shader;
}
// The getActorScale() is called from extension.js to adjust the actor's size during the // The getActorScale() is called from extension.js to adjust the actor's size during the
// animation. This is useful if the effect requires drawing something beyond the usual // animation. This is useful if the effect requires drawing something beyond the usual
// bounds of the actor. This only works for GNOME 3.38+. // bounds of the actor. This only works for GNOME 3.38+.
static getActorScale(settings) { getActorScale(settings) {
return {x: 2.0, y: 2.0}; return {x: 2.0, y: 2.0};
} }
// This is called from extension.js if the extension is disabled. This should free all // This is called by the effect's base class whenever a new shader is required. Since
// static resources. // this shader depends on classes by GNOME Shell, we register it locally in this method
static cleanUp() { // as this file is also included from the preferences dialog where those classes would
freeShaders = []; // not be available.
} createShader() {
}
// Only register the shader class when this method is called for the first time.
if (!this._ShaderClass) {
////////////////////////////////////////////////////////////////////////////////////////// const Shader = Me.imports.src.Shader.Shader;
// The shader class for this effect will only be registered in GNOME Shell's process //
// (not in the preferences process). It's done this way as Clutter may not be installed //
// on the system and therefore the preferences would crash. //
//////////////////////////////////////////////////////////////////////////////////////////
if (utils.isInShellProcess()) { this._ShaderClass = GObject.registerClass({}, class ShaderClass extends Shader {
// We use the constructor of the shader to store all required uniform locations.
const Shell = imports.gi.Shell; _init(effect) {
super._init(effect);
ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect {
// This is called when the effect is used for the first time. This can be used to
// store all required uniform locations.
_init() {
super._init();
this._uSeed = this.get_uniform_location('uSeed'); this._uSeed = this.get_uniform_location('uSeed');
this._uShake = this.get_uniform_location('uShake'); this._uShake = this.get_uniform_location('uShake');
@@ -140,10 +107,13 @@ if (utils.isInShellProcess()) {
this._uRandomness = this.get_uniform_location('uRandomness'); this._uRandomness = this.get_uniform_location('uRandomness');
} }
// This is called each time the effect is used. This can be used to retrieve the // This is called once each time the shader is used. This can be used to retrieve
// configuration from the settings and update all uniforms accordingly. // the configuration from the settings and update all uniforms accordingly.
setUniforms(actor, settings, forOpening) { beginAnimation(actor, settings, forOpening) {
// If we are currently performing integration test, the animation uses a fixed seed. super.beginAnimation(actor, settings, forOpening);
// If we are currently performing integration test, the animation uses a fixed
// seed.
const testMode = settings.get_boolean('test-mode'); const testMode = settings.get_boolean('test-mode');
// clang-format off // clang-format off
@@ -154,26 +124,10 @@ if (utils.isInShellProcess()) {
this.set_uniform_float(this._uRandomness, 1, [settings.get_double('apparition-randomness')]); this.set_uniform_float(this._uRandomness, 1, [settings.get_double('apparition-randomness')]);
// clang-format on // clang-format on
} }
// This is called by extension.js when the shader is not used anymore. We will store
// this instance of the shader so that it can be re-used in th future.
free() {
freeShaders.push(this);
}
// This is called by the constructor. This means, it's only called when the effect
// is used for the first time.
vfunc_build_pipeline() {
const code = utils.loadGLSLResource(`/shaders/${Apparition.getNick()}.glsl`);
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
const declarations = code.substr(0, match.index);
const main = match[1];
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true);
}
}); });
}
// Finally, return a new instance of the shader class.
return new this._ShaderClass(this);
}
} }
+56 -100
View File
@@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext;
const ExtensionUtils = imports.misc.extensionUtils; const ExtensionUtils = imports.misc.extensionUtils;
const Me = imports.misc.extensionUtils.getCurrentExtension(); const Me = imports.misc.extensionUtils.getCurrentExtension();
const utils = Me.imports.src.utils; const utils = Me.imports.src.utils;
const Effect = Me.imports.src.Effect.Effect;
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// This effect shatters the window into pieces. For an explanation how this works, look // // This effect shatters the window into pieces. For an explanation how this works, look //
@@ -28,26 +29,15 @@ const utils = Me.imports.src.utils;
// of vfunc_paint_target further down in this file. // // of vfunc_paint_target further down in this file. //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect is registered further down in this file. When this // The effect class can be used to get some metadata (like the effect's name or supported
// effect is used for the first time, an instance of this shader class is created. Once // GNOME Shell versions), to initialize the respective page of the settings dialog, as
// the effect is finished, the shader will be stored in the freeShaders array and will // well as to create the actual shader for the effect.
// then be reused if a new shader is requested. ShaderClass which will be used whenever var BrokenGlass = class BrokenGlass extends Effect {
// this effect is used.
let ShaderClass = null;
let freeShaders = [];
// This texture will be loaded when the effect is used for the first time.
let shardTexture = null;
// The effect class is completely static. It can be used to get some metadata (like the
// effect's name or supported GNOME Shell versions), to initialize the respective page of
// the settings dialog, as well as to create the actual shader for the effect.
var BrokenGlass = class BrokenGlass {
// ---------------------------------------------------------------------------- metadata // ---------------------------------------------------------------------------- metadata
// This effect is only available on GNOME Shell 40+. // This effect is only available on GNOME Shell 40+.
static getMinShellVersion() { getMinShellVersion() {
return [40, 0]; return [40, 0];
} }
@@ -55,22 +45,21 @@ var BrokenGlass = class BrokenGlass {
// required. It should match the prefix of the settings keys which store whether the // required. It should match the prefix of the settings keys which store whether the
// effect is enabled currently (e.g. '*-close-effect'), and its animation time // effect is enabled currently (e.g. '*-close-effect'), and its animation time
// (e.g. '*-animation-time'). // (e.g. '*-animation-time').
static getNick() { getNick() {
return 'broken-glass'; return 'broken-glass';
} }
// This will be shown in the sidebar of the preferences dialog as well as in the // This will be shown in the sidebar of the preferences dialog as well as in the
// drop-down menus where the user can choose the effect. // drop-down menus where the user can choose the effect.
static getLabel() { getLabel() {
return _('Broken Glass'); return _('Broken Glass');
} }
// -------------------------------------------------------------------- API for prefs.js // -------------------------------------------------------------------- API for prefs.js
// This is called by the preferences dialog. It loads the settings page for this effect, // This is called by the preferences dialog. It loads the settings page for this effect,
// binds all properties to the settings and appends the page to the main stack of the // and binds all properties to the settings.
// preferences dialog. getPreferences(dialog) {
static getPreferences(dialog) {
// Add the settings page to the builder. // Add the settings page to the builder.
dialog.getBuilder().add_from_resource('/ui/gtk4/BrokenGlass.ui'); dialog.getBuilder().add_from_resource('/ui/gtk4/BrokenGlass.ui');
@@ -88,71 +77,51 @@ var BrokenGlass = class BrokenGlass {
// ---------------------------------------------------------------- API for extension.js // ---------------------------------------------------------------- API for extension.js
// This is called from extension.js whenever a window is opened or closed with this
// effect. It returns an instance of the shader class, trying to reuse previously
// created shaders.
static getShader(actor, settings, forOpening) {
let shader;
if (freeShaders.length == 0) {
shader = new ShaderClass();
} else {
shader = freeShaders.pop();
}
shader.setUniforms(actor, settings, forOpening);
return shader;
}
// The getActorScale() is called from extension.js to adjust the actor's size during the // The getActorScale() is called from extension.js to adjust the actor's size during the
// animation. This is useful if the effect requires drawing something beyond the usual // animation. This is useful if the effect requires drawing something beyond the usual
// bounds of the actor. This only works for GNOME 3.38+. // bounds of the actor. This only works for GNOME 3.38+.
static getActorScale(settings) { getActorScale(settings) {
return {x: 2.0, y: 2.0}; return {x: 2.0, y: 2.0};
} }
// This is called from extension.js if the extension is disabled. This should free all // This is called from extension.js if the extension is disabled. This should free all
// static resources. // static resources.
static cleanUp() { cleanUp() {
freeShaders = []; super.cleanUp();
shardTexture = null; this._shardTexture = null;
} }
}
// This is called by the effect's base class whenever a new shader is required. Since
// this shader depends on classes by GNOME Shell, we register it locally in this method
// as this file is also included from the preferences dialog where those classes would
// not be available.
createShader() {
////////////////////////////////////////////////////////////////////////////////////////// // Only register the shader class when this method is called for the first time.
// The shader class for this effect will only be registered in GNOME Shell's process // if (!this._ShaderClass) {
// (not in the preferences process). It's done this way as Clutter may not be installed //
// on the system and therefore the preferences would crash. //
//////////////////////////////////////////////////////////////////////////////////////////
if (utils.isInShellProcess()) { const {Clutter, GdkPixbuf, Cogl} = imports.gi;
const Shader = Me.imports.src.Shader.Shader;
const {Clutter, GdkPixbuf, Cogl, Shell} = imports.gi;
// This shader creates a complex-looking effect with rather simple means. Here is how it
// works: The window is drawn five times on top of each other (see the SHARD_LAYERS
// constant in the GLSL code). Each layer only draws some of the shards, all layers
// combined make up the entire window. The layers are then scaled, rotated, and moved
// independently from each other - this creates the impression that all shards are
// moving independently. In reality, there are only five groups of shards! Which shard
// belongs to which layer is defined by the green channel of the texture
// resources/img/shards.png. The red channel of the texture contains the distance to the
// shard edges. This information is used to fade out the shards.
ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect {
// This is called when the effect is used for the first time. This can be used to
// store all required uniform locations.
_init() {
super._init();
// Load the shards texture.
if (shardTexture == null) {
const shardData = GdkPixbuf.Pixbuf.new_from_resource('/img/shards.png'); const shardData = GdkPixbuf.Pixbuf.new_from_resource('/img/shards.png');
shardTexture = new Clutter.Image(); this._shardTexture = new Clutter.Image();
shardTexture.set_data(shardData.get_pixels(), Cogl.PixelFormat.RGB_888, this._shardTexture.set_data(shardData.get_pixels(), Cogl.PixelFormat.RGB_888,
shardData.width, shardData.height, shardData.rowstride); shardData.width, shardData.height, shardData.rowstride);
}
// This shader creates a complex-looking effect with rather simple means. Here is
// how it works: The window is drawn five times on top of each other (see the
// SHARD_LAYERS constant in the GLSL code). Each layer only draws some of the
// shards, all layers combined make up the entire window. The layers are then
// scaled, rotated, and moved independently from each other - this creates the
// impression that all shards are moving independently. In reality, there are only
// five groups of shards! Which shard belongs to which layer is defined by the green
// channel of the texture resources/img/shards.png. The red channel of the texture
// contains the distance to the shard edges. This information is used to fade out
// the shards.
this._ShaderClass = GObject.registerClass({}, class ShaderClass extends Shader {
// We use the constructor of the shader to store all required uniform locations.
_init(effect) {
super._init(effect);
this._uShardTexture = this.get_uniform_location('uShardTexture'); this._uShardTexture = this.get_uniform_location('uShardTexture');
this._uSeed = this.get_uniform_location('uSeed'); this._uSeed = this.get_uniform_location('uSeed');
@@ -162,9 +131,11 @@ if (utils.isInShellProcess()) {
this._uGravity = this.get_uniform_location('uGravity'); this._uGravity = this.get_uniform_location('uGravity');
} }
// This is called each time the effect is used. This can be used to retrieve the // This is called once each time the shader is used. This can be used to retrieve
// configuration from the settings and update all uniforms accordingly. // the configuration from the settings and update all uniforms accordingly.
setUniforms(actor, settings, forOpening) { beginAnimation(actor, settings, forOpening) {
super.beginAnimation(actor, settings, forOpening);
// Usually, the shards fly away from the center of the window. // Usually, the shards fly away from the center of the window.
let epicenterX = 0.5; let epicenterX = 0.5;
let epicenterY = 0.5; let epicenterY = 0.5;
@@ -180,7 +151,8 @@ if (utils.isInShellProcess()) {
} }
} }
// If we are currently performing integration test, the animation uses a fixed seed. // If we are currently performing integration test, the animation uses a fixed
// seed.
const testMode = settings.get_boolean('test-mode'); const testMode = settings.get_boolean('test-mode');
// clang-format off // clang-format off
@@ -192,30 +164,9 @@ if (utils.isInShellProcess()) {
// clang-format on // clang-format on
} }
// This is called by extension.js when the shader is not used anymore. We will store // This is overridden to bind the shard texture for drawing. Sadly, this seems to
// this instance of the shader so that it can be re-used in th future. // be impossible under GNOME 3.3x as this.get_pipeline() is not available. It was
free() { // called get_target() back then but this is not wrapped in GJS.
freeShaders.push(this);
}
// This is called by the constructor. This means, it's only called when the effect
// is used for the first time.
vfunc_build_pipeline() {
const code = utils.loadGLSLResource(`/shaders/${BrokenGlass.getNick()}.glsl`);
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
const declarations = code.substr(0, match.index);
const main = match[1];
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true);
}
// This is overridden to bind the shard texture for drawing. Sadly, this seems to be
// impossible under GNOME 3.3x as this.get_pipeline() is not available. It was called
// get_target() back then but this is not wrapped in GJS.
// https://gitlab.gnome.org/GNOME/mutter/-/blob/gnome-3-36/clutter/clutter/clutter-offscreen-effect.c#L598 // https://gitlab.gnome.org/GNOME/mutter/-/blob/gnome-3-36/clutter/clutter/clutter-offscreen-effect.c#L598
vfunc_paint_target(node, paint_context) { vfunc_paint_target(node, paint_context) {
const pipeline = this.get_pipeline(); const pipeline = this.get_pipeline();
@@ -225,11 +176,16 @@ if (utils.isInShellProcess()) {
Cogl.PipelineFilter.LINEAR); Cogl.PipelineFilter.LINEAR);
// Bind the shard texture. // Bind the shard texture.
pipeline.set_layer_texture(1, shardTexture.get_texture()); pipeline.set_layer_texture(1, this._effect._shardTexture.get_texture());
pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT); pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT);
pipeline.set_uniform_1i(this._uShardTexture, 1); pipeline.set_uniform_1i(this._uShardTexture, 1);
super.vfunc_paint_target(node, paint_context); super.vfunc_paint_target(node, paint_context);
} }
}); });
}
// Finally, return a new instance of the shader class.
return new this._ShaderClass(this);
}
} }
+70
View File
@@ -0,0 +1,70 @@
//////////////////////////////////////////////////////////////////////////////////////////
// ) ( //
// ( /( ( ( ) ( ( ( ( )\ ) ( ( //
// )\()) ))\ )( ( ( )\ ) )\))( )\ ( (()/( ( )\))( ( //
// ((_)\ /((_|()\ )\ ) )\ '(()/( ((_)()((_) )\ ) ((_)))\((_)()\ )\ //
// | |(_|_))( ((_)_(_/( _((_)) )(_)) _(()((_|_)_(_/( _| |((_)(()((_|(_) //
// | '_ \ || | '_| ' \)) | ' \()| || | \ V V / | ' \)) _` / _ \ V V (_-< //
// |_.__/\_,_|_| |_||_| |_|_|_| \_, | \_/\_/|_|_||_|\__,_\___/\_/\_//__/ //
// |__/ //
// Copyright (c) 2021 Simon Schneegans //
// Released under the GPLv3 or later. See LICENSE file for details. //
//////////////////////////////////////////////////////////////////////////////////////////
'use strict';
//////////////////////////////////////////////////////////////////////////////////////////
// This is the base class for all effects of Burn-My-Windows. It provides the logic //
// required for creating shader instances and reusing them as much as possible. //
//////////////////////////////////////////////////////////////////////////////////////////
var Effect = class Effect {
// The _freeShaders array contains previously created shaders which are not currently in
// use.
constructor() {
this._freeShaders = [];
}
// ---------------------------------------------------------------- API for extension.js
// This is called from extension.js whenever a window is opened or closed with this
// effect. It returns an instance of the shader class, trying to reuse previously
// created shaders. If a new shader instance is required, it calls this.createShader().
// This method must be defined be the derived class!
getShader(actor, settings, forOpening) {
let shader;
if (this._freeShaders.length == 0) {
shader = this.createShader(this);
} else {
shader = this._freeShaders.pop();
}
shader.updateAnimation(actor, settings, forOpening);
return shader;
}
// This is called from extension.js whenever a shader previously retrieved with
// getShader() is not used anymore.
freeShader(shader) {
this._freeShaders.push(shader);
}
// ------------------------------------- "virtual" methods - feel free to override them!
// The getActorScale() is called from extension.js to adjust the actor's size during the
// animation. Override this, if your effect requires drawing something beyond the usual
// bounds of the actor. This only works for GNOME 3.38+.
getActorScale(settings) {
return {x: 1.0, y: 1.0};
}
// This is called from extension.js if the extension is disabled. This should free all
// static resources. So if you have to delete some textures for example, you should
// override this.
cleanUp() {
freeShaders = [];
}
}
+33 -86
View File
@@ -20,28 +20,21 @@ const _ = imports.gettext.domain('burn-my-windows').gettext;
const ExtensionUtils = imports.misc.extensionUtils; const ExtensionUtils = imports.misc.extensionUtils;
const Me = imports.misc.extensionUtils.getCurrentExtension(); const Me = imports.misc.extensionUtils.getCurrentExtension();
const utils = Me.imports.src.utils; const utils = Me.imports.src.utils;
const Effect = Me.imports.src.Effect.Effect;
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// This effect looks a bit like the transporter effect from TOS. // // This effect looks a bit like the transporter effect from TOS. //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect is registered further down in this file. When this // The effect class can be used to get some metadata (like the effect's name or supported
// effect is used for the first time, an instance of this shader class is created. Once // GNOME Shell versions), to initialize the respective page of the settings dialog, as
// the effect is finished, the shader will be stored in the freeShaders array and will // well as to create the actual shader for the effect.
// then be reused if a new shader is requested. ShaderClass which will be used whenever var EnergizeA = class EnergizeA extends Effect {
// this effect is used.
let ShaderClass = null;
let freeShaders = [];
// The effect class is completely static. It can be used to get some metadata (like the
// effect's name or supported GNOME Shell versions), to initialize the respective page of
// the settings dialog, as well as to create the actual shader for the effect.
var EnergizeA = class EnergizeA {
// ---------------------------------------------------------------------------- metadata // ---------------------------------------------------------------------------- metadata
// The effect is available on all GNOME Shell versions supported by this extension. // The effect is available on all GNOME Shell versions supported by this extension.
static getMinShellVersion() { getMinShellVersion() {
return [3, 36]; return [3, 36];
} }
@@ -49,22 +42,21 @@ var EnergizeA = class EnergizeA {
// required. It should match the prefix of the settings keys which store whether the // required. It should match the prefix of the settings keys which store whether the
// effect is enabled currently (e.g. '*-close-effect'), and its animation time // effect is enabled currently (e.g. '*-close-effect'), and its animation time
// (e.g. '*-animation-time'). // (e.g. '*-animation-time').
static getNick() { getNick() {
return 'energize-a'; return 'energize-a';
} }
// This will be shown in the sidebar of the preferences dialog as well as in the // This will be shown in the sidebar of the preferences dialog as well as in the
// drop-down menus where the user can choose the effect. // drop-down menus where the user can choose the effect.
static getLabel() { getLabel() {
return _('Energize A'); return _('Energize A');
} }
// -------------------------------------------------------------------- API for prefs.js // -------------------------------------------------------------------- API for prefs.js
// This is called by the preferences dialog. It loads the settings page for this effect, // This is called by the preferences dialog. It loads the settings page for this effect,
// binds all properties to the settings and appends the page to the main stack of the // and binds all properties to the settings.
// preferences dialog. getPreferences(dialog) {
static getPreferences(dialog) {
// Add the settings page to the builder. // Add the settings page to the builder.
dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/EnergizeA.ui`); dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/EnergizeA.ui`);
@@ -80,61 +72,32 @@ var EnergizeA = class EnergizeA {
// ---------------------------------------------------------------- API for extension.js // ---------------------------------------------------------------- API for extension.js
// This is called from extension.js whenever a window is opened or closed with this // This is called by the effect's base class whenever a new shader is required. Since
// effect. It returns an instance of the shader class, trying to reuse previously // this shader depends on classes by GNOME Shell, we register it locally in this method
// created shaders. // as this file is also included from the preferences dialog where those classes would
static getShader(actor, settings, forOpening) { // not be available.
let shader; createShader() {
if (freeShaders.length == 0) { // Only register the shader class when this method is called for the first time.
shader = new ShaderClass(); if (!this._ShaderClass) {
} else {
shader = freeShaders.pop();
}
shader.setUniforms(actor, settings, forOpening); const Clutter = imports.gi.Clutter;
const Shader = Me.imports.src.Shader.Shader;
return shader; this._ShaderClass = GObject.registerClass({}, class ShaderClass extends Shader {
} // We use the constructor of the shader to store all required uniform locations.
_init(effect) {
// The getActorScale() is called from extension.js to adjust the actor's size during the super._init(effect);
// animation. This is useful if the effect requires drawing something beyond the usual
// bounds of the actor. This only works for GNOME 3.38+.
static getActorScale(settings) {
return {x: 1.0, y: 1.0};
}
// This is called from extension.js if the extension is disabled. This should free all
// static resources.
static cleanUp() {
freeShaders = [];
}
}
//////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect will only be registered in GNOME Shell's process //
// (not in the preferences process). It's done this way as Clutter may not be installed //
// on the system and therefore the preferences would crash. //
//////////////////////////////////////////////////////////////////////////////////////////
if (utils.isInShellProcess()) {
const {Clutter, Shell} = imports.gi;
ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect {
// This is called when the effect is used for the first time. This can be used to
// store all required uniform locations.
_init() {
super._init();
this._uColor = this.get_uniform_location('uColor'); this._uColor = this.get_uniform_location('uColor');
this._uScale = this.get_uniform_location('uScale'); this._uScale = this.get_uniform_location('uScale');
} }
// This is called each time the effect is used. This can be used to retrieve the // This is called once each time the shader is used. This can be used to retrieve
// configuration from the settings and update all uniforms accordingly. // the configuration from the settings and update all uniforms accordingly.
setUniforms(actor, settings, forOpening) { beginAnimation(actor, settings, forOpening) {
super.beginAnimation(actor, settings, forOpening);
const c = Clutter.Color.from_string(settings.get_string('energize-a-color'))[1]; const c = Clutter.Color.from_string(settings.get_string('energize-a-color'))[1];
// clang-format off // clang-format off
@@ -142,26 +105,10 @@ if (utils.isInShellProcess()) {
this.set_uniform_float(this._uScale, 1, [settings.get_double('energize-a-scale')]); this.set_uniform_float(this._uScale, 1, [settings.get_double('energize-a-scale')]);
// clang-format on // clang-format on
} }
// This is called by extension.js when the shader is not used anymore. We will store
// this instance of the shader so that it can be re-used in th future.
free() {
freeShaders.push(this);
}
// This is called by the constructor. This means, it's only called when the effect
// is used for the first time.
vfunc_build_pipeline() {
const code = utils.loadGLSLResource(`/shaders/${EnergizeA.getNick()}.glsl`);
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
const declarations = code.substr(0, match.index);
const main = match[1];
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true);
}
}); });
}
// Finally, return a new instance of the shader class.
return new this._ShaderClass(this);
}
} }
+33 -86
View File
@@ -20,28 +20,21 @@ const _ = imports.gettext.domain('burn-my-windows').gettext;
const ExtensionUtils = imports.misc.extensionUtils; const ExtensionUtils = imports.misc.extensionUtils;
const Me = imports.misc.extensionUtils.getCurrentExtension(); const Me = imports.misc.extensionUtils.getCurrentExtension();
const utils = Me.imports.src.utils; const utils = Me.imports.src.utils;
const Effect = Me.imports.src.Effect.Effect;
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// This effect looks a bit like the transporter effect from TNG. // // This effect looks a bit like the transporter effect from TNG. //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect is registered further down in this file. When this // The effect class can be used to get some metadata (like the effect's name or supported
// effect is used for the first time, an instance of this shader class is created. Once // GNOME Shell versions), to initialize the respective page of the settings dialog, as
// the effect is finished, the shader will be stored in the freeShaders array and will // well as to create the actual shader for the effect.
// then be reused if a new shader is requested. ShaderClass which will be used whenever var EnergizeB = class EnergizeB extends Effect {
// this effect is used.
let ShaderClass = null;
let freeShaders = [];
// The effect class is completely static. It can be used to get some metadata (like the
// effect's name or supported GNOME Shell versions), to initialize the respective page of
// the settings dialog, as well as to create the actual shader for the effect.
var EnergizeB = class EnergizeB {
// ---------------------------------------------------------------------------- metadata // ---------------------------------------------------------------------------- metadata
// The effect is available on all GNOME Shell versions supported by this extension. // The effect is available on all GNOME Shell versions supported by this extension.
static getMinShellVersion() { getMinShellVersion() {
return [3, 36]; return [3, 36];
} }
@@ -49,22 +42,21 @@ var EnergizeB = class EnergizeB {
// required. It should match the prefix of the settings keys which store whether the // required. It should match the prefix of the settings keys which store whether the
// effect is enabled currently (e.g. '*-close-effect'), and its animation time // effect is enabled currently (e.g. '*-close-effect'), and its animation time
// (e.g. '*-animation-time'). // (e.g. '*-animation-time').
static getNick() { getNick() {
return 'energize-b'; return 'energize-b';
} }
// This will be shown in the sidebar of the preferences dialog as well as in the // This will be shown in the sidebar of the preferences dialog as well as in the
// drop-down menus where the user can choose the effect. // drop-down menus where the user can choose the effect.
static getLabel() { getLabel() {
return _('Energize B'); return _('Energize B');
} }
// -------------------------------------------------------------------- API for prefs.js // -------------------------------------------------------------------- API for prefs.js
// This is called by the preferences dialog. It loads the settings page for this effect, // This is called by the preferences dialog. It loads the settings page for this effect,
// binds all properties to the settings and appends the page to the main stack of the // and binds all properties to the settings.
// preferences dialog. getPreferences(dialog) {
static getPreferences(dialog) {
// Add the settings page to the builder. // Add the settings page to the builder.
dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/EnergizeB.ui`); dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/EnergizeB.ui`);
@@ -80,61 +72,32 @@ var EnergizeB = class EnergizeB {
// ---------------------------------------------------------------- API for extension.js // ---------------------------------------------------------------- API for extension.js
// This is called from extension.js whenever a window is opened or closed with this // This is called by the effect's base class whenever a new shader is required. Since
// effect. It returns an instance of the shader class, trying to reuse previously // this shader depends on classes by GNOME Shell, we register it locally in this method
// created shaders. // as this file is also included from the preferences dialog where those classes would
static getShader(actor, settings, forOpening) { // not be available.
let shader; createShader() {
if (freeShaders.length == 0) { // Only register the shader class when this method is called for the first time.
shader = new ShaderClass(); if (!this._ShaderClass) {
} else {
shader = freeShaders.pop();
}
shader.setUniforms(actor, settings, forOpening); const Clutter = imports.gi.Clutter;
const Shader = Me.imports.src.Shader.Shader;
return shader; this._ShaderClass = GObject.registerClass({}, class ShaderClass extends Shader {
} // We use the constructor of the shader to store all required uniform locations.
_init(effect) {
// The getActorScale() is called from extension.js to adjust the actor's size during the super._init(effect);
// animation. This is useful if the effect requires drawing something beyond the usual
// bounds of the actor. This only works for GNOME 3.38+.
static getActorScale(settings) {
return {x: 1.0, y: 1.0};
}
// This is called from extension.js if the extension is disabled. This should free all
// static resources.
static cleanUp() {
freeShaders = [];
}
}
//////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect will only be registered in GNOME Shell's process //
// (not in the preferences process). It's done this way as Clutter may not be installed //
// on the system and therefore the preferences would crash. //
//////////////////////////////////////////////////////////////////////////////////////////
if (utils.isInShellProcess()) {
const {Clutter, Shell} = imports.gi;
ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect {
// This is called when the effect is used for the first time. This can be used to
// store all required uniform locations.
_init() {
super._init();
this._uColor = this.get_uniform_location('uColor'); this._uColor = this.get_uniform_location('uColor');
this._uScale = this.get_uniform_location('uScale'); this._uScale = this.get_uniform_location('uScale');
} }
// This is called each time the effect is used. This can be used to retrieve the // This is called once each time the shader is used. This can be used to retrieve
// configuration from the settings and update all uniforms accordingly. // the configuration from the settings and update all uniforms accordingly.
setUniforms(actor, settings, forOpening) { beginAnimation(actor, settings, forOpening) {
super.beginAnimation(actor, settings, forOpening);
const c = Clutter.Color.from_string(settings.get_string('energize-b-color'))[1]; const c = Clutter.Color.from_string(settings.get_string('energize-b-color'))[1];
// clang-format off // clang-format off
@@ -142,26 +105,10 @@ if (utils.isInShellProcess()) {
this.set_uniform_float(this._uScale, 1, [settings.get_double('energize-b-scale')]); this.set_uniform_float(this._uScale, 1, [settings.get_double('energize-b-scale')]);
// clang-format on // clang-format on
} }
// This is called by extension.js when the shader is not used anymore. We will store
// this instance of the shader so that it can be re-used in th future.
free() {
freeShaders.push(this);
}
// This is called by the constructor. This means, it's only called when the effect
// is used for the first time.
vfunc_build_pipeline() {
const code = utils.loadGLSLResource(`/shaders/${EnergizeB.getNick()}.glsl`);
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
const declarations = code.substr(0, match.index);
const main = match[1];
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true);
}
}); });
}
// Finally, return a new instance of the shader class.
return new this._ShaderClass(this);
}
} }
+59 -111
View File
@@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext;
const ExtensionUtils = imports.misc.extensionUtils; const ExtensionUtils = imports.misc.extensionUtils;
const Me = imports.misc.extensionUtils.getCurrentExtension(); const Me = imports.misc.extensionUtils.getCurrentExtension();
const utils = Me.imports.src.utils; const utils = Me.imports.src.utils;
const Effect = Me.imports.src.Effect.Effect;
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// This effect is a homage to the good old Compiz days. However, it is implemented // // This effect is a homage to the good old Compiz days. However, it is implemented //
@@ -29,23 +30,15 @@ const utils = Me.imports.src.utils;
// there are a couple of moving gradients which fade-in or fade-out the fire effect. // // there are a couple of moving gradients which fade-in or fade-out the fire effect. //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect is registered further down in this file. When this // The effect class can be used to get some metadata (like the effect's name or supported
// effect is used for the first time, an instance of this shader class is created. Once // GNOME Shell versions), to initialize the respective page of the settings dialog, as
// the effect is finished, the shader will be stored in the freeShaders array and will // well as to create the actual shader for the effect.
// then be reused if a new shader is requested. ShaderClass which will be used whenever var Fire = class Fire extends Effect {
// this effect is used.
let ShaderClass = null;
let freeShaders = [];
// The effect class is completely static. It can be used to get some metadata (like the
// effect's name or supported GNOME Shell versions), to initialize the respective page of
// the settings dialog, as well as to create the actual shader for the effect.
var Fire = class Fire {
// ---------------------------------------------------------------------------- metadata // ---------------------------------------------------------------------------- metadata
// The effect is available on all GNOME Shell versions supported by this extension. // The effect is available on all GNOME Shell versions supported by this extension.
static getMinShellVersion() { getMinShellVersion() {
return [3, 36]; return [3, 36];
} }
@@ -53,22 +46,21 @@ var Fire = class Fire {
// required. It should match the prefix of the settings keys which store whether the // required. It should match the prefix of the settings keys which store whether the
// effect is enabled currently (e.g. '*-close-effect'), and its animation time // effect is enabled currently (e.g. '*-close-effect'), and its animation time
// (e.g. '*-animation-time'). // (e.g. '*-animation-time').
static getNick() { getNick() {
return 'fire'; return 'fire';
} }
// This will be shown in the sidebar of the preferences dialog as well as in the // This will be shown in the sidebar of the preferences dialog as well as in the
// drop-down menus where the user can choose the effect. // drop-down menus where the user can choose the effect.
static getLabel() { getLabel() {
return _('Fire'); return _('Fire');
} }
// -------------------------------------------------------------------- API for prefs.js // -------------------------------------------------------------------- API for prefs.js
// This is called by the preferences dialog. It loads the settings page for this effect, // This is called by the preferences dialog. It loads the settings page for this effect,
// binds all properties to the settings and appends the page to the main stack of the // and binds all properties to the settings.
// preferences dialog. getPreferences(dialog) {
static getPreferences(dialog) {
// Add the settings page to the builder. // Add the settings page to the builder.
dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/Fire.ui`); dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/Fire.ui`);
@@ -94,7 +86,7 @@ var Fire = class Fire {
}); });
// Initialize the fire-preset dropdown. // Initialize the fire-preset dropdown.
Fire._createFirePresets(dialog); this._createFirePresets(dialog);
// Finally, return the new settings page. // Finally, return the new settings page.
return dialog.getBuilder().get_object('fire-prefs'); return dialog.getBuilder().get_object('fire-prefs');
@@ -102,40 +94,67 @@ var Fire = class Fire {
// ---------------------------------------------------------------- API for extension.js // ---------------------------------------------------------------- API for extension.js
// This is called from extension.js whenever a window is opened or closed with this // This is called by the effect's base class whenever a new shader is required. Since
// effect. It returns an instance of the shader class, trying to reuse previously // this shader depends on classes by GNOME Shell, we register it locally in this method
// created shaders. // as this file is also included from the preferences dialog where those classes would
static getShader(actor, settings, forOpening) { // not be available.
let shader; createShader() {
if (freeShaders.length == 0) { // Only register the shader class when this method is called for the first time.
shader = new ShaderClass(); if (!this._ShaderClass) {
} else {
shader = freeShaders.pop(); const Clutter = imports.gi.Clutter;
const Shader = Me.imports.src.Shader.Shader;
this._ShaderClass = GObject.registerClass({}, class ShaderClass extends Shader {
// We use the constructor of the shader to store all required uniform locations.
_init(effect) {
super._init(effect);
this._uGradient = [
this.get_uniform_location('uGradient1'),
this.get_uniform_location('uGradient2'),
this.get_uniform_location('uGradient3'),
this.get_uniform_location('uGradient4'),
this.get_uniform_location('uGradient5'),
];
this._u3DNoise = this.get_uniform_location('u3DNoise');
this._uScale = this.get_uniform_location('uScale');
this._uMovementSpeed = this.get_uniform_location('uMovementSpeed');
} }
shader.setUniforms(actor, settings, forOpening); // This is called once each time the shader is used. This can be used to retrieve
// the configuration from the settings and update all uniforms accordingly.
beginAnimation(actor, settings, forOpening) {
super.beginAnimation(actor, settings, forOpening);
return shader; // Load the gradient values from the settings.
for (let i = 1; i <= 5; i++) {
const c =
Clutter.Color.from_string(settings.get_string('fire-color-' + i))[1];
this.set_uniform_float(
this._uGradient[i - 1], 4,
[c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255]);
} }
// The getActorScale() is called from extension.js to adjust the actor's size during the // clang-format off
// animation. This is useful if the effect requires drawing something beyond the usual this.set_uniform_float(this._u3DNoise, 1, [settings.get_boolean('flame-3d-noise')]);
// bounds of the actor. This only works for GNOME 3.38+. this.set_uniform_float(this._uScale, 1, [settings.get_double('flame-scale')]);
static getActorScale(settings) { this.set_uniform_float(this._uMovementSpeed, 1, [settings.get_double('flame-movement-speed')]);
return {x: 1.0, y: 1.0}; // clang-format on
}
});
} }
// This is called from extension.js if the extension is disabled. This should free all // Finally, return a new instance of the shader class.
// static resources. return new this._ShaderClass(this);
static cleanUp() {
freeShaders = [];
} }
// ----------------------------------------------------------------------- private stuff // ----------------------------------------------------------------------- private stuff
// This populates the preset dropdown menu for the fire options. // This populates the preset dropdown menu for the fire options.
static _createFirePresets(dialog) { _createFirePresets(dialog) {
dialog.getBuilder().get_object('fire-prefs').connect('realize', (widget) => { dialog.getBuilder().get_object('fire-prefs').connect('realize', (widget) => {
const presets = [ const presets = [
{ {
@@ -221,74 +240,3 @@ var Fire = class Fire {
}); });
} }
} }
//////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect will only be registered in GNOME Shell's process //
// (not in the preferences process). It's done this way as Clutter may not be installed //
// on the system and therefore the preferences would crash. //
//////////////////////////////////////////////////////////////////////////////////////////
if (utils.isInShellProcess()) {
const {Clutter, Shell} = imports.gi;
ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect {
// This is called when the effect is used for the first time. This can be used to
// store all required uniform locations.
_init() {
super._init();
this._uGradient = [
this.get_uniform_location('uGradient1'),
this.get_uniform_location('uGradient2'),
this.get_uniform_location('uGradient3'),
this.get_uniform_location('uGradient4'),
this.get_uniform_location('uGradient5'),
];
this._u3DNoise = this.get_uniform_location('u3DNoise');
this._uScale = this.get_uniform_location('uScale');
this._uMovementSpeed = this.get_uniform_location('uMovementSpeed');
}
// This is called each time the effect is used. This can be used to retrieve the
// configuration from the settings and update all uniforms accordingly.
setUniforms(actor, settings, forOpening) {
// Load the gradient values from the settings.
for (let i = 1; i <= 5; i++) {
const c = Clutter.Color.from_string(settings.get_string('fire-color-' + i))[1];
this.set_uniform_float(this._uGradient[i - 1], 4,
[c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255]);
}
// clang-format off
this.set_uniform_float(this._u3DNoise, 1, [settings.get_boolean('flame-3d-noise')]);
this.set_uniform_float(this._uScale, 1, [settings.get_double('flame-scale')]);
this.set_uniform_float(this._uMovementSpeed, 1, [settings.get_double('flame-movement-speed')]);
// clang-format on
}
// This is called by extension.js when the shader is not used anymore. We will store
// this instance of the shader so that it can be re-used in th future.
free() {
freeShaders.push(this);
}
// This is called by the constructor. This means, it's only called when the effect
// is used for the first time.
vfunc_build_pipeline() {
const code = utils.loadGLSLResource(`/shaders/${Fire.getNick()}.glsl`);
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
const declarations = code.substr(0, match.index);
const main = match[1];
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true);
}
});
}
+40 -91
View File
@@ -20,29 +20,22 @@ const _ = imports.gettext.domain('burn-my-windows').gettext;
const ExtensionUtils = imports.misc.extensionUtils; const ExtensionUtils = imports.misc.extensionUtils;
const Me = imports.misc.extensionUtils.getCurrentExtension(); const Me = imports.misc.extensionUtils.getCurrentExtension();
const utils = Me.imports.src.utils; const utils = Me.imports.src.utils;
const Effect = Me.imports.src.Effect.Effect;
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// This effect overlays a glowing hexagonal grid over the window. The grid cells then // // This effect overlays a glowing hexagonal grid over the window. The grid cells then //
// gradually shrink until the window is fully dissolved. // // gradually shrink until the window is fully dissolved. //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect is registered further down in this file. When this // The effect class can be used to get some metadata (like the effect's name or supported
// effect is used for the first time, an instance of this shader class is created. Once // GNOME Shell versions), to initialize the respective page of the settings dialog, as
// the effect is finished, the shader will be stored in the freeShaders array and will // well as to create the actual shader for the effect.
// then be reused if a new shader is requested. ShaderClass which will be used whenever var Hexagon = class Hexagon extends Effect {
// this effect is used.
let ShaderClass = null;
let freeShaders = [];
// The effect class is completely static. It can be used to get some metadata (like the
// effect's name or supported GNOME Shell versions), to initialize the respective page of
// the settings dialog, as well as to create the actual shader for the effect.
var Hexagon = class Hexagon {
// ---------------------------------------------------------------------------- metadata // ---------------------------------------------------------------------------- metadata
// The effect is available on all GNOME Shell versions supported by this extension. // The effect is available on all GNOME Shell versions supported by this extension.
static getMinShellVersion() { getMinShellVersion() {
return [3, 36]; return [3, 36];
} }
@@ -50,22 +43,21 @@ var Hexagon = class Hexagon {
// required. It should match the prefix of the settings keys which store whether the // required. It should match the prefix of the settings keys which store whether the
// effect is enabled currently (e.g. '*-close-effect'), and its animation time // effect is enabled currently (e.g. '*-close-effect'), and its animation time
// (e.g. '*-animation-time'). // (e.g. '*-animation-time').
static getNick() { getNick() {
return 'hexagon'; return 'hexagon';
} }
// This will be shown in the sidebar of the preferences dialog as well as in the // This will be shown in the sidebar of the preferences dialog as well as in the
// drop-down menus where the user can choose the effect. // drop-down menus where the user can choose the effect.
static getLabel() { getLabel() {
return _('Hexagon'); return _('Hexagon');
} }
// -------------------------------------------------------------------- API for prefs.js // -------------------------------------------------------------------- API for prefs.js
// This is called by the preferences dialog. It loads the settings page for this effect, // This is called by the preferences dialog. It loads the settings page for this effect,
// binds all properties to the settings and appends the page to the main stack of the // and binds all properties to the settings.
// preferences dialog. getPreferences(dialog) {
static getPreferences(dialog) {
// Add the settings page to the builder. // Add the settings page to the builder.
dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/Hexagon.ui`); dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/Hexagon.ui`);
@@ -84,53 +76,22 @@ var Hexagon = class Hexagon {
// ---------------------------------------------------------------- API for extension.js // ---------------------------------------------------------------- API for extension.js
// This is called from extension.js whenever a window is opened or closed with this // This is called by the effect's base class whenever a new shader is required. Since
// effect. It returns an instance of the shader class, trying to reuse previously // this shader depends on classes by GNOME Shell, we register it locally in this method
// created shaders. // as this file is also included from the preferences dialog where those classes would
static getShader(actor, settings, forOpening) { // not be available.
let shader; createShader() {
if (freeShaders.length == 0) { // Only register the shader class when this method is called for the first time.
shader = new ShaderClass(); if (!this._ShaderClass) {
} else {
shader = freeShaders.pop();
}
shader.setUniforms(actor, settings, forOpening); const Clutter = imports.gi.Clutter;
const Shader = Me.imports.src.Shader.Shader;
return shader; this._ShaderClass = GObject.registerClass({}, class ShaderClass extends Shader {
} // We use the constructor of the shader to store all required uniform locations.
_init(effect) {
// The getActorScale() is called from extension.js to adjust the actor's size during the super._init(effect);
// animation. This is useful if the effect requires drawing something beyond the usual
// bounds of the actor. This only works for GNOME 3.38+.
static getActorScale(settings) {
return {x: 1.0, y: 1.0};
}
// This is called from extension.js if the extension is disabled. This should free all
// static resources.
static cleanUp() {
freeShaders = [];
}
}
//////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect will only be registered in GNOME Shell's process //
// (not in the preferences process). It's done this way as Clutter may not be installed //
// on the system and therefore the preferences would crash. //
//////////////////////////////////////////////////////////////////////////////////////////
if (utils.isInShellProcess()) {
const {Clutter, Shell} = imports.gi;
ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect {
// This is called when the effect is used for the first time. This can be used to
// store all required uniform locations.
_init() {
super._init();
this._uAdditiveBlending = this.get_uniform_location('uAdditiveBlending'); this._uAdditiveBlending = this.get_uniform_location('uAdditiveBlending');
this._uSeed = this.get_uniform_location('uSeed'); this._uSeed = this.get_uniform_location('uSeed');
@@ -140,16 +101,20 @@ if (utils.isInShellProcess()) {
this._uLineColor = this.get_uniform_location('uLineColor'); this._uLineColor = this.get_uniform_location('uLineColor');
} }
// This is called each time the effect is used. This can be used to retrieve the // This is called once each time the shader is used. This can be used to retrieve
// configuration from the settings and update all uniforms accordingly. // the configuration from the settings and update all uniforms accordingly.
setUniforms(actor, settings, forOpening) { beginAnimation(actor, settings, forOpening) {
super.beginAnimation(actor, settings, forOpening);
// Get the two configurable colors. They are directly injected into the shader code // Get the two configurable colors. They are directly injected into the shader
// below. // code below.
const gc = Clutter.Color.from_string(settings.get_string('hexagon-glow-color'))[1]; const gc =
const lc = Clutter.Color.from_string(settings.get_string('hexagon-line-color'))[1]; Clutter.Color.from_string(settings.get_string('hexagon-glow-color'))[1];
const lc =
Clutter.Color.from_string(settings.get_string('hexagon-line-color'))[1];
// If we are currently performing integration test, the animation uses a fixed seed. // If we are currently performing integration test, the animation uses a fixed
// seed.
const testMode = settings.get_boolean('test-mode'); const testMode = settings.get_boolean('test-mode');
// clang-format off // clang-format off
@@ -161,26 +126,10 @@ if (utils.isInShellProcess()) {
this.set_uniform_float(this._uLineColor, 4, [lc.red / 255, lc.green / 255, lc.blue / 255, lc.alpha / 255]); this.set_uniform_float(this._uLineColor, 4, [lc.red / 255, lc.green / 255, lc.blue / 255, lc.alpha / 255]);
// clang-format on // clang-format on
} }
// This is called by extension.js when the shader is not used anymore. We will store
// this instance of the shader so that it can be re-used in th future.
free() {
freeShaders.push(this);
}
// This is called by the constructor. This means, it's only called when the effect
// is used for the first time.
vfunc_build_pipeline() {
const code = utils.loadGLSLResource(`/shaders/${Hexagon.getNick()}.glsl`);
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
const declarations = code.substr(0, match.index);
const main = match[1];
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true);
}
}); });
}
// Finally, return a new instance of the shader class.
return new this._ShaderClass(this);
}
} }
+48 -94
View File
@@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext;
const ExtensionUtils = imports.misc.extensionUtils; const ExtensionUtils = imports.misc.extensionUtils;
const Me = imports.misc.extensionUtils.getCurrentExtension(); const Me = imports.misc.extensionUtils.getCurrentExtension();
const utils = Me.imports.src.utils; const utils = Me.imports.src.utils;
const Effect = Me.imports.src.Effect.Effect;
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// The Matrix shader multiplies a grid of random letters with some gradients which are // // The Matrix shader multiplies a grid of random letters with some gradients which are //
@@ -29,26 +30,15 @@ const utils = Me.imports.src.utils;
// documentation of vfunc_paint_target further down in this file. // // documentation of vfunc_paint_target further down in this file. //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect is registered further down in this file. When this // The effect class can be used to get some metadata (like the effect's name or supported
// effect is used for the first time, an instance of this shader class is created. Once // GNOME Shell versions), to initialize the respective page of the settings dialog, as
// the effect is finished, the shader will be stored in the freeShaders array and will // well as to create the actual shader for the effect.
// then be reused if a new shader is requested. ShaderClass which will be used whenever var Matrix = class Matrix extends Effect {
// this effect is used.
let ShaderClass = null;
let freeShaders = [];
// This texture will be loaded when the effect is used for the first time.
let fontTexture = null;
// The effect class is completely static. It can be used to get some metadata (like the
// effect's name or supported GNOME Shell versions), to initialize the respective page of
// the settings dialog, as well as to create the actual shader for the effect.
var Matrix = class Matrix {
// ---------------------------------------------------------------------------- metadata // ---------------------------------------------------------------------------- metadata
// This effect is only available on GNOME Shell 40+. // This effect is only available on GNOME Shell 40+.
static getMinShellVersion() { getMinShellVersion() {
return [40, 0]; return [40, 0];
} }
@@ -56,22 +46,21 @@ var Matrix = class Matrix {
// required. It should match the prefix of the settings keys which store whether the // required. It should match the prefix of the settings keys which store whether the
// effect is enabled currently (e.g. '*-close-effect'), and its animation time // effect is enabled currently (e.g. '*-close-effect'), and its animation time
// (e.g. '*-animation-time'). // (e.g. '*-animation-time').
static getNick() { getNick() {
return 'matrix'; return 'matrix';
} }
// This will be shown in the sidebar of the preferences dialog as well as in the // This will be shown in the sidebar of the preferences dialog as well as in the
// drop-down menus where the user can choose the effect. // drop-down menus where the user can choose the effect.
static getLabel() { getLabel() {
return _('Matrix'); return _('Matrix');
} }
// -------------------------------------------------------------------- API for prefs.js // -------------------------------------------------------------------- API for prefs.js
// This is called by the preferences dialog. It loads the settings page for this effect, // This is called by the preferences dialog. It loads the settings page for this effect,
// binds all properties to the settings and appends the page to the main stack of the // and binds all properties to the settings.
// preferences dialog. getPreferences(dialog) {
static getPreferences(dialog) {
// Add the settings page to the builder. // Add the settings page to the builder.
dialog.getBuilder().add_from_resource('/ui/gtk4/Matrix.ui'); dialog.getBuilder().add_from_resource('/ui/gtk4/Matrix.ui');
@@ -90,64 +79,43 @@ var Matrix = class Matrix {
// ---------------------------------------------------------------- API for extension.js // ---------------------------------------------------------------- API for extension.js
// This is called from extension.js whenever a window is opened or closed with this
// effect. It returns an instance of the shader class, trying to reuse previously
// created shaders.
static getShader(actor, settings, forOpening) {
let shader;
if (freeShaders.length == 0) {
shader = new ShaderClass();
} else {
shader = freeShaders.pop();
}
shader.setUniforms(actor, settings, forOpening);
return shader;
}
// The getActorScale() is called from extension.js to adjust the actor's size during the // The getActorScale() is called from extension.js to adjust the actor's size during the
// animation. This is useful if the effect requires drawing something beyond the usual // animation. This is useful if the effect requires drawing something beyond the usual
// bounds of the actor. This only works for GNOME 3.38+. // bounds of the actor. This only works for GNOME 3.38+.
static getActorScale(settings) { getActorScale(settings) {
return {x: 1.0, y: 1.0 + settings.get_double('matrix-overshoot')}; return {x: 1.0, y: 1.0 + settings.get_double('matrix-overshoot')};
} }
// This is called from extension.js if the extension is disabled. This should free all // This is called from extension.js if the extension is disabled. This should free all
// static resources. // static resources.
static cleanUp() { cleanUp() {
freeShaders = []; super.cleanUp();
fontTexture = null; this._fontTexture = null;
} }
}
// This is called by the effect's base class whenever a new shader is required. Since
// this shader depends on classes by GNOME Shell, we register it locally in this method
// as this file is also included from the preferences dialog where those classes would
// not be available.
createShader() {
////////////////////////////////////////////////////////////////////////////////////////// // Only register the shader class when this method is called for the first time.
// The shader class for this effect will only be registered in GNOME Shell's process // if (!this._ShaderClass) {
// (not in the preferences process). It's done this way as Clutter may not be installed //
// on the system and therefore the preferences would crash. //
//////////////////////////////////////////////////////////////////////////////////////////
if (utils.isInShellProcess()) { const {Clutter, GdkPixbuf, Cogl} = imports.gi;
const Shader = Me.imports.src.Shader.Shader;
const {Clutter, GdkPixbuf, Cogl, Shell} = imports.gi;
ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect {
// This is called when the effect is used for the first time. This can be used to
// store all required uniform locations.
_init() {
super._init();
// Load the font texture.
if (fontTexture == null) {
const fontData = GdkPixbuf.Pixbuf.new_from_resource('/img/matrixFont.png'); const fontData = GdkPixbuf.Pixbuf.new_from_resource('/img/matrixFont.png');
fontTexture = new Clutter.Image(); this._fontTexture = new Clutter.Image();
fontTexture.set_data( this._fontTexture.set_data(
fontData.get_pixels(), fontData.get_pixels(),
fontData.has_alpha ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888, fontData.has_alpha ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888,
fontData.width, fontData.height, fontData.rowstride); fontData.width, fontData.height, fontData.rowstride);
}
this._ShaderClass = GObject.registerClass({}, class ShaderClass extends Shader {
// We use the constructor of the shader to store all required uniform locations.
_init(effect) {
super._init(effect);
this._uFontTexture = this.get_uniform_location('uFontTexture'); this._uFontTexture = this.get_uniform_location('uFontTexture');
this._uTrailColor = this.get_uniform_location('uTrailColor'); this._uTrailColor = this.get_uniform_location('uTrailColor');
@@ -157,11 +125,15 @@ if (utils.isInShellProcess()) {
this._uOverShoot = this.get_uniform_location('uOverShoot'); this._uOverShoot = this.get_uniform_location('uOverShoot');
} }
// This is called each time the effect is used. This can be used to retrieve the // This is called once each time the shader is used. This can be used to retrieve
// configuration from the settings and update all uniforms accordingly. // the configuration from the settings and update all uniforms accordingly.
setUniforms(actor, settings, forOpening) { beginAnimation(actor, settings, forOpening) {
const c1 = Clutter.Color.from_string(settings.get_string('matrix-trail-color'))[1]; super.beginAnimation(actor, settings, forOpening);
const c2 = Clutter.Color.from_string(settings.get_string('matrix-tip-color'))[1];
const c1 =
Clutter.Color.from_string(settings.get_string('matrix-trail-color'))[1];
const c2 =
Clutter.Color.from_string(settings.get_string('matrix-tip-color'))[1];
// clang-format off // clang-format off
this.set_uniform_float(this._uTrailColor, 3, [c1.red / 255, c1.green / 255, c1.blue / 255]); this.set_uniform_float(this._uTrailColor, 3, [c1.red / 255, c1.green / 255, c1.blue / 255]);
@@ -172,39 +144,21 @@ if (utils.isInShellProcess()) {
// clang-format on // clang-format on
} }
// This is called by extension.js when the shader is not used anymore. We will store // This is overridden to bind the font texture for drawing. Sadly, this seems to
// this instance of the shader so that it can be re-used in th future. // be impossible under GNOME 3.3x as this.get_pipeline() is not available. It was
free() { // called get_target() back then but this is not wrapped in GJS.
freeShaders.push(this);
}
// This is called by the constructor. This means, it's only called when the effect
// is used for the first time. The technique for this effect was inspired by
// https://www.shadertoy.com/view/ldccW4, however the implementation is quite
// different as the letters drop only once and there is no need for a noise texture.
vfunc_build_pipeline() {
const code = utils.loadGLSLResource(`/shaders/${Matrix.getNick()}.glsl`);
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
const declarations = code.substr(0, match.index);
const main = match[1];
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true);
}
// This is overridden to bind the font texture for drawing. Sadly, this seems to be
// impossible under GNOME 3.3x as this.get_pipeline() is not available. It was called
// get_target() back then but this is not wrapped in GJS.
// https://gitlab.gnome.org/GNOME/mutter/-/blob/gnome-3-36/clutter/clutter/clutter-offscreen-effect.c#L598 // https://gitlab.gnome.org/GNOME/mutter/-/blob/gnome-3-36/clutter/clutter/clutter-offscreen-effect.c#L598
vfunc_paint_target(node, paint_context) { vfunc_paint_target(node, paint_context) {
const pipeline = this.get_pipeline(); const pipeline = this.get_pipeline();
pipeline.set_layer_texture(1, fontTexture.get_texture()); pipeline.set_layer_texture(1, this._effect._fontTexture.get_texture());
pipeline.set_uniform_1i(this._uFontTexture, 1); pipeline.set_uniform_1i(this._uFontTexture, 1);
super.vfunc_paint_target(node, paint_context); super.vfunc_paint_target(node, paint_context);
} }
}); });
}
// Finally, return a new instance of the shader class.
return new this._ShaderClass(this);
}
} }
+104
View File
@@ -0,0 +1,104 @@
//////////////////////////////////////////////////////////////////////////////////////////
// ) ( //
// ( /( ( ( ) ( ( ( ( )\ ) ( ( //
// )\()) ))\ )( ( ( )\ ) )\))( )\ ( (()/( ( )\))( ( //
// ((_)\ /((_|()\ )\ ) )\ '(()/( ((_)()((_) )\ ) ((_)))\((_)()\ )\ //
// | |(_|_))( ((_)_(_/( _((_)) )(_)) _(()((_|_)_(_/( _| |((_)(()((_|(_) //
// | '_ \ || | '_| ' \)) | ' \()| || | \ V V / | ' \)) _` / _ \ V V (_-< //
// |_.__/\_,_|_| |_||_| |_|_|_| \_, | \_/\_/|_|_||_|\__,_\___/\_/\_//__/ //
// |__/ //
// Copyright (c) 2021 Simon Schneegans //
// Released under the GPLv3 or later. See LICENSE file for details. //
//////////////////////////////////////////////////////////////////////////////////////////
'use strict';
const {Gio, Shell, GObject} = imports.gi;
const ByteArray = imports.byteArray;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = imports.misc.extensionUtils.getCurrentExtension();
const utils = Me.imports.src.utils;
//////////////////////////////////////////////////////////////////////////////////////////
// This is the base class for all shaders of Burn-My-Windows. It automagically loads //
// the shader's source code from the resource file resources/shaders/<nick>.glsl and //
// ensures that some standard uniforms are always updated. //
//////////////////////////////////////////////////////////////////////////////////////////
var Shader = GObject.registerClass({}, class Shader extends Shell.GLSLEffect {
// The constructor is used to store all required uniform locations. Make sure to chain
// up to this base constructor before trying to access the uniform locations!
_init(effect) {
this._effect = effect;
// This will call vfunc_build_pipeline().
super._init();
this._uForOpening = this.get_uniform_location('uForOpening');
this._uProgress = this.get_uniform_location('uProgress');
this._uTime = this.get_uniform_location('uTime');
this._uSize = this.get_uniform_location('uSize');
}
// This is called each time the shader is used. This can be used to retrieve the
// configuration from the settings and update all uniforms accordingly.
beginAnimation(actor, settings, forOpening) {
this.set_uniform_float(this._uForOpening, 1, [forOpening]);
this.set_uniform_float(this._uSize, 2, [actor.width, actor.height]);
}
// This is called at each frame during the animation. This can be used to update
// uniforms which need to change each frame.
updateAnimation(progress, time) {
this.set_uniform_float(this._uProgress, 1, [progress]);
this.set_uniform_float(this._uTime, 1, [time]);
}
// This is called by extension.js when the shader is not used anymore. We will
// store this instance of the shader so that it can be re-used in th future.
endAnimation() {
this._effect.freeShader(this);
}
// This is called by the constructor. This means, it's only called when the
// effect is used for the first time.
vfunc_build_pipeline() {
const code = this._loadGLSLResource(`/shaders/${this._effect.getNick()}.glsl`);
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
const declarations = code.substr(0, match.index);
const main = match[1];
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true);
}
// ----------------------------------------------------------------------- private stuff
// This loads the file at 'path' contained in the extension's resources to a JavaScript
// string.
_loadStringResource(path) {
const data = Gio.resources_lookup_data(path, 0);
return ByteArray.toString(ByteArray.fromGBytes(data));
}
// This loads a GLSL file from the extension's resources to a JavaScript string. Any
// #include statements in this file are replaced with the corresponding file contents.
_loadGLSLResource(path) {
let code = this._loadStringResource(path);
// This regex matches either #include "..." or #include <...>. The part between the
// brackets is captured in the capture group.
const regex = RegExp('#include ["<](.+)[">]', 'g');
code = code.replace(regex, (m, file) => {
return this._loadStringResource('/shaders/' + file);
});
// Add a trailing newline. Else the GLSL compiler complains...
return code + '\n';
}
});
+47 -91
View File
@@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext;
const ExtensionUtils = imports.misc.extensionUtils; const ExtensionUtils = imports.misc.extensionUtils;
const Me = imports.misc.extensionUtils.getCurrentExtension(); const Me = imports.misc.extensionUtils.getCurrentExtension();
const utils = Me.imports.src.utils; const utils = Me.imports.src.utils;
const Effect = Me.imports.src.Effect.Effect;
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// This effects dissolves your windows into a cloud of dust. For this, it uses an // // This effects dissolves your windows into a cloud of dust. For this, it uses an //
@@ -31,26 +32,15 @@ const utils = Me.imports.src.utils;
// documentation of vfunc_paint_target further down in this file. // // documentation of vfunc_paint_target further down in this file. //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect is registered further down in this file. When this // The effect class can be used to get some metadata (like the effect's name or supported
// effect is used for the first time, an instance of this shader class is created. Once // GNOME Shell versions), to initialize the respective page of the settings dialog, as
// the effect is finished, the shader will be stored in the freeShaders array and will // well as to create the actual shader for the effect.
// then be reused if a new shader is requested. ShaderClass which will be used whenever var SnapOfDisintegration = class SnapOfDisintegration extends Effect {
// this effect is used.
let ShaderClass = null;
let freeShaders = [];
// This texture will be loaded when the effect is used for the first time.
let dustTexture = null;
// The effect class is completely static. It can be used to get some metadata (like the
// effect's name or supported GNOME Shell versions), to initialize the respective page of
// the settings dialog, as well as to create the actual shader for the effect.
var SnapOfDisintegration = class SnapOfDisintegration {
// ---------------------------------------------------------------------------- metadata // ---------------------------------------------------------------------------- metadata
// This effect is only available on GNOME Shell 40+. // This effect is only available on GNOME Shell 40+.
static getMinShellVersion() { getMinShellVersion() {
return [40, 0]; return [40, 0];
} }
@@ -58,22 +48,21 @@ var SnapOfDisintegration = class SnapOfDisintegration {
// required. It should match the prefix of the settings keys which store whether the // required. It should match the prefix of the settings keys which store whether the
// effect is enabled currently (e.g. '*-close-effect'), and its animation time // effect is enabled currently (e.g. '*-close-effect'), and its animation time
// (e.g. '*-animation-time'). // (e.g. '*-animation-time').
static getNick() { getNick() {
return 'snap'; return 'snap';
} }
// This will be shown in the sidebar of the preferences dialog as well as in the // This will be shown in the sidebar of the preferences dialog as well as in the
// drop-down menus where the user can choose the effect. // drop-down menus where the user can choose the effect.
static getLabel() { getLabel() {
return _('Snap of Disintegration'); return _('Snap of Disintegration');
} }
// -------------------------------------------------------------------- API for prefs.js // -------------------------------------------------------------------- API for prefs.js
// This is called by the preferences dialog. It loads the settings page for this effect, // This is called by the preferences dialog. It loads the settings page for this effect,
// binds all properties to the settings and appends the page to the main stack of the // and binds all properties to the settings.
// preferences dialog. getPreferences(dialog) {
static getPreferences(dialog) {
// Add the settings page to the builder. // Add the settings page to the builder.
dialog.getBuilder().add_from_resource('/ui/gtk4/SnapOfDisintegration.ui'); dialog.getBuilder().add_from_resource('/ui/gtk4/SnapOfDisintegration.ui');
@@ -89,62 +78,42 @@ var SnapOfDisintegration = class SnapOfDisintegration {
// ---------------------------------------------------------------- API for extension.js // ---------------------------------------------------------------- API for extension.js
// This is called from extension.js whenever a window is opened or closed with this
// effect. It returns an instance of the shader class, trying to reuse previously
// created shaders.
static getShader(actor, settings, forOpening) {
let shader;
if (freeShaders.length == 0) {
shader = new ShaderClass();
} else {
shader = freeShaders.pop();
}
shader.setUniforms(actor, settings, forOpening);
return shader;
}
// The getActorScale() is called from extension.js to adjust the actor's size during the // The getActorScale() is called from extension.js to adjust the actor's size during the
// animation. This is useful if the effect requires drawing something beyond the usual // animation. This is useful if the effect requires drawing something beyond the usual
// bounds of the actor. This only works for GNOME 3.38+. // bounds of the actor. This only works for GNOME 3.38+.
static getActorScale(settings) { getActorScale(settings) {
return {x: 1.2, y: 1.2}; return {x: 1.2, y: 1.2};
} }
// This is called from extension.js if the extension is disabled. This should free all // This is called from extension.js if the extension is disabled. This should free all
// static resources. // static resources.
static cleanUp() { cleanUp() {
freeShaders = []; super.cleanUp();
dustTexture = null; this._dustTexture = null;
} }
}
// This is called by the effect's base class whenever a new shader is required. Since
// this shader depends on classes by GNOME Shell, we register it locally in this method
// as this file is also included from the preferences dialog where those classes would
// not be available.
createShader() {
////////////////////////////////////////////////////////////////////////////////////////// // Only register the shader class when this method is called for the first time.
// The shader class for this effect will only be registered in GNOME Shell's process // if (!this._ShaderClass) {
// (not in the preferences process). It's done this way as Clutter may not be installed //
// on the system and therefore the preferences would crash. //
//////////////////////////////////////////////////////////////////////////////////////////
if (utils.isInShellProcess()) { const {Clutter, GdkPixbuf, Cogl} = imports.gi;
const Shader = Me.imports.src.Shader.Shader;
const {Clutter, GdkPixbuf, Cogl, Shell} = imports.gi;
ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect {
// This is called when the effect is used for the first time. This can be used to
// store all required uniform locations.
_init() {
super._init();
// Load the dust texture. // Load the dust texture.
if (dustTexture == null) {
const dustData = GdkPixbuf.Pixbuf.new_from_resource('/img/dust.png'); const dustData = GdkPixbuf.Pixbuf.new_from_resource('/img/dust.png');
dustTexture = new Clutter.Image(); this._dustTexture = new Clutter.Image();
dustTexture.set_data(dustData.get_pixels(), Cogl.PixelFormat.RGB_888, this._dustTexture.set_data(dustData.get_pixels(), Cogl.PixelFormat.RGB_888,
dustData.width, dustData.height, dustData.rowstride); dustData.width, dustData.height, dustData.rowstride);
}
this._ShaderClass = GObject.registerClass({}, class ShaderClass extends Shader {
// We use the constructor of the shader to store all required uniform locations.
_init(effect) {
super._init(effect);
this._uDustTexture = this.get_uniform_location('uDustTexture'); this._uDustTexture = this.get_uniform_location('uDustTexture');
this._uDustColor = this.get_uniform_location('uDustColor'); this._uDustColor = this.get_uniform_location('uDustColor');
@@ -152,13 +121,16 @@ if (utils.isInShellProcess()) {
this._uDustScale = this.get_uniform_location('uDustScale'); this._uDustScale = this.get_uniform_location('uDustScale');
} }
// This is called each time the effect is used. This can be used to retrieve the // This is called once each time the shader is used. This can be used to retrieve
// configuration from the settings and update all uniforms accordingly. // the configuration from the settings and update all uniforms accordingly.
setUniforms(actor, settings, forOpening) { beginAnimation(actor, settings, forOpening) {
super.beginAnimation(actor, settings, forOpening);
// The dust particles will fade to this color over time. // The dust particles will fade to this color over time.
const c = Clutter.Color.from_string(settings.get_string('snap-color'))[1]; const c = Clutter.Color.from_string(settings.get_string('snap-color'))[1];
// If we are currently performing integration test, the animation uses a fixed seed. // If we are currently performing integration test, the animation uses a fixed
// seed.
const testMode = settings.get_boolean('test-mode'); const testMode = settings.get_boolean('test-mode');
// clang-format off // clang-format off
@@ -168,40 +140,24 @@ if (utils.isInShellProcess()) {
// clang-format on // clang-format on
} }
// This is called by extension.js when the shader is not used anymore. We will store // This is overridden to bind the dust texture for drawing. Sadly, this seems to
// this instance of the shader so that it can be re-used in th future. // be
free() { // impossible under GNOME 3.3x as this.get_pipeline() is not available. It was
freeShaders.push(this); // called get_target() back then but this is not wrapped in GJS.
}
// This is called by the constructor. This means, it's only called when the effect
// is used for the first time.
vfunc_build_pipeline() {
const code =
utils.loadGLSLResource(`/shaders/${SnapOfDisintegration.getNick()}.glsl`);
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
const declarations = code.substr(0, match.index);
const main = match[1];
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true);
}
// This is overridden to bind the dust texture for drawing. Sadly, this seems to be
// impossible under GNOME 3.3x as this.get_pipeline() is not available. It was called
// get_target() back then but this is not wrapped in GJS.
// https://gitlab.gnome.org/GNOME/mutter/-/blob/gnome-3-36/clutter/clutter/clutter-offscreen-effect.c#L598 // https://gitlab.gnome.org/GNOME/mutter/-/blob/gnome-3-36/clutter/clutter/clutter-offscreen-effect.c#L598
vfunc_paint_target(node, paint_context) { vfunc_paint_target(node, paint_context) {
const pipeline = this.get_pipeline(); const pipeline = this.get_pipeline();
pipeline.set_layer_filters(0, Cogl.PipelineFilter.LINEAR, pipeline.set_layer_filters(0, Cogl.PipelineFilter.LINEAR,
Cogl.PipelineFilter.LINEAR); Cogl.PipelineFilter.LINEAR);
pipeline.set_layer_texture(1, dustTexture.get_texture()); pipeline.set_layer_texture(1, this._effect._dustTexture.get_texture());
pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT); pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT);
pipeline.set_uniform_1i(this._uDustTexture, 1); pipeline.set_uniform_1i(this._uDustTexture, 1);
super.vfunc_paint_target(node, paint_context); super.vfunc_paint_target(node, paint_context);
} }
}); });
}
// Finally, return a new instance of the shader class.
return new this._ShaderClass(this);
}
} }
+49 -91
View File
@@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext;
const ExtensionUtils = imports.misc.extensionUtils; const ExtensionUtils = imports.misc.extensionUtils;
const Me = imports.misc.extensionUtils.getCurrentExtension(); const Me = imports.misc.extensionUtils.getCurrentExtension();
const utils = Me.imports.src.utils; const utils = Me.imports.src.utils;
const Effect = Me.imports.src.Effect.Effect;
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// This effect tears your windows apart with a series of violent scratches! // // This effect tears your windows apart with a series of violent scratches! //
@@ -27,26 +28,15 @@ const utils = Me.imports.src.utils;
// documentation of vfunc_paint_target further down in this file. // // documentation of vfunc_paint_target further down in this file. //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect is registered further down in this file. When this // The effect class can be used to get some metadata (like the effect's name or supported
// effect is used for the first time, an instance of this shader class is created. Once // GNOME Shell versions), to initialize the respective page of the settings dialog, as
// the effect is finished, the shader will be stored in the freeShaders array and will // well as to create the actual shader for the effect.
// then be reused if a new shader is requested. ShaderClass which will be used whenever var TRexAttack = class TRexAttack extends Effect {
// this effect is used.
let ShaderClass = null;
let freeShaders = [];
// This texture will be loaded when the effect is used for the first time.
let clawTexture = null;
// The effect class is completely static. It can be used to get some metadata (like the
// effect's name or supported GNOME Shell versions), to initialize the respective page of
// the settings dialog, as well as to create the actual shader for the effect.
var TRexAttack = class TRexAttack {
// ---------------------------------------------------------------------------- metadata // ---------------------------------------------------------------------------- metadata
// This effect is only available on GNOME Shell 40+. // This effect is only available on GNOME Shell 40+.
static getMinShellVersion() { getMinShellVersion() {
return [40, 0]; return [40, 0];
} }
@@ -54,22 +44,21 @@ var TRexAttack = class TRexAttack {
// required. It should match the prefix of the settings keys which store whether the // required. It should match the prefix of the settings keys which store whether the
// effect is enabled currently (e.g. '*-close-effect'), and its animation time // effect is enabled currently (e.g. '*-close-effect'), and its animation time
// (e.g. '*-animation-time'). // (e.g. '*-animation-time').
static getNick() { getNick() {
return 'trex'; return 'trex';
} }
// This will be shown in the sidebar of the preferences dialog as well as in the // This will be shown in the sidebar of the preferences dialog as well as in the
// drop-down menus where the user can choose the effect. // drop-down menus where the user can choose the effect.
static getLabel() { getLabel() {
return _('T-Rex Attack'); return _('T-Rex Attack');
} }
// -------------------------------------------------------------------- API for prefs.js // -------------------------------------------------------------------- API for prefs.js
// This is called by the preferences dialog. It loads the settings page for this effect, // This is called by the preferences dialog. It loads the settings page for this effect,
// binds all properties to the settings and appends the page to the main stack of the // and binds all properties to the settings.
// preferences dialog. getPreferences(dialog) {
static getPreferences(dialog) {
// Add the settings page to the builder. // Add the settings page to the builder.
dialog.getBuilder().add_from_resource('/ui/gtk4/TRexAttack.ui'); dialog.getBuilder().add_from_resource('/ui/gtk4/TRexAttack.ui');
@@ -87,63 +76,44 @@ var TRexAttack = class TRexAttack {
// ---------------------------------------------------------------- API for extension.js // ---------------------------------------------------------------- API for extension.js
// This is called from extension.js whenever a window is opened or closed with this
// effect. It returns an instance of the shader class, trying to reuse previously
// created shaders.
static getShader(actor, settings, forOpening) {
let shader;
if (freeShaders.length == 0) {
shader = new ShaderClass();
} else {
shader = freeShaders.pop();
}
shader.setUniforms(actor, settings, forOpening);
return shader;
}
// The getActorScale() is called from extension.js to adjust the actor's size during the // The getActorScale() is called from extension.js to adjust the actor's size during the
// animation. This is useful if the effect requires drawing something beyond the usual // animation. This is useful if the effect requires drawing something beyond the usual
// bounds of the actor. This only works for GNOME 3.38+. // bounds of the actor. This only works for GNOME 3.38+.
static getActorScale(settings) { getActorScale(settings) {
const scale = 1.0 + 0.5 * settings.get_double('claw-scratch-warp'); const scale = 1.0 + 0.5 * settings.get_double('claw-scratch-warp');
return {x: scale, y: scale}; return {x: scale, y: scale};
} }
// This is called from extension.js if the extension is disabled. This should free all // This is called from extension.js if the extension is disabled. This should free all
// static resources. // static resources.
static cleanUp() { cleanUp() {
freeShaders = []; super.cleanUp();
clawTexture = null; this._clawTexture = null;
} }
}
// This is called by the effect's base class whenever a new shader is required. Since
// this shader depends on classes by GNOME Shell, we register it locally in this method
// as this file is also included from the preferences dialog where those classes would
// not be available.
createShader() {
////////////////////////////////////////////////////////////////////////////////////////// // Only register the shader class when this method is called for the first time.
// The shader class for this effect will only be registered in GNOME Shell's process // if (!this._ShaderClass) {
// (not in the preferences process). It's done this way as Clutter may not be installed //
// on the system and therefore the preferences would crash. //
//////////////////////////////////////////////////////////////////////////////////////////
if (utils.isInShellProcess()) { const {Clutter, GdkPixbuf, Cogl} = imports.gi;
const Shader = Me.imports.src.Shader.Shader;
const {Clutter, GdkPixbuf, Cogl, Shell} = imports.gi;
ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect {
// This is called when the effect is used for the first time. This can be used to
// store all required uniform locations.
_init() {
super._init();
// Load the claw texture. // Load the claw texture.
if (clawTexture == null) {
const clawData = GdkPixbuf.Pixbuf.new_from_resource('/img/claws.png'); const clawData = GdkPixbuf.Pixbuf.new_from_resource('/img/claws.png');
clawTexture = new Clutter.Image(); this._clawTexture = new Clutter.Image();
clawTexture.set_data(clawData.get_pixels(), Cogl.PixelFormat.RGB_888, this._clawTexture.set_data(clawData.get_pixels(), Cogl.PixelFormat.RGB_888,
clawData.width, clawData.height, clawData.rowstride); clawData.width, clawData.height, clawData.rowstride);
}
this._ShaderClass = GObject.registerClass({}, class ShaderClass extends Shader {
// We use the constructor of the shader to store all required uniform locations.
_init(effect) {
super._init(effect);
this._uClawTexture = this.get_uniform_location('uClawTexture'); this._uClawTexture = this.get_uniform_location('uClawTexture');
this._uFlashColor = this.get_uniform_location('uFlashColor'); this._uFlashColor = this.get_uniform_location('uFlashColor');
@@ -153,12 +123,16 @@ if (utils.isInShellProcess()) {
this._uWarpIntensity = this.get_uniform_location('uWarpIntensity'); this._uWarpIntensity = this.get_uniform_location('uWarpIntensity');
} }
// This is called each time the effect is used. This can be used to retrieve the // This is called once each time the shader is used. This can be used to retrieve
// configuration from the settings and update all uniforms accordingly. // the configuration from the settings and update all uniforms accordingly.
setUniforms(actor, settings, forOpening) { beginAnimation(actor, settings, forOpening) {
const c = Clutter.Color.from_string(settings.get_string('claw-scratch-color'))[1]; super.beginAnimation(actor, settings, forOpening);
// If we are currently performing integration test, the animation uses a fixed seed. const c =
Clutter.Color.from_string(settings.get_string('claw-scratch-color'))[1];
// If we are currently performing integration test, the animation uses a fixed
// seed.
const testMode = settings.get_boolean('test-mode'); const testMode = settings.get_boolean('test-mode');
// clang-format off // clang-format off
@@ -170,37 +144,21 @@ if (utils.isInShellProcess()) {
// clang-format on // clang-format on
} }
// This is called by extension.js when the shader is not used anymore. We will store // This is overridden to bind the claw texture for drawing. Sadly, this seems to
// this instance of the shader so that it can be re-used in th future. // be impossible under GNOME 3.3x as this.get_pipeline() is not available. It was
free() { // called get_target() back then but this is not wrapped in GJS.
freeShaders.push(this);
}
// This is called by the constructor. This means, it's only called when the effect
// is used for the first time.
vfunc_build_pipeline() {
const code = utils.loadGLSLResource(`/shaders/${TRexAttack.getNick()}.glsl`);
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
const declarations = code.substr(0, match.index);
const main = match[1];
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true);
}
// This is overridden to bind the claw texture for drawing. Sadly, this seems to be
// impossible under GNOME 3.3x as this.get_pipeline() is not available. It was called
// get_target() back then but this is not wrapped in GJS.
// https://gitlab.gnome.org/GNOME/mutter/-/blob/gnome-3-36/clutter/clutter/clutter-offscreen-effect.c#L598 // https://gitlab.gnome.org/GNOME/mutter/-/blob/gnome-3-36/clutter/clutter/clutter-offscreen-effect.c#L598
vfunc_paint_target(node, paint_context) { vfunc_paint_target(node, paint_context) {
const pipeline = this.get_pipeline(); const pipeline = this.get_pipeline();
pipeline.set_layer_texture(1, clawTexture.get_texture()); pipeline.set_layer_texture(1, this._effect._clawTexture.get_texture());
pipeline.set_uniform_1i(this._uClawTexture, 1); pipeline.set_uniform_1i(this._uClawTexture, 1);
super.vfunc_paint_target(node, paint_context); super.vfunc_paint_target(node, paint_context);
} }
}); });
}
// Finally, return a new instance of the shader class.
return new this._ShaderClass(this);
}
} }
+35 -87
View File
@@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext;
const ExtensionUtils = imports.misc.extensionUtils; const ExtensionUtils = imports.misc.extensionUtils;
const Me = imports.misc.extensionUtils.getCurrentExtension(); const Me = imports.misc.extensionUtils.getCurrentExtension();
const utils = Me.imports.src.utils; const utils = Me.imports.src.utils;
const Effect = Me.imports.src.Effect.Effect;
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// This effect hides the actor by making it first transparent from top and bottom // // This effect hides the actor by making it first transparent from top and bottom //
@@ -27,23 +28,15 @@ const utils = Me.imports.src.utils;
// the center. // // the center. //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect is registered further down in this file. When this // The effect class can be used to get some metadata (like the effect's name or supported
// effect is used for the first time, an instance of this shader class is created. Once // GNOME Shell versions), to initialize the respective page of the settings dialog, as
// the effect is finished, the shader will be stored in the freeShaders array and will // well as to create the actual shader for the effect.
// then be reused if a new shader is requested. ShaderClass which will be used whenever var TVEffect = class TVEffect extends Effect {
// this effect is used.
let ShaderClass = null;
let freeShaders = [];
// The effect class is completely static. It can be used to get some metadata (like the
// effect's name or supported GNOME Shell versions), to initialize the respective page of
// the settings dialog, as well as to create the actual shader for the effect.
var TVEffect = class TVEffect {
// ---------------------------------------------------------------------------- metadata // ---------------------------------------------------------------------------- metadata
// The effect is available on all GNOME Shell versions supported by this extension. // The effect is available on all GNOME Shell versions supported by this extension.
static getMinShellVersion() { getMinShellVersion() {
return [3, 36]; return [3, 36];
} }
@@ -51,22 +44,21 @@ var TVEffect = class TVEffect {
// required. It should match the prefix of the settings keys which store whether the // required. It should match the prefix of the settings keys which store whether the
// effect is enabled currently (e.g. '*-close-effect'), and its animation time // effect is enabled currently (e.g. '*-close-effect'), and its animation time
// (e.g. '*-animation-time'). // (e.g. '*-animation-time').
static getNick() { getNick() {
return 'tv'; return 'tv';
} }
// This will be shown in the sidebar of the preferences dialog as well as in the // This will be shown in the sidebar of the preferences dialog as well as in the
// drop-down menus where the user can choose the effect. // drop-down menus where the user can choose the effect.
static getLabel() { getLabel() {
return _('TV Effect'); return _('TV Effect');
} }
// -------------------------------------------------------------------- API for prefs.js // -------------------------------------------------------------------- API for prefs.js
// This is called by the preferences dialog. It loads the settings page for this effect, // This is called by the preferences dialog. It loads the settings page for this effect,
// binds all properties to the settings and appends the page to the main stack of the // and binds all properties to the settings.
// preferences dialog. getPreferences(dialog) {
static getPreferences(dialog) {
// Add the settings page to the builder. // Add the settings page to the builder.
dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/TVEffect.ui`); dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/TVEffect.ui`);
@@ -81,83 +73,39 @@ var TVEffect = class TVEffect {
// ---------------------------------------------------------------- API for extension.js // ---------------------------------------------------------------- API for extension.js
// This is called from extension.js whenever a window is opened or closed with this // This is called by the effect's base class whenever a new shader is required. Since
// effect. It returns an instance of the shader class, trying to reuse previously // this shader depends on classes by GNOME Shell, we register it locally in this method
// created shaders. // as this file is also included from the preferences dialog where those classes would
static getShader(actor, settings, forOpening) { // not be available.
let shader; createShader() {
if (freeShaders.length == 0) { // Only register the shader class when this method is called for the first time.
shader = new ShaderClass(); if (!this._ShaderClass) {
} else {
shader = freeShaders.pop();
}
shader.setUniforms(actor, settings, forOpening); const Clutter = imports.gi.Clutter;
const Shader = Me.imports.src.Shader.Shader;
return shader; this._ShaderClass = GObject.registerClass({}, class ShaderClass extends Shader {
} // We use the constructor of the shader to store all required uniform locations.
_init(effect) {
// The getActorScale() is called from extension.js to adjust the actor's size during the super._init(effect);
// animation. This is useful if the effect requires drawing something beyond the usual
// bounds of the actor. This only works for GNOME 3.38+.
static getActorScale(settings) {
return {x: 1.0, y: 1.0};
}
// This is called from extension.js if the extension is disabled. This should free all
// static resources.
static cleanUp() {
freeShaders = [];
}
}
//////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect will only be registered in GNOME Shell's process //
// (not in the preferences process). It's done this way as Clutter may not be installed //
// on the system and therefore the preferences would crash. //
//////////////////////////////////////////////////////////////////////////////////////////
if (utils.isInShellProcess()) {
const {Clutter, Shell} = imports.gi;
ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect {
// This is called when the effect is used for the first time. This can be used to
// store all required uniform locations.
_init() {
super._init();
this._uColor = this.get_uniform_location('uColor'); this._uColor = this.get_uniform_location('uColor');
} }
// This is called each time the effect is used. This can be used to retrieve the // This is called once each time the shader is used. This can be used to retrieve
// configuration from the settings and update all uniforms accordingly. // the configuration from the settings and update all uniforms accordingly.
setUniforms(actor, settings, forOpening) { beginAnimation(actor, settings, forOpening) {
super.beginAnimation(actor, settings, forOpening);
const c = Clutter.Color.from_string(settings.get_string('tv-effect-color'))[1]; const c = Clutter.Color.from_string(settings.get_string('tv-effect-color'))[1];
this.set_uniform_float(this._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); this.set_uniform_float(this._uColor, 3,
} [c.red / 255, c.green / 255, c.blue / 255]);
// This is called by extension.js when the shader is not used anymore. We will store
// this instance of the shader so that it can be re-used in th future.
free() {
freeShaders.push(this);
}
// This is called by the constructor. This means, it's only called when the effect
// is used for the first time.
vfunc_build_pipeline() {
const code = utils.loadGLSLResource(`/shaders/${TVEffect.getNick()}.glsl`);
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
const declarations = code.substr(0, match.index);
const main = match[1];
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true);
} }
}); });
}
// Finally, return a new instance of the shader class.
return new this._ShaderClass(this);
}
} }
+35 -88
View File
@@ -20,52 +20,43 @@ const _ = imports.gettext.domain('burn-my-windows').gettext;
const ExtensionUtils = imports.misc.extensionUtils; const ExtensionUtils = imports.misc.extensionUtils;
const Me = imports.misc.extensionUtils.getCurrentExtension(); const Me = imports.misc.extensionUtils.getCurrentExtension();
const utils = Me.imports.src.utils; const utils = Me.imports.src.utils;
const Effect = Me.imports.src.Effect.Effect;
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// This effect lets your windows be carried to the realm of dreams by some little // // This effect lets your windows be carried to the realm of dreams by some little //
// fairies. It's implemented with several overlaid grids of randomly moving points. // // fairies. It's implemented with several overlaid grids of randomly moving points. //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect is registered further down in this file. When this // The effect class can be used to get some metadata (like the effect's name or supported
// effect is used for the first time, an instance of this shader class is created. Once // GNOME Shell versions), to initialize the respective page of the settings dialog, as
// the effect is finished, the shader will be stored in the freeShaders array and will // well as to create the actual shader for the effect.
// then be reused if a new shader is requested. ShaderClass which will be used whenever var Wisps = class Wisps extends Effect {
// this effect is used.
let ShaderClass = null;
let freeShaders = [];
// This will be called in various places where a unique identifier for this effect is
// required. It should match the prefix of the settings keys which store whether the
// effect is enabled currently (e.g. '*-close-effect'), and its animation time
// (e.g. '*-animation-time').
var Wisps = class Wisps {
// ---------------------------------------------------------------------------- metadata // ---------------------------------------------------------------------------- metadata
// The effect is available on all GNOME Shell versions supported by this extension. // The effect is available on all GNOME Shell versions supported by this extension.
static getMinShellVersion() { getMinShellVersion() {
return [3, 36]; return [3, 36];
} }
// This will be called in various places where a unique identifier for this effect is // This will be called in various places where a unique identifier for this effect is
// required. It should match the prefix of the settings keys which store whether the // required. It should match the prefix of the settings keys which store whether the
// effect is enabled currently (e.g. the '*-close-effect'). // effect is enabled currently (e.g. the '*-close-effect').
static getNick() { getNick() {
return 'wisps'; return 'wisps';
} }
// This will be shown in the sidebar of the preferences dialog as well as in the // This will be shown in the sidebar of the preferences dialog as well as in the
// drop-down menus where the user can choose the effect. // drop-down menus where the user can choose the effect.
static getLabel() { getLabel() {
return _('Wisps'); return _('Wisps');
} }
// -------------------------------------------------------------------- API for prefs.js // -------------------------------------------------------------------- API for prefs.js
// This is called by the preferences dialog. It loads the settings page for this effect, // This is called by the preferences dialog. It loads the settings page for this effect,
// binds all properties to the settings and appends the page to the main stack of the // and binds all properties to the settings.
// preferences dialog. getPreferences(dialog) {
static getPreferences(dialog) {
// Add the settings page to the builder. // Add the settings page to the builder.
dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/Wisps.ui`); dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/Wisps.ui`);
@@ -81,65 +72,37 @@ var Wisps = class Wisps {
// ---------------------------------------------------------------- API for extension.js // ---------------------------------------------------------------- API for extension.js
// This is called from extension.js whenever a window is opened or closed with this // This is called by the effect's base class whenever a new shader is required. Since
// effect. It returns an instance of the shader class, trying to reuse previously // this shader depends on classes by GNOME Shell, we register it locally in this method
// created shaders. // as this file is also included from the preferences dialog where those classes would
static getShader(actor, settings, forOpening) { // not be available.
let shader; createShader() {
if (freeShaders.length == 0) { // Only register the shader class when this method is called for the first time.
shader = new ShaderClass(); if (!this._ShaderClass) {
} else {
shader = freeShaders.pop();
}
shader.setUniforms(actor, settings, forOpening); const Clutter = imports.gi.Clutter;
const Shader = Me.imports.src.Shader.Shader;
return shader; this._ShaderClass = GObject.registerClass({}, class ShaderClass extends Shader {
} // We use the constructor of the shader to store all required uniform locations.
_init(effect) {
// The getActorScale() is called from extension.js to adjust the actor's size during the super._init(effect);
// animation. This is useful if the effect requires drawing something beyond the usual
// bounds of the actor. This only works for GNOME 3.38+.
static getActorScale(settings) {
return {x: 1.0, y: 1.0};
}
// This is called from extension.js if the extension is disabled. This should free all
// static resources.
static cleanUp() {
freeShaders = [];
}
}
//////////////////////////////////////////////////////////////////////////////////////////
// The shader class for this effect will only be registered in GNOME Shell's process //
// (not in the preferences process). It's done this way as Clutter may not be installed //
// on the system and therefore the preferences would crash. //
//////////////////////////////////////////////////////////////////////////////////////////
if (utils.isInShellProcess()) {
const {Clutter, Shell} = imports.gi;
ShaderClass = GObject.registerClass({}, class ShaderClass extends Shell.GLSLEffect {
// This is called when the effect is used for the first time. This can be used to
// store all required uniform locations.
_init() {
super._init();
this._uSeed = this.get_uniform_location('uSeed'); this._uSeed = this.get_uniform_location('uSeed');
this._uColor = this.get_uniform_location('uColor'); this._uColor = this.get_uniform_location('uColor');
this._uScale = this.get_uniform_location('uScale'); this._uScale = this.get_uniform_location('uScale');
} }
// This is called each time the effect is used. This can be used to retrieve the // This is called once each time the shader is used. This can be used to retrieve
// configuration from the settings and update all uniforms accordingly. // the configuration from the settings and update all uniforms accordingly.
setUniforms(actor, settings, forOpening) { beginAnimation(actor, settings, forOpening) {
super.beginAnimation(actor, settings, forOpening);
const c = Clutter.Color.from_string(settings.get_string('wisps-color'))[1]; const c = Clutter.Color.from_string(settings.get_string('wisps-color'))[1];
// If we are currently performing integration test, the animation uses a fixed seed. // If we are currently performing integration test, the animation uses a fixed
// seed.
const testMode = settings.get_boolean('test-mode'); const testMode = settings.get_boolean('test-mode');
// clang-format off // clang-format off
@@ -148,26 +111,10 @@ if (utils.isInShellProcess()) {
this.set_uniform_float(this._uScale, 1, [settings.get_double('wisps-scale')]); this.set_uniform_float(this._uScale, 1, [settings.get_double('wisps-scale')]);
// clang-format on // clang-format on
} }
// This is called by extension.js when the shader is not used anymore. We will store
// this instance of the shader so that it can be re-used in th future.
free() {
freeShaders.push(this);
}
// This is called by the constructor. This means, it's only called when the effect
// is used for the first time.
vfunc_build_pipeline() {
const code = utils.loadGLSLResource(`/shaders/${Wisps.getNick()}.glsl`);
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
const declarations = code.substr(0, match.index);
const main = match[1];
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true);
}
}); });
}
// Finally, return a new instance of the shader class.
return new this._ShaderClass(this);
}
} }
+1 -26
View File
@@ -13,8 +13,7 @@
'use strict'; 'use strict';
const {Gtk, Gio} = imports.gi; const {Gtk} = imports.gi;
const ByteArray = imports.byteArray;
// Returns the given argument, except for "alpha", "beta", and "rc". In these cases -3, // Returns the given argument, except for "alpha", "beta", and "rc". In these cases -3,
// -2, and -1 are returned respectively. // -2, and -1 are returned respectively.
@@ -91,27 +90,3 @@ function shellVersionIsAtLeast(major, minor) {
return false; return false;
} }
// This loads the file at 'path' contained in the extension's resources to a JavaScript
// string.
function loadStringResource(path) {
const data = Gio.resources_lookup_data(path, 0);
return ByteArray.toString(ByteArray.fromGBytes(data));
}
// This loads a GLSL file from the extension's resources to a JavaScript string. Any
// #include statements in this file are replaced with the corresponding file contents.
function loadGLSLResource(path) {
let code = loadStringResource(path);
// This regex matches either #include "..." or #include <...>. The part between the
// brackets is captured in the capture group.
const regex = RegExp('#include ["<](.+)[">]', 'g');
code = code.replace(regex, (m, file) => {
return loadStringResource('/shaders/' + file);
});
// Add a trailing newline. Else the GLSL compiler complains...
return code + '\n';
}