background: parallax wallpaper

This commit is contained in:
end-4 2025-07-16 00:15:31 +07:00
parent abc9b4cdb7
commit 08e3a9bddb
3 changed files with 142 additions and 67 deletions

View file

@ -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<var> 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
}
}
}
}

View file

@ -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 {

View file

@ -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)