From 08e3a9bddb7ad2ed8b073aa63cbceeecc33f3771 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 16 Jul 2025 00:15:31 +0700 Subject: [PATCH] background: parallax wallpaper --- .../ii/modules/background/Background.qml | 176 ++++++++++++------ .../quickshell/ii/modules/common/Config.qml | 4 + .../ii/scripts/images/least_busy_region.py | 29 +-- 3 files changed, 142 insertions(+), 67 deletions(-) diff --git a/.config/quickshell/ii/modules/background/Background.qml b/.config/quickshell/ii/modules/background/Background.qml index 341b6513..55d13d64 100644 --- a/.config/quickshell/ii/modules/background/Background.qml +++ b/.config/quickshell/ii/modules/background/Background.qml @@ -11,6 +11,7 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Wayland +import Quickshell.Hyprland Scope { id: root @@ -26,7 +27,19 @@ Scope { id: bgRoot required property var modelData + // Workspaces + property HyprlandMonitor monitor: Hyprland.monitorFor(modelData) + property list relevantWindows: HyprlandData.windowList.filter(win => win.monitor == monitor.id && win.workspace.id >= 0).sort((a, b) => a.workspace.id - b.workspace.id) + property int firstWorkspaceId: relevantWindows[0]?.workspace.id || 1 + property int lastWorkspaceId: relevantWindows[relevantWindows.length - 1]?.workspace.id || 10 + // Wallpaper property string wallpaperPath: Config.options.background.wallpaperPath + property real preferredWallpaperScale: Config.options.background.parallax.workspaceZoom + property real effectiveWallpaperScale: 1 // Some reasonable init value, to be updated + property int wallpaperWidth: modelData.width // Some reasonable init value, to be updated + property int wallpaperHeight: modelData.height // Some reasonable init value, to be updated + property real movableXSpace: (effectiveWallpaperScale - 1) / 2 * screen.width + property real movableYSpace: (effectiveWallpaperScale - 1) / 2 * screen.height // Position property real clockX: (modelData.width / 2) + ((Math.random() < 0.5 ? -1 : 1) * modelData.width) property real clockY: (modelData.height / 2) + ((Math.random() < 0.5 ? -1 : 1) * modelData.height) @@ -50,29 +63,63 @@ Scope { } color: "transparent" + onWallpaperPathChanged: { + bgRoot.updateZoomScale() + // Clock position gets updated after zoom scale is updated + } + + // Wallpaper zoom scale + function updateZoomScale() { + getWallpaperSizeProc.path = bgRoot.wallpaperPath + getWallpaperSizeProc.running = true; + } + Process { + id: getWallpaperSizeProc + property string path: bgRoot.wallpaperPath + command: [ "magick", "identify", "-format", "%w %h", path ] + stdout: StdioCollector { + id: wallpaperSizeOutputCollector + onStreamFinished: { + const output = wallpaperSizeOutputCollector.text + const [width, height] = output.split(" ").map(Number); + bgRoot.wallpaperWidth = width + bgRoot.wallpaperHeight = height + bgRoot.effectiveWallpaperScale = Math.max(1, Math.min( + bgRoot.preferredWallpaperScale, + width / bgRoot.screen.width, + height / bgRoot.screen.height + )); + + bgRoot.updateClockPosition() + } + } + } + // Clock positioning function updateClockPosition() { - leastBusyRegionProc.path = wallpaperPath // Somehow this is needed to make the proc correctly use the new path + // Somehow all this manual setting is needed to make the proc correctly use the new values + leastBusyRegionProc.path = bgRoot.wallpaperPath leastBusyRegionProc.contentWidth = clock.implicitWidth leastBusyRegionProc.contentHeight = clock.implicitHeight + leastBusyRegionProc.horizontalPadding = (effectiveWallpaperScale - 1) / 2 * screen.width + 100 + leastBusyRegionProc.verticalPadding = (effectiveWallpaperScale - 1) / 2 * screen.height + 100 leastBusyRegionProc.running = false; leastBusyRegionProc.running = true; } - onWallpaperPathChanged: { - // console.log("[Background] Wallpaper path changed to:", wallpaperPath) - bgRoot.updateClockPosition() - } Process { id: leastBusyRegionProc - running: true property string path: bgRoot.wallpaperPath - property int contentWidth: bgRoot.screen.width - property int contentHeight: bgRoot.screen.height + property int contentWidth: 300 + property int contentHeight: 300 + property int horizontalPadding: bgRoot.movableXSpace + property int verticalPadding: bgRoot.movableYSpace command: [Quickshell.configPath("scripts/images/least_busy_region.py"), "--screen-width", bgRoot.screen.width, "--screen-height", bgRoot.screen.height, "--width", contentWidth, "--height", contentHeight, + "--horizontal-padding", horizontalPadding, + "--vertical-padding", verticalPadding, path ] stdout: StdioCollector { @@ -91,58 +138,79 @@ Scope { // Wallpaper Image { - z: 0 - anchors.fill: parent + property real value // 0 to 1, for offset + value: { + // Range = half-groups that workspaces span on + const chunkSize = 3; + const lower = Math.floor(bgRoot.firstWorkspaceId / chunkSize) * chunkSize; + const upper = Math.ceil(bgRoot.lastWorkspaceId / chunkSize) * chunkSize; + const range = upper - lower; + return (bgRoot.monitor.activeWorkspace.id - lower) / range; + } + property real effectiveValue: Math.max(0, Math.min(1, value)) + x: -(bgRoot.movableXSpace) - (effectiveValue - 0.5) * 2 * bgRoot.movableXSpace + y: -(bgRoot.movableYSpace) source: bgRoot.wallpaperPath fillMode: Image.PreserveAspectCrop + Behavior on x { + NumberAnimation { + duration: 600 + easing.type: Easing.OutCubic + } + } sourceSize { - width: bgRoot.screen.width - height: bgRoot.screen.height - } - } - - // The clock - Item { - id: clock - z: 1 - anchors { - left: parent.left - top: parent.top - leftMargin: (root.fixedClockPosition ? root.fixedClockX : bgRoot.clockX) - implicitWidth / 2 - topMargin: (root.fixedClockPosition ? root.fixedClockY : bgRoot.clockY) - implicitHeight / 2 - Behavior on leftMargin { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - Behavior on topMargin { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } + width: bgRoot.screen.width * bgRoot.effectiveWallpaperScale + height: bgRoot.screen.height * bgRoot.effectiveWallpaperScale } - implicitWidth: clockColumn.implicitWidth - implicitHeight: clockColumn.implicitHeight - - ColumnLayout { - id: clockColumn - anchors.centerIn: parent - spacing: -5 - - StyledText { - Layout.fillWidth: true - horizontalAlignment: bgRoot.textHorizontalAlignment - font.pixelSize: 95 - color: bgRoot.colText - style: Text.Raised - styleColor: Appearance.colors.colShadow - text: DateTime.time + // The clock + Item { + id: clock + anchors { + left: parent.left + top: parent.top + leftMargin: ((root.fixedClockPosition ? root.fixedClockX : bgRoot.clockX * bgRoot.effectiveWallpaperScale) - implicitWidth / 2) + topMargin: ((root.fixedClockPosition ? root.fixedClockY : bgRoot.clockY * bgRoot.effectiveWallpaperScale) - implicitHeight / 2) + Behavior on leftMargin { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + Behavior on topMargin { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } } - StyledText { - Layout.fillWidth: true - horizontalAlignment: bgRoot.textHorizontalAlignment - font.pixelSize: 25 - color: bgRoot.colText - style: Text.Raised - styleColor: Appearance.colors.colShadow - text: DateTime.date + + implicitWidth: clockColumn.implicitWidth + implicitHeight: clockColumn.implicitHeight + + ColumnLayout { + id: clockColumn + anchors.centerIn: parent + spacing: -5 + + StyledText { + Layout.fillWidth: true + horizontalAlignment: bgRoot.textHorizontalAlignment + font { + pixelSize: 85 + weight: Font.Medium + } + color: bgRoot.colText + style: Text.Raised + styleColor: Appearance.colors.colShadow + text: DateTime.time + } + StyledText { + Layout.fillWidth: true + horizontalAlignment: bgRoot.textHorizontalAlignment + font { + pixelSize: 20 + weight: Font.Medium + } + color: bgRoot.colText + style: Text.Raised + styleColor: Appearance.colors.colShadow + text: DateTime.date + } } } } diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml index 64f13651..582c11d8 100644 --- a/.config/quickshell/ii/modules/common/Config.qml +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -99,6 +99,10 @@ Singleton { property real clockX: -500 property real clockY: -500 property string wallpaperPath: Quickshell.configPath("assets/images/default_wallpaper.png") + property JsonObject parallax: JsonObject { + property real workspaceZoom: 1.07 // Relative to your screen, not wallpaper size + property bool enableWorkspace: true + } } property JsonObject bar: JsonObject { diff --git a/.config/quickshell/ii/scripts/images/least_busy_region.py b/.config/quickshell/ii/scripts/images/least_busy_region.py index bad58bd8..2b1d104e 100755 --- a/.config/quickshell/ii/scripts/images/least_busy_region.py +++ b/.config/quickshell/ii/scripts/images/least_busy_region.py @@ -18,7 +18,7 @@ def center_crop(img, target_w, target_h): y2 = y1 + target_h return img[y1:y2, x1:x2] -def find_least_busy_region(image_path, region_width=300, region_height=200, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", padding=50): +def find_least_busy_region(image_path, region_width=300, region_height=200, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", horizontal_padding=50, vertical_padding=50): img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(f"Image not found: {image_path}") @@ -59,10 +59,10 @@ def find_least_busy_region(image_path, region_width=300, region_height=200, scre min_var = None min_coords = (0, 0) area = region_width * region_height - x_start = padding - y_start = padding - x_end = w - region_width - padding + 1 - y_end = h - region_height - padding + 1 + x_start = horizontal_padding + y_start = vertical_padding + x_end = w - region_width - horizontal_padding + 1 + y_end = h - region_height - vertical_padding + 1 for y in range(y_start, max(y_end, y_start+1), stride): for x in range(x_start, max(x_end, x_start+1), stride): x1, y1 = x, y @@ -76,7 +76,7 @@ def find_least_busy_region(image_path, region_width=300, region_height=200, scre min_coords = (x, y) return min_coords, min_var -def find_largest_region(image_path, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", threshold=100.0, aspect_ratio=1.0, padding=50): +def find_largest_region(image_path, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", threshold=100.0, aspect_ratio=1.0, horizontal_padding=50, vertical_padding=50): img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(f"Image not found: {image_path}") @@ -130,10 +130,10 @@ def find_largest_region(image_path, screen_width=None, screen_height=None, verbo max_size = mid - 1 continue found = False - x_start = padding - y_start = padding - x_end = w - region_w - padding + 1 - y_end = h - region_h - padding + 1 + x_start = horizontal_padding + y_start = vertical_padding + x_end = w - region_w - horizontal_padding + 1 + y_end = h - region_h - vertical_padding + 1 for y in range(y_start, max(y_end, y_start+1), stride): for x in range(x_start, max(x_end, x_start+1), stride): x1, y1 = x, y @@ -263,7 +263,8 @@ def main(): parser.add_argument("-l", "--largest-region", action="store_true", help="Find the largest region under the variance threshold and output its center") parser.add_argument("-t", "--variance-threshold", type=float, default=1000.0, help="Variance threshold for largest region mode") parser.add_argument("--aspect-ratio", type=float, default=1.78, help="Aspect ratio (width/height) for largest region mode") - parser.add_argument("--padding", type=int, default=50, help="Minimum distance from region to image edge (default: 50)") + parser.add_argument("--horizontal-padding", "-hp", type=int, default=50, help="Minimum horizontal distance from region to image edge") + parser.add_argument("--vertical-padding", "-vp", type=int, default=50, help="Minimum vertical distance from region to image edge") args = parser.parse_args() if args.largest_region: @@ -276,7 +277,8 @@ def main(): screen_mode=args.screen_mode, threshold=args.variance_threshold, aspect_ratio=args.aspect_ratio, - padding=args.padding + horizontal_padding=args.horizontal_padding, + vertical_padding=args.vertical_padding ) if center: if args.visual_output: @@ -312,7 +314,8 @@ def main(): verbose=args.verbose, stride=args.stride, screen_mode=args.screen_mode, - padding=args.padding + horizontal_padding=args.horizontal_padding, + vertical_padding=args.vertical_padding ) if args.visual_output: draw_region(args.image_path, coords, region_width=args.width, region_height=args.height, screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode)