diff --git a/quickshell/Shaders/frag/ripple.frag b/quickshell/Shaders/frag/ripple.frag new file mode 100644 index 00000000..d19db814 --- /dev/null +++ b/quickshell/Shaders/frag/ripple.frag @@ -0,0 +1,56 @@ +#version 450 + +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 widthPx; + float heightPx; + float cornerRadiusPx; + float rippleCenterX; + float rippleCenterY; + float rippleRadius; + float rippleOpacity; + float offsetX; + float offsetY; + float parentWidth; + float parentHeight; + vec4 rippleCol; +} ubuf; + +float sdRoundRect(vec2 p, vec2 b, float r) { + vec2 q = abs(p) - (b - vec2(r)); + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r; +} + +void main() { + vec2 px = qt_TexCoord0 * vec2(ubuf.widthPx, ubuf.heightPx) + vec2(ubuf.offsetX, ubuf.offsetY); + + float aa = fwidth(length(px)) * 1.5; + + float circleD = length(px - vec2(ubuf.rippleCenterX, ubuf.rippleCenterY)) - ubuf.rippleRadius; + float circleMask = 1.0 - smoothstep(-aa, aa, circleD); + if (circleMask <= 0.0) { + fragColor = vec4(0.0); + return; + } + + float rrMask = 1.0; + if (ubuf.cornerRadiusPx > 0.0) { + vec2 halfSize = vec2(ubuf.parentWidth, ubuf.parentHeight) * 0.5; + float r = min(ubuf.cornerRadiusPx, min(halfSize.x, halfSize.y)); + float rrD = sdRoundRect(px - halfSize, halfSize, r); + rrMask = 1.0 - smoothstep(-aa, aa, rrD); + } + + float mask = circleMask * rrMask; + if (mask <= 0.0) { + fragColor = vec4(0.0); + return; + } + + float a = ubuf.rippleCol.a * ubuf.rippleOpacity * mask * ubuf.qt_Opacity; + fragColor = vec4(ubuf.rippleCol.rgb * a, a); +} diff --git a/quickshell/Shaders/qsb/ripple.frag.qsb b/quickshell/Shaders/qsb/ripple.frag.qsb new file mode 100644 index 00000000..9dd21dc4 Binary files /dev/null and b/quickshell/Shaders/qsb/ripple.frag.qsb differ diff --git a/quickshell/Widgets/DankRipple.qml b/quickshell/Widgets/DankRipple.qml index c3e11491..94501d15 100644 --- a/quickshell/Widgets/DankRipple.qml +++ b/quickshell/Widgets/DankRipple.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Effects import qs.Common Item { @@ -11,7 +10,7 @@ Item { property real _rippleX: 0 property real _rippleY: 0 - property real _rippleSize: 0 + property real _rippleMaxRadius: 0 readonly property alias animating: rippleAnim.running anchors.fill: parent @@ -24,7 +23,7 @@ Item { _rippleY = y; const dist = (ox, oy) => ox * ox + oy * oy; - _rippleSize = Math.sqrt(Math.max(dist(x, y), dist(x, height - y), dist(width - x, y), dist(width - x, height - y))) * 2; + _rippleMaxRadius = Math.sqrt(Math.max(dist(x, y), dist(x, height - y), dist(width - x, y), dist(width - x, height - y))); rippleAnim.restart(); } @@ -33,45 +32,32 @@ Item { id: rippleAnim PropertyAction { - target: ripple - property: "x" + target: rippleFx + property: "rippleCenterX" value: root._rippleX } PropertyAction { - target: ripple - property: "y" + target: rippleFx + property: "rippleCenterY" value: root._rippleY } PropertyAction { - target: ripple - property: "implicitWidth" + target: rippleFx + property: "rippleRadius" value: 0 } PropertyAction { - target: ripple - property: "implicitHeight" - value: 0 - } - PropertyAction { - target: ripple - property: "opacity" + target: rippleFx + property: "rippleOpacity" value: 0.10 } ParallelAnimation { DankAnim { - target: ripple - property: "implicitWidth" + target: rippleFx + property: "rippleRadius" from: 0 - to: root._rippleSize - duration: Theme.expressiveDurations.expressiveDefaultSpatial - easing.bezierCurve: Theme.expressiveCurves.standardDecel - } - DankAnim { - target: ripple - property: "implicitHeight" - from: 0 - to: root._rippleSize + to: root._rippleMaxRadius duration: Theme.expressiveDurations.expressiveDefaultSpatial easing.bezierCurve: Theme.expressiveCurves.standardDecel } @@ -80,8 +66,8 @@ Item { duration: Math.round(Theme.expressiveDurations.expressiveDefaultSpatial * 0.6) } DankAnim { - target: ripple - property: "opacity" + target: rippleFx + property: "rippleOpacity" to: 0 duration: Theme.expressiveDurations.expressiveDefaultSpatial easing.bezierCurve: Theme.expressiveCurves.standard @@ -90,27 +76,29 @@ Item { } } - // VRAM optimization: Use clip instead of layer+MultiEffect for rounded corners - // The layer.enabled approach creates an offscreen texture for every clickable element - // with rounded corners, which adds 200-400MB VRAM across the UI. - // Using clip: true is GPU-native and much cheaper. - Rectangle { - anchors.fill: parent - radius: root.cornerRadius - color: "transparent" - clip: root.cornerRadius > 0 + ShaderEffect { + id: rippleFx + visible: rippleAnim.running - Rectangle { - id: ripple + property real rippleCenterX: 0 + property real rippleCenterY: 0 + property real rippleRadius: 0 + property real rippleOpacity: 0 - radius: Math.min(width, height) / 2 - color: root.rippleColor - opacity: 0 + x: Math.max(0, rippleCenterX - rippleRadius) + y: Math.max(0, rippleCenterY - rippleRadius) + width: Math.max(0, Math.min(root.width, rippleCenterX + rippleRadius) - x) + height: Math.max(0, Math.min(root.height, rippleCenterY + rippleRadius) - y) - transform: Translate { - x: -ripple.width / 2 - y: -ripple.height / 2 - } - } + property real widthPx: width + property real heightPx: height + property real cornerRadiusPx: root.cornerRadius + property real offsetX: x + property real offsetY: y + property real parentWidth: root.width + property real parentHeight: root.height + property vector4d rippleCol: Qt.vector4d(root.rippleColor.r, root.rippleColor.g, root.rippleColor.b, root.rippleColor.a) + + fragmentShader: Qt.resolvedUrl("../Shaders/qsb/ripple.frag.qsb") } }