From e1017b770773ac8610dd2d646de060728d9a3687 Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Sat, 14 May 2022 13:58:28 +0200 Subject: [PATCH] :wrench: Use factory to create shaders --- extension.js | 97 +++++++++--------- prefs.js | 39 +++---- src/Apparition.js | 84 ++++++---------- src/BrokenGlass.js | 196 ++++++++++++++++-------------------- src/Effect.js | 7 -- src/EnergizeA.js | 74 +++++++------- src/EnergizeB.js | 75 +++++++------- src/Fire.js | 109 ++++++++++---------- src/Hexagon.js | 108 ++++++++++---------- src/Matrix.js | 137 +++++++++++-------------- src/Shader.js | 150 +++++++++++++++------------ src/ShaderFactory.js | 68 +++++++++++++ src/SnapOfDisintegration.js | 139 +++++++++++-------------- src/TRexAttack.js | 143 ++++++++++++-------------- src/TVEffect.js | 66 ++++++------ src/Wisps.js | 86 ++++++++-------- src/utils.js | 6 -- 17 files changed, 767 insertions(+), 817 deletions(-) create mode 100644 src/ShaderFactory.js diff --git a/extension.js b/extension.js index 7a70c64..44e71a8 100644 --- a/extension.js +++ b/extension.js @@ -31,21 +31,6 @@ const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); const utils = Me.imports.src.utils; -// New effects must be registered here and in prefs.js. -const ALL_EFFECTS = [ - 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 extensions modifies the window-close and window-open animations with all kinds // // of effects. The effects are implemented using GLSL shaders which are applied to the // @@ -63,6 +48,21 @@ class Extension { // from GNOME Tweaks, when you log in or when the screen is unlocked. enable() { + // New effects must be registered here and in prefs.js. + this._ALL_EFFECTS = [ + 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(), + ]; + // Load all of our resources. this._resources = Gio.Resource.load(Me.path + '/resources/burn-my-windows.gresource'); Gio.resources_register(this._resources); @@ -222,7 +222,7 @@ class Extension { const shader = actor.get_effect('burn-my-windows-effect'); if (shader) { actor.remove_effect(shader); - shader.endAnimation(); + shader.returnToFactory(); } } @@ -306,7 +306,7 @@ class Extension { disable() { // Free all effect resources. - ALL_EFFECTS.forEach(effect => effect.cleanUp()); + this._ALL_EFFECTS = []; // Unregister our resources. Gio.resources_unregister(this._resources); @@ -365,13 +365,13 @@ class Extension { return; } - // There is the weird case where an animation is already. This happens when a window - // is closed which has been created before the session was started (e.g. when GNOME - // Shell has been restarted in the meantime). + // There is the weird case where an animation is already ongoing. This happens when a + // window is closed which has been created before the session was started (e.g. when + // GNOME Shell has been restarted in the meantime). const oldShader = actor.get_effect('burn-my-windows-effect'); if (oldShader) { actor.remove_effect(oldShader); - oldShader.endAnimation(); + oldShader.returnToFactory(); } // ------------------------------------------------------------------ choose an effect @@ -381,7 +381,7 @@ class Extension { // First we check if an effect is to be previewed. if (previewNick != '') { - effect = ALL_EFFECTS.find(effect => effect.getNick() == previewNick); + effect = this._ALL_EFFECTS.find(effect => effect.getNick() == previewNick); // Only preview the effect once. this._settings.set_string(action + '-preview-effect', ''); @@ -391,7 +391,7 @@ class Extension { else { // Therefore, we first create a list of all currently enabled effects. - const enabled = ALL_EFFECTS.filter(effect => { + const enabled = this._ALL_EFFECTS.filter(effect => { return this._settings.get_boolean(`${effect.getNick()}-${action}-effect`); }); @@ -474,38 +474,41 @@ class Extension { // -------------------------------------------------------------------- add the shader // Now add a cool shader to our window actor! - const shader = effect.getShader(actor, this._settings, forOpening); + const shader = effect.shaderFactory.getShader(); - if (shader) { - // There should always be an opacity transition going on... - const transition = actor.get_transition('opacity'); + // There should always be an opacity transition going on... + const transition = actor.get_transition('opacity'); - if (!transition) { - this._fixAnimationTimes(isDialogWindow, forOpening, null); - utils.debug('Cannot setup shader without opacity transition.') - return; - } + if (!transition) { + this._fixAnimationTimes(isDialogWindow, forOpening, null); + utils.debug('Cannot setup shader without opacity transition.') + return; + } - actor.add_effect_with_name('burn-my-windows-effect', shader); + // Assign the effect to the window actor! + actor.add_effect_with_name('burn-my-windows-effect', shader); - shader.beginAnimation(actor, this._settings, forOpening); + // Set one-time uniforms. + shader.beginAnimation(this._settings, forOpening, actor); - // Update uniforms at each frame. + // If not in test mode, we update uniforms at each frame. + if (testMode) { + shader.updateAnimation(0.5, duration / 2); + } else { transition.connect('new-frame', (t) => { - shader.updateAnimation(testMode ? 0.5 : t.get_progress(), - testMode ? duration / 2 : 0.001 * t.get_elapsed_time()); + shader.updateAnimation(t.get_progress(), 0.001 * t.get_elapsed_time()); }); + } - // Remove the effect if the animation finished or was interrupted. - if (forOpening) { - transition.connect('stopped', () => { - const oldShader = actor.get_effect('burn-my-windows-effect'); - if (oldShader) { - actor.remove_effect(oldShader); - oldShader.endAnimation(); - } - }); - } + // Remove the effect if the animation finished or was interrupted. + if (forOpening) { + transition.connect('stopped', () => { + const oldShader = actor.get_effect('burn-my-windows-effect'); + if (oldShader) { + actor.remove_effect(oldShader); + oldShader.returnToFactory(); + } + }); } // Finally, ensure that all animation times are set properly so that other extensions diff --git a/prefs.js b/prefs.js index 0fa3457..bb45daa 100644 --- a/prefs.js +++ b/prefs.js @@ -30,21 +30,6 @@ const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); const utils = Me.imports.src.utils; -// New effects must be registered here and in extension.js. -const ALL_EFFECTS = [ - 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. var BurnMyWindowsEffectPage = null; @@ -59,6 +44,22 @@ var PreferencesDialog = class PreferencesDialog { // ------------------------------------------------------------ constructor / destructor constructor() { + + // New effects must be registered here and in extension.js. + this._ALL_EFFECTS = [ + 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(), + ]; + // Load all of our resources. this._resources = Gio.Resource.load(Me.path + '/resources/burn-my-windows.gresource'); Gio.resources_register(this._resources); @@ -113,7 +114,7 @@ var PreferencesDialog = class PreferencesDialog { const group = new Adw.PreferencesGroup({title: _('Effect Options')}); this.gtkBoxAppend(this._widget, group); - ALL_EFFECTS.forEach(effect => { + this._ALL_EFFECTS.forEach(effect => { const [minMajor, minMinor] = effect.getMinShellVersion(); if (utils.shellVersionIsAtLeast(minMajor, minMinor)) { @@ -186,7 +187,7 @@ var PreferencesDialog = class PreferencesDialog { this.gtkBoxAppend(this._widget, stack); // Add all other effect pages. - ALL_EFFECTS.forEach(effect => { + this._ALL_EFFECTS.forEach(effect => { const [minMajor, minMinor] = effect.getMinShellVersion(); if (utils.shellVersionIsAtLeast(minMajor, minMinor)) { @@ -302,7 +303,7 @@ var PreferencesDialog = class PreferencesDialog { const group = Gio.SimpleActionGroup.new(); window.insert_action_group('open-effects', group); - ALL_EFFECTS.forEach(effect => { + this._ALL_EFFECTS.forEach(effect => { const [minMajor, minMinor] = effect.getMinShellVersion(); if (utils.shellVersionIsAtLeast(minMajor, minMinor)) { const nick = effect.getNick(); @@ -324,7 +325,7 @@ var PreferencesDialog = class PreferencesDialog { const group = Gio.SimpleActionGroup.new(); window.insert_action_group('close-effects', group); - ALL_EFFECTS.forEach(effect => { + this._ALL_EFFECTS.forEach(effect => { const [minMajor, minMinor] = effect.getMinShellVersion(); if (utils.shellVersionIsAtLeast(minMajor, minMinor)) { const nick = effect.getNick(); diff --git a/src/Apparition.js b/src/Apparition.js index 507ef9f..0bd2b8f 100644 --- a/src/Apparition.js +++ b/src/Apparition.js @@ -13,25 +13,50 @@ 'use strict'; -const GObject = imports.gi.GObject; - 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; +const ShaderFactory = Me.imports.src.ShaderFactory.ShaderFactory; ////////////////////////////////////////////////////////////////////////////////////////// // This effect hides the actor by violently sucking it into the void of magic. // -// towards the middle and then hiding the resulting line from left and right towards // -// the center. // ////////////////////////////////////////////////////////////////////////////////////////// // 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 { +var Apparition = class Apparition { + + // The constructor creates a ShaderFactory which will be used by extension.js to create + // shader instances for this effect. The shaders will be automagically created using the + // GLSL file in resources/shaders/.glsl. The callback will be called for each + // newly created shader instance. + constructor() { + this.shaderFactory = new ShaderFactory(this.getNick(), (shader) => { + // Store uniform locations of newly created shaders. + shader._uSeed = shader.get_uniform_location('uSeed'); + shader._uShake = shader.get_uniform_location('uShake'); + shader._uTwirl = shader.get_uniform_location('uTwirl'); + shader._uSuction = shader.get_uniform_location('uSuction'); + shader._uRandomness = shader.get_uniform_location('uRandomness'); + + // Write all uniform values at the start of each animation. + shader.connect('begin-animation', (shader, settings) => { + // If we are performing an integration tests, we use a fixed seed. + const testMode = settings.get_boolean('test-mode'); + + // clang-format off + shader.set_uniform_float(shader._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + shader.set_uniform_float(shader._uShake, 1, [settings.get_double('apparition-shake-intensity')]); + shader.set_uniform_float(shader._uTwirl, 1, [settings.get_double('apparition-twirl-intensity')]); + shader.set_uniform_float(shader._uSuction, 1, [settings.get_double('apparition-suction-intensity')]); + shader.set_uniform_float(shader._uRandomness, 1, [settings.get_double('apparition-randomness')]); + // clang-format on + }); + }); + } // ---------------------------------------------------------------------------- metadata @@ -83,51 +108,4 @@ var Apparition = class Apparition extends Effect { getActorScale(settings) { return {x: 2.0, y: 2.0}; } - - // 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); - } } diff --git a/src/BrokenGlass.js b/src/BrokenGlass.js index 505dd69..ac8bcc3 100644 --- a/src/BrokenGlass.js +++ b/src/BrokenGlass.js @@ -13,26 +13,109 @@ 'use strict'; -const GObject = imports.gi.GObject; - 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; +const ShaderFactory = Me.imports.src.ShaderFactory.ShaderFactory; ////////////////////////////////////////////////////////////////////////////////////////// // This effect shatters the window into pieces. For an explanation how this works, look // // further down in this file into the documentation of the Shader class. This effect is // // not available on GNOME 3.3x, due to the limitation described in the documentation // // of vfunc_paint_target further down in this file. // +// 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. // ////////////////////////////////////////////////////////////////////////////////////////// // 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 { +var BrokenGlass = class BrokenGlass { + + // The constructor creates a ShaderFactory which will be used by extension.js to create + // shader instances for this effect. The shaders will be automagically created using the + // GLSL file in resources/shaders/.glsl. The callback will be called for each + // newly created shader instance. + constructor() { + this.shaderFactory = new ShaderFactory(this.getNick(), (shader) => { + // We import these modules in this function as they are not available in the + // preferences process. This callback is only called within GNOME Shell's process. + const {Clutter, GdkPixbuf, Cogl} = imports.gi; + + // Create the texture in the first call. + if (!this._shardTexture) { + 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); + } + + // Store all uniform locations. + shader._uShardTexture = shader.get_uniform_location('uShardTexture'); + shader._uSeed = shader.get_uniform_location('uSeed'); + shader._uEpicenter = shader.get_uniform_location('uEpicenter'); + shader._uShardScale = shader.get_uniform_location('uShardScale'); + shader._uBlowForce = shader.get_uniform_location('uBlowForce'); + shader._uGravity = shader.get_uniform_location('uGravity'); + + // And update all uniforms at the start of each animation. + shader.connect('begin-animation', (shader, settings, forOpening, actor) => { + // 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 + shader.set_uniform_float(shader._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + shader.set_uniform_float(shader._uEpicenter, 2, [epicenterX, epicenterY]); + shader.set_uniform_float(shader._uShardScale, 1, [settings.get_double('broken-glass-scale')]); + shader.set_uniform_float(shader._uBlowForce, 1, [settings.get_double('broken-glass-blow-force')]); + shader.set_uniform_float(shader._uGravity, 1, [settings.get_double('broken-glass-gravity')]); + // clang-format on + }); + + // This is required 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 + shader.connect('paint-target', (shader) => { + const pipeline = shader.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._shardTexture.get_texture()); + pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT); + pipeline.set_uniform_1i(shader._uShardTexture, 1); + }); + }); + } // ---------------------------------------------------------------------------- metadata @@ -83,109 +166,4 @@ var BrokenGlass = class BrokenGlass extends Effect { 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. - 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); - } } diff --git a/src/Effect.js b/src/Effect.js index 2356833..92bacd2 100644 --- a/src/Effect.js +++ b/src/Effect.js @@ -60,11 +60,4 @@ var Effect = class Effect { 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 fea0eb6..697b689 100644 --- a/src/EnergizeA.js +++ b/src/EnergizeA.js @@ -20,7 +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; +const ShaderFactory = Me.imports.src.ShaderFactory.ShaderFactory; ////////////////////////////////////////////////////////////////////////////////////////// // This effect looks a bit like the transporter effect from TOS. // @@ -29,7 +29,34 @@ const Effect = Me.imports.src.Effect.Effect; // 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 { +var EnergizeA = class EnergizeA { + + // The constructor creates a ShaderFactory which will be used by extension.js to create + // shader instances for this effect. The shaders will be automagically created using the + // GLSL file in resources/shaders/.glsl. The callback will be called for each + // newly created shader instance. + constructor() { + this.shaderFactory = new ShaderFactory(this.getNick(), (shader) => { + // We import Clutter in this function as it is not available in the preferences + // process. This creator function of the ShaderFactory is only called within GNOME + // Shell's process. + const Clutter = imports.gi.Clutter; + + // Store uniform locations of newly created shaders. + shader._uColor = shader.get_uniform_location('uColor'); + shader._uScale = shader.get_uniform_location('uScale'); + + // Write all uniform values at the start of each animation. + shader.connect('begin-animation', (shader, settings) => { + const c = Clutter.Color.from_string(settings.get_string('energize-a-color'))[1]; + + // clang-format off + shader.set_uniform_float(shader._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); + shader.set_uniform_float(shader._uScale, 1, [settings.get_double('energize-a-scale')]); + // clang-format on + }); + }); + } // ---------------------------------------------------------------------------- metadata @@ -72,43 +99,10 @@ var EnergizeA = class EnergizeA extends Effect { // ---------------------------------------------------------------- API for extension.js - // 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 = 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 - } - }); - } - - // 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+. + getActorScale(settings) { + return {x: 1.0, y: 1.0}; } } \ No newline at end of file diff --git a/src/EnergizeB.js b/src/EnergizeB.js index 9773892..ae2a7b4 100644 --- a/src/EnergizeB.js +++ b/src/EnergizeB.js @@ -20,7 +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; +const ShaderFactory = Me.imports.src.ShaderFactory.ShaderFactory; ////////////////////////////////////////////////////////////////////////////////////////// // This effect looks a bit like the transporter effect from TNG. // @@ -29,7 +29,35 @@ const Effect = Me.imports.src.Effect.Effect; // 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 { +var EnergizeB = class EnergizeB { + + // The constructor creates a ShaderFactory which will be used by extension.js to create + // shader instances for this effect. The shaders will be automagically created using the + // GLSL file in resources/shaders/.glsl. The callback will be called for each + // newly created shader instance. + constructor() { + this.shaderFactory = new ShaderFactory(this.getNick(), (shader) => { + // We import Clutter in this function as it is not available in the preferences + // process. This creator function of the ShaderFactory is only called within GNOME + // Shell's process. + const Clutter = imports.gi.Clutter; + + // Store uniform locations of newly created shaders. + shader._uColor = shader.get_uniform_location('uColor'); + shader._uScale = shader.get_uniform_location('uScale'); + + // Write all uniform values at the start of each animation. + shader.connect('begin-animation', (shader, settings) => { + const c = Clutter.Color.from_string(settings.get_string('energize-b-color'))[1]; + + // clang-format off + shader.set_uniform_float(shader._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); + shader.set_uniform_float(shader._uScale, 1, [settings.get_double('energize-b-scale')]); + // clang-format on + }); + }); + } + // ---------------------------------------------------------------------------- metadata @@ -72,43 +100,10 @@ var EnergizeB = class EnergizeB extends Effect { // ---------------------------------------------------------------- API for extension.js - // 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 = 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 - } - }); - } - - // 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+. + getActorScale(settings) { + return {x: 1.0, y: 1.0}; } } diff --git a/src/Fire.js b/src/Fire.js index 4a8e2ac..cd4f461 100644 --- a/src/Fire.js +++ b/src/Fire.js @@ -13,14 +13,14 @@ 'use strict'; -const {Gio, GObject} = imports.gi; +const {Gio} = imports.gi; 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; +const ShaderFactory = Me.imports.src.ShaderFactory.ShaderFactory; ////////////////////////////////////////////////////////////////////////////////////////// // This effect is a homage to the good old Compiz days. However, it is implemented // @@ -33,7 +33,50 @@ const Effect = Me.imports.src.Effect.Effect; // 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 { +var Fire = class Fire { + + + // The constructor creates a ShaderFactory which will be used by extension.js to create + // shader instances for this effect. The shaders will be automagically created using the + // GLSL file in resources/shaders/.glsl. The callback will be called for each + // newly created shader instance. + constructor() { + this.shaderFactory = new ShaderFactory(this.getNick(), (shader) => { + // We import Clutter in this function as it is not available in the preferences + // process. This creator function of the ShaderFactory is only called within GNOME + // Shell's process. + const Clutter = imports.gi.Clutter; + + // Store all uniform locations. + shader._uGradient = [ + shader.get_uniform_location('uGradient1'), + shader.get_uniform_location('uGradient2'), + shader.get_uniform_location('uGradient3'), + shader.get_uniform_location('uGradient4'), + shader.get_uniform_location('uGradient5'), + ]; + + shader._u3DNoise = shader.get_uniform_location('u3DNoise'); + shader._uScale = shader.get_uniform_location('uScale'); + shader._uMovementSpeed = shader.get_uniform_location('uMovementSpeed'); + + // And update all uniforms at the start of each animation. + shader.connect('begin-animation', (shader, settings) => { + for (let i = 1; i <= 5; i++) { + const c = Clutter.Color.from_string(settings.get_string('fire-color-' + i))[1]; + shader.set_uniform_float( + shader._uGradient[i - 1], 4, + [c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255]); + } + + // clang-format off + shader.set_uniform_float(shader._u3DNoise, 1, [settings.get_boolean('flame-3d-noise')]); + shader.set_uniform_float(shader._uScale, 1, [settings.get_double('flame-scale')]); + shader.set_uniform_float(shader._uMovementSpeed, 1, [settings.get_double('flame-movement-speed')]); + // clang-format on + }); + }); + } // ---------------------------------------------------------------------------- metadata @@ -94,61 +137,11 @@ var Fire = class Fire extends Effect { // ---------------------------------------------------------------- API for extension.js - // 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 = 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 - } - }); - } - - // 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+. + getActorScale(settings) { + return {x: 1.0, y: 1.0}; } // ----------------------------------------------------------------------- private stuff diff --git a/src/Hexagon.js b/src/Hexagon.js index 0c1eec6..8687871 100644 --- a/src/Hexagon.js +++ b/src/Hexagon.js @@ -20,7 +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; +const ShaderFactory = Me.imports.src.ShaderFactory.ShaderFactory; ////////////////////////////////////////////////////////////////////////////////////////// // This effect overlays a glowing hexagonal grid over the window. The grid cells then // @@ -30,7 +30,51 @@ const Effect = Me.imports.src.Effect.Effect; // 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 { +var Hexagon = class Hexagon { + + // The constructor creates a ShaderFactory which will be used by extension.js to create + // shader instances for this effect. The shaders will be automagically created using the + // GLSL file in resources/shaders/.glsl. The callback will be called for each + // newly created shader instance. + constructor() { + this.shaderFactory = new ShaderFactory(this.getNick(), (shader) => { + // We import Clutter in this function as it is not available in the preferences + // process. This creator function of the ShaderFactory is only called within GNOME + // Shell's process. + const Clutter = imports.gi.Clutter; + + // Store uniform locations of newly created shaders. + shader._uAdditiveBlending = shader.get_uniform_location('uAdditiveBlending'); + shader._uSeed = shader.get_uniform_location('uSeed'); + shader._uScale = shader.get_uniform_location('uScale'); + shader._uLineWidth = shader.get_uniform_location('uLineWidth'); + shader._uGlowColor = shader.get_uniform_location('uGlowColor'); + shader._uLineColor = shader.get_uniform_location('uLineColor'); + + // Write all uniform values at the start of each animation. + shader.connect('begin-animation', (shader, settings) => { + // 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 + shader.set_uniform_float(shader._uAdditiveBlending, 1, [settings.get_boolean('hexagon-additive-blending')]); + shader.set_uniform_float(shader._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + shader.set_uniform_float(shader._uScale, 1, [settings.get_double('hexagon-scale')]); + shader.set_uniform_float(shader._uLineWidth, 1, [settings.get_double('hexagon-line-width')]); + shader.set_uniform_float(shader._uGlowColor, 4, [gc.red / 255, gc.green / 255, gc.blue / 255, gc.alpha / 255]); + shader.set_uniform_float(shader._uLineColor, 4, [lc.red / 255, lc.green / 255, lc.blue / 255, lc.alpha / 255]); + // clang-format on + }); + }); + } // ---------------------------------------------------------------------------- metadata @@ -76,60 +120,10 @@ var Hexagon = class Hexagon extends Effect { // ---------------------------------------------------------------- API for extension.js - // 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 = 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 - } - }); - } - - // 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+. + getActorScale(settings) { + return {x: 1.0, y: 1.0}; } } diff --git a/src/Matrix.js b/src/Matrix.js index eadadb4..80903f4 100644 --- a/src/Matrix.js +++ b/src/Matrix.js @@ -20,7 +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; +const ShaderFactory = Me.imports.src.ShaderFactory.ShaderFactory; ////////////////////////////////////////////////////////////////////////////////////////// // The Matrix shader multiplies a grid of random letters with some gradients which are // @@ -33,7 +33,64 @@ const Effect = Me.imports.src.Effect.Effect; // 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 { +var Matrix = class Matrix { + + // The constructor creates a ShaderFactory which will be used by extension.js to create + // shader instances for this effect. The shaders will be automagically created using the + // GLSL file in resources/shaders/.glsl. The callback will be called for each + // newly created shader instance. + constructor() { + this.shaderFactory = new ShaderFactory(this.getNick(), (shader) => { + // We import these modules in this function as they are not available in the + // preferences process. This callback is only called within GNOME Shell's process. + const {Clutter, GdkPixbuf, Cogl} = imports.gi; + + // Create the texture in the first call. + if (!this._fontTexture) { + 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); + } + + // Store uniform locations of newly created shaders. + shader._uFontTexture = shader.get_uniform_location('uFontTexture'); + shader._uTrailColor = shader.get_uniform_location('uTrailColor'); + shader._uTipColor = shader.get_uniform_location('uTipColor'); + shader._uLetterSize = shader.get_uniform_location('uLetterSize'); + shader._uRandomness = shader.get_uniform_location('uRandomness'); + shader._uOverShoot = shader.get_uniform_location('uOverShoot'); + + // Write all uniform values at the start of each animation. + shader.connect('begin-animation', (shader, settings) => { + 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 + shader.set_uniform_float(shader._uTrailColor, 3, [c1.red / 255, c1.green / 255, c1.blue / 255]); + shader.set_uniform_float(shader._uTipColor, 3, [c2.red / 255, c2.green / 255, c2.blue / 255]); + shader.set_uniform_float(shader._uLetterSize, 1, [settings.get_int('matrix-scale')]); + shader.set_uniform_float(shader._uRandomness, 1, [settings.get_double('matrix-randomness')]); + shader.set_uniform_float(shader._uOverShoot, 1, [settings.get_double('matrix-overshoot')]); + // clang-format on + }); + + // This is required 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 + shader.connect('paint-target', (shader) => { + const pipeline = shader.get_pipeline(); + + // Bind the font texture. + pipeline.set_layer_texture(1, this._fontTexture.get_texture()); + pipeline.set_uniform_1i(shader._uFontTexture, 1); + }); + }); + } // ---------------------------------------------------------------------------- metadata @@ -85,80 +142,4 @@ var Matrix = class Matrix extends Effect { 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. - 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); - } } diff --git a/src/Shader.js b/src/Shader.js index 3ae0a23..6dbbeb4 100644 --- a/src/Shader.js +++ b/src/Shader.js @@ -13,8 +13,8 @@ 'use strict'; -const {Gio, Shell, GObject} = imports.gi; -const ByteArray = imports.byteArray; +const {Gio, Shell, GObject, Clutter} = imports.gi; +const ByteArray = imports.byteArray; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); @@ -24,81 +24,105 @@ 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. // +// Using Shell.GLSLEffect as a base class has some benefits and some drawbacks. The // +// main benefit when compared to Clutter.ShaderEffect is that setting uniforms of types // +// vec2, vec3 or vec4 is supported via the API (with Clutter.ShaderEffect the GJS // +// binding does not work properly). However, there are two drawbacks: On the one hand, // +// the shader source code is cached statically - this mean if we want to have a // +// different shader, we have to derive a new class. Therefore, each effect has to // +// derive its own class from the class below. The other drawback is the hard-coded use // +// of straight alpha (as opposed to premultiplied). This makes the shaders a bit more // +// complicated than required. // ////////////////////////////////////////////////////////////////////////////////////////// -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; +var Shader = GObject.registerClass( + { + Signals: { + 'begin-animation': + {param_types: [Gio.Settings.$gtype, GObject.TYPE_BOOLEAN, Clutter.Actor.$gtype]}, + 'update-animation': {param_types: [GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE]}, + 'paint-target': {param_types: []}, + } + }, + 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! It + // automagically loads the shader's source code from the resource file + // resources/shaders/.glsl resolving any #includes in this file. + _init(params) { + this._nick = params.nick; - // This will call vfunc_build_pipeline(). - super._init(); + // 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._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 each time the shader is used. This can be used to retrieve the + // configuration from the settings and update all uniforms accordingly. + beginAnimation(settings, forOpening, actor) { + 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.emit('begin-animation', settings, forOpening, actor); + } - // 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 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 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`); + this.emit('update-animation', progress, time); + } - // Match anything between the curly brackets of "void main() {...}". - const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}'); - const match = regex.exec(code); + // 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._nick}.glsl`); - const declarations = code.substr(0, match.index); - const main = match[1]; + // Match anything between the curly brackets of "void main() {...}". + const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}'); + const match = regex.exec(code); - this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true); - } + const declarations = code.substr(0, match.index); + const main = match[1]; - // ----------------------------------------------------------------------- private stuff + this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, declarations, main, true); + } - // 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 is overridden to bind textures for drawing. + vfunc_paint_target(node, paint_context) { + this.emit('paint-target'); + super.vfunc_paint_target(node, paint_context); + } - // 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); + // --------------------------------------------------------------------- private stuff - // This regex matches either #include "..." or #include <...>. The part between the - // brackets is captured in the capture group. - const regex = RegExp('#include ["<](.+)[">]', 'g'); + // 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)); + } - code = code.replace(regex, (m, file) => { - return this._loadStringResource('/shaders/' + file); - }); + // 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); - // Add a trailing newline. Else the GLSL compiler complains... - return code + '\n'; - } -}); + // 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/ShaderFactory.js b/src/ShaderFactory.js new file mode 100644 index 0000000..ed77b35 --- /dev/null +++ b/src/ShaderFactory.js @@ -0,0 +1,68 @@ +////////////////////////////////////////////////////////////////////////////////////////// +// ) ( // +// ( /( ( ( ) ( ( ( ( )\ ) ( ( // +// )\()) ))\ )( ( ( )\ ) )\))( )\ ( (()/( ( )\))( ( // +// ((_)\ /((_|()\ )\ ) )\ '(()/( ((_)()((_) )\ ) ((_)))\((_)()\ )\ // +// | |(_|_))( ((_)_(_/( _((_)) )(_)) _(()((_|_)_(_/( _| |((_)(()((_|(_) // +// | '_ \ || | '_| ' \)) | ' \()| || | \ V V / | ' \)) _` / _ \ V V (_-< // +// |_.__/\_,_|_| |_||_| |_|_|_| \_, | \_/\_/|_|_||_|\__,_\___/\_/\_//__/ // +// |__/ // +// Copyright (c) 2021 Simon Schneegans // +// Released under the GPLv3 or later. See LICENSE file for details. // +////////////////////////////////////////////////////////////////////////////////////////// + +'use strict'; + +const GObject = imports.gi.GObject; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = imports.misc.extensionUtils.getCurrentExtension(); + +////////////////////////////////////////////////////////////////////////////////////////// +// 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 ShaderFactory = class ShaderFactory { + + // The _freeShaders array contains previously created shaders which are not currently in + // use. + constructor(nick, setupFunc) { + this._freeShaders = []; + this._nick = nick; + this._setupFunc = setupFunc; + } + + // ---------------------------------------------------------------- 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() { + let shader; + + if (this._freeShaders.length == 0) { + + const typeName = `BurnMyWindowsShader_${this._nick}`; + + if (GObject.type_from_name(typeName) == null) { + GObject.registerClass({GTypeName: typeName}, + class Shader extends Me.imports.src.Shader.Shader {}); + } + + shader = GObject.Object.new(GObject.type_from_name(typeName), {'nick': this._nick}); + + shader.returnToFactory = () => { + this._freeShaders.push(shader); + }; + + this._setupFunc(shader); + + } else { + shader = this._freeShaders.pop(); + } + + return shader; + } +} diff --git a/src/SnapOfDisintegration.js b/src/SnapOfDisintegration.js index ce464a7..8c6f4fb 100644 --- a/src/SnapOfDisintegration.js +++ b/src/SnapOfDisintegration.js @@ -20,7 +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; +const ShaderFactory = Me.imports.src.ShaderFactory.ShaderFactory; ////////////////////////////////////////////////////////////////////////////////////////// // This effects dissolves your windows into a cloud of dust. For this, it uses an // @@ -35,7 +35,66 @@ const Effect = Me.imports.src.Effect.Effect; // 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 { +var SnapOfDisintegration = class SnapOfDisintegration { + + // The constructor creates a ShaderFactory which will be used by extension.js to create + // shader instances for this effect. The shaders will be automagically created using the + // GLSL file in resources/shaders/.glsl. The callback will be called for each + // newly created shader instance. + constructor() { + this.shaderFactory = new ShaderFactory(this.getNick(), (shader) => { + // We import these modules in this function as they are not available in the + // preferences process. This callback is only called within GNOME Shell's process. + const {Clutter, GdkPixbuf, Cogl} = imports.gi; + + // Create the texture in the first call. + if (!this._dustTexture) { + 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); + } + + // Store uniform locations of newly created shaders. + shader._uDustTexture = shader.get_uniform_location('uDustTexture'); + shader._uDustColor = shader.get_uniform_location('uDustColor'); + shader._uSeed = shader.get_uniform_location('uSeed'); + shader._uDustScale = shader.get_uniform_location('uDustScale'); + + // Write all uniform values at the start of each animation. + shader.connect('begin-animation', (shader, settings) => { + // 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 + shader.set_uniform_float(shader._uDustColor, 4, [c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255]); + shader.set_uniform_float(shader._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + shader.set_uniform_float(shader._uDustScale, 1, [settings.get_double('snap-scale')]); + // clang-format on + }); + + // This is required 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 + shader.connect('paint-target', (shader) => { + const pipeline = shader.get_pipeline(); + + // Use linear filtering for the window texture. + pipeline.set_layer_filters(0, Cogl.PipelineFilter.LINEAR, + Cogl.PipelineFilter.LINEAR); + + // Bind the dust texture. + pipeline.set_layer_texture(1, this._dustTexture.get_texture()); + pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT); + pipeline.set_uniform_1i(shader._uDustTexture, 1); + }); + }); + } // ---------------------------------------------------------------------------- metadata @@ -84,80 +143,4 @@ var SnapOfDisintegration = class SnapOfDisintegration extends Effect { 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. - 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() { - - // 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; - - // Load the dust texture. - 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._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); - } - }); - } - - // 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 b45f159..d9b9a43 100644 --- a/src/TRexAttack.js +++ b/src/TRexAttack.js @@ -20,7 +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; +const ShaderFactory = Me.imports.src.ShaderFactory.ShaderFactory; ////////////////////////////////////////////////////////////////////////////////////////// // This effect tears your windows apart with a series of violent scratches! // @@ -31,7 +31,68 @@ const Effect = Me.imports.src.Effect.Effect; // 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 { +var TRexAttack = class TRexAttack { + + // The constructor creates a ShaderFactory which will be used by extension.js to create + // shader instances for this effect. The shaders will be automagically created using the + // GLSL file in resources/shaders/.glsl. The callback will be called for each + // newly created shader instance. + constructor() { + this.shaderFactory = new ShaderFactory(this.getNick(), (shader) => { + // We import these modules in this function as they are not available in the + // preferences process. This callback is only called within GNOME Shell's process. + const {Clutter, GdkPixbuf, Cogl} = imports.gi; + + // Create the texture in the first call. + if (!this._clawTexture) { + 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); + } + + // Store uniform locations of newly created shaders. + shader._uClawTexture = shader.get_uniform_location('uClawTexture'); + shader._uFlashColor = shader.get_uniform_location('uFlashColor'); + shader._uSeed = shader.get_uniform_location('uSeed'); + shader._uClawSize = shader.get_uniform_location('uClawSize'); + shader._uNumClaws = shader.get_uniform_location('uNumClaws'); + shader._uWarpIntensity = shader.get_uniform_location('uWarpIntensity'); + + // Write all uniform values at the start of each animation. + shader.connect('begin-animation', (shader, settings) => { + 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 + shader.set_uniform_float(shader._uFlashColor, 4, [c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255]); + shader.set_uniform_float(shader._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + shader.set_uniform_float(shader._uClawSize, 1, [settings.get_double('claw-scratch-scale')]); + shader.set_uniform_float(shader._uNumClaws, 1, [settings.get_int('claw-scratch-count')]); + shader.set_uniform_float(shader._uWarpIntensity, 1, [settings.get_double('claw-scratch-warp')]); + // clang-format on + }); + + // This is required 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 + shader.connect('paint-target', (shader) => { + const pipeline = shader.get_pipeline(); + + // Use linear filtering for the window texture. + pipeline.set_layer_filters(0, Cogl.PipelineFilter.LINEAR, + Cogl.PipelineFilter.LINEAR); + + // Bind the claw texture. + pipeline.set_layer_texture(1, this._clawTexture.get_texture()); + pipeline.set_uniform_1i(shader._uClawTexture, 1); + }); + }); + } // ---------------------------------------------------------------------------- metadata @@ -83,82 +144,4 @@ var TRexAttack = class TRexAttack extends Effect { 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. - 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() { - - // 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; - - // Load the claw texture. - 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._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); - } - }); - } - - // 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 f046d45..c0dbebd 100644 --- a/src/TVEffect.js +++ b/src/TVEffect.js @@ -20,7 +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; +const ShaderFactory = Me.imports.src.ShaderFactory.ShaderFactory; ////////////////////////////////////////////////////////////////////////////////////////// // This effect hides the actor by making it first transparent from top and bottom // @@ -31,7 +31,30 @@ const Effect = Me.imports.src.Effect.Effect; // 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 { +var TVEffect = class TVEffect { + + // The constructor creates a ShaderFactory which will be used by extension.js to create + // shader instances for this effect. The shaders will be automagically created using the + // GLSL file in resources/shaders/.glsl. The callback will be called for each + // newly created shader instance. + constructor() { + this.shaderFactory = new ShaderFactory(this.getNick(), (shader) => { + // We import Clutter in this function as it is not available in the preferences + // process. This creator function of the ShaderFactory is only called within GNOME + // Shell's process. + const Clutter = imports.gi.Clutter; + + // Store uniform locations of newly created shaders. + shader._uColor = shader.get_uniform_location('uColor'); + + // Write all uniform values at the start of each animation. + shader.connect('begin-animation', (shader, settings) => { + const c = Clutter.Color.from_string(settings.get_string('tv-effect-color'))[1]; + shader.set_uniform_float(shader._uColor, 3, + [c.red / 255, c.green / 255, c.blue / 255]); + }); + }); + } // ---------------------------------------------------------------------------- metadata @@ -73,39 +96,10 @@ var TVEffect = class TVEffect extends Effect { // ---------------------------------------------------------------- API for extension.js - // 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 = 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]); - } - }); - } - - // 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+. + getActorScale(settings) { + return {x: 1.0, y: 1.0}; } } diff --git a/src/Wisps.js b/src/Wisps.js index 8c791bf..42a7ea1 100644 --- a/src/Wisps.js +++ b/src/Wisps.js @@ -20,7 +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; +const ShaderFactory = Me.imports.src.ShaderFactory.ShaderFactory; ////////////////////////////////////////////////////////////////////////////////////////// // This effect lets your windows be carried to the realm of dreams by some little // @@ -30,7 +30,40 @@ const Effect = Me.imports.src.Effect.Effect; // 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 { +var Wisps = class Wisps { + + // The constructor creates a ShaderFactory which will be used by extension.js to create + // shader instances for this effect. The shaders will be automagically created using the + // GLSL file in resources/shaders/.glsl. The callback will be called for each + // newly created shader instance. + constructor() { + this.shaderFactory = new ShaderFactory(this.getNick(), (shader) => { + // We import Clutter in this function as it is not available in the preferences + // process. This creator function of the ShaderFactory is only called within GNOME + // Shell's process. + const Clutter = imports.gi.Clutter; + + // Store uniform locations of newly created shaders. + shader._uSeed = shader.get_uniform_location('uSeed'); + shader._uColor = shader.get_uniform_location('uColor'); + shader._uScale = shader.get_uniform_location('uScale'); + + // Write all uniform values at the start of each animation. + shader.connect('begin-animation', (shader, settings) => { + 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 + shader.set_uniform_float(shader._uSeed, 2, [testMode ? 0 : Math.random(), testMode ? 0 : Math.random()]); + shader.set_uniform_float(shader._uColor, 3, [c.red / 255, c.green / 255, c.blue / 255]); + shader.set_uniform_float(shader._uScale, 1, [settings.get_double('wisps-scale')]); + // clang-format on + }); + }); + } // ---------------------------------------------------------------------------- metadata @@ -72,49 +105,10 @@ var Wisps = class Wisps extends Effect { // ---------------------------------------------------------------- API for extension.js - // 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 = 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 - } - }); - } - - // 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+. + getActorScale(settings) { + return {x: 1.0, y: 1.0}; } } diff --git a/src/utils.js b/src/utils.js index 463dc83..9f528d8 100644 --- a/src/utils.js +++ b/src/utils.js @@ -65,12 +65,6 @@ function getGTKString() { return isGTK4() ? 'gtk4' : 'gtk3'; } -// This method returns true if called in GNOME Shell's process, false if called in the -// preferences process. -function isInShellProcess() { - return window.global && global.stage; -} - // This method returns true if the current GNOME Shell version matches the given // arguments. function shellVersionIs(major, minor) {