diff --git a/Modules/Bar/MediaMini.qml b/Modules/Bar/MediaMini.qml index a061031..be07e87 100644 --- a/Modules/Bar/MediaMini.qml +++ b/Modules/Bar/MediaMini.qml @@ -109,14 +109,14 @@ Row { visible: Settings.data.audio.showMiniplayerAlbumArt Rectangle { - width: 16 * scaling - height: 16 * scaling + width: 18 * scaling + height: 18 * scaling radius: width * 0.5 color: Color.transparent antialiasing: true clip: true - NImageRounded { + NImageCircled { id: trackArt visible: MediaService.trackArtUrl.toString() !== "" anchors.fill: parent @@ -126,8 +126,6 @@ Row { fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow" borderWidth: 0 border.color: Color.transparent - imageRadius: width - antialiasing: true } // Fallback icon when no album art available diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index b980fa3..5f41bd7 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -465,13 +465,12 @@ Loader { } } - NImageRounded { + NImageCircled { anchors.centerIn: parent width: 100 * scaling height: 100 * scaling imagePath: Settings.data.general.avatarImage fallbackIcon: "person" - imageRadius: width * 0.5 } // Hover animation diff --git a/Modules/SettingsPanel/Tabs/AboutTab.qml b/Modules/SettingsPanel/Tabs/AboutTab.qml index a7b7b3d..b76c3d9 100644 --- a/Modules/SettingsPanel/Tabs/AboutTab.qml +++ b/Modules/SettingsPanel/Tabs/AboutTab.qml @@ -211,14 +211,13 @@ ColumnLayout { Layout.preferredWidth: Style.baseWidgetSize * 2 * scaling Layout.preferredHeight: Style.baseWidgetSize * 2 * scaling - NImageRounded { + NImageCircled { imagePath: modelData.avatar_url || "" anchors.fill: parent anchors.margins: Style.marginXS * scaling fallbackIcon: "person" borderColor: Color.mPrimary - borderWidth: Math.max(1, Style.borderL * scaling) - imageRadius: width * 0.5 + borderWidth: Math.max(1, Style.borderM * scaling) } } diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index 19da93f..f05cfa3 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -44,13 +44,13 @@ ColumnLayout { spacing: Style.marginL * scaling // Avatar preview - NImageRounded { + NImageCircled { width: 64 * scaling height: 64 * scaling imagePath: Settings.data.general.avatarImage fallbackIcon: "person" borderColor: Color.mPrimary - borderWidth: Math.max(1, Style.borderM) + borderWidth: Math.max(1, Style.borderM * scaling) } NTextInput { diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 7809a5f..aad0839 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -164,7 +164,7 @@ NBox { border.width: Math.max(1, Style.borderS * scaling) clip: true - NImageRounded { + NImageCircled { id: trackArt visible: MediaService.trackArtUrl.toString() !== "" @@ -174,7 +174,6 @@ NBox { fallbackIcon: "music_note" borderColor: Color.mOutline borderWidth: Math.max(1, Style.borderS * scaling) - imageRadius: width * 0.5 } // Fallback icon when no album art available diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index b9b7df9..7374159 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -28,7 +28,7 @@ NBox { anchors.margins: Style.marginM * scaling spacing: Style.marginM * scaling - NImageRounded { + NImageCircled { width: Style.baseWidgetSize * 1.25 * scaling height: Style.baseWidgetSize * 1.25 * scaling imagePath: Settings.data.general.avatarImage diff --git a/Shaders/frag/circled_image.frag b/Shaders/frag/circled_image.frag new file mode 100644 index 0000000..308a9c5 --- /dev/null +++ b/Shaders/frag/circled_image.frag @@ -0,0 +1,30 @@ +#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 imageOpacity; +} ubuf; + +void main() { + // Center coordinates around (0, 0) + vec2 uv = qt_TexCoord0 - 0.5; + + // Calculate distance from center + float distance = length(uv); + + // Create circular mask - anything beyond radius 0.5 is transparent + float mask = 1.0 - smoothstep(0.48, 0.52, distance); + + // Sample the texture + vec4 color = texture(source, qt_TexCoord0); + + // Apply the circular mask and opacity + float finalAlpha = color.a * mask * ubuf.imageOpacity * ubuf.qt_Opacity; + fragColor = vec4(color.rgb * finalAlpha, finalAlpha); +} \ No newline at end of file diff --git a/Shaders/frag/rounded_image.frag b/Shaders/frag/rounded_image.frag new file mode 100644 index 0000000..9d493b2 --- /dev/null +++ b/Shaders/frag/rounded_image.frag @@ -0,0 +1,56 @@ +#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; + // Custom properties with non-conflicting names + float itemWidth; + float itemHeight; + float cornerRadius; + float imageOpacity; +} ubuf; + +// Function to calculate the signed distance from a point to a rounded box +float roundedBoxSDF(vec2 centerPos, vec2 boxSize, float radius) { + vec2 d = abs(centerPos) - boxSize + radius; + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - radius; +} + +void main() { + // Get size from uniforms + vec2 itemSize = vec2(ubuf.itemWidth, ubuf.itemHeight); + float cornerRadius = ubuf.cornerRadius; + float itemOpacity = ubuf.imageOpacity; + + // Normalize coordinates to [-0.5, 0.5] range + vec2 uv = qt_TexCoord0 - 0.5; + + // Scale by aspect ratio to maintain uniform rounding + vec2 aspectRatio = itemSize / max(itemSize.x, itemSize.y); + uv *= aspectRatio; + + // Calculate half size in normalized space + vec2 halfSize = 0.5 * aspectRatio; + + // Normalize the corner radius + float normalizedRadius = cornerRadius / max(itemSize.x, itemSize.y); + + // Calculate distance to rounded rectangle + float distance = roundedBoxSDF(uv, halfSize, normalizedRadius); + + // Create smooth alpha mask + float smoothedAlpha = 1.0 - smoothstep(0.0, fwidth(distance), distance); + + // Sample the texture + vec4 color = texture(source, qt_TexCoord0); + + // Apply the rounded mask and opacity + // Make sure areas outside the rounded rect are completely transparent + float finalAlpha = color.a * smoothedAlpha * itemOpacity * ubuf.qt_Opacity; + fragColor = vec4(color.rgb * finalAlpha, finalAlpha); +} \ No newline at end of file diff --git a/Shaders/qsb/circled_image.frag.qsb b/Shaders/qsb/circled_image.frag.qsb new file mode 100644 index 0000000..37a99ef Binary files /dev/null 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 new file mode 100644 index 0000000..c404fc7 Binary files /dev/null and b/Shaders/qsb/rounded_image.frag.qsb differ diff --git a/Widgets/NImageCircled.qml b/Widgets/NImageCircled.qml new file mode 100644 index 0000000..7220a52 --- /dev/null +++ b/Widgets/NImageCircled.qml @@ -0,0 +1,73 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Widgets +import qs.Commons +import qs.Services + +Rectangle { + id: root + + property string imagePath: "" + property string fallbackIcon: "" + property color borderColor: Color.transparent + property real borderWidth: 0 + + color: Color.transparent + radius: parent.width * 0.5 + anchors.margins: Style.marginXXS * scaling + + Rectangle { + color: Color.transparent + anchors.fill: parent + + Image { + id: img + anchors.fill: parent + source: imagePath + visible: false // Hide since we're using it as shader source + mipmap: true + smooth: true + asynchronous: true + antialiasing: true + fillMode: Image.PreserveAspectCrop + } + + ShaderEffect { + anchors.fill: parent + + property var source: ShaderEffectSource { + sourceItem: img + hideSource: true + live: true + recursive: false + format: ShaderEffectSource.RGBA + } + + property real imageOpacity: root.opacity + fragmentShader: "file:Shaders/qsb/circled_image.frag.qsb" + supportsAtlasTextures: false + blending: true + } + + // Fallback icon + NIcon { + anchors.centerIn: parent + text: fallbackIcon + font.pointSize: Style.fontSizeXXL * scaling + visible: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "") + z: 0 + } + } + + //Border + Rectangle { + anchors.fill: parent + radius: parent.radius + color: Color.transparent + border.color: parent.borderColor + border.width: parent.borderWidth + antialiasing: true + z: 10 + } +} diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index c7fbde9..18f047f 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -23,13 +23,12 @@ Rectangle { Rectangle { color: Color.transparent anchors.fill: parent - anchors.margins: borderWidth Image { id: img anchors.fill: parent source: imagePath - visible: false + visible: false // Hide since we're using it as shader source mipmap: true smooth: true asynchronous: true @@ -37,24 +36,33 @@ Rectangle { fillMode: Image.PreserveAspectCrop } - MultiEffect { + ShaderEffect { anchors.fill: parent - source: img - maskEnabled: true - maskSource: mask - maskSpreadAtMax: 0.75 - visible: imagePath !== "" - } - Item { - id: mask - anchors.fill: parent - layer.enabled: true - visible: false + property var source: ShaderEffectSource { + sourceItem: img + hideSource: true + live: true + recursive: false + format: ShaderEffectSource.RGBA + } + + // Use custom property names to avoid conflicts with final properties + property real itemWidth: root.width + property real itemHeight: root.height + property real cornerRadius: root.radius + property real imageOpacity: root.opacity + fragmentShader: "file:Shaders/qsb/rounded_image.frag.qsb" + + // Qt6 specific properties - ensure proper blending + supportsAtlasTextures: false + blending: true + // Make sure the background is transparent Rectangle { + id: background anchors.fill: parent - radius: scaledRadius - antialiasing: true + color: "transparent" + z: -1 } }