Files
dotfiles/quickshell/components/GlowText.qml
T
nvrl e9735d1129 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.
2026-10-04 23:12:50 +02:00

111 lines
3.6 KiB
QML

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; }
}