From e9735d11292d74ddf3aa8ce4f033cdfc98d95352 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sun, 4 Oct 2026 23:12:50 +0200 Subject: [PATCH] feat(quickshell): "Take your time." glows and flickers A new GlowText component lights a line from inside: a soft two-layer halo that breathes slowly, and every 4-14 seconds a short, uneven stutter like a tube catching. Only opacity and the halo move, so the glyphs stay on the pixel grid. The lock screen and the power menu use it; the power menu's stops while it is closed. --- quickshell/README.md | 3 +- quickshell/components/GlowText.qml | 110 +++++++++++++++++++++++++++++ quickshell/lock/LockFace.qml | 13 ++-- quickshell/overlays/PowerMenu.qml | 14 ++-- 4 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 quickshell/components/GlowText.qml diff --git a/quickshell/README.md b/quickshell/README.md index 5357432..ba1fe23 100644 --- a/quickshell/README.md +++ b/quickshell/README.md @@ -252,7 +252,8 @@ session. It is the wallpaper, barely softened, with the face set in the dark of the porthole: the mark, the time in light display type, the date, "Take your -time.", a status log +time." glowing faintly with the odd flicker (`components/GlowText.qml`), a +status log (battery, network, what is playing, unread notices), and a rounded field that fills with dots as you type. The dots breathe while PAM checks; a wrong password turns them and the field's rim to rust and nudges the field once. diff --git a/quickshell/components/GlowText.qml b/quickshell/components/GlowText.qml new file mode 100644 index 0000000..a9bed15 --- /dev/null +++ b/quickshell/components/GlowText.qml @@ -0,0 +1,110 @@ +import QtQuick +import QtQuick.Effects +import "root:/config" + +// A line of type lit from inside, like a tube in a dark console: a soft halo +// that breathes slowly, and every so often a short, uneven stutter where the +// light catches — two or three dips, then steady again for a while. The +// stutters come at random intervals so it never settles into a rhythm you +// could count. +// +// Only the halo and the opacity move, never the geometry, so the glyphs stay +// on the pixel grid. Set `active` false while the text is off screen to stop +// the animation. +Item { + id: root + + property string text: "" + property color color: Theme.accent + property color glowColor: root.color + property string family: Theme.fontDisplay + property int pixelSize: Theme.fsLarge + property int weight: Font.Light + property real letterSpacing: 0 + property bool active: true + + // 0..1, how lit the tube is at this instant. + property real lit: 1 + // Slow breathing on the halo alone, between `low` and 1. + property real breath: 1 + + implicitWidth: label.implicitWidth + implicitHeight: label.implicitHeight + + Text { + id: label + visible: false + text: root.text + color: root.color + font.family: root.family + font.pixelSize: root.pixelSize + font.weight: root.weight + font.letterSpacing: root.letterSpacing + // Greyscale antialiasing: the glyphs are drawn into the effect's + // texture, and native subpixel rendering there leaves colour fringes. + renderType: Text.CurveRendering + } + + // A wide, faint halo under a tight, brighter one, so the light both + // spreads into the dark and hugs the strokes. + MultiEffect { + source: label + anchors.fill: label + autoPaddingEnabled: true + opacity: root.lit + + shadowEnabled: true + shadowColor: root.glowColor + shadowHorizontalOffset: 0 + shadowVerticalOffset: 0 + shadowBlur: 1.0 + blurMax: Math.max(32, Math.round(root.pixelSize * 2)) + shadowOpacity: 0.7 * root.breath + shadowScale: 1.02 + } + + MultiEffect { + source: label + anchors.fill: label + autoPaddingEnabled: true + opacity: 0.55 + 0.45 * root.lit + + shadowEnabled: true + shadowColor: root.glowColor + shadowHorizontalOffset: 0 + shadowVerticalOffset: 0 + shadowBlur: 1.0 + blurMax: Math.max(8, Math.round(root.pixelSize * 0.35)) + shadowOpacity: 0.9 * root.lit + } + + SequentialAnimation on breath { + running: root.active + loops: Animation.Infinite + NumberAnimation { to: 0.6; duration: 2600; easing.type: Easing.InOutSine } + NumberAnimation { to: 1.0; duration: 2600; easing.type: Easing.InOutSine } + } + + // One stutter: uneven dips of uneven length, then back to full. + SequentialAnimation { + id: stutter + NumberAnimation { target: root; property: "lit"; to: 0.25; duration: 40 } + NumberAnimation { target: root; property: "lit"; to: 0.9; duration: 60 } + PauseAnimation { duration: 70 } + NumberAnimation { target: root; property: "lit"; to: 0.4; duration: 30 } + NumberAnimation { target: root; property: "lit"; to: 1.0; duration: 180; easing.type: Easing.OutCubic } + } + + Timer { + id: nextStutter + running: root.active + repeat: true + interval: 6000 + onTriggered: { + stutter.restart(); + nextStutter.interval = 4000 + Math.random() * 10000; + } + } + + onActiveChanged: if (!root.active) { stutter.stop(); root.lit = 1; } +} diff --git a/quickshell/lock/LockFace.qml b/quickshell/lock/LockFace.qml index a41f80b..9cacb63 100644 --- a/quickshell/lock/LockFace.qml +++ b/quickshell/lock/LockFace.qml @@ -132,15 +132,14 @@ Item { } // The one line the face says to you. Nothing here is in a hurry. - Text { - topPadding: Math.round(Theme.padL * surface.uiScale) - leftPadding: Math.round(4 * surface.uiScale) + Item { width: 1; height: Math.round(Theme.padL * surface.uiScale) } + + GlowText { + x: Math.round(4 * surface.uiScale) text: "Take your time." color: Theme.subtext - font.family: Theme.fontDisplay - font.pixelSize: Math.round(Theme.fsLarge * surface.uiScale) - font.weight: Font.Light - renderType: Text.NativeRendering + glowColor: Theme.accent + pixelSize: Math.round(Theme.fsLarge * surface.uiScale) } Item { width: 1; height: Math.round(Theme.padXL * surface.uiScale) } diff --git a/quickshell/overlays/PowerMenu.qml b/quickshell/overlays/PowerMenu.qml index 3e44750..61dc0a0 100644 --- a/quickshell/overlays/PowerMenu.qml +++ b/quickshell/overlays/PowerMenu.qml @@ -98,15 +98,13 @@ PanelWindow { size: 26 } - Text { - topPadding: Theme.padM + Item { width: 1; height: Theme.padM } + + GlowText { text: "Take your time." - color: Theme.accent - font.family: Theme.fontDisplay - font.pixelSize: Theme.fsHuge - font.weight: Font.Light - font.letterSpacing: 1 - renderType: Text.NativeRendering + pixelSize: Theme.fsHuge + letterSpacing: 1 + active: root.open } Text {