350 lines
14 KiB
JavaScript
350 lines
14 KiB
JavaScript
//////////////////////////////////////////////////////////////////////////////////////////
|
|
// ) ( //
|
|
// ( /( ( ( ) ( ( ( ( )\ ) ( ( //
|
|
// )\()) ))\ )( ( ( )\ ) )\))( )\ ( (()/( ( )\))( ( //
|
|
// ((_)\ /((_|()\ )\ ) )\ '(()/( ((_)()((_) )\ ) ((_)))\((_)()\ )\ //
|
|
// | |(_|_))( ((_)_(_/( _((_)) )(_)) _(()((_|_)_(_/( _| |((_)(()((_|(_) //
|
|
// | '_ \ || | '_| ' \)) | ' \()| || | \ V V / | ' \)) _` / _ \ V V (_-< //
|
|
// |_.__/\_,_|_| |_||_| |_|_|_| \_, | \_/\_/|_|_||_|\__,_\___/\_/\_//__/ //
|
|
// |__/ //
|
|
// Copyright (c) 2021 Simon Schneegans //
|
|
// Released under the GPLv3 or later. See LICENSE file for details. //
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
'use strict';
|
|
|
|
const {Gio, GObject} = imports.gi;
|
|
|
|
const ExtensionUtils = imports.misc.extensionUtils;
|
|
const Me = imports.misc.extensionUtils.getCurrentExtension();
|
|
const utils = Me.imports.src.utils;
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
// This effect is a homage to the good old Compiz days. However, it is implemented //
|
|
// quite differently. While Compiz used a particle system, this effect uses a noise //
|
|
// shader. The noise is moved vertically over time and mapped to a configurable color //
|
|
// gradient. It is faded to transparency towards the edges of the window. In addition, //
|
|
// 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.
|
|
let Shader = 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 FireEffect = class FireEffect {
|
|
|
|
// ---------------------------------------------------------------------------- metadata
|
|
|
|
// The effect is available on all GNOME Shell versions supported by this extension.
|
|
static 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() {
|
|
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() {
|
|
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 initPreferences(dialog) {
|
|
|
|
// Add the settings page to the builder.
|
|
dialog.getBuilder().add_from_resource(`/ui/${utils.getGTKString()}/firePage.ui`);
|
|
|
|
// Bind all properties.
|
|
dialog.bindAdjustment('fire-animation-time');
|
|
dialog.bindAdjustment('flame-movement-speed');
|
|
dialog.bindAdjustment('flame-scale');
|
|
dialog.bindSwitch('flame-3d-noise');
|
|
dialog.bindColorButton('fire-color-1');
|
|
dialog.bindColorButton('fire-color-2');
|
|
dialog.bindColorButton('fire-color-3');
|
|
dialog.bindColorButton('fire-color-4');
|
|
dialog.bindColorButton('fire-color-5');
|
|
|
|
// The fire-gradient-reset button needs to be bound explicitly.
|
|
dialog.getBuilder().get_object('reset-fire-colors').connect('clicked', () => {
|
|
dialog.getSettings().reset('fire-color-1');
|
|
dialog.getSettings().reset('fire-color-2');
|
|
dialog.getSettings().reset('fire-color-3');
|
|
dialog.getSettings().reset('fire-color-4');
|
|
dialog.getSettings().reset('fire-color-5');
|
|
});
|
|
|
|
// Initialize the fire-preset dropdown.
|
|
FireEffect._createFirePresets(dialog);
|
|
|
|
// Finally, append the settings page to the main stack.
|
|
const stack = dialog.getBuilder().get_object('main-stack');
|
|
stack.add_titled(
|
|
dialog.getBuilder().get_object('fire-prefs'), FireEffect.getNick(),
|
|
FireEffect.getLabel());
|
|
}
|
|
|
|
// ---------------------------------------------------------------- API for extension.js
|
|
|
|
// This is called from extension.js whenever a window is closed with this effect.
|
|
static createShader(settings) {
|
|
return new Shader(settings);
|
|
}
|
|
|
|
// This is also called from extension.js. It is used to tweak the ongoing transitions of
|
|
// the actor - usually windows are faded to transparency and scaled down slightly by
|
|
// GNOME Shell. Here, we modify this behavior as well as the transition duration.
|
|
static tweakTransitions(actor, settings) {
|
|
const animationTime = settings.get_int('fire-animation-time');
|
|
|
|
const tweakTransition = (property, value) => {
|
|
const transition = actor.get_transition(property);
|
|
if (transition) {
|
|
transition.set_to(value);
|
|
transition.set_duration(animationTime);
|
|
}
|
|
};
|
|
|
|
// We re-target these transitions so that the window is neither scaled nor faded.
|
|
tweakTransition('opacity', 255);
|
|
tweakTransition('scale-x', 1);
|
|
tweakTransition('scale-y', 1);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------- private stuff
|
|
|
|
// This populates the preset dropdown menu for the fire options.
|
|
static _createFirePresets(dialog) {
|
|
dialog.getBuilder().get_object('settings-widget').connect('realize', (widget) => {
|
|
const presets = [
|
|
{
|
|
name: 'Default Fire',
|
|
scale: 1.0,
|
|
speed: 0.5,
|
|
color1: 'rgba(76, 51, 25, 0.0)',
|
|
color2: 'rgba(180, 55, 30, 0.7)',
|
|
color3: 'rgba(255, 76, 38, 0.9)',
|
|
color4: 'rgba(255, 166, 25, 1)',
|
|
color5: 'rgba(255, 255, 255, 1)'
|
|
},
|
|
{
|
|
name: 'Hell Fire',
|
|
scale: 1.5,
|
|
speed: 0.2,
|
|
color1: 'rgba(0,0,0,0)',
|
|
color2: 'rgba(103,7,80,0.5)',
|
|
color3: 'rgba(150,0,24,0.9)',
|
|
color4: 'rgb(255,200,0)',
|
|
color5: 'rgba(255, 255, 255, 1)'
|
|
},
|
|
{
|
|
name: 'Dark and Smutty',
|
|
scale: 1.0,
|
|
speed: 0.5,
|
|
color1: 'rgba(0,0,0,0)',
|
|
color2: 'rgba(36,3,0,0.5)',
|
|
color3: 'rgba(150,0,24,0.9)',
|
|
color4: 'rgb(255,177,21)',
|
|
color5: 'rgb(255,238,166)'
|
|
},
|
|
{
|
|
name: 'Cold Breeze',
|
|
scale: 1.5,
|
|
speed: -0.1,
|
|
color1: 'rgba(0,110,255,0)',
|
|
color2: 'rgba(30,111,180,0.24)',
|
|
color3: 'rgba(38,181,255,0.54)',
|
|
color4: 'rgba(34,162,255,0.84)',
|
|
color5: 'rgb(97,189,255)'
|
|
},
|
|
{
|
|
name: 'Santa is Coming',
|
|
scale: 0.4,
|
|
speed: -0.5,
|
|
color1: 'rgba(0,110,255,0)',
|
|
color2: 'rgba(208,233,255,0.24)',
|
|
color3: 'rgba(207,235,255,0.84)',
|
|
color4: 'rgb(208,243,255)',
|
|
color5: 'rgb(255,255,255)'
|
|
}
|
|
];
|
|
|
|
const menu = Gio.Menu.new();
|
|
const group = Gio.SimpleActionGroup.new();
|
|
const groupName = 'presets';
|
|
|
|
// Add all presets.
|
|
presets.forEach((preset, i) => {
|
|
const actionName = 'fire' + i;
|
|
menu.append(preset.name, groupName + '.' + actionName);
|
|
let action = Gio.SimpleAction.new(actionName, null);
|
|
|
|
// Load the preset on activation.
|
|
action.connect('activate', () => {
|
|
dialog.getSettings().set_double('flame-movement-speed', preset.speed);
|
|
dialog.getSettings().set_double('flame-scale', preset.scale);
|
|
dialog.getSettings().set_string('fire-color-1', preset.color1);
|
|
dialog.getSettings().set_string('fire-color-2', preset.color2);
|
|
dialog.getSettings().set_string('fire-color-3', preset.color3);
|
|
dialog.getSettings().set_string('fire-color-4', preset.color4);
|
|
dialog.getSettings().set_string('fire-color-5', preset.color5);
|
|
});
|
|
|
|
group.add_action(action);
|
|
});
|
|
|
|
dialog.getBuilder().get_object('fire-preset-button').set_menu_model(menu);
|
|
|
|
const root = utils.isGTK4() ? widget.get_root() : widget.get_toplevel();
|
|
root.insert_action_group(groupName, group);
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
// 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 = imports.gi.Clutter;
|
|
const shaderSnippets = Me.imports.src.shaderSnippets;
|
|
|
|
Shader = GObject.registerClass({}, class Shader extends Clutter.ShaderEffect {
|
|
_init(settings) {
|
|
super._init({shader_type: Clutter.ShaderType.FRAGMENT_SHADER});
|
|
|
|
// Load the gradient values from the settings. We directly inject the values in the
|
|
// GLSL code below. The shader is compiled once for each window-closing anyways. In
|
|
// the future, we may want to prevent this frequent recompilations of shaders,
|
|
// though.
|
|
const gradient = [];
|
|
for (let i = 1; i <= 5; i++) {
|
|
const color =
|
|
Clutter.Color.from_string(settings.get_string('fire-color-' + i))[1];
|
|
gradient.push(`vec4(${color.red / 255}, ${color.green / 255}, ${
|
|
color.blue / 255}, ${color.alpha / 255})`);
|
|
}
|
|
|
|
this.set_shader_source(`
|
|
|
|
// Inject some common shader snippets.
|
|
${shaderSnippets.standardUniforms()}
|
|
${shaderSnippets.noise()}
|
|
${shaderSnippets.edgeMask()}
|
|
|
|
// These may be configurable in the future.
|
|
const float EDGE_FADE = 90;
|
|
const float FADE_WIDTH = 0.1;
|
|
const float HIDE_TIME = 0.4;
|
|
const vec2 FIRE_SCALE = vec2(400, 600) * ${settings.get_double('flame-scale')};
|
|
const float FIRE_SPEED = ${settings.get_double('flame-movement-speed')};
|
|
|
|
// This maps the input value from [0..1] to a color from the gradient.
|
|
vec4 getFireColor(float v) {
|
|
const float steps[5] = float[](0.0, 0.2, 0.35, 0.5, 0.8);
|
|
const vec4 colors[5] = vec4[](
|
|
${gradient[0]},
|
|
${gradient[1]},
|
|
${gradient[2]},
|
|
${gradient[3]},
|
|
${gradient[4]}
|
|
);
|
|
|
|
if (v < steps[0]) {
|
|
return colors[0];
|
|
}
|
|
|
|
for (int i=0; i<4; ++i) {
|
|
if (v <= steps[i+1]) {
|
|
return mix(colors[i], colors[i+1], vec4(v - steps[i])/(steps[i+1]-steps[i]));
|
|
}
|
|
}
|
|
|
|
return colors[4];
|
|
}
|
|
|
|
// This method requires the uniforms from standardUniforms() to be available.
|
|
// It returns two values: The first is an alpha value which can be used for the window
|
|
// texture. This gradually dissolves the window from top to bottom. The second can be used
|
|
// to mask any effect, it will be most opaque where the window is currently fading and
|
|
// gradually dissolve to zero over time.
|
|
// hideTime: A value in [0..1]. It determines the percentage of the animation which
|
|
// is spent for hiding the window. 1-hideTime will be spent thereafter for
|
|
// dissolving the effect mask.
|
|
// fadeWidth: The relative size of the window-hiding gradient in [0..1].
|
|
// edgeFadeWidth: The pixel width of the effect fading range at the edges of the window.
|
|
vec2 effectMask(float hideTime, float fadeWidth, float edgeFadeWidth) {
|
|
float burnProgress = clamp(uProgress/hideTime, 0, 1);
|
|
float afterBurnProgress = clamp((uProgress-hideTime)/(1-hideTime), 0, 1);
|
|
|
|
// Gradient from top to bottom.
|
|
float t = cogl_tex_coord_in[0].t * (1 - fadeWidth);
|
|
|
|
// Visible part of the window. Gradually dissolves towards the bottom.
|
|
float windowMask = 1 - clamp((burnProgress - t) / fadeWidth, 0, 1);
|
|
|
|
// Gradient from top burning window.
|
|
float effectMask = clamp(t*(1-windowMask)/burnProgress, 0, 1);
|
|
|
|
// Fade-out when the window burned down.
|
|
if (uProgress > hideTime) {
|
|
float fade = sqrt(1-afterBurnProgress*afterBurnProgress);
|
|
effectMask *= mix(1, 1-t, afterBurnProgress) * fade;
|
|
}
|
|
|
|
// Fade at window borders.
|
|
effectMask *= getAbsoluteEdgeMask(edgeFadeWidth);
|
|
|
|
return vec2(windowMask, effectMask);
|
|
}
|
|
|
|
void main() {
|
|
|
|
// Get a noise value which moves vertically in time.
|
|
vec2 uv = cogl_tex_coord_in[0].st * vec2(uSizeX, uSizeY) / FIRE_SCALE;
|
|
uv.y += uTime * FIRE_SPEED;
|
|
|
|
#if ${settings.get_boolean('flame-3d-noise') ? 1 : 0}
|
|
float noise = simplex3DFractal(vec3(uv*4.0, uTime*FIRE_SPEED*1.5));
|
|
#else
|
|
float noise = simplex2DFractal(uv * 4.0);
|
|
#endif
|
|
|
|
// Modulate noise by effect mask.
|
|
vec2 effectMask = effectMask(HIDE_TIME, FADE_WIDTH, EDGE_FADE);
|
|
noise *= effectMask.y;
|
|
|
|
// Map noise value to color.
|
|
vec4 fire = getFireColor(noise);
|
|
fire.rgb *= fire.a;
|
|
|
|
// Get the window texture and fade it according to the effect mask.
|
|
cogl_color_out = texture2D(uTexture, cogl_tex_coord_in[0].st) * effectMask.x;
|
|
|
|
// Add the fire to the window.
|
|
cogl_color_out += fire;
|
|
|
|
// These are pretty useful for understanding how this works.
|
|
// cogl_color_out = vec4(vec3(noise), 1);
|
|
// cogl_color_out = vec4(vec3(effectMask.x), 1);
|
|
// cogl_color_out = vec4(vec3(effectMask.y), 1);
|
|
}
|
|
`);
|
|
};
|
|
});
|
|
} |