aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/appearance
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/appearance')
-rw-r--r--desktop/modules/appearance/WallpaperSpread.qml116
-rw-r--r--desktop/modules/appearance/Wallpapers.qml5
-rw-r--r--desktop/modules/appearance/burn.frag60
-rw-r--r--desktop/modules/appearance/burn.frag.qsbbin0 -> 3983 bytes
4 files changed, 181 insertions, 0 deletions
diff --git a/desktop/modules/appearance/WallpaperSpread.qml b/desktop/modules/appearance/WallpaperSpread.qml
new file mode 100644
index 0000000..c133343
--- /dev/null
+++ b/desktop/modules/appearance/WallpaperSpread.qml
@@ -0,0 +1,116 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import Quickshell
+import Quickshell.Wayland
+import QtQuick
+
+// The transition when Apply sets a wallpaper. swaybg has none: wallp kills
+// the old one and starts a new one, which can show a frame of nothing. A
+// surface on the bottom layer, above swaybg and below every window, covers
+// the swap: the old picture burns away over the new one, and the
+// surface goes once wallp has finished, by when the new swaybg is drawing
+// the same picture underneath. Sets from a terminal get no transition: by
+// the time wall_h changes, swaybg has already swapped.
+Scope {
+ // ponytail: wallp's default outputs, like Wallpapers.panels; read
+ // wallp.conf if they are ever changed there.
+ Variants {
+ model: [{ which: "H", output: "DP-1" }, { which: "V", output: "DP-3" }]
+
+ PanelWindow {
+ id: win
+ required property var modelData
+ screen: Quickshell.screens.find(s => s.name === modelData.output) ?? null
+
+ property bool running: false
+ property bool done: false
+ property real progress: 0
+ property point origin: Qt.point(0.5, 0.5)
+ visible: running
+
+ anchors { top: true; bottom: true; left: true; right: true }
+ color: "transparent"
+ exclusionMode: ExclusionMode.Ignore
+ mask: Region {}
+ WlrLayershell.layer: WlrLayer.Bottom
+ WlrLayershell.namespace: "keystone-wallpaper-spread"
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
+
+ Connections {
+ target: Wallpapers
+ function onSwapping(which, from, to) {
+ if (which !== win.modelData.which || !win.screen) return;
+ anim.stop();
+ // Behind the char band, so no pixel starts out scorched.
+ win.progress = -0.05;
+ // A different place each time the fire starts.
+ win.origin = Qt.point(Math.random(), Math.random());
+ win.done = false;
+ oldImg.source = from;
+ newImg.source = to;
+ win.running = true;
+ }
+ // A failed set may have left either picture underneath, so
+ // the cover goes at once rather than claiming the new one.
+ function onApplied(ok) {
+ win.done = true;
+ if (!ok) win.running = false;
+ else if (!anim.running) win.running = false;
+ }
+ }
+
+ // Both decoded before the first block drops, or the old one
+ // would pop in late over a half-drawn screen.
+ readonly property bool ready: running && oldImg.status === Image.Ready
+ && newImg.status === Image.Ready
+ onReadyChanged: if (ready) anim.restart()
+
+ // -m fill is a centred crop, which is what PreserveAspectCrop does.
+ Image {
+ id: newImg
+ anchors.fill: parent
+ fillMode: Image.PreserveAspectCrop
+ sourceSize: Qt.size(win.width, win.height)
+ asynchronous: true
+ cache: false
+ }
+
+ Image {
+ id: oldImg
+ anchors.fill: parent
+ fillMode: Image.PreserveAspectCrop
+ sourceSize: Qt.size(win.width, win.height)
+ asynchronous: true
+ cache: false
+ layer.enabled: win.running
+ layer.effect: ShaderEffect {
+ property real progress: win.progress
+ property real aspect: win.width / Math.max(1, win.height)
+ property point origin: win.origin
+ fragmentShader: Qt.resolvedUrl("burn.frag.qsb")
+ }
+ }
+
+ NumberAnimation {
+ id: anim
+ target: win
+ property: "progress"
+ // Past 1: fbm can push a threshold a little over it. Linear,
+ // since an ease spends its slow ends where nothing burns.
+ to: 1.05
+ duration: 3500
+ easing.type: Easing.Linear
+ onFinished: if (win.done) win.running = false
+ }
+ }
+ }
+}
diff --git a/desktop/modules/appearance/Wallpapers.qml b/desktop/modules/appearance/Wallpapers.qml
index 7d671e1..9e9d4ad 100644
--- a/desktop/modules/appearance/Wallpapers.qml
+++ b/desktop/modules/appearance/Wallpapers.qml
@@ -57,6 +57,9 @@ Singleton {
property bool scanning: true
property bool applying: false
signal applied(bool ok, string message)
+ // Sent before wallp runs, while current still names the old picture, so
+ // WallpaperSpread can cover the swap with it.
+ signal swapping(string which, string from, string to)
// Sorted by most recent first: the wallpaper wanted now is usually one
// added recently. -maxdepth keeps a stray git checkout from being walked.
@@ -225,6 +228,8 @@ Singleton {
function apply() {
if (applying || !dirty) return;
applying = true;
+ if (pendingH !== "" && currentH !== "") swapping("H", currentH, pendingH);
+ if (pendingV !== "" && currentV !== "") swapping("V", currentV, pendingV);
const args = [];
if (pendingH !== "") args.push(`H=${JSON.stringify(pendingH)}`);
if (pendingV !== "") args.push(`V=${JSON.stringify(pendingV)}`);
diff --git a/desktop/modules/appearance/burn.frag b/desktop/modules/appearance/burn.frag
new file mode 100644
index 0000000..a5cb9c3
--- /dev/null
+++ b/desktop/modules/appearance/burn.frag
@@ -0,0 +1,60 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// The old wallpaper burning away like a sheet of paper. A pixel goes once
+// progress passes its threshold: its distance from where the fire started
+// mixed with fractal noise, so the front is ragged at every scale. Just ahead
+// of the front the paper glows and chars. Compiled with:
+// qsb --qt6 -o burn.frag.qsb burn.frag
+#version 440
+layout(location = 0) in vec2 qt_TexCoord0;
+layout(location = 0) out vec4 fragColor;
+layout(std140, binding = 0) uniform buf {
+ mat4 qt_Matrix;
+ float qt_Opacity;
+ float progress;
+ float aspect;
+ vec2 origin;
+};
+layout(binding = 1) uniform sampler2D source;
+
+float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
+
+float noise(vec2 p) {
+ vec2 i = floor(p), f = fract(p);
+ vec2 u = f * f * (3.0 - 2.0 * f);
+ return mix(mix(hash(i), hash(i + vec2(1, 0)), u.x),
+ mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), u.x), u.y);
+}
+
+float fbm(vec2 p) {
+ float v = 0.0, a = 0.5;
+ for (int i = 0; i < 5; i++) { v += a * noise(p); p *= 2.03; a *= 0.5; }
+ return v;
+}
+
+void main() {
+ vec2 asp = vec2(aspect, 1.0);
+ float d = length((qt_TexCoord0 - origin) * asp) / length(max(origin, 1.0 - origin) * asp);
+ float t = mix(d, fbm(qt_TexCoord0 * asp * 5.0), 0.5);
+ float e = t - progress;
+ if (e <= 0.0) { fragColor = vec4(0.0); return; }
+
+ // Front to intact paper: flame, then char, then the picture.
+ const float w = 0.05;
+ float k = clamp(e / w, 0.0, 1.0);
+ vec3 fire = vec3(1.6, 0.75, 0.2);
+ vec3 ash = vec3(0.07, 0.04, 0.02);
+ vec3 paper = texture(source, qt_TexCoord0).rgb;
+ vec3 col = k < 0.3 ? mix(fire, ash, k / 0.3) : mix(ash, paper, smoothstep(0.3, 1.0, k));
+ float a = smoothstep(0.0, 0.004, e);
+ fragColor = vec4(col * a, a) * qt_Opacity;
+}
diff --git a/desktop/modules/appearance/burn.frag.qsb b/desktop/modules/appearance/burn.frag.qsb
new file mode 100644
index 0000000..599a911
--- /dev/null
+++ b/desktop/modules/appearance/burn.frag.qsb
Binary files differ