diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 7eac008..605aac6 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -174,6 +174,7 @@ Singleton { property string directory: "/usr/share/wallpapers" property bool enableMultiMonitorDirectories: false property bool setWallpaperOnAllMonitors: true + property string fillMode: "crop" property bool randomEnabled: false property int randomIntervalSec: 300 // 5 min property int transitionDuration: 1500 // 1500 ms diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 594425f..5b5112b 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -39,12 +39,24 @@ Variants { // Used to debounce wallpaper changes property string futureWallpaper: "" + // Fillmode default is "crop" + property real fillMode: 1.0 + // On startup assign wallpaper immediately Component.onCompleted: { + fillMode = WallpaperService.getFillModeUniform() + var path = modelData ? WallpaperService.getWallpaper(modelData.name) : "" setWallpaperImmediate(path) } + Connections { + target: Settings.data.wallpaper + function onFillModeChanged() { + fillMode = WallpaperService.getFillModeUniform() + } + } + // External state management Connections { target: WallpaperService @@ -84,8 +96,6 @@ Variants { Image { id: currentWallpaper - anchors.fill: parent - fillMode: Image.PreserveAspectCrop source: "" smooth: true mipmap: false @@ -97,8 +107,6 @@ Variants { Image { id: nextWallpaper - anchors.fill: parent - fillMode: Image.PreserveAspectCrop source: "" smooth: true mipmap: false @@ -116,6 +124,17 @@ Variants { property variant source1: currentWallpaper property variant source2: nextWallpaper property real progress: root.transitionProgress + + // Fill mode properties + property real fillMode: root.fillMode + property real imageWidth1: source1.sourceSize.width + property real imageHeight1: source1.sourceSize.height + property real imageWidth2: source2.sourceSize.width + property real imageHeight2: source2.sourceSize.height + property real screenWidth: width + property real screenHeight: height + property vector4d fillColor: Qt.vector4d(0.0, 0.0, 0.0, 1.0) // Black + fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_fade.frag.qsb") } @@ -131,6 +150,16 @@ Variants { property real smoothness: root.edgeSmoothness property real direction: root.wipeDirection + // Fill mode properties + property real fillMode: root.fillMode + property real imageWidth1: source1.sourceSize.width + property real imageHeight1: source1.sourceSize.height + property real imageWidth2: source2.sourceSize.width + property real imageHeight2: source2.sourceSize.height + property real screenWidth: width + property real screenHeight: height + property vector4d fillColor: Qt.vector4d(0.0, 0.0, 0.0, 1.0) // Black + fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_wipe.frag.qsb") } @@ -148,6 +177,16 @@ Variants { property real centerX: root.discCenterX property real centerY: root.discCenterY + // Fill mode properties + property real fillMode: root.fillMode + property real imageWidth1: source1.sourceSize.width + property real imageHeight1: source1.sourceSize.height + property real imageWidth2: source2.sourceSize.width + property real imageHeight2: source2.sourceSize.height + property real screenWidth: width + property real screenHeight: height + property vector4d fillColor: Qt.vector4d(0.0, 0.0, 0.0, 1.0) // Black + fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_disc.frag.qsb") } @@ -165,6 +204,16 @@ Variants { property real stripeCount: root.stripesCount property real angle: root.stripesAngle + // Fill mode properties + property real fillMode: root.fillMode + property real imageWidth1: source1.sourceSize.width + property real imageHeight1: source1.sourceSize.height + property real imageWidth2: source2.sourceSize.width + property real imageHeight2: source2.sourceSize.height + property real screenWidth: width + property real screenHeight: height + property vector4d fillColor: Qt.vector4d(0.0, 0.0, 0.0, 1.0) // Black + fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_stripes.frag.qsb") } diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index fcf715b..b758cf9 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -84,6 +84,93 @@ ColumnLayout { Layout.bottomMargin: Style.marginXL * scaling } + ColumnLayout { + visible: Settings.data.wallpaper.enabled + spacing: Style.marginL * scaling + Layout.fillWidth: true + + NText { + text: "Look & Feel" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mSecondary + } + + // Fill Mode + NComboBox { + label: "Fill Mode" + description: "Select how the image should scale to match your monitor's resolution." + model: WallpaperService.fillModeModel + currentKey: Settings.data.wallpaper.fillMode + onSelected: key => Settings.data.wallpaper.fillMode = key + } + + // Transition Type + NComboBox { + label: "Transition Type" + description: "Animation type when switching between wallpapers." + model: WallpaperService.transitionsModel + currentKey: Settings.data.wallpaper.transitionType + onSelected: key => Settings.data.wallpaper.transitionType = key + } + + // Transition Duration + ColumnLayout { + NLabel { + label: "Transition Duration" + description: "Duration of transition animations in seconds." + } + + RowLayout { + spacing: Style.marginL * scaling + NSlider { + Layout.fillWidth: true + from: 100 + to: 5000 + stepSize: 100 + value: Settings.data.wallpaper.transitionDuration + onMoved: Settings.data.wallpaper.transitionDuration = value + cutoutColor: Color.mSurface + } + NText { + text: (Settings.data.wallpaper.transitionDuration / 1000).toFixed(2) + "s" + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + } + } + } + + // Edge Smoothness + ColumnLayout { + NLabel { + label: "Transition Edge Smoothness" + description: "Duration of transition animations in seconds." + } + + RowLayout { + spacing: Style.marginL * scaling + NSlider { + Layout.fillWidth: true + from: 0.0 + to: 1.0 + value: Settings.data.wallpaper.transitionEdgeSmoothness + onMoved: Settings.data.wallpaper.transitionEdgeSmoothness = value + cutoutColor: Color.mSurface + } + NText { + text: Math.round(Settings.data.wallpaper.transitionEdgeSmoothness * 100) + "%" + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + } + } + } + } + + NDivider { + visible: Settings.data.wallpaper.enabled + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } + ColumnLayout { visible: Settings.data.wallpaper.enabled spacing: Style.marginL * scaling @@ -202,64 +289,6 @@ ColumnLayout { } } } - - // Transition Type - NComboBox { - label: "Transition Type" - description: "Animation type when switching between wallpapers." - model: WallpaperService.transitionsModel - currentKey: Settings.data.wallpaper.transitionType - onSelected: key => Settings.data.wallpaper.transitionType = key - } - - // Transition Duration - ColumnLayout { - NLabel { - label: "Transition Duration" - description: "Duration of transition animations in seconds." - } - - RowLayout { - spacing: Style.marginL * scaling - NSlider { - Layout.fillWidth: true - from: 100 - to: 5000 - stepSize: 100 - value: Settings.data.wallpaper.transitionDuration - onMoved: Settings.data.wallpaper.transitionDuration = value - cutoutColor: Color.mSurface - } - NText { - text: (Settings.data.wallpaper.transitionDuration / 1000).toFixed(2) + "s" - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - } - } - } - - // Edge Smoothness - ColumnLayout { - NLabel { - label: "Transition Edge Smoothness" - description: "Duration of transition animations in seconds." - } - - RowLayout { - spacing: Style.marginL * scaling - NSlider { - Layout.fillWidth: true - from: 0.0 - to: 1.0 - value: Settings.data.wallpaper.transitionEdgeSmoothness - onMoved: Settings.data.wallpaper.transitionEdgeSmoothness = value - cutoutColor: Color.mSurface - } - NText { - text: Math.round(Settings.data.wallpaper.transitionEdgeSmoothness * 100) + "%" - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - } - } - } } // Reusable component for interval preset chips diff --git a/Services/WallpaperService.qml b/Services/WallpaperService.qml index 7e9408e..a8a5e19 100644 --- a/Services/WallpaperService.qml +++ b/Services/WallpaperService.qml @@ -49,6 +49,51 @@ Singleton { } } + readonly property ListModel fillModeModel: ListModel { + // Centers image without resizing + // Pads with fillColor if image is smaller than screen + ListElement { + key: "center" + name: "Center" + uniform: 0.0 + } + // Scales image to fill entire screen + // Crops portions that exceed screen bounds + // Maintains aspect ratio + ListElement { + key: "crop" + name: "Crop (Fill/Cover)" + uniform: 1.0 + } + // Scales image to fit entirely within screen + // Maintains aspect ratio + // May show fillColor bars on sides + ListElement { + key: "fit" + name: "Fit (Contain)" + uniform: 2.0 + } + // Stretches image to exact screen dimensions + // Does NOT maintain aspect ratio + // May distort the image + ListElement { + key: "stretch" + name: "Stretch" + uniform: 3.0 + } + } + + function getFillModeUniform() { + for (var i = 0; i < fillModeModel.count; i++) { + const mode = fillModeModel.get(i) + if (mode.key === Settings.data.wallpaper.fillMode) { + return mode.uniform + } + } + // Fallback to crop + return 1.0 + } + // All transition keys but filter out "none" and "random" so we are left with the real transitions readonly property var allTransitions: Array.from({ "length": transitionsModel.count diff --git a/Shaders/frag/wp_disc.frag b/Shaders/frag/wp_disc.frag index 4bde549..0fc2122 100644 --- a/Shaders/frag/wp_disc.frag +++ b/Shaders/frag/wp_disc.frag @@ -15,17 +15,82 @@ layout(std140, binding = 0) uniform buf { float centerY; // Y coordinate of disc center (0.0 to 1.0) float smoothness; // Edge smoothness (0.0 to 1.0, 0=sharp, 1=very smooth) float aspectRatio; // Width / Height of the screen + + // Fill mode parameters + float fillMode; // 0=no(center), 1=crop(fill), 2=fit(contain), 3=stretch + float imageWidth1; // Width of source1 image + float imageHeight1; // Height of source1 image + float imageWidth2; // Width of source2 image + float imageHeight2; // Height of source2 image + float screenWidth; // Screen width + float screenHeight; // Screen height + vec4 fillColor; // Fill color for empty areas (default: black) } ubuf; +// Calculate UV coordinates based on fill mode +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + float imageAspect = imgWidth / imgHeight; + float screenAspect = ubuf.screenWidth / ubuf.screenHeight; + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + // Mode 0: no (center) - No resize, center image at original size + // Convert UV to pixel coordinates, offset, then back to UV in image space + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + // Mode 1: crop (fill/cover) - Fill screen, crop excess (default) + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + // Mode 2: fit (contain) - Fit inside screen, maintain aspect ratio + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + + // Convert screen UV to pixel coordinates + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + // Adjust for offset and scale + vec2 imagePixel = (screenPixel - offset) / scale; + // Convert back to UV coordinates in image space + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + // Mode 3: stretch - Use original UV (stretches to fit) + // No transformation needed for stretch mode + + return transformedUV; +} + +// Sample texture with fill mode and handle out-of-bounds +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight) { + vec2 transformedUV = calculateUV(uv, imgWidth, imgHeight); + + // Check if UV is out of bounds + if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || + transformedUV.y < 0.0 || transformedUV.y > 1.0) { + return ubuf.fillColor; + } + + return texture(tex, transformedUV); +} + void main() { vec2 uv = qt_TexCoord0; - vec4 color1 = texture(source1, uv); // Current (old) wallpaper - vec4 color2 = texture(source2, uv); // Next (new) wallpaper - + + // Sample textures with fill mode + vec4 color1 = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1); + vec4 color2 = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2); + // Map smoothness from 0.0-1.0 to 0.001-0.5 range // Using a non-linear mapping for better control float mappedSmoothness = mix(0.001, 0.5, ubuf.smoothness * ubuf.smoothness); - + // Adjust UV coordinates to compensate for aspect ratio // This makes distances circular instead of elliptical vec2 adjustedUV = vec2(uv.x * ubuf.aspectRatio, uv.y); @@ -54,4 +119,4 @@ void main() { fragColor = mix(color2, color1, factor); fragColor *= ubuf.qt_Opacity; -} +} \ No newline at end of file diff --git a/Shaders/frag/wp_fade.frag b/Shaders/frag/wp_fade.frag index 24917a5..47aa7c6 100644 --- a/Shaders/frag/wp_fade.frag +++ b/Shaders/frag/wp_fade.frag @@ -1,19 +1,88 @@ +// ===== wp_fade.frag ===== #version 450 layout(location = 0) in vec2 qt_TexCoord0; layout(location = 0) out vec4 fragColor; + layout(binding = 1) uniform sampler2D source1; layout(binding = 2) uniform sampler2D source2; + layout(std140, binding = 0) uniform buf { mat4 qt_Matrix; float qt_Opacity; float progress; -}; + + // Fill mode parameters + float fillMode; // 0=no(center), 1=crop(fill), 2=fit(contain), 3=stretch + float imageWidth1; // Width of source1 image + float imageHeight1; // Height of source1 image + float imageWidth2; // Width of source2 image + float imageHeight2; // Height of source2 image + float screenWidth; // Screen width + float screenHeight; // Screen height + vec4 fillColor; // Fill color for empty areas (default: black) +} ubuf; + +// Calculate UV coordinates based on fill mode +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + float imageAspect = imgWidth / imgHeight; + float screenAspect = ubuf.screenWidth / ubuf.screenHeight; + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + // Mode 0: no (center) - No resize, center image at original size + // Convert UV to pixel coordinates, offset, then back to UV in image space + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + // Mode 1: crop (fill/cover) - Fill screen, crop excess (default) + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + // Mode 2: fit (contain) - Fit inside screen, maintain aspect ratio + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + + // Convert screen UV to pixel coordinates + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + // Adjust for offset and scale + vec2 imagePixel = (screenPixel - offset) / scale; + // Convert back to UV coordinates in image space + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + // Mode 3: stretch - Use original UV (stretches to fit) + // No transformation needed for stretch mode + + return transformedUV; +} + +// Sample texture with fill mode and handle out-of-bounds +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight) { + vec2 transformedUV = calculateUV(uv, imgWidth, imgHeight); + + // Check if UV is out of bounds + if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || + transformedUV.y < 0.0 || transformedUV.y > 1.0) { + return ubuf.fillColor; + } + + return texture(tex, transformedUV); +} void main() { - vec4 color1 = texture(source1, qt_TexCoord0); - vec4 color2 = texture(source2, qt_TexCoord0); + vec2 uv = qt_TexCoord0; + + // Sample textures with fill mode + vec4 color1 = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1); + vec4 color2 = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2); // Mix the two textures based on progress value - fragColor = mix(color1, color2, progress) * qt_Opacity; + fragColor = mix(color1, color2, ubuf.progress) * ubuf.qt_Opacity; } \ No newline at end of file diff --git a/Shaders/frag/wp_stripes.frag b/Shaders/frag/wp_stripes.frag index f141227..c2684ce 100644 --- a/Shaders/frag/wp_stripes.frag +++ b/Shaders/frag/wp_stripes.frag @@ -15,12 +15,77 @@ layout(std140, binding = 0) uniform buf { float angle; // Angle of stripes in degrees (default 30.0) float smoothness; // Edge smoothness (0.0 to 1.0, 0=sharp, 1=very smooth) float aspectRatio; // Width / Height of the screen + + // Fill mode parameters + float fillMode; // 0=no(center), 1=crop(fill), 2=fit(contain), 3=stretch + float imageWidth1; // Width of source1 image + float imageHeight1; // Height of source1 image + float imageWidth2; // Width of source2 image + float imageHeight2; // Height of source2 image + float screenWidth; // Screen width + float screenHeight; // Screen height + vec4 fillColor; // Fill color for empty areas (default: black) } ubuf; +// Calculate UV coordinates based on fill mode +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + float imageAspect = imgWidth / imgHeight; + float screenAspect = ubuf.screenWidth / ubuf.screenHeight; + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + // Mode 0: no (center) - No resize, center image at original size + // Convert UV to pixel coordinates, offset, then back to UV in image space + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + // Mode 1: crop (fill/cover) - Fill screen, crop excess (default) + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + // Mode 2: fit (contain) - Fit inside screen, maintain aspect ratio + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + + // Convert screen UV to pixel coordinates + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + // Adjust for offset and scale + vec2 imagePixel = (screenPixel - offset) / scale; + // Convert back to UV coordinates in image space + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + // Mode 3: stretch - Use original UV (stretches to fit) + // No transformation needed for stretch mode + + return transformedUV; +} + +// Sample texture with fill mode and handle out-of-bounds +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight) { + vec2 transformedUV = calculateUV(uv, imgWidth, imgHeight); + + // Check if UV is out of bounds + if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || + transformedUV.y < 0.0 || transformedUV.y > 1.0) { + return ubuf.fillColor; + } + + return texture(tex, transformedUV); +} + void main() { vec2 uv = qt_TexCoord0; - vec4 color1 = texture(source1, uv); // Current (old) wallpaper - vec4 color2 = texture(source2, uv); // Next (new) wallpaper + + // Sample textures with fill mode + vec4 color1 = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1); + vec4 color2 = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2); // Map smoothness from 0.0-1.0 to 0.001-0.3 range // Using a non-linear mapping for better control at low values diff --git a/Shaders/frag/wp_wipe.frag b/Shaders/frag/wp_wipe.frag index 10b948a..46b21e6 100644 --- a/Shaders/frag/wp_wipe.frag +++ b/Shaders/frag/wp_wipe.frag @@ -1,4 +1,3 @@ - // ===== wp_wipe.frag ===== #version 450 @@ -14,12 +13,77 @@ layout(std140, binding = 0) uniform buf { float progress; // Transition progress (0.0 to 1.0) float direction; // 0=left, 1=right, 2=up, 3=down float smoothness; // Edge smoothness (0.0 to 1.0, 0=sharp, 1=very smooth) + + // Fill mode parameters + float fillMode; // 0=no(center), 1=crop(fill), 2=fit(contain), 3=stretch + float imageWidth1; // Width of source1 image + float imageHeight1; // Height of source1 image + float imageWidth2; // Width of source2 image + float imageHeight2; // Height of source2 image + float screenWidth; // Screen width + float screenHeight; // Screen height + vec4 fillColor; // Fill color for empty areas (default: black) } ubuf; +// Calculate UV coordinates based on fill mode +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + float imageAspect = imgWidth / imgHeight; + float screenAspect = ubuf.screenWidth / ubuf.screenHeight; + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + // Mode 0: no (center) - No resize, center image at original size + // Convert UV to pixel coordinates, offset, then back to UV in image space + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + // Mode 1: crop (fill/cover) - Fill screen, crop excess (default) + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + // Mode 2: fit (contain) - Fit inside screen, maintain aspect ratio + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + + // Convert screen UV to pixel coordinates + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + // Adjust for offset and scale + vec2 imagePixel = (screenPixel - offset) / scale; + // Convert back to UV coordinates in image space + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + // Mode 3: stretch - Use original UV (stretches to fit) + // No transformation needed for stretch mode + + return transformedUV; +} + +// Sample texture with fill mode and handle out-of-bounds +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight) { + vec2 transformedUV = calculateUV(uv, imgWidth, imgHeight); + + // Check if UV is out of bounds + if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || + transformedUV.y < 0.0 || transformedUV.y > 1.0) { + return ubuf.fillColor; + } + + return texture(tex, transformedUV); +} + void main() { vec2 uv = qt_TexCoord0; - vec4 color1 = texture(source1, uv); // Current (old) wallpaper - vec4 color2 = texture(source2, uv); // Next (new) wallpaper + + // Sample textures with fill mode + vec4 color1 = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1); + vec4 color2 = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2); // Map smoothness from 0.0-1.0 to 0.001-0.5 range // Using a non-linear mapping for better control diff --git a/Shaders/qsb/wp_disc.frag.qsb b/Shaders/qsb/wp_disc.frag.qsb index 9c66bfb..8e1705e 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 89641d8..3081ea8 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 7f739ae..db496ea 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 478c27d..a7dc6b1 100644 Binary files a/Shaders/qsb/wp_wipe.frag.qsb and b/Shaders/qsb/wp_wipe.frag.qsb differ