mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-25 04:25:22 -04:00
shader-based scrolling wallpaper mode (#1802)
* feat: parallax-scroll wallpaper Add a `Scrolling` wallpaper fill mode that translates the wallpaper crop with the active workspace — like Android home-screen parallax, but along niri's vertical workspace axis. The image is scaled to cover the screen on its non-scroll axis, and the active workspace index drives a fractional offset into the cropped overflow along the scroll axis. Scroll position is spring-animated CPU-side and handed to a minimal single-texture shader as a UV offset. Per-monitor scroll position is published into SessionData so the lock screen renders the same crop as the active workspace, keeping visual continuity across lock/unlock. Two implementation details worth calling out for review: - QSG_USE_SIMPLE_ANIMATION_DRIVER=1 is exported to the spawned quickshell process. The default animation driver advances in fixed ~16ms steps, capping the scroll at 60Hz and desyncing it from compositor motion on high-refresh displays; the simple driver advances by real elapsed time, restoring native-refresh pacing. Removing it visibly regresses to 60Hz. - The wallpaper survives wl_output rebind cycles (e.g. OLED image-cleaning on DPMS soft-off), which otherwise leave a stuck or void background. Recovery re-anchors the scroll target on output-lifecycle signals, rebuilds the ShaderEffect against the current render context, and re-attaches the wallpaper-layer surface on unlock for parallax-active monitors — guarded against lock state so the shader gets reliable frame hints. * simplify bindings and gate lock screen shader in a loader --------- Co-authored-by: bbedward <bbedward@gmail.com>
This commit is contained in:
@@ -7,7 +7,11 @@ import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
Variants {
|
||||
id: variants
|
||||
readonly property var log: Log.scoped("WallpaperBackground")
|
||||
// An entry present in PanelWindow.onCompleted means we're recreating
|
||||
// after a wl_output rebind, not at initial startup.
|
||||
property var _seenScreens: ({})
|
||||
model: {
|
||||
if (SessionData.isGreeterMode) {
|
||||
return Quickshell.screens;
|
||||
@@ -59,6 +63,15 @@ Variants {
|
||||
property string actualTransitionType: transitionType
|
||||
property bool isInitialized: false
|
||||
|
||||
property string scrollMode: SettingsData.wallpaperFillMode
|
||||
property bool scrollingEnabled: scrollMode === "Scrolling"
|
||||
property int currentWorkspaceIndex: 0
|
||||
property int totalWorkspaces: 1
|
||||
// Also requires the image to overflow on the compositor's scroll
|
||||
// axis — niri scrolls Y, Hyprland scrolls X — otherwise the
|
||||
// currentWallpaper Fill fallback handles it.
|
||||
property bool effectiveScrolling: scrollingEnabled && totalWorkspaces > 1 && (!imageMetrics.ready || (CompositorService.isNiri && imageMetrics.nativeWidth / imageMetrics.nativeHeight < root.textureWidth / root.textureHeight - 0.01) || (CompositorService.isHyprland && imageMetrics.nativeWidth / imageMetrics.nativeHeight > root.textureWidth / root.textureHeight + 0.01))
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onIsLightModeChanged() {
|
||||
@@ -70,6 +83,27 @@ Variants {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NiriService
|
||||
enabled: CompositorService.isNiri && root.scrollingEnabled
|
||||
|
||||
function onAllWorkspacesChanged() {
|
||||
root.updateWorkspaceData();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService.isHyprland ? Hyprland : null
|
||||
enabled: CompositorService.isHyprland && root.scrollingEnabled
|
||||
|
||||
function onRawEvent(event) {
|
||||
if (event.name === "workspace" || event.name === "workspacev2") {
|
||||
root.updateWorkspaceData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTransitionTypeChanged: {
|
||||
if (transitionType !== "random") {
|
||||
actualTransitionType = transitionType;
|
||||
@@ -96,7 +130,7 @@ Variants {
|
||||
property string _deferredSource: ""
|
||||
readonly property bool overviewBlurActive: CompositorService.isNiri && SettingsData.blurWallpaperOnOverview && NiriService.inOverview && currentWallpaper.source !== ""
|
||||
readonly property var backingWindow: Window.window
|
||||
readonly property bool renderActive: !source || effectActive || overviewBlurActive || pendingWallpaper !== "" || _deferredSource !== "" || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading
|
||||
readonly property bool renderActive: !source || effectActive || overviewBlurActive || pendingWallpaper !== "" || _deferredSource !== "" || frameAnim.running || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading
|
||||
property int _settleFrames: 3
|
||||
|
||||
function invalidate() {
|
||||
@@ -128,6 +162,7 @@ Variants {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
root.invalidate();
|
||||
root._onOutputRebind();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +185,9 @@ Variants {
|
||||
if (IdleService.isShellLocked)
|
||||
return;
|
||||
root.invalidate();
|
||||
// Catches silent rebinds during lock that no signal reports.
|
||||
if (root.effectiveScrolling)
|
||||
surfaceReattach.restart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,11 +199,38 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace/scale signals don't re-fire when the output list is
|
||||
// unchanged across a rebind, so re-derive scroll state by hand.
|
||||
function _onOutputRebind() {
|
||||
if (!root.scrollingEnabled)
|
||||
return;
|
||||
root.firstScrollUpdate = true;
|
||||
Qt.callLater(root.updateWorkspaceData);
|
||||
parallaxLoader.rebuild();
|
||||
if (root.effectiveScrolling && !IdleService.isShellLocked)
|
||||
surfaceReattach.restart();
|
||||
}
|
||||
|
||||
// Bouncing visible re-attaches a layer surface wedged by a rebind;
|
||||
// debounced so a burst of signals yields one re-attach.
|
||||
Timer {
|
||||
id: surfaceReattach
|
||||
interval: 0
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
wallpaperWindow.visible = false;
|
||||
Qt.callLater(() => {
|
||||
wallpaperWindow.visible = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NiriService
|
||||
function onDisplayScalesChanged() {
|
||||
root._recheckScreenScale();
|
||||
root.invalidate();
|
||||
root._onOutputRebind();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +239,7 @@ Variants {
|
||||
function onWlrOutputAvailableChanged() {
|
||||
root._recheckScreenScale();
|
||||
root.invalidate();
|
||||
root._onOutputRebind();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +253,7 @@ Variants {
|
||||
} else {
|
||||
root._recheckScreenScale();
|
||||
}
|
||||
root._onOutputRebind();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +277,8 @@ Variants {
|
||||
|
||||
function getFillMode(modeName) {
|
||||
switch (modeName) {
|
||||
case "Scrolling":
|
||||
return Image.PreserveAspectCrop;
|
||||
case "Stretch":
|
||||
return Image.Stretch;
|
||||
case "Fit":
|
||||
@@ -231,8 +300,199 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
function updateWorkspaceData() {
|
||||
if (!scrollingEnabled)
|
||||
return;
|
||||
|
||||
let newTargetX = 50.0;
|
||||
let newTargetY = 50.0;
|
||||
|
||||
if (CompositorService.isNiri) {
|
||||
const outputWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === modelData.name);
|
||||
totalWorkspaces = outputWorkspaces.length;
|
||||
|
||||
const activeWs = outputWorkspaces.find(ws => ws.is_active);
|
||||
currentWorkspaceIndex = activeWs ? activeWs.idx : 0;
|
||||
|
||||
const scrollPercent = totalWorkspaces > 1 ? ((currentWorkspaceIndex - 1) / (totalWorkspaces - 1)) * 100.0 : 0.0;
|
||||
|
||||
newTargetY = scrollPercent;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
const workspaces = Hyprland.workspaces?.values || [];
|
||||
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === modelData.name).sort((a, b) => a.id - b.id);
|
||||
|
||||
totalWorkspaces = monitorWorkspaces.length;
|
||||
const focusedId = Hyprland.focusedWorkspace?.id;
|
||||
currentWorkspaceIndex = monitorWorkspaces.findIndex(ws => ws.id === focusedId);
|
||||
|
||||
if (currentWorkspaceIndex < 0)
|
||||
currentWorkspaceIndex = 0;
|
||||
|
||||
const scrollPercent = totalWorkspaces > 1 ? ((currentWorkspaceIndex - 1) / (totalWorkspaces - 1)) * 100.0 : 0.0;
|
||||
|
||||
newTargetX = scrollPercent;
|
||||
}
|
||||
|
||||
scrollAnim.startAnimation(newTargetX, newTargetY);
|
||||
}
|
||||
|
||||
property bool firstScrollUpdate: true
|
||||
|
||||
QtObject {
|
||||
id: scrollAnim
|
||||
property real startTime: 0
|
||||
property real startX: 0.0
|
||||
property real startY: 0.0
|
||||
property real targetX: 0.0
|
||||
property real targetY: 0.0
|
||||
|
||||
property real damping: CompositorService.isNiri ? 63.25 : 89.44
|
||||
property real stiffness: CompositorService.isNiri ? 1000.0 : 2000.0
|
||||
property real mass: 1.0
|
||||
|
||||
function springPositionJS(t, from, to) {
|
||||
if (t <= 0)
|
||||
return from;
|
||||
const beta = damping / (2 * mass);
|
||||
const omega0 = Math.sqrt(stiffness / mass);
|
||||
const x0 = from - to;
|
||||
const envelope = Math.exp(-beta * t);
|
||||
if (Math.abs(x0 * envelope) < 0.01)
|
||||
return to;
|
||||
|
||||
if (Math.abs(beta - omega0) < 0.0001) {
|
||||
return to + envelope * (x0 + beta * x0 * t);
|
||||
} else if (beta < omega0) {
|
||||
const omega1 = Math.sqrt(omega0 * omega0 - beta * beta);
|
||||
return to + envelope * (x0 * Math.cos(omega1 * t) + (beta * x0 / omega1) * Math.sin(omega1 * t));
|
||||
} else {
|
||||
const omega2 = Math.sqrt(beta * beta - omega0 * omega0);
|
||||
const cosh = x => (Math.exp(x) + Math.exp(-x)) / 2;
|
||||
const sinh = x => (Math.exp(x) - Math.exp(-x)) / 2;
|
||||
return to + envelope * (x0 * cosh(omega2 * t) + (beta * x0 / omega2) * sinh(omega2 * t));
|
||||
}
|
||||
}
|
||||
|
||||
function startAnimation(newTargetX, newTargetY) {
|
||||
const now = Date.now() / 1000.0;
|
||||
const t = Math.max(0, frameAnim.currentTime - startTime);
|
||||
const currentX = springPositionJS(t, startX, targetX);
|
||||
const currentY = springPositionJS(t, startY, targetY);
|
||||
|
||||
if (Math.abs(newTargetX - currentX) < 0.01 && Math.abs(newTargetY - currentY) < 0.01) {
|
||||
if (root.firstScrollUpdate)
|
||||
root.firstScrollUpdate = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// First update: use much stiffer spring for quick snap-to
|
||||
if (root.firstScrollUpdate) {
|
||||
root.firstScrollUpdate = false;
|
||||
damping = 200.0;
|
||||
stiffness = 8000.0;
|
||||
} else {
|
||||
// Restore normal spring parameters
|
||||
damping = CompositorService.isNiri ? 63.25 : 89.44;
|
||||
stiffness = CompositorService.isNiri ? 1000.0 : 2000.0;
|
||||
}
|
||||
|
||||
startX = currentX;
|
||||
startY = currentY;
|
||||
targetX = newTargetX;
|
||||
targetY = newTargetY;
|
||||
startTime = frameAnim.running ? frameAnim.currentTime : now;
|
||||
if (!frameAnim.running) {
|
||||
frameAnim.currentTime = now;
|
||||
frameAnim.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CPU-side scroll position - computed once per frame instead of per-pixel in shader
|
||||
// Initialize at (0, 0) to avoid pillarbox flash; first update will snap to correct position
|
||||
property real currentScrollX: 0.0
|
||||
property real currentScrollY: 0.0
|
||||
|
||||
function publishScrollPosition() {
|
||||
if (effectiveScrolling) {
|
||||
SessionData.setMonitorScrollPosition(modelData.name, currentScrollX, currentScrollY);
|
||||
} else {
|
||||
// Not scrolling - publish centered (50, 50)
|
||||
SessionData.setMonitorScrollPosition(modelData.name, 50, 50);
|
||||
}
|
||||
}
|
||||
|
||||
FrameAnimation {
|
||||
id: frameAnim
|
||||
running: false
|
||||
|
||||
property real currentTime: 0
|
||||
|
||||
onRunningChanged: {
|
||||
if (running) {
|
||||
currentTime = Date.now() / 1000.0;
|
||||
} else {
|
||||
root.publishScrollPosition(); // Animation settled
|
||||
// Hold the render loop open so the final settled frame
|
||||
// commits before updatesEnabled drops out from under us.
|
||||
root.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
onTriggered: {
|
||||
// Clamp huge frameTime from a paused-render-loop wakeup;
|
||||
// otherwise the spring's `t` jumps past settling.
|
||||
const dt = frameTime > 0.1 ? 0.0 : frameTime;
|
||||
currentTime += dt;
|
||||
|
||||
const t = currentTime - scrollAnim.startTime;
|
||||
root.currentScrollX = scrollAnim.springPositionJS(t, scrollAnim.startX, scrollAnim.targetX);
|
||||
root.currentScrollY = scrollAnim.springPositionJS(t, scrollAnim.startY, scrollAnim.targetY);
|
||||
|
||||
const settledX = Math.abs(scrollAnim.targetX - root.currentScrollX) < 0.01;
|
||||
const settledY = Math.abs(scrollAnim.targetY - root.currentScrollY) < 0.01;
|
||||
|
||||
if (settledX && settledY) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
isInitialized = true;
|
||||
|
||||
if (scrollingEnabled) {
|
||||
updateWorkspaceData();
|
||||
}
|
||||
|
||||
Qt.callLater(publishScrollPosition);
|
||||
|
||||
// Detect rebind via _seenScreens; schedule surface re-attach
|
||||
// (deferred to unlock if locked).
|
||||
const wasSeen = variants._seenScreens[modelData.name] === true;
|
||||
variants._seenScreens[modelData.name] = true;
|
||||
// If currently locked, the unlock handler will re-attach;
|
||||
// otherwise re-attach now.
|
||||
if (wasSeen && root.effectiveScrolling && !IdleService.isShellLocked) {
|
||||
surfaceReattach.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
SessionData.clearMonitorScrollPosition(modelData.name);
|
||||
}
|
||||
|
||||
onScrollingEnabledChanged: {
|
||||
if (scrollingEnabled) {
|
||||
firstScrollUpdate = true;
|
||||
updateWorkspaceData();
|
||||
} else {
|
||||
frameAnim.stop();
|
||||
}
|
||||
}
|
||||
|
||||
onEffectiveScrollingChanged: {
|
||||
publishScrollPosition();
|
||||
}
|
||||
|
||||
onSourceChanged: {
|
||||
@@ -265,6 +525,17 @@ Variants {
|
||||
root.screenScale = CompositorService.getScreenScale(modelData);
|
||||
currentWallpaper.source = newSource;
|
||||
nextWallpaper.source = "";
|
||||
|
||||
// Reset scroll state for new image - will snap to correct position on first update
|
||||
if (scrollingEnabled) {
|
||||
firstScrollUpdate = true;
|
||||
currentScrollX = 0.0;
|
||||
currentScrollY = 0.0;
|
||||
scrollAnim.startX = 0.0;
|
||||
scrollAnim.startY = 0.0;
|
||||
scrollAnim.targetX = 0.0;
|
||||
scrollAnim.targetY = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
function startTransition() {
|
||||
@@ -301,6 +572,11 @@ Variants {
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.effectiveScrolling) {
|
||||
setWallpaperImmediate(newPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.transitionType === "random") {
|
||||
root.actualTransitionType = SessionData.includedTransitions.length === 0 ? "none" : SessionData.includedTransitions[Math.floor(Math.random() * SessionData.includedTransitions.length)];
|
||||
}
|
||||
@@ -347,16 +623,60 @@ Variants {
|
||||
property int textureWidth: Math.min(Math.round(modelData.width * screenScale), maxTextureSize)
|
||||
property int textureHeight: Math.min(Math.round(modelData.height * screenScale), maxTextureSize)
|
||||
|
||||
QtObject {
|
||||
id: imageMetrics
|
||||
property real nativeWidth: 0
|
||||
property real nativeHeight: 0
|
||||
property bool ready: nativeWidth > 0 && nativeHeight > 0
|
||||
|
||||
function capture(w, h) {
|
||||
if (nativeWidth === 0 && w > 0) {
|
||||
nativeWidth = w;
|
||||
nativeHeight = h;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
nativeWidth = 0;
|
||||
nativeHeight = 0;
|
||||
}
|
||||
|
||||
readonly property real canvasWidth: {
|
||||
if (!ready || !root.effectiveScrolling)
|
||||
return root.textureWidth;
|
||||
const imageAspect = nativeWidth / nativeHeight;
|
||||
const screenAspect = root.textureWidth / root.textureHeight;
|
||||
if (imageAspect < screenAspect) {
|
||||
return root.textureWidth;
|
||||
} else {
|
||||
return root.textureHeight * imageAspect;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real canvasHeight: {
|
||||
if (!ready || !root.effectiveScrolling)
|
||||
return root.textureHeight;
|
||||
const imageAspect = nativeWidth / nativeHeight;
|
||||
const screenAspect = root.textureWidth / root.textureHeight;
|
||||
if (imageAspect < screenAspect) {
|
||||
return root.textureWidth / imageAspect;
|
||||
} else {
|
||||
return root.textureHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: currentWallpaper
|
||||
anchors.fill: parent
|
||||
visible: true
|
||||
visible: !root.effectiveScrolling
|
||||
opacity: 1
|
||||
layer.enabled: false
|
||||
asynchronous: true
|
||||
retainWhileLoading: true
|
||||
smooth: true
|
||||
cache: true
|
||||
|
||||
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
||||
fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(modelData.name))
|
||||
|
||||
@@ -364,6 +684,13 @@ Variants {
|
||||
if (status === Image.Error) {
|
||||
log.warn("failed to load active wallpaper for", modelData.name + ":", source);
|
||||
}
|
||||
if (status === Image.Ready) {
|
||||
imageMetrics.capture(implicitWidth, implicitHeight);
|
||||
}
|
||||
}
|
||||
|
||||
onSourceChanged: {
|
||||
imageMetrics.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,6 +704,7 @@ Variants {
|
||||
retainWhileLoading: true
|
||||
smooth: true
|
||||
cache: true
|
||||
|
||||
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
||||
fillMode: root.getFillMode(SessionData.getMonitorWallpaperFillMode(modelData.name))
|
||||
|
||||
@@ -434,6 +762,89 @@ Variants {
|
||||
recursive: false
|
||||
}
|
||||
|
||||
// Parallax scrolling pipeline — bypasses transition machinery.
|
||||
Image {
|
||||
id: parallaxImage
|
||||
visible: false
|
||||
width: imageMetrics.canvasWidth
|
||||
height: imageMetrics.canvasHeight
|
||||
source: root.effectiveScrolling ? currentWallpaper.source : ""
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
cache: true
|
||||
sourceSize: Qt.size(imageMetrics.canvasWidth, imageMetrics.canvasHeight)
|
||||
fillMode: Image.Stretch
|
||||
}
|
||||
|
||||
ShaderEffectSource {
|
||||
id: srcParallax
|
||||
sourceItem: root.effectiveScrolling && imageMetrics.ready && parallaxImage.status === Image.Ready ? parallaxImage : null
|
||||
hideSource: false
|
||||
live: true
|
||||
mipmap: false
|
||||
recursive: false
|
||||
textureSize: Qt.size(imageMetrics.canvasWidth, imageMetrics.canvasHeight)
|
||||
}
|
||||
|
||||
// Pre-computed UV parameters for shader
|
||||
QtObject {
|
||||
id: parallaxUV
|
||||
readonly property real imageAspect: imageMetrics.ready ? imageMetrics.canvasWidth / imageMetrics.canvasHeight : 1.0
|
||||
readonly property real screenAspect: root.textureWidth / root.textureHeight
|
||||
|
||||
// Scale factor to fit image to screen (preserving aspect, cropping excess)
|
||||
readonly property real scale: Math.max(root.textureWidth / imageMetrics.canvasWidth, root.textureHeight / imageMetrics.canvasHeight)
|
||||
readonly property real scaledWidth: imageMetrics.canvasWidth * scale
|
||||
readonly property real scaledHeight: imageMetrics.canvasHeight * scale
|
||||
|
||||
// UV scale: portion of texture visible on screen
|
||||
readonly property real uvScaleX: root.textureWidth / scaledWidth
|
||||
readonly property real uvScaleY: root.textureHeight / scaledHeight
|
||||
|
||||
// Scroll range: how much UV space we can scroll through
|
||||
// Only allow scrolling in the dimension where image exceeds screen
|
||||
readonly property real scrollRangeX: imageAspect > screenAspect + 0.01 ? (1.0 - uvScaleX) : (1.0 - uvScaleX) * 0.5
|
||||
readonly property real scrollRangeY: imageAspect < screenAspect - 0.01 ? (1.0 - uvScaleY) : (1.0 - uvScaleY) * 0.5
|
||||
readonly property bool scrollsHorizontal: imageAspect > screenAspect + 0.01
|
||||
readonly property bool scrollsVertical: imageAspect < screenAspect - 0.01
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: parallaxLoader
|
||||
anchors.fill: parent
|
||||
active: root.effectiveScrolling && !root.effectActive && imageMetrics.ready && parallaxImage.status === Image.Ready
|
||||
sourceComponent: parallaxScrollComp
|
||||
|
||||
// Rebuild after a rebind orphans the texture; callLater defeats
|
||||
// sourceComponent coalescing.
|
||||
function rebuild() {
|
||||
if (!active)
|
||||
return;
|
||||
sourceComponent = null;
|
||||
Qt.callLater(() => {
|
||||
sourceComponent = parallaxScrollComp;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: parallaxScrollComp
|
||||
ShaderEffect {
|
||||
anchors.fill: parent
|
||||
|
||||
property variant source: srcParallax.sourceItem ? srcParallax : srcDummy
|
||||
|
||||
property real scrollX: root.currentScrollX
|
||||
property real scrollY: root.currentScrollY
|
||||
property real uvScaleX: parallaxUV.uvScaleX
|
||||
property real uvScaleY: parallaxUV.uvScaleY
|
||||
property real scrollRangeX: parallaxUV.scrollsHorizontal ? parallaxUV.scrollRangeX : 0.0
|
||||
property real scrollRangeY: parallaxUV.scrollsVertical ? parallaxUV.scrollRangeY : 0.0
|
||||
|
||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wp_parallax_scroll.frag.qsb")
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: effectLoader
|
||||
anchors.fill: parent
|
||||
@@ -652,7 +1063,7 @@ Variants {
|
||||
|
||||
sourceComponent: MultiEffect {
|
||||
anchors.fill: parent
|
||||
source: effectLoader.active ? effectLoader.item : currentWallpaper
|
||||
source: effectLoader.active ? effectLoader.item : (parallaxLoader.active ? parallaxLoader.item : currentWallpaper)
|
||||
blurEnabled: true
|
||||
blur: 0.8
|
||||
blurMax: 75
|
||||
|
||||
Reference in New Issue
Block a user