diff options
Diffstat (limited to 'desktop/modules/appearance')
| -rw-r--r-- | desktop/modules/appearance/WallpaperSpread.qml | 116 | ||||
| -rw-r--r-- | desktop/modules/appearance/Wallpapers.qml | 5 | ||||
| -rw-r--r-- | desktop/modules/appearance/burn.frag | 60 | ||||
| -rw-r--r-- | desktop/modules/appearance/burn.frag.qsb | bin | 0 -> 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 Binary files differnew file mode 100644 index 0000000..599a911 --- /dev/null +++ b/desktop/modules/appearance/burn.frag.qsb |
