From 44d836c97533d7933803e757abec8d45f9fc0c05 Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 17 Feb 2026 09:27:18 -0500 Subject: [PATCH] ripple: use a shader for ripple effect --- quickshell/Shaders/frag/ripple.frag | 56 +++++++++++++++++ quickshell/Shaders/qsb/ripple.frag.qsb | Bin 0 -> 3229 bytes quickshell/Widgets/DankRipple.qml | 84 +++++++++++-------------- 3 files changed, 92 insertions(+), 48 deletions(-) create mode 100644 quickshell/Shaders/frag/ripple.frag create mode 100644 quickshell/Shaders/qsb/ripple.frag.qsb 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 0000000000000000000000000000000000000000..9dd21dc4b026b50e437a6c5b5f24fd0de582c9c5 GIT binary patch literal 3229 zcmV;O3}W*D06Cy|ob6ltcN@hOo;Zn}I3c`4%BxEZsRWK92Pc6z2?0AKPzM{w5T_=rIgYKrIfUQ_ zGiezf=p?#6(#Rp|8vwpqwpTaWJwnZd ziS^-AV=I8|H?S^XBL=n-*dYU31#Hy7Rs$P1ux??%D~nDd%(cf0(;QFdV%E) zY#p#!16vQQWMCHnt72YnrqAmd^QzjLjkagZ`=vB9U6Q|2&Idnlq$eJqMfVlN@4gkqa>BiXCV7xaGkFn3d?&Z|J1~vrxuQB}6?d!0fY^NX3Y-?KR zunptlwg;@Aq#1t*YiHitMj3Og+nngu8SQzY;#T``N-c3?46h3n!+Ons5Sx=Xgo-2G ze%n~{Utqi|F_vFqj@jp(G&5bInQWQ!XF^tEoJwaio_OBxjI~E&?de#%WVBgd80$YB zYd;y|KOJj76XQP{Yrhn0zlQc+?9+@;d*XH)CCA$?p<;9gI6POk0N0$|;M{6(hJcR~ zhxP7(9OF*_+Y5Vp3|+TD*FHnne&CvO2ROGIoMGVF-U#Fve+<|G*c_4iSIV_JV2u4B z)^j~+WSxf)CwHLl53qF?`gk1N{yea|q4!Q;TJK@#)&7sc|9cGo$AN3k1UUB^oFl;X z7$+ge_!(gL!QP}Xj{A*q+-KOBf{mkwjbp&iU~FH2{4tDe6qx287b@l-LHmoqbia*$ z-F{H0J+YpmmD>({8-T10j0RIT~tH8AVz_8DKb94u=KWFH!L3d>6 zJ^@^F>fp>9oCV-ICQd?*@#lbj3HT|DD+H$XemP-Wj{s*5=ZQy!%1zfAoX3D`&g0-* zXK>B{Ur(NJ`zu)Q8z6ti=zGHGdjwqe_cZ$Se)_6V`$@N-f?d4_z6Q=ykbfGO=6@Z0 z?t2Dp?eB9!8l_ ztu5CYzXtX$=8V_5%dq*LP-imlUFQD=GVd9j_rbXre*G3aVPGEsTVY_o1IG5oQI~xP z`47omW!eG5&X2H*Vh|HHuaEF0S8qtWZ(V{c+z3Hwe9oZw+L#y?t&ya(&r6 zIBJg_IAVv9JK;p$2{*KMTY)=Q^8&ZG8n`)6@@Ku`Y}G21Do!-i275k% zy!Gx3dYGG?n;8m+7ci8nKH<;?wajd;V>1xLle5@1-p&L!LLoI5nPC&Mf@_5?e zLvl#o2;_utBy>)-k3b|eJ@)k#Cs>OD-PR3K_ zFsvJuY94L|VbCuJpV1K@EjCG52avD?X>+n2hpZH&GChhaYq`-H$n0o|CojmA+!10o zXnH!N@|LKA!IprMBR1gna8f1?JK@7*WgwF!eps$lqS-KVtEsJy({J$&PcRIxV5JPf zJ$vQs$}TmTd=*Rfoq83iD#3og?9iq-+8WObX=9)&x}3l%lP|Cph#?@^%&$aE@ z<)df$=vh8`mXDt0qi6Z(IX_3wOUXyij^(50d>%cXpLFU}YLQY8uq8FHFT&g zUTv7H|5UxvDF~LN;8?0KX;vI|d&OZdT5;HmRU9hO#TCbEj`lpMzOl6BV2jdr(X~B( z)=8%&&+IiBnbwrAeM8PtK!Rzdp&}cKWFncC;Wf_xQ ztX#?Qi&oxqihd>JlgXopz|C8mw;nBfVd(kAt<|6sRicHe8*c9H?eqLxsg`%GJ!Lm? zN_H6KOWw>r0}s7&ejESF%DqjFn*i4cd)mk2n*W3Le~ zo4ndtj(|?*{q%H>KU3NC@#6`Sp1(-7F+E)isp>9Oa=s}p>lx24dA@7ewiD6H&D0{- zwo@smH18~gw(3l$6KacSHgKIhEs!5ZFuH{+jYNDV3Ey%SRR~UwPA@nLP8jXpje-Q+ z#)}Sg(#|z#onCtIM3-s7nhT+xQhIu6qG%pBjpt zt&)~VA&=Ac#W%e%Qi-Qsb*dzNnP%>iziL^u2Kw}` zTbd!A_|m1>{l8QW{X1X3{L68(lrLgBIJ4O6n7DiD^~`FSg7AJO_KrsX%PaLC9@`6! PwtkJpdGh)v-o)DmJ!56Z literal 0 HcmV?d00001 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") } }