diff --git a/docs/how-to-create-new-effects.md b/docs/how-to-create-new-effects.md index 8bce434..cbd3c2a 100644 --- a/docs/how-to-create-new-effects.md +++ b/docs/how-to-create-new-effects.md @@ -128,7 +128,7 @@ var SimpleFade = class SimpleFade { // ---------------------------------------------------------------------------- metadata // The effect is available on all GNOME Shell versions supported by this extension. - static getMinShellVersion() { + getMinShellVersion() { 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 // effect is enabled currently (e.g. '*-close-effect'), and its animation time // (e.g. '*-animation-time'). - static getNick() { + getNick() { return 'simple-fade'; } // 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. - static getLabel() { + getLabel() { 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, // binds all properties to the settings and appends the page to the main stack of the // preferences dialog. - static getPreferences(dialog) { + getPreferences(dialog) { // Empty for now... Code is added here later in the tutorial! 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 // effect. It returns an instance of the shader class, trying to reuse previously // created shaders. - static getShader(actor, settings, forOpening) { + getShader(actor, settings, forOpening) { let shader; if (freeShaders.length == 0) { @@ -170,7 +170,7 @@ var SimpleFade = class SimpleFade { shader = freeShaders.pop(); } - shader.setUniforms(actor, settings, forOpening); + shader.updateAnimation(actor, settings, forOpening); 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 // 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) { + getActorScale(settings) { return {x: 1.0, y: 1.0}; } } @@ -203,9 +203,9 @@ if (utils.isInShellProcess()) { 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. - setUniforms(actor, settings, forOpening) { + updateAnimation(actor, settings, forOpening) { this.set_uniform_float(this._uForOpening, 1, [forOpening]); this.set_uniform_float(this._uFadeWidth, 1, [settings.get_double('simple-fade-width')]); } diff --git a/extension.js b/extension.js index c375799..7a70c64 100644 --- a/extension.js +++ b/extension.js @@ -33,17 +33,17 @@ const utils = Me.imports.src.utils; // New effects must be registered here and in prefs.js. const ALL_EFFECTS = [ - Me.imports.src.Apparition.Apparition, - Me.imports.src.BrokenGlass.BrokenGlass, - Me.imports.src.EnergizeA.EnergizeA, - Me.imports.src.EnergizeB.EnergizeB, - Me.imports.src.Fire.Fire, - Me.imports.src.Hexagon.Hexagon, - Me.imports.src.Matrix.Matrix, - Me.imports.src.SnapOfDisintegration.SnapOfDisintegration, - Me.imports.src.TRexAttack.TRexAttack, - Me.imports.src.TVEffect.TVEffect, - Me.imports.src.Wisps.Wisps, + new Me.imports.src.Apparition.Apparition(), + new Me.imports.src.BrokenGlass.BrokenGlass(), + new Me.imports.src.EnergizeA.EnergizeA(), + new Me.imports.src.EnergizeB.EnergizeB(), + new Me.imports.src.Fire.Fire(), + new Me.imports.src.Hexagon.Hexagon(), + new Me.imports.src.Matrix.Matrix(), + new Me.imports.src.SnapOfDisintegration.SnapOfDisintegration(), + new Me.imports.src.TRexAttack.TRexAttack(), + new Me.imports.src.TVEffect.TVEffect(), + new Me.imports.src.Wisps.Wisps(), ]; ////////////////////////////////////////////////////////////////////////////////////////// @@ -222,7 +222,7 @@ class Extension { const shader = actor.get_effect('burn-my-windows-effect'); if (shader) { actor.remove_effect(shader); - shader.free(); + shader.endAnimation(); } } @@ -306,7 +306,7 @@ class Extension { disable() { // Free all effect resources. - ALL_EFFECTS.forEach(Effect => Effect.cleanUp()); + ALL_EFFECTS.forEach(effect => effect.cleanUp()); // Unregister our resources. Gio.resources_unregister(this._resources); @@ -371,7 +371,7 @@ class Extension { const oldShader = actor.get_effect('burn-my-windows-effect'); if (oldShader) { actor.remove_effect(oldShader); - oldShader.free(); + oldShader.endAnimation(); } // ------------------------------------------------------------------ choose an effect @@ -381,9 +381,7 @@ class Extension { // First we check if an effect is to be previewed. if (previewNick != '') { - effect = ALL_EFFECTS.find(Effect => { - return Effect.getNick() == previewNick; - }); + effect = ALL_EFFECTS.find(effect => effect.getNick() == previewNick); // Only preview the effect once. this._settings.set_string(action + '-preview-effect', ''); @@ -393,8 +391,8 @@ class Extension { else { // Therefore, we first create a list of all currently enabled effects. - const enabled = ALL_EFFECTS.filter(Effect => { - return this._settings.get_boolean(`${Effect.getNick()}-${action}-effect`); + const enabled = ALL_EFFECTS.filter(effect => { + return this._settings.get_boolean(`${effect.getNick()}-${action}-effect`); }); // And then choose a random effect. @@ -490,17 +488,12 @@ class Extension { actor.add_effect_with_name('burn-my-windows-effect', shader); + shader.beginAnimation(actor, this._settings, forOpening); + // Update uniforms at each frame. transition.connect('new-frame', (t) => { - shader.set_uniform_float(shader.get_uniform_location('uForOpening'), 1, - [forOpening]); - 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]); + shader.updateAnimation(testMode ? 0.5 : t.get_progress(), + testMode ? duration / 2 : 0.001 * t.get_elapsed_time()); }); // 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'); if (oldShader) { actor.remove_effect(oldShader); - oldShader.free(); + oldShader.endAnimation(); } }); } diff --git a/prefs.js b/prefs.js index d5809b5..0fa3457 100644 --- a/prefs.js +++ b/prefs.js @@ -32,17 +32,17 @@ const utils = Me.imports.src.utils; // New effects must be registered here and in extension.js. const ALL_EFFECTS = [ - Me.imports.src.Apparition.Apparition, - Me.imports.src.BrokenGlass.BrokenGlass, - Me.imports.src.EnergizeA.EnergizeA, - Me.imports.src.EnergizeB.EnergizeB, - Me.imports.src.Fire.Fire, - Me.imports.src.Hexagon.Hexagon, - Me.imports.src.Matrix.Matrix, - Me.imports.src.SnapOfDisintegration.SnapOfDisintegration, - Me.imports.src.TRexAttack.TRexAttack, - Me.imports.src.TVEffect.TVEffect, - Me.imports.src.Wisps.Wisps, + new Me.imports.src.Apparition.Apparition(), + new Me.imports.src.BrokenGlass.BrokenGlass(), + new Me.imports.src.EnergizeA.EnergizeA(), + new Me.imports.src.EnergizeB.EnergizeB(), + new Me.imports.src.Fire.Fire(), + new Me.imports.src.Hexagon.Hexagon(), + new Me.imports.src.Matrix.Matrix(), + new Me.imports.src.SnapOfDisintegration.SnapOfDisintegration(), + new Me.imports.src.TRexAttack.TRexAttack(), + new Me.imports.src.TVEffect.TVEffect(), + new Me.imports.src.Wisps.Wisps(), ]; // 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')}); this.gtkBoxAppend(this._widget, group); - ALL_EFFECTS.forEach(Effect => { - const [minMajor, minMinor] = Effect.getMinShellVersion(); + ALL_EFFECTS.forEach(effect => { + const [minMajor, minMinor] = effect.getMinShellVersion(); 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'})); // Open a subpage with the effect's settings. row.connect('activated', () => { - const page = new BurnMyWindowsEffectPage(Effect, this); + const page = new BurnMyWindowsEffectPage(effect, this); page.valign = Gtk.Align.CENTER; page.margin_top = 10; page.margin_bottom = 10; page.margin_start = 10; page.margin_end = 10; - // Add the Effect's preferences (if any). - const preferences = Effect.getPreferences(this); + // Add the effect's preferences (if any). + const preferences = effect.getPreferences(this); if (preferences) { this.gtkBoxAppend(page, preferences); } @@ -186,23 +186,23 @@ var PreferencesDialog = class PreferencesDialog { this.gtkBoxAppend(this._widget, stack); // Add all other effect pages. - ALL_EFFECTS.forEach(Effect => { - const [minMajor, minMinor] = Effect.getMinShellVersion(); + ALL_EFFECTS.forEach(effect => { + const [minMajor, minMinor] = effect.getMinShellVersion(); if (utils.shellVersionIsAtLeast(minMajor, minMinor)) { - const page = new BurnMyWindowsEffectPage(Effect, this); + const page = new BurnMyWindowsEffectPage(effect, this); page.margin_start = 60; page.margin_end = 60; page.margin_top = 60; page.margin_bottom = 60; - // Add the Effect's preferences (if any). - const preferences = Effect.getPreferences(this); + // Add the effect's preferences (if any). + const preferences = effect.getPreferences(this); if (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(); window.insert_action_group('open-effects', group); - ALL_EFFECTS.forEach(Effect => { - const [minMajor, minMinor] = Effect.getMinShellVersion(); + ALL_EFFECTS.forEach(effect => { + const [minMajor, minMinor] = effect.getMinShellVersion(); if (utils.shellVersionIsAtLeast(minMajor, minMinor)) { - const nick = Effect.getNick(); - const label = Effect.getLabel(); + const nick = effect.getNick(); + const label = effect.getLabel(); const actionName = nick + '-open-effect'; const fullName = 'open-effects.' + actionName; @@ -324,11 +324,11 @@ var PreferencesDialog = class PreferencesDialog { const group = Gio.SimpleActionGroup.new(); window.insert_action_group('close-effects', group); - ALL_EFFECTS.forEach(Effect => { - const [minMajor, minMinor] = Effect.getMinShellVersion(); + ALL_EFFECTS.forEach(effect => { + const [minMajor, minMinor] = effect.getMinShellVersion(); if (utils.shellVersionIsAtLeast(minMajor, minMinor)) { - const nick = Effect.getNick(); - const label = Effect.getLabel(); + const nick = effect.getNick(); + const label = effect.getLabel(); const actionName = nick + '-close-effect'; const fullName = 'close-effects.' + actionName; @@ -490,17 +490,17 @@ var PreferencesDialog = class PreferencesDialog { InternalChildren: ['label', 'button'], }, class BurnMyWindowsEffectPage extends Gtk.Box { // ------------------------------ - _init(Effect, dialog) { + _init(effect, dialog) { super._init(); // 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. this._button.connect('clicked', () => { // Set the to-be-previewed effect. - dialog.getSettings().set_string('open-preview-effect', Effect.getNick()); - dialog.getSettings().set_string('close-preview-effect', Effect.getNick()); + dialog.getSettings().set_string('open-preview-effect', effect.getNick()); + dialog.getSettings().set_string('close-preview-effect', effect.getNick()); // Make sure that the window.show() firther below "sees" this change. Gio.Settings.sync(); @@ -508,7 +508,7 @@ var PreferencesDialog = class PreferencesDialog { // Create the preview-window. const window = new Gtk.Window({ // 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_height: 450, modal: true, diff --git a/src/Apparition.js b/src/Apparition.js index a6d1b53..507ef9f 100644 --- a/src/Apparition.js +++ b/src/Apparition.js @@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); 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. // @@ -27,24 +28,16 @@ const utils = Me.imports.src.utils; // the center. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. When this -// effect is used for the first time, an instance of this shader class is created. Once -// the effect is finished, the shader will be stored in the freeShaders array and will -// then be reused if a new shader is requested. ShaderClass which will be used whenever -// 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 { +// The effect class 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 extends Effect { // ---------------------------------------------------------------------------- metadata // The effect is not available on GNOME Shell 3.36 as it requires scaling of the window // actor. - static getMinShellVersion() { + getMinShellVersion() { 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 // effect is enabled currently (e.g. '*-close-effect'), and its animation time // (e.g. '*-animation-time'). - static getNick() { + getNick() { return 'apparition'; } // 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. - static getLabel() { + getLabel() { return _('Apparition'); } // -------------------------------------------------------------------- API for prefs.js // 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 - // preferences dialog. - static getPreferences(dialog) { + // and binds all properties to the settings. + getPreferences(dialog) { // Add the settings page to the builder. dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/Apparition.ui`); @@ -85,95 +77,57 @@ var Apparition = class Apparition { // ---------------------------------------------------------------- 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 // 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) { + getActorScale(settings) { return {x: 2.0, y: 2.0}; } - // This is called from extension.js if the extension is disabled. This should free all - // static resources. - static cleanUp() { - freeShaders = []; + // 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. + if (!this._ShaderClass) { + + 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._uSeed = this.get_uniform_location('uSeed'); + this._uShake = this.get_uniform_location('uShake'); + this._uTwirl = this.get_uniform_location('uTwirl'); + this._uSuction = this.get_uniform_location('uSuction'); + this._uRandomness = this.get_uniform_location('uRandomness'); + } + + // 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); + + // If we are currently performing integration test, the animation uses a fixed + // seed. + const testMode = settings.get_boolean('test-mode'); + + // clang-format off + this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + this.set_uniform_float(this._uShake, 1, [settings.get_double('apparition-shake-intensity')]); + this.set_uniform_float(this._uTwirl, 1, [settings.get_double('apparition-twirl-intensity')]); + this.set_uniform_float(this._uSuction, 1, [settings.get_double('apparition-suction-intensity')]); + this.set_uniform_float(this._uRandomness, 1, [settings.get_double('apparition-randomness')]); + // clang-format on + } + }); + } + + // Finally, return a new instance of the shader class. + return new this._ShaderClass(this); } } - - -////////////////////////////////////////////////////////////////////////////////////////// -// 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 Shell = imports.gi.Shell; - - 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._uShake = this.get_uniform_location('uShake'); - this._uTwirl = this.get_uniform_location('uTwirl'); - this._uSuction = this.get_uniform_location('uSuction'); - this._uRandomness = this.get_uniform_location('uRandomness'); - } - - // 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) { - // If we are currently performing integration test, the animation uses a fixed seed. - const testMode = settings.get_boolean('test-mode'); - - // clang-format off - this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); - this.set_uniform_float(this._uShake, 1, [settings.get_double('apparition-shake-intensity')]); - this.set_uniform_float(this._uTwirl, 1, [settings.get_double('apparition-twirl-intensity')]); - this.set_uniform_float(this._uSuction, 1, [settings.get_double('apparition-suction-intensity')]); - this.set_uniform_float(this._uRandomness, 1, [settings.get_double('apparition-randomness')]); - // 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); - } - }); -} \ No newline at end of file diff --git a/src/BrokenGlass.js b/src/BrokenGlass.js index b3d0787..505dd69 100644 --- a/src/BrokenGlass.js +++ b/src/BrokenGlass.js @@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); 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 // @@ -28,26 +29,15 @@ const utils = Me.imports.src.utils; // of vfunc_paint_target further down in this file. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. When this -// effect is used for the first time, an instance of this shader class is created. Once -// the effect is finished, the shader will be stored in the freeShaders array and will -// then be reused if a new shader is requested. ShaderClass which will be used whenever -// 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 { +// The effect class 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 extends Effect { // ---------------------------------------------------------------------------- metadata // This effect is only available on GNOME Shell 40+. - static getMinShellVersion() { + getMinShellVersion() { 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 // effect is enabled currently (e.g. '*-close-effect'), and its animation time // (e.g. '*-animation-time'). - static getNick() { + getNick() { return 'broken-glass'; } // 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. - static getLabel() { + getLabel() { return _('Broken Glass'); } // -------------------------------------------------------------------- API for prefs.js // 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 - // preferences dialog. - static getPreferences(dialog) { + // and binds all properties to the settings. + getPreferences(dialog) { // Add the settings page to the builder. dialog.getBuilder().add_from_resource('/ui/gtk4/BrokenGlass.ui'); @@ -88,148 +77,115 @@ var BrokenGlass = class BrokenGlass { // ---------------------------------------------------------------- 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 // 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) { + getActorScale(settings) { return {x: 2.0, y: 2.0}; } // This is called from extension.js if the extension is disabled. This should free all // static resources. - static cleanUp() { - freeShaders = []; - shardTexture = null; + cleanUp() { + super.cleanUp(); + 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. + if (!this._ShaderClass) { + + const {Clutter, GdkPixbuf, Cogl} = imports.gi; + const Shader = Me.imports.src.Shader.Shader; + + const shardData = GdkPixbuf.Pixbuf.new_from_resource('/img/shards.png'); + this._shardTexture = new Clutter.Image(); + this._shardTexture.set_data(shardData.get_pixels(), Cogl.PixelFormat.RGB_888, + 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._uSeed = this.get_uniform_location('uSeed'); + this._uEpicenter = this.get_uniform_location('uEpicenter'); + this._uShardScale = this.get_uniform_location('uShardScale'); + this._uBlowForce = this.get_uniform_location('uBlowForce'); + this._uGravity = this.get_uniform_location('uGravity'); + } + + // 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); + + // Usually, the shards fly away from the center of the window. + let epicenterX = 0.5; + let epicenterY = 0.5; + + // However, if this option is set, we use the mouse pointer position. + if (!forOpening && settings.get_boolean('broken-glass-use-pointer')) { + const [x, y] = global.get_pointer(); + const [ok, localX, localY] = actor.transform_stage_point(x, y); + + if (ok) { + epicenterX = localX / actor.width; + epicenterY = localY / actor.height; + } + } + + // If we are currently performing integration test, the animation uses a fixed + // seed. + const testMode = settings.get_boolean('test-mode'); + + // clang-format off + this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + this.set_uniform_float(this._uEpicenter, 2, [epicenterX, epicenterY]); + this.set_uniform_float(this._uShardScale, 1, [settings.get_double('broken-glass-scale')]); + this.set_uniform_float(this._uBlowForce, 1, [settings.get_double('broken-glass-blow-force')]); + this.set_uniform_float(this._uGravity, 1, [settings.get_double('broken-glass-gravity')]); + // clang-format on + } + + // 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 + vfunc_paint_target(node, paint_context) { + const pipeline = this.get_pipeline(); + + // Use linear filtering for the window texture. + pipeline.set_layer_filters(0, Cogl.PipelineFilter.LINEAR, + Cogl.PipelineFilter.LINEAR); + + // Bind the shard texture. + pipeline.set_layer_texture(1, this._effect._shardTexture.get_texture()); + pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT); + pipeline.set_uniform_1i(this._uShardTexture, 1); + + super.vfunc_paint_target(node, paint_context); + } + }); + } + + // Finally, return a new instance of the shader class. + return new this._ShaderClass(this); } } - - -////////////////////////////////////////////////////////////////////////////////////////// -// 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, 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'); - shardTexture = new Clutter.Image(); - shardTexture.set_data(shardData.get_pixels(), Cogl.PixelFormat.RGB_888, - shardData.width, shardData.height, shardData.rowstride); - } - - this._uShardTexture = this.get_uniform_location('uShardTexture'); - this._uSeed = this.get_uniform_location('uSeed'); - this._uEpicenter = this.get_uniform_location('uEpicenter'); - this._uShardScale = this.get_uniform_location('uShardScale'); - this._uBlowForce = this.get_uniform_location('uBlowForce'); - this._uGravity = this.get_uniform_location('uGravity'); - } - - // 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) { - // Usually, the shards fly away from the center of the window. - let epicenterX = 0.5; - let epicenterY = 0.5; - - // However, if this option is set, we use the mouse pointer position. - if (!forOpening && settings.get_boolean('broken-glass-use-pointer')) { - const [x, y] = global.get_pointer(); - const [ok, localX, localY] = actor.transform_stage_point(x, y); - - if (ok) { - epicenterX = localX / actor.width; - epicenterY = localY / actor.height; - } - } - - // If we are currently performing integration test, the animation uses a fixed seed. - const testMode = settings.get_boolean('test-mode'); - - // clang-format off - this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); - this.set_uniform_float(this._uEpicenter, 2, [epicenterX, epicenterY]); - this.set_uniform_float(this._uShardScale, 1, [settings.get_double('broken-glass-scale')]); - this.set_uniform_float(this._uBlowForce, 1, [settings.get_double('broken-glass-blow-force')]); - this.set_uniform_float(this._uGravity, 1, [settings.get_double('broken-glass-gravity')]); - // 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/${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 - vfunc_paint_target(node, paint_context) { - const pipeline = this.get_pipeline(); - - // Use linear filtering for the window texture. - pipeline.set_layer_filters(0, Cogl.PipelineFilter.LINEAR, - Cogl.PipelineFilter.LINEAR); - - // Bind the shard texture. - pipeline.set_layer_texture(1, shardTexture.get_texture()); - pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT); - pipeline.set_uniform_1i(this._uShardTexture, 1); - - super.vfunc_paint_target(node, paint_context); - } - }); -} \ No newline at end of file diff --git a/src/Effect.js b/src/Effect.js new file mode 100644 index 0000000..2356833 --- /dev/null +++ b/src/Effect.js @@ -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 = []; + } +} diff --git a/src/EnergizeA.js b/src/EnergizeA.js index 40c4d79..fea0eb6 100644 --- a/src/EnergizeA.js +++ b/src/EnergizeA.js @@ -20,28 +20,21 @@ const _ = imports.gettext.domain('burn-my-windows').gettext; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); const utils = Me.imports.src.utils; +const Effect = Me.imports.src.Effect.Effect; ////////////////////////////////////////////////////////////////////////////////////////// // 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 -// effect is used for the first time, an instance of this shader class is created. Once -// the effect is finished, the shader will be stored in the freeShaders array and will -// then be reused if a new shader is requested. ShaderClass which will be used whenever -// 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 { +// The effect class 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 extends Effect { // ---------------------------------------------------------------------------- metadata // The effect is available on all GNOME Shell versions supported by this extension. - static getMinShellVersion() { + getMinShellVersion() { 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 // effect is enabled currently (e.g. '*-close-effect'), and its animation time // (e.g. '*-animation-time'). - static getNick() { + getNick() { return 'energize-a'; } // 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. - static getLabel() { + getLabel() { return _('Energize A'); } // -------------------------------------------------------------------- API for prefs.js // 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 - // preferences dialog. - static getPreferences(dialog) { + // and binds all properties to the settings. + getPreferences(dialog) { // Add the settings page to the builder. dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/EnergizeA.ui`); @@ -80,88 +72,43 @@ var EnergizeA = class EnergizeA { // ---------------------------------------------------------------- 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; + // 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() { - if (freeShaders.length == 0) { - shader = new ShaderClass(); - } else { - shader = freeShaders.pop(); + // Only register the shader class when this method is called for the first time. + if (!this._ShaderClass) { + + 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._uColor = this.get_uniform_location('uColor'); + this._uScale = this.get_uniform_location('uScale'); + } + + // 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); + + const c = Clutter.Color.from_string(settings.get_string('energize-a-color'))[1]; + + // clang-format off + this.set_uniform_float(this._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); + this.set_uniform_float(this._uScale, 1, [settings.get_double('energize-a-scale')]); + // clang-format on + } + }); } - shader.setUniforms(actor, settings, forOpening); - - return shader; + // Finally, return a new instance of the shader class. + return new this._ShaderClass(this); } - - // 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 - // 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._uScale = this.get_uniform_location('uScale'); - } - - // 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) { - const c = Clutter.Color.from_string(settings.get_string('energize-a-color'))[1]; - - // clang-format off - this.set_uniform_float(this._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); - this.set_uniform_float(this._uScale, 1, [settings.get_double('energize-a-scale')]); - // 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); - } - }); } \ No newline at end of file diff --git a/src/EnergizeB.js b/src/EnergizeB.js index 68abf74..9773892 100644 --- a/src/EnergizeB.js +++ b/src/EnergizeB.js @@ -20,28 +20,21 @@ const _ = imports.gettext.domain('burn-my-windows').gettext; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); const utils = Me.imports.src.utils; +const Effect = Me.imports.src.Effect.Effect; ////////////////////////////////////////////////////////////////////////////////////////// // 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 -// effect is used for the first time, an instance of this shader class is created. Once -// the effect is finished, the shader will be stored in the freeShaders array and will -// then be reused if a new shader is requested. ShaderClass which will be used whenever -// 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 { +// The effect class 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 extends Effect { // ---------------------------------------------------------------------------- metadata // The effect is available on all GNOME Shell versions supported by this extension. - static getMinShellVersion() { + getMinShellVersion() { 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 // effect is enabled currently (e.g. '*-close-effect'), and its animation time // (e.g. '*-animation-time'). - static getNick() { + getNick() { return 'energize-b'; } // 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. - static getLabel() { + getLabel() { return _('Energize B'); } // -------------------------------------------------------------------- API for prefs.js // 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 - // preferences dialog. - static getPreferences(dialog) { + // and binds all properties to the settings. + getPreferences(dialog) { // Add the settings page to the builder. dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/EnergizeB.ui`); @@ -80,88 +72,43 @@ var EnergizeB = class EnergizeB { // ---------------------------------------------------------------- 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; + // 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() { - if (freeShaders.length == 0) { - shader = new ShaderClass(); - } else { - shader = freeShaders.pop(); + // Only register the shader class when this method is called for the first time. + if (!this._ShaderClass) { + + 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._uColor = this.get_uniform_location('uColor'); + this._uScale = this.get_uniform_location('uScale'); + } + + // 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); + + const c = Clutter.Color.from_string(settings.get_string('energize-b-color'))[1]; + + // clang-format off + this.set_uniform_float(this._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); + this.set_uniform_float(this._uScale, 1, [settings.get_double('energize-b-scale')]); + // clang-format on + } + }); } - shader.setUniforms(actor, settings, forOpening); - - return shader; - } - - // 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 - // 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 = []; + // Finally, return a new instance of the shader class. + return new this._ShaderClass(this); } } - - -////////////////////////////////////////////////////////////////////////////////////////// -// 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._uScale = this.get_uniform_location('uScale'); - } - - // 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) { - const c = Clutter.Color.from_string(settings.get_string('energize-b-color'))[1]; - - // clang-format off - this.set_uniform_float(this._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); - this.set_uniform_float(this._uScale, 1, [settings.get_double('energize-b-scale')]); - // 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); - } - }); -} \ No newline at end of file diff --git a/src/Fire.js b/src/Fire.js index 33429e7..4a8e2ac 100644 --- a/src/Fire.js +++ b/src/Fire.js @@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); 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 // @@ -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. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. When this -// effect is used for the first time, an instance of this shader class is created. Once -// the effect is finished, the shader will be stored in the freeShaders array and will -// then be reused if a new shader is requested. ShaderClass which will be used whenever -// 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 { +// The effect class 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 extends Effect { // ---------------------------------------------------------------------------- metadata // The effect is available on all GNOME Shell versions supported by this extension. - static getMinShellVersion() { + getMinShellVersion() { 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 // effect is enabled currently (e.g. '*-close-effect'), and its animation time // (e.g. '*-animation-time'). - static getNick() { + getNick() { return 'fire'; } // 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. - static getLabel() { + getLabel() { return _('Fire'); } // -------------------------------------------------------------------- API for prefs.js // 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 - // preferences dialog. - static getPreferences(dialog) { + // and binds all properties to the settings. + getPreferences(dialog) { // Add the settings page to the builder. dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/Fire.ui`); @@ -94,7 +86,7 @@ var Fire = class Fire { }); // Initialize the fire-preset dropdown. - Fire._createFirePresets(dialog); + this._createFirePresets(dialog); // Finally, return the new settings page. return dialog.getBuilder().get_object('fire-prefs'); @@ -102,40 +94,67 @@ var Fire = class Fire { // ---------------------------------------------------------------- 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; + // 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() { - if (freeShaders.length == 0) { - shader = new ShaderClass(); - } else { - shader = freeShaders.pop(); + // Only register the shader class when this method is called for the first time. + if (!this._ShaderClass) { + + 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'); + } + + // 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); + + // 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 + } + }); } - shader.setUniforms(actor, settings, forOpening); - - return shader; - } - - // 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 - // 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 = []; + // Finally, return a new instance of the shader class. + return new this._ShaderClass(this); } // ----------------------------------------------------------------------- private stuff // This populates the preset dropdown menu for the fire options. - static _createFirePresets(dialog) { + _createFirePresets(dialog) { dialog.getBuilder().get_object('fire-prefs').connect('realize', (widget) => { 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); - } - }); -} \ No newline at end of file diff --git a/src/Hexagon.js b/src/Hexagon.js index 7eb7a6a..0c1eec6 100644 --- a/src/Hexagon.js +++ b/src/Hexagon.js @@ -20,29 +20,22 @@ const _ = imports.gettext.domain('burn-my-windows').gettext; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); 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 // // gradually shrink until the window is fully dissolved. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. When this -// effect is used for the first time, an instance of this shader class is created. Once -// the effect is finished, the shader will be stored in the freeShaders array and will -// then be reused if a new shader is requested. ShaderClass which will be used whenever -// 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 { +// The effect class 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 extends Effect { // ---------------------------------------------------------------------------- metadata // The effect is available on all GNOME Shell versions supported by this extension. - static getMinShellVersion() { + getMinShellVersion() { 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 // effect is enabled currently (e.g. '*-close-effect'), and its animation time // (e.g. '*-animation-time'). - static getNick() { + getNick() { return 'hexagon'; } // 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. - static getLabel() { + getLabel() { return _('Hexagon'); } // -------------------------------------------------------------------- API for prefs.js // 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 - // preferences dialog. - static getPreferences(dialog) { + // and binds all properties to the settings. + getPreferences(dialog) { // Add the settings page to the builder. dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/Hexagon.ui`); @@ -84,103 +76,60 @@ var Hexagon = class Hexagon { // ---------------------------------------------------------------- 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; + // 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() { - if (freeShaders.length == 0) { - shader = new ShaderClass(); - } else { - shader = freeShaders.pop(); + // Only register the shader class when this method is called for the first time. + if (!this._ShaderClass) { + + 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._uAdditiveBlending = this.get_uniform_location('uAdditiveBlending'); + this._uSeed = this.get_uniform_location('uSeed'); + this._uScale = this.get_uniform_location('uScale'); + this._uLineWidth = this.get_uniform_location('uLineWidth'); + this._uGlowColor = this.get_uniform_location('uGlowColor'); + this._uLineColor = this.get_uniform_location('uLineColor'); + } + + // 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); + + // Get the two configurable colors. They are directly injected into the shader + // code below. + const gc = + 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. + const testMode = settings.get_boolean('test-mode'); + + // clang-format off + this.set_uniform_float(this._uAdditiveBlending, 1, [settings.get_boolean('hexagon-additive-blending')]); + this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + this.set_uniform_float(this._uScale, 1, [settings.get_double('hexagon-scale')]); + this.set_uniform_float(this._uLineWidth, 1, [settings.get_double('hexagon-line-width')]); + this.set_uniform_float(this._uGlowColor, 4, [gc.red / 255, gc.green / 255, gc.blue / 255, gc.alpha / 255]); + this.set_uniform_float(this._uLineColor, 4, [lc.red / 255, lc.green / 255, lc.blue / 255, lc.alpha / 255]); + // clang-format on + } + }); } - shader.setUniforms(actor, settings, forOpening); - - return shader; - } - - // 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 - // 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 = []; + // Finally, return a new instance of the shader class. + return new this._ShaderClass(this); } } - - -////////////////////////////////////////////////////////////////////////////////////////// -// 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._uSeed = this.get_uniform_location('uSeed'); - this._uScale = this.get_uniform_location('uScale'); - this._uLineWidth = this.get_uniform_location('uLineWidth'); - this._uGlowColor = this.get_uniform_location('uGlowColor'); - this._uLineColor = this.get_uniform_location('uLineColor'); - } - - // 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) { - - // Get the two configurable colors. They are directly injected into the shader code - // below. - const gc = 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. - const testMode = settings.get_boolean('test-mode'); - - // clang-format off - this.set_uniform_float(this._uAdditiveBlending, 1, [settings.get_boolean('hexagon-additive-blending')]); - this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); - this.set_uniform_float(this._uScale, 1, [settings.get_double('hexagon-scale')]); - this.set_uniform_float(this._uLineWidth, 1, [settings.get_double('hexagon-line-width')]); - this.set_uniform_float(this._uGlowColor, 4, [gc.red / 255, gc.green / 255, gc.blue / 255, gc.alpha / 255]); - this.set_uniform_float(this._uLineColor, 4, [lc.red / 255, lc.green / 255, lc.blue / 255, lc.alpha / 255]); - // 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); - } - }); -} \ No newline at end of file diff --git a/src/Matrix.js b/src/Matrix.js index 97dcbe6..eadadb4 100644 --- a/src/Matrix.js +++ b/src/Matrix.js @@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); 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 // @@ -29,26 +30,15 @@ const utils = Me.imports.src.utils; // 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 -// effect is used for the first time, an instance of this shader class is created. Once -// the effect is finished, the shader will be stored in the freeShaders array and will -// then be reused if a new shader is requested. ShaderClass which will be used whenever -// 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 { +// The effect class 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 extends Effect { // ---------------------------------------------------------------------------- metadata // This effect is only available on GNOME Shell 40+. - static getMinShellVersion() { + getMinShellVersion() { 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 // effect is enabled currently (e.g. '*-close-effect'), and its animation time // (e.g. '*-animation-time'). - static getNick() { + getNick() { return 'matrix'; } // 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. - static getLabel() { + getLabel() { return _('Matrix'); } // -------------------------------------------------------------------- API for prefs.js // 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 - // preferences dialog. - static getPreferences(dialog) { + // and binds all properties to the settings. + getPreferences(dialog) { // Add the settings page to the builder. dialog.getBuilder().add_from_resource('/ui/gtk4/Matrix.ui'); @@ -90,121 +79,86 @@ var Matrix = class Matrix { // ---------------------------------------------------------------- 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 // 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) { + getActorScale(settings) { 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 // static resources. - static cleanUp() { - freeShaders = []; - fontTexture = null; + cleanUp() { + super.cleanUp(); + 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. + if (!this._ShaderClass) { + + const {Clutter, GdkPixbuf, Cogl} = imports.gi; + const Shader = Me.imports.src.Shader.Shader; + + const fontData = GdkPixbuf.Pixbuf.new_from_resource('/img/matrixFont.png'); + this._fontTexture = new Clutter.Image(); + this._fontTexture.set_data( + fontData.get_pixels(), + fontData.has_alpha ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888, + 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._uTrailColor = this.get_uniform_location('uTrailColor'); + this._uTipColor = this.get_uniform_location('uTipColor'); + this._uLetterSize = this.get_uniform_location('uLetterSize'); + this._uRandomness = this.get_uniform_location('uRandomness'); + this._uOverShoot = this.get_uniform_location('uOverShoot'); + } + + // 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); + + 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 + this.set_uniform_float(this._uTrailColor, 3, [c1.red / 255, c1.green / 255, c1.blue / 255]); + this.set_uniform_float(this._uTipColor, 3, [c2.red / 255, c2.green / 255, c2.blue / 255]); + this.set_uniform_float(this._uLetterSize, 1, [settings.get_int('matrix-scale')]); + this.set_uniform_float(this._uRandomness, 1, [settings.get_double('matrix-randomness')]); + this.set_uniform_float(this._uOverShoot, 1, [settings.get_double('matrix-overshoot')]); + // clang-format on + } + + // 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 + vfunc_paint_target(node, paint_context) { + const pipeline = this.get_pipeline(); + pipeline.set_layer_texture(1, this._effect._fontTexture.get_texture()); + pipeline.set_uniform_1i(this._uFontTexture, 1); + + super.vfunc_paint_target(node, paint_context); + } + }); + } + + // Finally, return a new instance of the shader class. + return new this._ShaderClass(this); } } - - -////////////////////////////////////////////////////////////////////////////////////////// -// 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, 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'); - fontTexture = new Clutter.Image(); - fontTexture.set_data( - fontData.get_pixels(), - fontData.has_alpha ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888, - fontData.width, fontData.height, fontData.rowstride); - } - - this._uFontTexture = this.get_uniform_location('uFontTexture'); - this._uTrailColor = this.get_uniform_location('uTrailColor'); - this._uTipColor = this.get_uniform_location('uTipColor'); - this._uLetterSize = this.get_uniform_location('uLetterSize'); - this._uRandomness = this.get_uniform_location('uRandomness'); - this._uOverShoot = this.get_uniform_location('uOverShoot'); - } - - // 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) { - 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 - this.set_uniform_float(this._uTrailColor, 3, [c1.red / 255, c1.green / 255, c1.blue / 255]); - this.set_uniform_float(this._uTipColor, 3, [c2.red / 255, c2.green / 255, c2.blue / 255]); - this.set_uniform_float(this._uLetterSize, 1, [settings.get_int('matrix-scale')]); - this.set_uniform_float(this._uRandomness, 1, [settings.get_double('matrix-randomness')]); - this.set_uniform_float(this._uOverShoot, 1, [settings.get_double('matrix-overshoot')]); - // 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. 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 - vfunc_paint_target(node, paint_context) { - const pipeline = this.get_pipeline(); - pipeline.set_layer_texture(1, fontTexture.get_texture()); - pipeline.set_uniform_1i(this._uFontTexture, 1); - - super.vfunc_paint_target(node, paint_context); - } - }); -} \ No newline at end of file diff --git a/src/Shader.js b/src/Shader.js new file mode 100644 index 0000000..3ae0a23 --- /dev/null +++ b/src/Shader.js @@ -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/.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'; + } +}); diff --git a/src/SnapOfDisintegration.js b/src/SnapOfDisintegration.js index b4500b2..ce464a7 100644 --- a/src/SnapOfDisintegration.js +++ b/src/SnapOfDisintegration.js @@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); 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 // @@ -31,26 +32,15 @@ const utils = Me.imports.src.utils; // 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 -// effect is used for the first time, an instance of this shader class is created. Once -// the effect is finished, the shader will be stored in the freeShaders array and will -// then be reused if a new shader is requested. ShaderClass which will be used whenever -// 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 { +// The effect class 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 extends Effect { // ---------------------------------------------------------------------------- metadata // This effect is only available on GNOME Shell 40+. - static getMinShellVersion() { + getMinShellVersion() { 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 // effect is enabled currently (e.g. '*-close-effect'), and its animation time // (e.g. '*-animation-time'). - static getNick() { + getNick() { return 'snap'; } // 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. - static getLabel() { + getLabel() { return _('Snap of Disintegration'); } // -------------------------------------------------------------------- API for prefs.js // 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 - // preferences dialog. - static getPreferences(dialog) { + // and binds all properties to the settings. + getPreferences(dialog) { // Add the settings page to the builder. dialog.getBuilder().add_from_resource('/ui/gtk4/SnapOfDisintegration.ui'); @@ -89,119 +78,86 @@ var SnapOfDisintegration = class SnapOfDisintegration { // ---------------------------------------------------------------- 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 // 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) { + getActorScale(settings) { return {x: 1.2, y: 1.2}; } // This is called from extension.js if the extension is disabled. This should free all // static resources. - static cleanUp() { - freeShaders = []; - dustTexture = null; + cleanUp() { + super.cleanUp(); + 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() { -////////////////////////////////////////////////////////////////////////////////////////// -// 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. // -////////////////////////////////////////////////////////////////////////////////////////// + // Only register the shader class when this method is called for the first time. + if (!this._ShaderClass) { -if (utils.isInShellProcess()) { - - 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(); + const {Clutter, GdkPixbuf, Cogl} = imports.gi; + const Shader = Me.imports.src.Shader.Shader; // Load the dust texture. - if (dustTexture == null) { - const dustData = GdkPixbuf.Pixbuf.new_from_resource('/img/dust.png'); - dustTexture = new Clutter.Image(); - dustTexture.set_data(dustData.get_pixels(), Cogl.PixelFormat.RGB_888, - dustData.width, dustData.height, dustData.rowstride); - } + const dustData = GdkPixbuf.Pixbuf.new_from_resource('/img/dust.png'); + this._dustTexture = new Clutter.Image(); + this._dustTexture.set_data(dustData.get_pixels(), Cogl.PixelFormat.RGB_888, + dustData.width, dustData.height, dustData.rowstride); - this._uDustTexture = this.get_uniform_location('uDustTexture'); - this._uDustColor = this.get_uniform_location('uDustColor'); - this._uSeed = this.get_uniform_location('uSeed'); - this._uDustScale = this.get_uniform_location('uDustScale'); + 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._uDustColor = this.get_uniform_location('uDustColor'); + this._uSeed = this.get_uniform_location('uSeed'); + this._uDustScale = this.get_uniform_location('uDustScale'); + } + + // 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); + + // The dust particles will fade to this color over time. + 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. + const testMode = settings.get_boolean('test-mode'); + + // clang-format off + this.set_uniform_float(this._uDustColor, 4, [c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255]); + this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + this.set_uniform_float(this._uDustScale, 1, [settings.get_double('snap-scale')]); + // clang-format on + } + + // 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 + vfunc_paint_target(node, paint_context) { + const pipeline = this.get_pipeline(); + pipeline.set_layer_filters(0, Cogl.PipelineFilter.LINEAR, + Cogl.PipelineFilter.LINEAR); + pipeline.set_layer_texture(1, this._effect._dustTexture.get_texture()); + pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT); + pipeline.set_uniform_1i(this._uDustTexture, 1); + super.vfunc_paint_target(node, paint_context); + } + }); } - // 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) { - // The dust particles will fade to this color over time. - 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. - const testMode = settings.get_boolean('test-mode'); - - // clang-format off - this.set_uniform_float(this._uDustColor, 4, [c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255]); - this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); - this.set_uniform_float(this._uDustScale, 1, [settings.get_double('snap-scale')]); - // 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/${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 - vfunc_paint_target(node, paint_context) { - const pipeline = this.get_pipeline(); - pipeline.set_layer_filters(0, Cogl.PipelineFilter.LINEAR, - Cogl.PipelineFilter.LINEAR); - pipeline.set_layer_texture(1, dustTexture.get_texture()); - pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT); - pipeline.set_uniform_1i(this._uDustTexture, 1); - super.vfunc_paint_target(node, paint_context); - } - }); -} \ No newline at end of file + // Finally, return a new instance of the shader class. + return new this._ShaderClass(this); + } +} diff --git a/src/TRexAttack.js b/src/TRexAttack.js index 2410ce1..b45f159 100644 --- a/src/TRexAttack.js +++ b/src/TRexAttack.js @@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); 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! // @@ -27,26 +28,15 @@ const utils = Me.imports.src.utils; // 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 -// effect is used for the first time, an instance of this shader class is created. Once -// the effect is finished, the shader will be stored in the freeShaders array and will -// then be reused if a new shader is requested. ShaderClass which will be used whenever -// 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 { +// The effect class 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 extends Effect { // ---------------------------------------------------------------------------- metadata // This effect is only available on GNOME Shell 40+. - static getMinShellVersion() { + getMinShellVersion() { 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 // effect is enabled currently (e.g. '*-close-effect'), and its animation time // (e.g. '*-animation-time'). - static getNick() { + getNick() { return 'trex'; } // 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. - static getLabel() { + getLabel() { return _('T-Rex Attack'); } // -------------------------------------------------------------------- API for prefs.js // 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 - // preferences dialog. - static getPreferences(dialog) { + // and binds all properties to the settings. + getPreferences(dialog) { // Add the settings page to the builder. dialog.getBuilder().add_from_resource('/ui/gtk4/TRexAttack.ui'); @@ -87,120 +76,89 @@ var TRexAttack = class TRexAttack { // ---------------------------------------------------------------- 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 // 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) { + getActorScale(settings) { const scale = 1.0 + 0.5 * settings.get_double('claw-scratch-warp'); return {x: scale, y: scale}; } // This is called from extension.js if the extension is disabled. This should free all // static resources. - static cleanUp() { - freeShaders = []; - clawTexture = null; + cleanUp() { + super.cleanUp(); + 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() { -////////////////////////////////////////////////////////////////////////////////////////// -// 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. // -////////////////////////////////////////////////////////////////////////////////////////// + // Only register the shader class when this method is called for the first time. + if (!this._ShaderClass) { -if (utils.isInShellProcess()) { - - 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(); + const {Clutter, GdkPixbuf, Cogl} = imports.gi; + const Shader = Me.imports.src.Shader.Shader; // Load the claw texture. - if (clawTexture == null) { - const clawData = GdkPixbuf.Pixbuf.new_from_resource('/img/claws.png'); - clawTexture = new Clutter.Image(); - clawTexture.set_data(clawData.get_pixels(), Cogl.PixelFormat.RGB_888, - clawData.width, clawData.height, clawData.rowstride); - } + const clawData = GdkPixbuf.Pixbuf.new_from_resource('/img/claws.png'); + this._clawTexture = new Clutter.Image(); + this._clawTexture.set_data(clawData.get_pixels(), Cogl.PixelFormat.RGB_888, + clawData.width, clawData.height, clawData.rowstride); - this._uClawTexture = this.get_uniform_location('uClawTexture'); - this._uFlashColor = this.get_uniform_location('uFlashColor'); - this._uSeed = this.get_uniform_location('uSeed'); - this._uClawSize = this.get_uniform_location('uClawSize'); - this._uNumClaws = this.get_uniform_location('uNumClaws'); - this._uWarpIntensity = this.get_uniform_location('uWarpIntensity'); + + 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._uFlashColor = this.get_uniform_location('uFlashColor'); + this._uSeed = this.get_uniform_location('uSeed'); + this._uClawSize = this.get_uniform_location('uClawSize'); + this._uNumClaws = this.get_uniform_location('uNumClaws'); + this._uWarpIntensity = this.get_uniform_location('uWarpIntensity'); + } + + // 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); + + 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'); + + // clang-format off + this.set_uniform_float(this._uFlashColor, 4, [c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255]); + this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + this.set_uniform_float(this._uClawSize, 1, [settings.get_double('claw-scratch-scale')]); + this.set_uniform_float(this._uNumClaws, 1, [settings.get_int('claw-scratch-count')]); + this.set_uniform_float(this._uWarpIntensity, 1, [settings.get_double('claw-scratch-warp')]); + // clang-format on + } + + // 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 + vfunc_paint_target(node, paint_context) { + const pipeline = this.get_pipeline(); + pipeline.set_layer_texture(1, this._effect._clawTexture.get_texture()); + pipeline.set_uniform_1i(this._uClawTexture, 1); + + super.vfunc_paint_target(node, paint_context); + } + }); } - // 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) { - 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'); - - // clang-format off - this.set_uniform_float(this._uFlashColor, 4, [c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255]); - this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); - this.set_uniform_float(this._uClawSize, 1, [settings.get_double('claw-scratch-scale')]); - this.set_uniform_float(this._uNumClaws, 1, [settings.get_int('claw-scratch-count')]); - this.set_uniform_float(this._uWarpIntensity, 1, [settings.get_double('claw-scratch-warp')]); - // 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/${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 - vfunc_paint_target(node, paint_context) { - const pipeline = this.get_pipeline(); - pipeline.set_layer_texture(1, clawTexture.get_texture()); - pipeline.set_uniform_1i(this._uClawTexture, 1); - - super.vfunc_paint_target(node, paint_context); - } - }); -} \ No newline at end of file + // Finally, return a new instance of the shader class. + return new this._ShaderClass(this); + } +} diff --git a/src/TVEffect.js b/src/TVEffect.js index db03fba..f046d45 100644 --- a/src/TVEffect.js +++ b/src/TVEffect.js @@ -20,6 +20,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); 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 // @@ -27,23 +28,15 @@ const utils = Me.imports.src.utils; // the center. // ////////////////////////////////////////////////////////////////////////////////////////// -// The shader class for this effect is registered further down in this file. When this -// effect is used for the first time, an instance of this shader class is created. Once -// the effect is finished, the shader will be stored in the freeShaders array and will -// then be reused if a new shader is requested. ShaderClass which will be used whenever -// 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 { +// The effect class 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 extends Effect { // ---------------------------------------------------------------------------- metadata // The effect is available on all GNOME Shell versions supported by this extension. - static getMinShellVersion() { + getMinShellVersion() { 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 // effect is enabled currently (e.g. '*-close-effect'), and its animation time // (e.g. '*-animation-time'). - static getNick() { + getNick() { return 'tv'; } // 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. - static getLabel() { + getLabel() { return _('TV Effect'); } // -------------------------------------------------------------------- API for prefs.js // 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 - // preferences dialog. - static getPreferences(dialog) { + // and binds all properties to the settings. + getPreferences(dialog) { // Add the settings page to the builder. dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/TVEffect.ui`); @@ -81,83 +73,39 @@ var TVEffect = class TVEffect { // ---------------------------------------------------------------- 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; + // 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() { - if (freeShaders.length == 0) { - shader = new ShaderClass(); - } else { - shader = freeShaders.pop(); + // Only register the shader class when this method is called for the first time. + if (!this._ShaderClass) { + + 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._uColor = this.get_uniform_location('uColor'); + } + + // 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); + + 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]); + } + }); } - shader.setUniforms(actor, settings, forOpening); - - return shader; - } - - // 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 - // 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 = []; + // Finally, return a new instance of the shader class. + return new this._ShaderClass(this); } } - - -////////////////////////////////////////////////////////////////////////////////////////// -// 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 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) { - 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 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); - } - }); -} \ No newline at end of file diff --git a/src/Wisps.js b/src/Wisps.js index 65e2d17..8c791bf 100644 --- a/src/Wisps.js +++ b/src/Wisps.js @@ -20,52 +20,43 @@ const _ = imports.gettext.domain('burn-my-windows').gettext; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); 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 // // 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 -// effect is used for the first time, an instance of this shader class is created. Once -// the effect is finished, the shader will be stored in the freeShaders array and will -// then be reused if a new shader is requested. ShaderClass which will be used whenever -// 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 { +// The effect class 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 Wisps = class Wisps extends Effect { // ---------------------------------------------------------------------------- metadata // The effect is available on all GNOME Shell versions supported by this extension. - static getMinShellVersion() { + getMinShellVersion() { return [3, 36]; } // 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. the '*-close-effect'). - static getNick() { + getNick() { return 'wisps'; } // 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. - static getLabel() { + getLabel() { return _('Wisps'); } // -------------------------------------------------------------------- API for prefs.js // 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 - // preferences dialog. - static getPreferences(dialog) { + // and binds all properties to the settings. + getPreferences(dialog) { // Add the settings page to the builder. dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/Wisps.ui`); @@ -81,93 +72,49 @@ var Wisps = class Wisps { // ---------------------------------------------------------------- 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; + // 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() { - if (freeShaders.length == 0) { - shader = new ShaderClass(); - } else { - shader = freeShaders.pop(); + // Only register the shader class when this method is called for the first time. + if (!this._ShaderClass) { + + 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._uSeed = this.get_uniform_location('uSeed'); + this._uColor = this.get_uniform_location('uColor'); + this._uScale = this.get_uniform_location('uScale'); + } + + // 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); + + 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. + const testMode = settings.get_boolean('test-mode'); + + // clang-format off + this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + this.set_uniform_float(this._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); + this.set_uniform_float(this._uScale, 1, [settings.get_double('wisps-scale')]); + // clang-format on + } + }); } - shader.setUniforms(actor, settings, forOpening); - - return shader; - } - - // 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 - // 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 = []; + // Finally, return a new instance of the shader class. + return new this._ShaderClass(this); } } - - -////////////////////////////////////////////////////////////////////////////////////////// -// 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._uColor = this.get_uniform_location('uColor'); - this._uScale = this.get_uniform_location('uScale'); - } - - // 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) { - 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. - const testMode = settings.get_boolean('test-mode'); - - // clang-format off - this.set_uniform_float(this._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); - this.set_uniform_float(this._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); - this.set_uniform_float(this._uScale, 1, [settings.get_double('wisps-scale')]); - // 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); - } - }); -} \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index 4f298f8..463dc83 100644 --- a/src/utils.js +++ b/src/utils.js @@ -13,8 +13,7 @@ 'use strict'; -const {Gtk, Gio} = imports.gi; -const ByteArray = imports.byteArray; +const {Gtk} = imports.gi; // Returns the given argument, except for "alpha", "beta", and "rc". In these cases -3, // -2, and -1 are returned respectively. @@ -90,28 +89,4 @@ function shellVersionIsAtLeast(major, minor) { } 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'; } \ No newline at end of file