diff --git a/Modules/Panels/ControlCenter/Cards/WeatherCard.qml b/Modules/Panels/ControlCenter/Cards/WeatherCard.qml index 37019035..29a5d9fa 100644 --- a/Modules/Panels/ControlCenter/Cards/WeatherCard.qml +++ b/Modules/Panels/ControlCenter/Cards/WeatherCard.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Layouts +import QtQuick.Effects import Quickshell import qs.Commons import qs.Services.Location @@ -13,14 +14,54 @@ NBox { property bool showLocation: true readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null) + // Test mode: set to "rain" or "snow" + property string testWeatherEffect: "" + + // Weather condition detection + readonly property int currentWeatherCode: weatherReady ? LocationService.data.weather.current_weather.weathercode : 0 + readonly property bool isRaining: testWeatherEffect === "rain" || (testWeatherEffect === "" && currentWeatherCode >= 51 && currentWeatherCode <= 67) + readonly property bool isSnowing: testWeatherEffect === "snow" || (testWeatherEffect === "" && ((currentWeatherCode >= 71 && currentWeatherCode <= 77) || (currentWeatherCode >= 85 && currentWeatherCode <= 86))) + + // Animated time for shaders + property real shaderTime: 0 + NumberAnimation on shaderTime { + running: root.isRaining || root.isSnowing + loops: Animation.Infinite + from: 0 + to: 1000 + duration: 100000 + } + visible: Settings.data.location.weatherEnabled implicitHeight: Math.max(100 * Style.uiScaleRatio, content.implicitHeight + (Style.marginXL * 2)) + // Weather effect layer (rain/snow) + ShaderEffect { + id: weatherEffect + anchors.fill: parent + // Snow fills the box, rain matches content margins + anchors.margins: root.isSnowing ? root.border.width : Style.marginXL + visible: root.isRaining || root.isSnowing + + property var source: ShaderEffectSource { + sourceItem: content + hideSource: root.isRaining // Only hide for rain (distortion), show for snow + } + + property real time: root.shaderTime + property real itemWidth: weatherEffect.width + property real itemHeight: weatherEffect.height + property color bgColor: root.color + property real cornerRadius: root.isSnowing ? (root.radius - root.border.width) : 0 + + fragmentShader: root.isSnowing ? + Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/weather_snow.frag.qsb") : + Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/weather_rain.frag.qsb") + } + ColumnLayout { id: content - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top + anchors.fill: parent anchors.margins: Style.marginXL spacing: Style.marginM clip: true diff --git a/Shaders/frag/weather_rain.frag b/Shaders/frag/weather_rain.frag new file mode 100644 index 00000000..bd3bc391 --- /dev/null +++ b/Shaders/frag/weather_rain.frag @@ -0,0 +1,84 @@ +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float time; + float itemWidth; + float itemHeight; + vec4 bgColor; + float cornerRadius; +} ubuf; + +// Signed distance function for rounded rectangle +float roundedBoxSDF(vec2 center, vec2 size, float radius) { + vec2 q = abs(center) - size + radius; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius; +} + +vec3 hash3(vec2 p) { + vec3 q = vec3(dot(p, vec2(127.1, 311.7)), + dot(p, vec2(269.5, 183.3)), + dot(p, vec2(419.2, 371.9))); + return fract(sin(q) * 43758.5453); +} + +float noise(vec2 x, float iTime) { + vec2 p = floor(x); + vec2 f = fract(x); + + float va = 0.0; + for (int j = -2; j <= 2; j++) { + for (int i = -2; i <= 2; i++) { + vec2 g = vec2(float(i), float(j)); + vec3 o = hash3(p + g); + vec2 r = g - f + o.xy; + float d = sqrt(dot(r, r)); + float ripple = max(mix(smoothstep(0.99, 0.999, max(cos(d - iTime * 2.0 + (o.x + o.y) * 5.0), 0.0)), 0.0, d), 0.0); + va += ripple; + } + } + + return va; +} + +void main() { + vec2 uv = qt_TexCoord0; + float iTime = ubuf.time * 0.07; + + // Aspect ratio correction for circular ripples + float aspect = ubuf.itemWidth / ubuf.itemHeight; + vec2 uvAspect = vec2(uv.x * aspect, uv.y); + + float f = noise(6.0 * uvAspect, iTime) * smoothstep(0.0, 0.2, sin(uv.x * 3.141592) * sin(uv.y * 3.141592)); + + // Calculate normal from noise for distortion + float normalScale = 0.5; + vec2 e = normalScale / vec2(ubuf.itemWidth, ubuf.itemHeight); + vec2 eAspect = vec2(e.x * aspect, e.y); + float cx = noise(6.0 * (uvAspect + eAspect), iTime) * smoothstep(0.0, 0.2, sin((uv.x + e.x) * 3.141592) * sin(uv.y * 3.141592)); + float cy = noise(6.0 * (uvAspect + eAspect.yx), iTime) * smoothstep(0.0, 0.2, sin(uv.x * 3.141592) * sin((uv.y + e.y) * 3.141592)); + vec2 n = vec2(cx - f, cy - f); + + // Scale distortion back to texture space (undo aspect correction for X) + vec2 distortion = vec2(n.x / aspect, n.y); + + // Sample source with distortion + vec4 col = texture(source, uv + distortion); + + // Apply rounded corner mask + vec2 pixelPos = qt_TexCoord0 * vec2(ubuf.itemWidth, ubuf.itemHeight); + vec2 center = pixelPos - vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + vec2 halfSize = vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + float dist = roundedBoxSDF(center, halfSize, ubuf.cornerRadius); + float cornerMask = 1.0 - smoothstep(-1.0, 0.0, dist); + + // Output with premultiplied alpha + float finalAlpha = col.a * ubuf.qt_Opacity * cornerMask; + fragColor = vec4(col.rgb * finalAlpha, finalAlpha); +} diff --git a/Shaders/frag/weather_snow.frag b/Shaders/frag/weather_snow.frag new file mode 100644 index 00000000..e00a9529 --- /dev/null +++ b/Shaders/frag/weather_snow.frag @@ -0,0 +1,75 @@ +#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 time; + float itemWidth; + float itemHeight; + vec4 bgColor; + float cornerRadius; +} ubuf; + +// Signed distance function for rounded rectangle +float roundedBoxSDF(vec2 center, vec2 size, float radius) { + vec2 q = abs(center) - size + radius; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius; +} + +void main() { + // Aspect ratio correction + float aspect = ubuf.itemWidth / ubuf.itemHeight; + vec2 uv = qt_TexCoord0; + uv.x *= aspect; + uv.y = 1.0 - uv.y; + + float iTime = ubuf.time * 0.15; + + float snow = 0.0; + + for (int k = 0; k < 6; k++) { + for (int i = 0; i < 12; i++) { + float cellSize = 2.0 + (float(i) * 3.0); + float downSpeed = 0.3 + (sin(iTime * 0.4 + float(k + i * 20)) + 1.0) * 0.00008; + + vec2 uvAnim = uv + vec2( + 0.01 * sin((iTime + float(k * 6185)) * 0.6 + float(i)) * (5.0 / float(i + 1)), + downSpeed * (iTime + float(k * 1352)) * (1.0 / float(i + 1)) + ); + + vec2 uvStep = (ceil((uvAnim) * cellSize - vec2(0.5, 0.5)) / cellSize); + float x = fract(sin(dot(uvStep.xy, vec2(12.9898 + float(k) * 12.0, 78.233 + float(k) * 315.156))) * 43758.5453 + float(k) * 12.0) - 0.5; + float y = fract(sin(dot(uvStep.xy, vec2(62.2364 + float(k) * 23.0, 94.674 + float(k) * 95.0))) * 62159.8432 + float(k) * 12.0) - 0.5; + + float randomMagnitude1 = sin(iTime * 2.5) * 0.7 / cellSize; + float randomMagnitude2 = cos(iTime * 1.65) * 0.7 / cellSize; + + float d = 5.0 * distance((uvStep.xy + vec2(x * sin(y), y) * randomMagnitude1 + vec2(y, x) * randomMagnitude2), uvAnim.xy); + + float omiVal = fract(sin(dot(uvStep.xy, vec2(32.4691, 94.615))) * 31572.1684); + if (omiVal < 0.03) { + float newd = (x + 1.0) * 0.4 * clamp(1.9 - d * (15.0 + (x * 6.3)) * (cellSize / 1.4), 0.0, 1.0); + snow += newd; + } + } + } + + // Blend white snow over background color + float snowAlpha = clamp(snow * 2.0, 0.0, 1.0); + vec3 snowColor = vec3(1.0); + vec3 blended = mix(ubuf.bgColor.rgb, snowColor, snowAlpha); + + // Apply rounded corner mask + vec2 pixelPos = qt_TexCoord0 * vec2(ubuf.itemWidth, ubuf.itemHeight); + vec2 center = pixelPos - vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + vec2 halfSize = vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + float dist = roundedBoxSDF(center, halfSize, ubuf.cornerRadius); + float cornerMask = 1.0 - smoothstep(-1.0, 0.0, dist); + + // Output with premultiplied alpha + float finalAlpha = ubuf.qt_Opacity * cornerMask; + fragColor = vec4(blended * finalAlpha, finalAlpha); +} diff --git a/Shaders/qsb/circled_image.frag.qsb b/Shaders/qsb/circled_image.frag.qsb index 37a99ef0..4faa90f9 100644 Binary files a/Shaders/qsb/circled_image.frag.qsb and b/Shaders/qsb/circled_image.frag.qsb differ diff --git a/Shaders/qsb/rounded_image.frag.qsb b/Shaders/qsb/rounded_image.frag.qsb index c404fc76..666f6434 100644 Binary files a/Shaders/qsb/rounded_image.frag.qsb and b/Shaders/qsb/rounded_image.frag.qsb differ diff --git a/Shaders/qsb/weather_rain.frag.qsb b/Shaders/qsb/weather_rain.frag.qsb new file mode 100644 index 00000000..ffdcff54 Binary files /dev/null and b/Shaders/qsb/weather_rain.frag.qsb differ diff --git a/Shaders/qsb/weather_snow.frag.qsb b/Shaders/qsb/weather_snow.frag.qsb new file mode 100644 index 00000000..b971260f Binary files /dev/null and b/Shaders/qsb/weather_snow.frag.qsb differ diff --git a/Shaders/qsb/wp_disc.frag.qsb b/Shaders/qsb/wp_disc.frag.qsb index 5dddf017..f246c341 100644 Binary files a/Shaders/qsb/wp_disc.frag.qsb and b/Shaders/qsb/wp_disc.frag.qsb differ diff --git a/Shaders/qsb/wp_fade.frag.qsb b/Shaders/qsb/wp_fade.frag.qsb index 3081ea8e..f42e5019 100644 Binary files a/Shaders/qsb/wp_fade.frag.qsb and b/Shaders/qsb/wp_fade.frag.qsb differ diff --git a/Shaders/qsb/wp_stripes.frag.qsb b/Shaders/qsb/wp_stripes.frag.qsb index db496eaa..9ca9cc9f 100644 Binary files a/Shaders/qsb/wp_stripes.frag.qsb and b/Shaders/qsb/wp_stripes.frag.qsb differ diff --git a/Shaders/qsb/wp_wipe.frag.qsb b/Shaders/qsb/wp_wipe.frag.qsb index a7dc6b14..1f70ae20 100644 Binary files a/Shaders/qsb/wp_wipe.frag.qsb and b/Shaders/qsb/wp_wipe.frag.qsb differ