From 13e90faac1b069904093f3afdba9b24ef9c61194 Mon Sep 17 00:00:00 2001 From: SubhadeepJasu Date: Sun, 15 Feb 2026 11:58:21 +0530 Subject: [PATCH] Add TV effect shader and animation while shutting down --- data/gala.gresource.xml | 1 + data/shaders/tv-effect.frag | 63 +++++++++++++++++++++++ src/Curtains/TVEffect.vala | 45 ++++++++++++++++ src/Main.vala | 3 ++ src/Widgets/Curtains/ShutdownCurtain.vala | 43 ++++++++++++++++ src/meson.build | 2 + 6 files changed, 157 insertions(+) create mode 100644 data/shaders/tv-effect.frag create mode 100644 src/Curtains/TVEffect.vala create mode 100644 src/Widgets/Curtains/ShutdownCurtain.vala diff --git a/data/gala.gresource.xml b/data/gala.gresource.xml index 7713468a1..9c4fc4bed 100644 --- a/data/gala.gresource.xml +++ b/data/gala.gresource.xml @@ -24,6 +24,7 @@ shaders/colorblindness-correction.frag shaders/monochrome.frag shaders/rounded-corners.frag + shaders/tv-effect.frag gala-daemon.css diff --git a/data/shaders/tv-effect.frag b/data/shaders/tv-effect.frag new file mode 100644 index 000000000..337a7a604 --- /dev/null +++ b/data/shaders/tv-effect.frag @@ -0,0 +1,63 @@ +/* + * Copyright 2026 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +uniform sampler2D tex; +uniform float OCCLUSION; // 0.0 when the TV is fully on, 1.0 when it's fully off +uniform float HEIGHT; // Screen height in pixels + +float cubicBezier(float x, float a, float b, float c, float d) { + float _x = 1.0 - x; + return a * _x * _x * _x + + b * 3.0 * _x * _x * x + + c * 3.0 * _x * x * x + + d * x * x * x; +} + +vec4 tvEffect(vec2 uv, float scale, float occlusion) { + float y_scaled = 0.5 + (uv.y - 0.5) / (scale * scale); + float x_scaled = uv.x; + + // Wait for scale to get small enough before shrinking the bright line to a dot + if (scale < 0.1) { + float scale_fract = scale / 0.1; + x_scaled = 0.5 + (uv.x - 0.5) / (scale_fract * scale_fract); + } + + // Outside of the scaled area should be black + if (scale >= 0.0 && y_scaled >= 0.0 && y_scaled <= 1.0 && x_scaled >= 0.0 && x_scaled <= 1.0) { + vec4 color = texture2D(tex, vec2(x_scaled, y_scaled)); + // Make it brighter as the window gets smaller + return color + occlusion * color + occlusion * 0.75; + } else { + return vec4(0.0, 0.0, 0.0, 1.0); + } +} + +void main() { + // Ease out the occlusion + float occlusion = cubicBezier(OCCLUSION, 0.0, 0.98, 0.75, 1.0); + float scale = 1.0 - occlusion; + vec2 uv = cogl_tex_coord0_in.xy; + + // Apply a 5x5 Gaussian blur, with support of 0.5 and sigma 1.0 + float kernel[5]; + kernel[0] = 0.0614; + kernel[1] = 0.2448; + kernel[2] = 0.3877; + kernel[3] = 0.2448; + kernel[4] = 0.0614; + + float blurSize = occlusion * 5.0 / HEIGHT; + + vec4 color = vec4(0.0); + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) { + vec2 offset = vec2(float(i - 2), float(j - 2)) * blurSize; + color += tvEffect(uv + offset, scale, occlusion) * kernel[i] * kernel[j]; + } + } + + cogl_color_out = color; +} diff --git a/src/Curtains/TVEffect.vala b/src/Curtains/TVEffect.vala new file mode 100644 index 000000000..5a1b52e4e --- /dev/null +++ b/src/Curtains/TVEffect.vala @@ -0,0 +1,45 @@ +/* + * Copyright 2026 elementary, Inc. + * SPDX-License-Identifier: LGPL-3.0-or-later + */ + +public class Gala.TVEffect : Clutter.ShaderEffect { + private float _occlusion = 0.0f; + public float occlusion { get { + return _occlusion; + } set { + _occlusion = value; + set_uniform_value ("OCCLUSION", value); + queue_repaint (); + }} + + private float _height = 512.0f; + public float height { get { + return _height; + } set { + _height = value; + set_uniform_value ("HEIGHT", value); + queue_repaint (); + }} + + public TVEffect (float occlusion = 0.0f) { + Object ( +#if HAS_MUTTER48 + shader_type: Cogl.ShaderType.FRAGMENT, +#else + shader_type: Clutter.ShaderType.FRAGMENT_SHADER, +#endif + occlusion: occlusion + ); + + try { + var bytes = GLib.resources_lookup_data ( + "/io/elementary/desktop/gala/shaders/tv-effect.frag", + GLib.ResourceLookupFlags.NONE + ); + set_shader_source ((string) bytes.get_data ()); + } catch (Error e) { + warning ("Failed to load TV effect shader: %s", e.message); + } + } +} diff --git a/src/Main.vala b/src/Main.vala index a487a061c..be667f8a8 100644 --- a/src/Main.vala +++ b/src/Main.vala @@ -93,6 +93,9 @@ namespace Gala { WindowStateSaver.on_shutdown (); + var shutdown_curtain = new Gala.ShutdownCurtain (ctx); + shutdown_curtain.animate (); + return Posix.EXIT_SUCCESS; } } diff --git a/src/Widgets/Curtains/ShutdownCurtain.vala b/src/Widgets/Curtains/ShutdownCurtain.vala new file mode 100644 index 000000000..80f85a7b6 --- /dev/null +++ b/src/Widgets/Curtains/ShutdownCurtain.vala @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2020, 2025, 2026 elementary, Inc. (https://elementary.io) + */ + +public class Gala.ShutdownCurtain : Clutter.Actor { + private unowned Clutter.Stage stage; + public uint animation_duration { get; set construct; default = 300; } + + public ShutdownCurtain (Meta.Context context) { + int screen_width, screen_height; + context.get_display ().get_size (out screen_width, out screen_height); + width = screen_width; + height = screen_height; + stage = (Clutter.Stage) context.get_display ().get_stage (); + } + + public void animate () { + var animation_thread = new Thread ("tv-effect-animation", start); + animation_thread.join (); + } + + private void start () { + int time = 0; + var tv_effect = new TVEffect (); + tv_effect.occlusion = 0; + tv_effect.height = height; + stage.add_effect_with_name ( + "tv-effect", + tv_effect + ); + + while (time < animation_duration) { + tv_effect.occlusion = (animation_duration - time) / (float) animation_duration; + + time += 16; + Thread.usleep (16000); + } + + stage.remove_effect_by_name ("tv-effect"); + Thread.usleep (2000); + } +} diff --git a/src/meson.build b/src/meson.build index 496ba7eca..bc8124866 100644 --- a/src/meson.build +++ b/src/meson.build @@ -65,10 +65,12 @@ gala_bin_sources = files( 'Widgets/PixelPicker.vala', 'Widgets/PointerLocator.vala', 'Widgets/SessionLocker.vala', + 'Widgets/Curtains/ShutdownCurtain.vala', 'Widgets/SelectionArea.vala', 'Widgets/WindowOverview.vala', 'Widgets/WindowSwitcher/WindowSwitcher.vala', 'Widgets/WindowSwitcher/WindowSwitcherIcon.vala', + 'Curtains/TVEffect.vala' ) gala_bin = executable(