Merge pull request 'test popouts' (#61) from test-popouts into main

Reviewed-on: #61
This commit was merged in pull request #61.
This commit is contained in:
2026-04-18 00:17:35 +02:00
25 changed files with 2067 additions and 151 deletions
+6 -5
View File
@@ -54,7 +54,7 @@ CustomMouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: (pressed && dragStart.y < bar.implicitHeight) ? Qt.ClosedHandCursor : undefined cursorShape: (pressed && dragStart.y < bar.implicitHeight) ? Qt.ClosedHandCursor : undefined
hoverEnabled: true hoverEnabled: true
propagateComposedEvents: true propagateComposedEvents: false
onContainsMouseChanged: { onContainsMouseChanged: {
if (!containsMouse) { if (!containsMouse) {
@@ -72,9 +72,6 @@ CustomMouseArea {
} }
} }
onPositionChanged: event => { onPositionChanged: event => {
if (popouts.isDetached)
return;
const x = event.x; const x = event.x;
const y = event.y; const y = event.y;
const dragX = x - dragStart.x; const dragX = x - dragStart.x;
@@ -95,6 +92,10 @@ CustomMouseArea {
visibilities.settings = false; visibilities.settings = false;
} }
if (Config.dock.hoverToReveal && pressed && dragStart.y > root.screen.height - root.bar.implicitHeight)
if (dragY < -10)
visibilities.dock = true;
if (panels.sidebar.width === 0) { if (panels.sidebar.width === 0) {
const showOsd = inRightPanel(panels.osd, x, y); const showOsd = inRightPanel(panels.osd, x, y);
@@ -115,7 +116,7 @@ CustomMouseArea {
} }
} }
if (!visibilities.dock && !visibilities.launcher && inBottomPanel(panels.dock, x, y)) if (Config.dock.enable && !Config.dock.hoverToReveal && !visibilities.dock && !visibilities.launcher && inBottomPanel(panels.dock, x, y))
visibilities.dock = true; visibilities.dock = true;
if (y < root.bar.implicitHeight) { if (y < root.bar.implicitHeight) {
+4 -11
View File
@@ -26,7 +26,8 @@ Item {
readonly property alias launcher: launcher readonly property alias launcher: launcher
readonly property alias notifications: notifications readonly property alias notifications: notifications
readonly property alias osd: osd readonly property alias osd: osd
readonly property alias popouts: popouts readonly property alias popouts: popouts.content
readonly property alias popoutsWrapper: popouts
readonly property alias resources: resources readonly property alias resources: resources
required property ShellScreen screen required property ShellScreen screen
readonly property alias settings: settings readonly property alias settings: settings
@@ -68,18 +69,11 @@ Item {
visibilities: root.visibilities visibilities: root.visibilities
} }
Modules.Wrapper { Modules.ClipWrapper {
id: popouts id: popouts
anchors.top: parent.top anchors.top: parent.top
screen: root.screen screen: root.screen
x: {
const off = currentCenter - nonAnimWidth / 2;
const diff = root.width - Math.floor(off + nonAnimWidth);
if (diff < 0)
return off + diff;
return Math.floor(Math.max(off, 0));
}
} }
Toasts.Toasts { Toasts.Toasts {
@@ -140,8 +134,7 @@ Item {
Settings.Wrapper { Settings.Wrapper {
id: settings id: settings
anchors.horizontalCenter: parent.horizontalCenter anchors.centerIn: parent
anchors.top: parent.top
panels: root panels: root
screen: root.screen screen: root.screen
visibilities: root.visibilities visibilities: root.visibilities
+145 -8
View File
@@ -5,6 +5,7 @@ import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import ZShell.Blobs
import qs.Daemons import qs.Daemons
import qs.Components import qs.Components
import qs.Modules import qs.Modules
@@ -144,16 +145,107 @@ Variants {
shadowEnabled: true shadowEnabled: true
} }
Border { BlobGroup {
bar: bar id: blobGroup
visibilities: visibilities
color: DynamicColors.palette.m3surface
Behavior on color {
CAnim {
}
}
} }
Backgrounds { BlobInvertedRect {
bar: bar anchors.fill: parent
panels: panels anchors.margins: -50
visibilities: visibilities borderBottom: Config.barConfig.border - anchors.margins
z: 1 borderLeft: Config.barConfig.border - anchors.margins
borderRight: Config.barConfig.border - anchors.margins
borderTop: bar.implicitHeight - anchors.margins
group: blobGroup
radius: Config.barConfig.rounding
}
PanelBg {
id: dashBg
deformAmount: 0.1
panel: panels.dashboard
radius: Appearance.rounding.normal
}
PanelBg {
id: launcherBg
deformAmount: 0.1
panel: panels.launcher
radius: Appearance.rounding.smallest + 5
}
PanelBg {
id: sidebarBg
bottomLeftRadius: 0
deformAmount: 0.03
exclude: panels.sidebar.offsetscale > 0.08 ? [] : [utilsBg]
implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2
panel: panels.sidebar
}
PanelBg {
id: osdBg
deformAmount: 0.25
panel: panels.osd
radius: 20
}
PanelBg {
id: notifsBg
panel: panels.notifications
}
PanelBg {
id: utilsBg
deformAmount: panels.sidebar.visible ? 0.1 : 0.15
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg]
panel: panels.utilities
topLeftRadius: 0
}
PanelBg {
id: popoutBg
deformAmount: 0.15
implicitWidth: panels.popouts.width
panel: panels.popoutsWrapper
}
PanelBg {
id: resourcesBg
deformAmount: 0.15
panel: panels.resources
radius: Appearance.rounding.normal
}
PanelBg {
id: settingsBg
deformAmount: 0.05
panel: panels.settings
radius: Appearance.rounding.large
}
PanelBg {
id: dockBg
deformAmount: 0.1
panel: panels.dock
radius: Appearance.rounding.normal
} }
} }
@@ -195,6 +287,37 @@ Variants {
drawingItem: drawing drawingItem: drawing
screen: scope.modelData screen: scope.modelData
visibilities: visibilities visibilities: visibilities
dashboard.transform: Matrix4x4 {
matrix: dashBg.deformMatrix
}
dock.transform: Matrix4x4 {
matrix: dockBg.deformMatrix
}
launcher.transform: Matrix4x4 {
matrix: launcherBg.deformMatrix
}
notifications.transform: Matrix4x4 {
matrix: notifsBg.deformMatrix
}
osd.transform: Matrix4x4 {
matrix: osdBg.deformMatrix
}
popouts.transform: Matrix4x4 {
matrix: popoutBg.deformMatrix
}
resources.transform: Matrix4x4 {
matrix: resourcesBg.deformMatrix
}
settings.transform: Matrix4x4 {
matrix: settingsBg.deformMatrix
}
sidebar.transform: Matrix4x4 {
matrix: sidebarBg.deformMatrix
}
utilities.transform: Matrix4x4 {
matrix: utilsBg.deformMatrix
}
} }
BarLoader { BarLoader {
@@ -203,10 +326,24 @@ Variants {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
popouts: panels.popouts popouts: panels.popouts
popoutsWrapper: panels.popoutsWrapper
screen: scope.modelData screen: scope.modelData
visibilities: visibilities visibilities: visibilities
} }
} }
} }
} }
component PanelBg: BlobRect {
property real deformAmount: 0.15
required property Item panel
deformScale: deformAmount / 10000
group: panel.width > 0 && panel.height > 0 ? blobGroup : null
implicitHeight: panel.height
implicitWidth: panel.width
radius: Appearance.rounding.smallest
x: panel.x + Config.barConfig.border
y: panel.y + bar.implicitHeight
}
} }
+1
View File
@@ -16,6 +16,7 @@ RowLayout {
id: root id: root
required property Wrapper popouts required property Wrapper popouts
required property ClipWrapper popoutsWrapper
required property ShellScreen screen required property ShellScreen screen
readonly property int vPadding: 6 readonly property int vPadding: 6
required property PersistentProperties visibilities required property PersistentProperties visibilities
+2
View File
@@ -20,6 +20,7 @@ Item {
property bool isHovered property bool isHovered
readonly property int padding: Math.max(Appearance.padding.smaller, Config.barConfig.border) readonly property int padding: Math.max(Appearance.padding.smaller, Config.barConfig.border)
required property Wrapper popouts required property Wrapper popouts
required property ClipWrapper popoutsWrapper
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeVisible: (!Config.barConfig.autoHide || visibilities.bar || isHovered) readonly property bool shouldBeVisible: (!Config.barConfig.autoHide || visibilities.bar || isHovered)
readonly property int vPadding: 6 readonly property int vPadding: 6
@@ -76,6 +77,7 @@ Item {
sourceComponent: Bar { sourceComponent: Bar {
height: root.contentHeight height: root.contentHeight
popouts: root.popouts popouts: root.popouts
popoutsWrapper: root.popoutsWrapper
screen: root.screen screen: root.screen
visibilities: root.visibilities visibilities: root.visibilities
} }
+57
View File
@@ -0,0 +1,57 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Components
import qs.Config
Item {
id: root
readonly property alias content: content
property real offsetScale: y > 0 || content.hasCurrent ? 0 : 1
required property ShellScreen screen
clip: true
implicitHeight: content.implicitHeight * (1 - offsetScale)
implicitWidth: content.implicitWidth
visible: width > 0 && height > 0
x: {
const off = content.currentCenter - content.nonAnimWidth / 2;
const diff = parent.width - Math.floor(off + content.nonAnimWidth);
if (diff < 0)
return off + diff;
return Math.floor(Math.max(off, 0));
}
Behavior on offsetScale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on x {
enabled: root.offsetScale < 1
Anim {
duration: content.animLength
easing.bezierCurve: content.animCurve
}
}
Behavior on y {
Anim {
duration: content.animLength
easing.bezierCurve: content.animCurve
}
}
Wrapper {
id: content
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: (-implicitHeight - 5) * root.offsetScale
offsetScale: root.offsetScale
screen: root.screen
}
}
+9 -20
View File
@@ -15,7 +15,7 @@ Item {
readonly property Item current: currentPopout?.item ?? null readonly property Item current: currentPopout?.item ?? null
readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null
required property Item wrapper required property PopoutState popouts
implicitHeight: (currentPopout?.implicitHeight ?? 0) + 5 * 2 implicitHeight: (currentPopout?.implicitHeight ?? 0) + 5 * 2
implicitWidth: (currentPopout?.implicitWidth ?? 0) + 5 * 2 implicitWidth: (currentPopout?.implicitWidth ?? 0) + 5 * 2
@@ -49,40 +49,31 @@ Item {
Connections { Connections {
function onHasCurrentChanged(): void { function onHasCurrentChanged(): void {
if (root.wrapper.hasCurrent && trayMenu.shouldBeActive) { if (root.popouts.hasCurrent && trayMenu.shouldBeActive) {
trayMenu.sourceComponent = null; trayMenu.sourceComponent = null;
trayMenu.sourceComponent = trayMenuComponent; trayMenu.sourceComponent = trayMenuComponent;
} }
} }
target: root.wrapper target: root.popouts
} }
Component { Component {
id: trayMenuComponent id: trayMenuComponent
TrayMenuPopout { TrayMenuPopout {
popouts: root.wrapper popouts: root.popouts
trayItem: trayMenu.modelData.menu trayItem: trayMenu.modelData.menu
} }
} }
} }
} }
Popout {
name: "overview"
sourceComponent: OverviewPopout {
screen: root.wrapper.screen
wrapper: root.wrapper
}
}
Popout { Popout {
name: "upower" name: "upower"
sourceComponent: UPowerPopout { sourceComponent: UPowerPopout {
wrapper: root.wrapper wrapper: root.popouts
} }
} }
@@ -90,7 +81,7 @@ Item {
name: "network" name: "network"
sourceComponent: NetworkPopout { sourceComponent: NetworkPopout {
wrapper: root.wrapper wrapper: root.popouts
} }
} }
@@ -98,7 +89,7 @@ Item {
name: "updates" name: "updates"
sourceComponent: UpdatesPopout { sourceComponent: UpdatesPopout {
wrapper: root.wrapper wrapper: root.popouts
} }
} }
} }
@@ -107,12 +98,10 @@ Item {
id: popout id: popout
required property string name required property string name
readonly property bool shouldBeActive: root.wrapper.currentName === name readonly property bool shouldBeActive: root.popouts.currentName === name
active: false active: false
anchors.horizontalCenter: parent.horizontalCenter anchors.centerIn: parent
anchors.top: parent.top
anchors.topMargin: 5
opacity: 0 opacity: 0
scale: 0.8 scale: 0.8
+8
View File
@@ -0,0 +1,8 @@
import QtQuick
QtObject {
property string currentName
property bool hasCurrent
signal detachRequested(mode: string)
}
+27 -27
View File
@@ -9,15 +9,16 @@ import qs.Helpers
ColumnLayout { ColumnLayout {
id: root id: root
width: Math.min(parent ? parent.width : 600, 600)
spacing: 15 spacing: 15
width: Math.min(parent ? parent.width : 600, 600)
Rectangle { Rectangle {
id: previewContainer id: previewContainer
Layout.fillWidth: true
Layout.preferredHeight: width * (Quickshell.screens.length > 0 ? (Quickshell.screens[0].height / Math.max(1, Quickshell.screens[0].width)) : 9/16) Layout.fillHeight: true
Layout.preferredWidth: height * (Quickshell.screens.length > 0 ? (Quickshell.screens[0].height / Math.max(1, Quickshell.screens[0].width)) : 16 / 9)
clip: true clip: true
color: DynamicColors.surfaceContainer color: DynamicColors.palette.m3surfaceContainer
radius: Config.appearance.rounding.scale * 10 radius: Config.appearance.rounding.scale * 10
Image { Image {
@@ -30,51 +31,50 @@ ColumnLayout {
Rectangle { Rectangle {
id: cropRect id: cropRect
property real paintedWidth: img.paintedWidth > 0 ? img.paintedWidth : img.width property real cropHeight: (imageAspect > screenAspect ? paintedHeight : paintedWidth / screenAspect) / Config.background.zoom
property real cropWidth: (imageAspect > screenAspect ? paintedHeight * screenAspect : paintedWidth) / Config.background.zoom
property real imageAspect: Math.max(1, paintedWidth) / Math.max(1, paintedHeight)
property real paintedHeight: img.paintedHeight > 0 ? img.paintedHeight : img.height property real paintedHeight: img.paintedHeight > 0 ? img.paintedHeight : img.height
property real paintedWidth: img.paintedWidth > 0 ? img.paintedWidth : img.width
property real paintedX: (img.width - paintedWidth) / 2 property real paintedX: (img.width - paintedWidth) / 2
property real paintedY: (img.height - paintedHeight) / 2 property real paintedY: (img.height - paintedHeight) / 2
property real screenAspect: Quickshell.screens.length > 0 ? (Quickshell.screens[0].width / Math.max(1, Quickshell.screens[0].height)) : 16 / 9
property real screenAspect: Quickshell.screens.length > 0 ? (Quickshell.screens[0].width / Math.max(1, Quickshell.screens[0].height)) : 16/9
property real imageAspect: Math.max(1, paintedWidth) / Math.max(1, paintedHeight) border.color: DynamicColors.palette.m3primary
property real cropWidth: (imageAspect > screenAspect ? paintedHeight * screenAspect : paintedWidth) / Config.background.zoom
property real cropHeight: (imageAspect > screenAspect ? paintedHeight : paintedWidth / screenAspect) / Config.background.zoom
border.color: DynamicColors.primary
border.width: 2 border.width: 2
color: DynamicColors.primaryContainer.withAlpha(0.3) color: Qt.alpha(DynamicColors.palette.m3primaryContainer, 0.3)
width: cropWidth
height: cropHeight height: cropHeight
width: cropWidth
x: paintedX + (paintedWidth - width) * Config.background.alignX x: paintedX + (paintedWidth - width) * Config.background.alignX
y: paintedY + (paintedHeight - height) * Config.background.alignY y: paintedY + (paintedHeight - height) * Config.background.alignY
DragHandler { DragHandler {
target: null target: null
onActiveTranslationChanged: { onActiveTranslationChanged: {
if (active) { if (active) {
let newX = cropRect.x - cropRect.paintedX + translation.x; let newX = cropRect.x - cropRect.paintedX + translation.x;
let newY = cropRect.y - cropRect.paintedY + translation.y; let newY = cropRect.y - cropRect.paintedY + translation.y;
let rangeX = cropRect.paintedWidth - cropRect.width; let rangeX = cropRect.paintedWidth - cropRect.width;
let rangeY = cropRect.paintedHeight - cropRect.height; let rangeY = cropRect.paintedHeight - cropRect.height;
if (rangeX > 0) { if (rangeX > 0) {
let valX = newX / rangeX; let valX = newX / rangeX;
Config.background.alignX = Math.max(0.0, Math.min(1.0, valX)); Config.background.alignX = Math.max(0.0, Math.min(1.0, valX));
Config.save();
} }
if (rangeY > 0) { if (rangeY > 0) {
let valY = newY / rangeY; let valY = newY / rangeY;
Config.background.alignY = Math.max(0.0, Math.min(1.0, valY)); Config.background.alignY = Math.max(0.0, Math.min(1.0, valY));
Config.save();
} }
} }
} }
} }
PinchHandler { PinchHandler {
maximumScale: 5.0 maximumScale: 5.0
minimumScale: 1.0 minimumScale: 1.0
@@ -90,13 +90,13 @@ ColumnLayout {
} }
} }
} }
SettingSpinBox { SettingSpinBox {
max: 5.0
min: 1.0
name: "Zoom" name: "Zoom"
object: Config.background object: Config.background
setting: "zoom" setting: "zoom"
min: 1.0
max: 5.0
step: 0.1 step: 0.1
} }
} }
+4 -3
View File
@@ -1,18 +1,19 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import qs.Components
import qs.Config
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import qs.Components
import qs.Modules
import qs.Config
StackView { StackView {
id: root id: root
property int biggestWidth: 0 property int biggestWidth: 0
required property Item popouts required property PopoutState popouts
property int rootWidth: 0 property int rootWidth: 0
required property QsMenuHandle trayItem required property QsMenuHandle trayItem
+53 -76
View File
@@ -8,51 +8,57 @@ import qs.Config
Item { Item {
id: root id: root
property list<real> animCurve: MaterialEasing.emphasized property list<real> animCurve: Appearance.anim.curves.expressiveDefaultSpatial
property int animLength: MaterialEasing.emphasizedDecelTime property int animLength: Appearance.anim.durations.expressiveDefaultSpatial
readonly property Item current: content.item?.current ?? null readonly property alias content: content
readonly property Item current: (content.item as Content)?.current ?? null
property real currentCenter property real currentCenter
property string currentName property alias currentName: popoutState.currentName
property string detachedMode property string detachedMode
property bool hasCurrent property alias hasCurrent: popoutState.hasCurrent
readonly property bool isDetached: detachedMode.length > 0 readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight
readonly property real nonAnimHeight: hasCurrent ? children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight : 0
readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth
required property real offsetScale
property string queuedMode property string queuedMode
required property ShellScreen screen required property ShellScreen screen
function close(): void { function close(): void {
hasCurrent = false; hasCurrent = false;
animCurve = MaterialEasing.emphasizedDecel;
animLength = MaterialEasing.emphasizedDecelTime;
detachedMode = ""; detachedMode = "";
animCurve = MaterialEasing.emphasized;
} }
function detach(mode: string): void { function detach(mode: string): void {
animLength = 600; setAnims(true);
if (mode === "winfo") { if (mode === "winfo") {
detachedMode = mode; detachedMode = mode;
} else { } else {
detachedMode = "any";
queuedMode = mode; queuedMode = mode;
detachedMode = "any";
} }
setAnims(false);
focus = true; focus = true;
} }
clip: true function setAnims(detach: bool): void {
const type = `expressive${detach ? "Slow" : "Default"}Spatial`;
animLength = Appearance.anim.durations[type];
animCurve = Appearance.anim.curves[type];
}
focus: hasCurrent
implicitHeight: nonAnimHeight implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth implicitWidth: nonAnimWidth
visible: width > 0 && height > 0
Behavior on implicitHeight { Behavior on implicitHeight {
enabled: root.offsetScale < 1
Anim { Anim {
duration: root.animLength duration: root.animLength
easing.bezierCurve: root.animCurve easing.bezierCurve: root.animCurve
} }
} }
Behavior on implicitWidth { Behavior on implicitWidth {
enabled: root.implicitHeight > 0 enabled: root.offsetScale < 1
Anim { Anim {
duration: root.animLength duration: root.animLength
@@ -60,85 +66,56 @@ Item {
} }
} }
// Comp { PopoutState {
// shouldBeActive: root.detachedMode === "winfo" id: popoutState
// asynchronous: true
// anchors.centerIn: parent
//
// sourceComponent: WindowInfo {
// screen: root.screen
// client: Hypr.activeToplevel
// }
// }
// Comp {
// shouldBeActive: root.detachedMode === "any"
// asynchronous: true
// anchors.centerIn: parent
//
// sourceComponent: ControlCenter {
// screen: root.screen
// active: root.queuedMode
//
// function close(): void {
// root.close();
// }
// }
// }
Behavior on x {
enabled: root.implicitHeight > 0
Anim {
duration: root.animLength
easing.bezierCurve: root.animCurve
}
}
Behavior on y {
Anim {
duration: root.animLength
easing.bezierCurve: root.animCurve
}
}
Keys.onEscapePressed: close()
HyprlandFocusGrab {
active: root.isDetached
windows: [QsWindow.window]
onCleared: root.close()
}
Binding {
property: "WlrLayershell.keyboardFocus"
target: QsWindow.window
value: WlrKeyboardFocus.OnDemand
when: root.isDetached
} }
Comp { Comp {
id: content id: content
anchors.horizontalCenter: parent.horizontalCenter anchors.centerIn: parent
anchors.top: parent.top shouldBeActive: root.hasCurrent && !root.detachedMode
asynchronous: true
shouldBeActive: root.hasCurrent
sourceComponent: Content { sourceComponent: Content {
wrapper: root popouts: popoutState
} }
} }
// Comp {
// id: winfo
//
// anchors.centerIn: parent
// shouldBeActive: root.detachedMode === "winfo"
//
// sourceComponent: WindowInfo {
// client: Hypr.activeToplevel
// screen: root.screen
// }
// }
//
// Comp {
// id: controlCenter
//
// anchors.centerIn: parent
// shouldBeActive: root.detachedMode === "any"
//
// sourceComponent: ControlCenter {
// active: root.queuedMode
// screen: root.screen
//
// onClose: root.close()
// }
// }
component Comp: Loader { component Comp: Loader {
id: comp id: comp
property bool shouldBeActive property bool shouldBeActive
active: false active: false
asynchronous: true
opacity: 0 opacity: 0
// Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set
states: State { states: State {
name: "active" name: "active"
when: comp.shouldBeActive when: comp.shouldBeActive
+19
View File
@@ -0,0 +1,19 @@
qml_module(ZShell-blobs
URI ZShell.Blobs
SOURCES
blobgroup.cpp
blobshape.cpp
blobrect.cpp
blobinvertedrect.cpp
blobmaterial.cpp
LIBRARIES
Qt::Quick
)
qt_add_shaders(ZShell-blobs "blob_shaders"
BATCHABLE OPTIMIZED NOHLSL NOMSL
PREFIX "/"
FILES
shaders/blob.frag
shaders/blob.vert
)
+105
View File
@@ -0,0 +1,105 @@
#include "blobgroup.hpp"
#include "blobinvertedrect.hpp"
#include "blobshape.hpp"
BlobGroup::BlobGroup(QObject* parent)
: QObject(parent) {
}
BlobGroup::~BlobGroup() {
for (auto* shape : std::as_const(m_shapes))
shape->m_group = nullptr;
if (m_invertedRect)
static_cast<BlobShape*>(m_invertedRect)->m_group = nullptr;
}
void BlobGroup::setSmoothing(qreal s) {
if (qFuzzyCompare(m_smoothing, s))
return;
m_smoothing = s;
emit smoothingChanged();
markDirty();
}
void BlobGroup::setColor(const QColor& c) {
if (m_color == c)
return;
m_color = c;
emit colorChanged();
markDirty();
}
void BlobGroup::addShape(BlobShape* shape) {
if (!shape || m_shapes.contains(shape))
return;
m_shapes.append(shape);
markDirty();
}
void BlobGroup::removeShape(BlobShape* shape) {
m_shapes.removeOne(shape);
markDirty();
}
void BlobGroup::setInvertedRect(BlobInvertedRect* rect) {
if (m_invertedRect == rect)
return;
m_invertedRect = rect;
markDirty();
}
void BlobGroup::clearInvertedRect(BlobInvertedRect* rect) {
if (m_invertedRect != rect)
return;
m_invertedRect = nullptr;
markDirty();
}
void BlobGroup::markDirty() {
m_physicsUpdated = false;
for (auto* shape : std::as_const(m_shapes)) {
shape->polish();
shape->update();
}
if (m_invertedRect) {
static_cast<BlobShape*>(m_invertedRect)->polish();
static_cast<BlobShape*>(m_invertedRect)->update();
}
}
void BlobGroup::markShapeDirty(BlobShape* source) {
m_physicsUpdated = false;
source->polish();
source->update();
// Use cached padded rects to find spatial neighbors
const float pad = static_cast<float>(m_smoothing) * 2.0f;
const QRectF srcRect(static_cast<double>(source->m_cachedPaddedX - pad),
static_cast<double>(source->m_cachedPaddedY - pad), static_cast<double>(source->m_cachedPaddedW + pad * 2.0f),
static_cast<double>(source->m_cachedPaddedH + pad * 2.0f));
for (auto* shape : std::as_const(m_shapes)) {
if (shape == source)
continue;
const QRectF otherRect(static_cast<double>(shape->m_cachedPaddedX), static_cast<double>(shape->m_cachedPaddedY),
static_cast<double>(shape->m_cachedPaddedW), static_cast<double>(shape->m_cachedPaddedH));
if (srcRect.intersects(otherRect)) {
shape->polish();
shape->update();
}
}
if (m_invertedRect && static_cast<BlobShape*>(m_invertedRect) != source) {
static_cast<BlobShape*>(m_invertedRect)->polish();
static_cast<BlobShape*>(m_invertedRect)->update();
}
}
void BlobGroup::ensurePhysicsUpdated() {
if (m_physicsUpdated)
return;
m_physicsUpdated = true;
for (auto* shape : std::as_const(m_shapes))
shape->updatePhysics();
}
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include <qcolor.h>
#include <qlist.h>
#include <qobject.h>
#include <qqmlengine.h>
class BlobShape;
class BlobInvertedRect;
class BlobGroup : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY smoothingChanged)
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
public:
explicit BlobGroup(QObject* parent = nullptr);
~BlobGroup() override;
qreal smoothing() const {
return m_smoothing;
}
void setSmoothing(qreal s);
QColor color() const {
return m_color;
}
void setColor(const QColor& c);
void addShape(BlobShape* shape);
void removeShape(BlobShape* shape);
void setInvertedRect(BlobInvertedRect* rect);
void clearInvertedRect(BlobInvertedRect* rect);
const QList<BlobShape*>& shapes() const {
return m_shapes;
}
BlobInvertedRect* invertedRect() const {
return m_invertedRect;
}
void markDirty();
void markShapeDirty(BlobShape* source);
void ensurePhysicsUpdated();
signals:
void smoothingChanged();
void colorChanged();
private:
qreal m_smoothing = 32.0;
QColor m_color{ 0x44, 0x88, 0xff };
QList<BlobShape*> m_shapes;
BlobInvertedRect* m_invertedRect = nullptr;
bool m_physicsUpdated = false;
};
+185
View File
@@ -0,0 +1,185 @@
#include "blobinvertedrect.hpp"
#include "blobgroup.hpp"
#include "blobmaterial.hpp"
#include <qsggeometry.h>
#include <qsgnode.h>
#include <algorithm>
#include <cstring>
BlobInvertedRect::BlobInvertedRect(QQuickItem* parent)
: BlobShape(parent) {
}
static void setFrameIndices(quint16* idx) {
// Top strip: 0-1-4, 1-5-4
idx[0] = 0;
idx[1] = 1;
idx[2] = 4;
idx[3] = 1;
idx[4] = 5;
idx[5] = 4;
// Right strip: 1-2-5, 2-6-5
idx[6] = 1;
idx[7] = 2;
idx[8] = 5;
idx[9] = 2;
idx[10] = 6;
idx[11] = 5;
// Bottom strip: 2-3-6, 3-7-6
idx[12] = 2;
idx[13] = 3;
idx[14] = 6;
idx[15] = 3;
idx[16] = 7;
idx[17] = 6;
// Left strip: 3-0-7, 0-4-7
idx[18] = 3;
idx[19] = 0;
idx[20] = 7;
idx[21] = 0;
idx[22] = 4;
idx[23] = 7;
}
QSGNode* BlobInvertedRect::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
if (!m_group) {
delete oldNode;
return nullptr;
}
const float pad = static_cast<float>(m_group->smoothing());
// Compute inner hole boundary in local coords
// Inset past the inner border edge by 2x smoothing to cover the blend zone
const float inset = pad * 2.0f;
const float holeLeft = static_cast<float>(m_borderLeft) + inset;
const float holeTop = static_cast<float>(m_borderTop) + inset;
const float holeRight = static_cast<float>(width() - m_borderRight) - inset;
const float holeBot = static_cast<float>(height() - m_borderBottom) - inset;
// If the hole is too small or invalid, fall back to full quad
if (holeLeft >= holeRight || holeTop >= holeBot)
return BlobShape::updatePaintNode(oldNode, nullptr);
auto* node = static_cast<QSGGeometryNode*>(oldNode);
const bool needsRebuild = !node || node->geometry()->vertexCount() != 8;
if (needsRebuild) {
delete oldNode;
node = new QSGGeometryNode;
auto* geometry =
new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 8, 24, QSGGeometry::UnsignedShortType);
geometry->setDrawingMode(QSGGeometry::DrawTriangles);
node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry);
setFrameIndices(geometry->indexDataAsUShort());
auto* material = new BlobMaterial;
material->setFlag(QSGMaterial::Blending);
node->setMaterial(material);
node->setFlag(QSGNode::OwnsMaterial);
}
// Outer bounds (local coords)
const float x0 = static_cast<float>(m_localPaddedRect.x());
const float y0 = static_cast<float>(m_localPaddedRect.y());
const float x1 = x0 + static_cast<float>(m_localPaddedRect.width());
const float y1 = y0 + static_cast<float>(m_localPaddedRect.height());
const float w = x1 - x0;
const float h = y1 - y0;
// Update vertex positions and texture coordinates
auto* v = node->geometry()->vertexDataAsTexturedPoint2D();
// Outer corners
v[0].set(x0, y0, 0.0f, 0.0f);
v[1].set(x1, y0, 1.0f, 0.0f);
v[2].set(x1, y1, 1.0f, 1.0f);
v[3].set(x0, y1, 0.0f, 1.0f);
// Inner corners (hole)
v[4].set(holeLeft, holeTop, (holeLeft - x0) / w, (holeTop - y0) / h);
v[5].set(holeRight, holeTop, (holeRight - x0) / w, (holeTop - y0) / h);
v[6].set(holeRight, holeBot, (holeRight - x0) / w, (holeBot - y0) / h);
v[7].set(holeLeft, holeBot, (holeLeft - x0) / w, (holeBot - y0) / h);
node->markDirty(QSGNode::DirtyGeometry);
// Update material uniforms
auto* material = static_cast<BlobMaterial*>(node->material());
material->m_paddedX = m_cachedPaddedX;
material->m_paddedY = m_cachedPaddedY;
material->m_paddedW = m_cachedPaddedW;
material->m_paddedH = m_cachedPaddedH;
material->m_smoothFactor = pad;
material->m_myIndex = m_cachedMyIndex;
material->m_color = m_group->color();
material->m_hasInverted = m_cachedHasInverted ? 1 : 0;
material->m_invertedRadius = m_cachedInvertedRadius;
memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter));
memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner));
const int count = static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16)));
material->m_rectCount = count;
for (int i = 0; i < count; ++i)
material->m_rects[i] = m_cachedRects[i];
node->markDirty(QSGNode::DirtyMaterial);
return node;
}
BlobInvertedRect::~BlobInvertedRect() {
if (m_group)
m_group->clearInvertedRect(this);
}
void BlobInvertedRect::setBorderLeft(qreal v) {
if (qFuzzyCompare(m_borderLeft, v))
return;
m_borderLeft = v;
emit borderLeftChanged();
if (m_group)
m_group->markDirty();
}
void BlobInvertedRect::setBorderRight(qreal v) {
if (qFuzzyCompare(m_borderRight, v))
return;
m_borderRight = v;
emit borderRightChanged();
if (m_group)
m_group->markDirty();
}
void BlobInvertedRect::setBorderTop(qreal v) {
if (qFuzzyCompare(m_borderTop, v))
return;
m_borderTop = v;
emit borderTopChanged();
if (m_group)
m_group->markDirty();
}
void BlobInvertedRect::setBorderBottom(qreal v) {
if (qFuzzyCompare(m_borderBottom, v))
return;
m_borderBottom = v;
emit borderBottomChanged();
if (m_group)
m_group->markDirty();
}
void BlobInvertedRect::registerWithGroup() {
if (m_group)
m_group->setInvertedRect(this);
}
void BlobInvertedRect::unregisterFromGroup() {
if (m_group)
m_group->clearInvertedRect(this);
}
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include "blobshape.hpp"
#include <qqmlengine.h>
class BlobInvertedRect : public BlobShape {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY borderLeftChanged)
Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY borderRightChanged)
Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY borderTopChanged)
Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY borderBottomChanged)
public:
explicit BlobInvertedRect(QQuickItem* parent = nullptr);
~BlobInvertedRect() override;
qreal borderLeft() const {
return m_borderLeft;
}
void setBorderLeft(qreal v);
qreal borderRight() const {
return m_borderRight;
}
void setBorderRight(qreal v);
qreal borderTop() const {
return m_borderTop;
}
void setBorderTop(qreal v);
qreal borderBottom() const {
return m_borderBottom;
}
void setBorderBottom(qreal v);
signals:
void borderLeftChanged();
void borderRightChanged();
void borderTopChanged();
void borderBottomChanged();
protected:
bool isInvertedRect() const override {
return true;
}
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override;
void registerWithGroup() override;
void unregisterFromGroup() override;
private:
qreal m_borderLeft = 0;
qreal m_borderRight = 0;
qreal m_borderTop = 0;
qreal m_borderBottom = 0;
};
+96
View File
@@ -0,0 +1,96 @@
#include "blobmaterial.hpp"
#include <cstring>
QSGMaterialType* BlobMaterial::type() const {
static QSGMaterialType s_type;
return &s_type;
}
QSGMaterialShader* BlobMaterial::createShader(QSGRendererInterface::RenderMode) const {
return new BlobMaterialShader;
}
int BlobMaterial::compare(const QSGMaterial* other) const {
if (this < other)
return -1;
if (this > other)
return 1;
return 0;
}
BlobMaterialShader::BlobMaterialShader() {
setShaderFileName(VertexStage, QStringLiteral(":/shaders/blob.vert.qsb"));
setShaderFileName(FragmentStage, QStringLiteral(":/shaders/blob.frag.qsb"));
}
bool BlobMaterialShader::updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) {
Q_UNUSED(oldMaterial);
auto* mat = static_cast<BlobMaterial*>(newMaterial);
QByteArray* buf = state.uniformData();
Q_ASSERT(buf->size() >= 1440);
if (state.isMatrixDirty()) {
const QMatrix4x4 m = state.combinedMatrix();
memcpy(buf->data(), m.constData(), 64);
}
if (state.isOpacityDirty()) {
const float opacity = state.opacity();
memcpy(buf->data() + 64, &opacity, 4);
}
// Padded rect (offset 68)
memcpy(buf->data() + 68, &mat->m_paddedX, 4);
memcpy(buf->data() + 72, &mat->m_paddedY, 4);
memcpy(buf->data() + 76, &mat->m_paddedW, 4);
memcpy(buf->data() + 80, &mat->m_paddedH, 4);
// Smooth factor (offset 84)
memcpy(buf->data() + 84, &mat->m_smoothFactor, 4);
// Rect count (offset 88)
memcpy(buf->data() + 88, &mat->m_rectCount, 4);
// My index (offset 92)
memcpy(buf->data() + 92, &mat->m_myIndex, 4);
// Color as vec4 (offset 96, 16 bytes)
const float color[4] = {
static_cast<float>(mat->m_color.redF()),
static_cast<float>(mat->m_color.greenF()),
static_cast<float>(mat->m_color.blueF()),
static_cast<float>(mat->m_color.alphaF()),
};
memcpy(buf->data() + 96, color, 16);
// Has inverted (offset 112)
memcpy(buf->data() + 112, &mat->m_hasInverted, 4);
// Inverted radius (offset 116)
memcpy(buf->data() + 116, &mat->m_invertedRadius, 4);
// Padding at 120-127 (skip)
// Inverted outer (offset 128, 16 bytes)
memcpy(buf->data() + 128, mat->m_invertedOuter, 16);
// Inverted inner (offset 144, 16 bytes)
memcpy(buf->data() + 144, mat->m_invertedInner, 16);
// Rect data (offset 160, each rect = 5 vec4s = 80 bytes)
const int count = qMin(mat->m_rectCount, 16);
for (int i = 0; i < count; ++i) {
const auto& r = mat->m_rects[i];
const int base = 160 + i * 80;
const float d0[4] = { r.cx, r.cy, r.hw, r.hh };
const float d1[4] = { 0.0f, r.offsetX, r.offsetY, r.minEig };
const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f };
memcpy(buf->data() + base, d0, 16);
memcpy(buf->data() + base + 16, d1, 16);
memcpy(buf->data() + base + 32, r.invDeform, 16);
memcpy(buf->data() + base + 48, d3, 16);
memcpy(buf->data() + base + 64, r.radius, 16);
}
return true;
}
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include <qcolor.h>
#include <qsgmaterial.h>
#include <qsgmaterialshader.h>
struct BlobRectData {
float cx = 0, cy = 0, hw = 0, hh = 0;
float offsetX = 0, offsetY = 0;
float minEig = 1.0f;
// Inverse of 2x2 deformation matrix, column-major for GLSL
float invDeform[4] = { 1, 0, 0, 1 };
// Screen-space AABB half-extents of the deformed rect
float screenHalfX = 0, screenHalfY = 0;
// Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU
float radius[4] = { 0, 0, 0, 0 };
};
class BlobMaterial : public QSGMaterial {
public:
QSGMaterialType* type() const override;
QSGMaterialShader* createShader(QSGRendererInterface::RenderMode) const override;
int compare(const QSGMaterial* other) const override;
float m_paddedX = 0;
float m_paddedY = 0;
float m_paddedW = 0;
float m_paddedH = 0;
float m_smoothFactor = 32.0f;
int m_rectCount = 0;
int m_myIndex = -2;
QColor m_color{ 0x44, 0x88, 0xff };
int m_hasInverted = 0;
float m_invertedRadius = 0;
float m_invertedOuter[4] = {};
float m_invertedInner[4] = {};
BlobRectData m_rects[16] = {};
};
class BlobMaterialShader : public QSGMaterialShader {
public:
BlobMaterialShader();
bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) override;
};
+245
View File
@@ -0,0 +1,245 @@
#include "blobrect.hpp"
#include "blobgroup.hpp"
#include <algorithm>
#include <cmath>
BlobRect::BlobRect(QQuickItem* parent)
: BlobShape(parent) {
}
BlobRect::~BlobRect() {
if (m_group)
m_group->removeShape(this);
}
void BlobRect::updatePolish() {
BlobShape::updatePolish();
if (m_physicsActive) {
// Check if deformation is visually imperceptible
float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f);
float totalVel = std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
if (totalDelta < 0.004f && totalVel < 0.05f) {
// Snap to rest, no visible deformation
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_deformMatrix = QMatrix4x4();
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
m_physicsActive = false;
} else {
QMetaObject::invokeMethod(
this,
[this]() {
if (m_physicsActive && m_group)
m_group->markDirty();
},
Qt::QueuedConnection);
}
}
}
void BlobRect::updatePhysics() {
const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0));
if (!m_hasPrevPos) {
m_prevScenePos = scenePos;
m_elapsed.start();
m_hasPrevPos = true;
return;
}
const float dt = static_cast<float>(m_elapsed.restart()) / 1000.0f;
if (dt > 0.1f || dt < 0.001f) {
m_prevScenePos = scenePos;
// Still check atRest on skipped frames to avoid getting stuck
if (m_physicsActive)
checkAtRest(0.0f);
return;
}
const float velX = static_cast<float>(scenePos.x() - m_prevScenePos.x()) / dt;
const float velY = static_cast<float>(scenePos.y() - m_prevScenePos.y()) / dt;
m_prevScenePos = scenePos;
const float speed = std::sqrt(velX * velX + velY * velY);
if (!m_physicsActive) {
if (speed < 5.0f)
return;
m_physicsActive = true;
}
// Compute target deformation matrix from velocity
// R(θ) * diag(stretch, compress) * R(θ)^T
const float kStretchFactor = static_cast<float>(m_deformScale);
constexpr float kMaxStretch = 0.35f;
float target00 = 1.0f;
float target01 = 0.0f;
float target11 = 1.0f;
if (speed > 5.0f) {
const float targetStretch = 1.0f + std::min(speed * kStretchFactor, kMaxStretch);
const float targetCompress = 1.0f / targetStretch;
const float cosA = velX / speed;
const float sinA = velY / speed;
const float cos2 = cosA * cosA;
const float sin2 = sinA * sinA;
const float cs = cosA * sinA;
target00 = targetStretch * cos2 + targetCompress * sin2;
target01 = (targetStretch - targetCompress) * cs;
target11 = targetStretch * sin2 + targetCompress * cos2;
}
// Underdamped spring on each matrix component
const float kStiffness = static_cast<float>(m_stiffness);
const float kDamping = static_cast<float>(m_damping);
const float accel00 = -kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00;
m_dmVel00 += accel00 * dt;
m_dm00 += m_dmVel00 * dt;
const float accel01 = -kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01;
m_dmVel01 += accel01 * dt;
m_dm01 += m_dmVel01 * dt;
const float accel11 = -kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11;
m_dmVel11 += accel11 * dt;
m_dm11 += m_dmVel11 * dt;
m_deformMatrix = QMatrix4x4(m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
checkAtRest(speed);
}
void BlobRect::setTopLeftRadius(qreal r) {
if (!qFuzzyCompare(m_topLeftRadius, r)) {
m_topLeftRadius = r;
emit topLeftRadiusChanged();
if (m_group)
m_group->markDirty();
}
}
void BlobRect::setTopRightRadius(qreal r) {
if (!qFuzzyCompare(m_topRightRadius, r)) {
m_topRightRadius = r;
emit topRightRadiusChanged();
if (m_group)
m_group->markDirty();
}
}
void BlobRect::setBottomLeftRadius(qreal r) {
if (!qFuzzyCompare(m_bottomLeftRadius, r)) {
m_bottomLeftRadius = r;
emit bottomLeftRadiusChanged();
if (m_group)
m_group->markDirty();
}
}
void BlobRect::setBottomRightRadius(qreal r) {
if (!qFuzzyCompare(m_bottomRightRadius, r)) {
m_bottomRightRadius = r;
emit bottomRightRadiusChanged();
if (m_group)
m_group->markDirty();
}
}
void BlobRect::cornerRadii(float out[4]) const {
const auto base = static_cast<float>(m_radius);
out[0] = m_topRightRadius >= 0 ? static_cast<float>(m_topRightRadius) : base;
out[1] = m_bottomRightRadius >= 0 ? static_cast<float>(m_bottomRightRadius) : base;
out[2] = m_bottomLeftRadius >= 0 ? static_cast<float>(m_bottomLeftRadius) : base;
out[3] = m_topLeftRadius >= 0 ? static_cast<float>(m_topLeftRadius) : base;
}
bool BlobRect::isExcluded(const BlobShape* other) const {
for (const auto& ptr : m_exclude) {
if (ptr == other)
return true;
}
return false;
}
QQmlListProperty<BlobRect> BlobRect::exclude() {
return QQmlListProperty<BlobRect>(
this, nullptr, &excludeAppend, &excludeCount, &excludeAt, &excludeClear, &excludeReplace, &excludeRemoveLast);
}
void BlobRect::excludeAppend(QQmlListProperty<BlobRect>* prop, BlobRect* rect) {
auto* self = static_cast<BlobRect*>(prop->object);
self->m_exclude.append(rect);
if (self->m_group)
self->m_group->markDirty();
emit self->excludeChanged();
}
qsizetype BlobRect::excludeCount(QQmlListProperty<BlobRect>* prop) {
auto* self = static_cast<BlobRect*>(prop->object);
return self->m_exclude.size();
}
BlobRect* BlobRect::excludeAt(QQmlListProperty<BlobRect>* prop, qsizetype index) {
auto* self = static_cast<BlobRect*>(prop->object);
return self->m_exclude.at(index);
}
void BlobRect::excludeClear(QQmlListProperty<BlobRect>* prop) {
auto* self = static_cast<BlobRect*>(prop->object);
if (self->m_exclude.isEmpty())
return;
self->m_exclude.clear();
if (self->m_group)
self->m_group->markDirty();
emit self->excludeChanged();
}
void BlobRect::excludeReplace(QQmlListProperty<BlobRect>* prop, qsizetype index, BlobRect* rect) {
auto* self = static_cast<BlobRect*>(prop->object);
self->m_exclude[index] = rect;
if (self->m_group)
self->m_group->markDirty();
emit self->excludeChanged();
}
void BlobRect::excludeRemoveLast(QQmlListProperty<BlobRect>* prop) {
auto* self = static_cast<BlobRect*>(prop->object);
if (self->m_exclude.isEmpty())
return;
self->m_exclude.removeLast();
if (self->m_group)
self->m_group->markDirty();
emit self->excludeChanged();
}
void BlobRect::checkAtRest(float speed) {
constexpr float kEpsilon = 0.002f;
const bool atRest = std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon &&
std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon &&
std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && speed < 5.0f;
if (atRest) {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_dmVel00 = 0.0f;
m_dmVel01 = 0.0f;
m_dmVel11 = 0.0f;
m_deformMatrix = QMatrix4x4(); // identity
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
m_physicsActive = false;
}
}
+140
View File
@@ -0,0 +1,140 @@
#pragma once
#include "blobshape.hpp"
#include <qelapsedtimer.h>
#include <qpointer.h>
#include <qqmlengine.h>
#include <qqmllist.h>
class BlobRect : public BlobShape {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY stiffnessChanged)
Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged)
Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY deformScaleChanged)
Q_PROPERTY(QQmlListProperty<BlobRect> exclude READ exclude NOTIFY excludeChanged)
Q_PROPERTY(qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged)
Q_PROPERTY(qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY topRightRadiusChanged)
Q_PROPERTY(qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius NOTIFY bottomLeftRadiusChanged)
Q_PROPERTY(
qreal bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius NOTIFY bottomRightRadiusChanged)
public:
explicit BlobRect(QQuickItem* parent = nullptr);
~BlobRect() override;
qreal stiffness() const {
return m_stiffness;
}
void setStiffness(qreal s) {
if (!qFuzzyCompare(m_stiffness, s)) {
m_stiffness = s;
emit stiffnessChanged();
}
}
qreal damping() const {
return m_damping;
}
void setDamping(qreal d) {
if (!qFuzzyCompare(m_damping, d)) {
m_damping = d;
emit dampingChanged();
}
}
qreal deformScale() const {
return m_deformScale;
}
void setDeformScale(qreal s) {
if (!qFuzzyCompare(m_deformScale, s)) {
m_deformScale = s;
emit deformScaleChanged();
}
}
QQmlListProperty<BlobRect> exclude();
bool isExcluded(const BlobShape* other) const override;
void cornerRadii(float out[4]) const override;
qreal topLeftRadius() const {
return m_topLeftRadius;
}
void setTopLeftRadius(qreal r);
qreal topRightRadius() const {
return m_topRightRadius;
}
void setTopRightRadius(qreal r);
qreal bottomLeftRadius() const {
return m_bottomLeftRadius;
}
void setBottomLeftRadius(qreal r);
qreal bottomRightRadius() const {
return m_bottomRightRadius;
}
void setBottomRightRadius(qreal r);
signals:
void stiffnessChanged();
void dampingChanged();
void deformScaleChanged();
void excludeChanged();
void topLeftRadiusChanged();
void topRightRadiusChanged();
void bottomLeftRadiusChanged();
void bottomRightRadiusChanged();
protected:
void updatePolish() override;
void updatePhysics() override;
private:
void checkAtRest(float speed);
// Physics state
QPointF m_prevScenePos;
QElapsedTimer m_elapsed;
bool m_physicsActive = false;
bool m_hasPrevPos = false;
// Symmetric 2x2 deformation matrix components (3 independent: m00, m01,
// m11) Rest state is identity: m00=1, m01=0, m11=1
float m_dm00 = 1.0f;
float m_dm01 = 0.0f;
float m_dm11 = 1.0f;
// Spring velocities for each component
float m_dmVel00 = 0.0f;
float m_dmVel01 = 0.0f;
float m_dmVel11 = 0.0f;
qreal m_stiffness = 200.0;
qreal m_damping = 16.0;
qreal m_deformScale = 0.0005;
qreal m_topLeftRadius = -1;
qreal m_topRightRadius = -1;
qreal m_bottomLeftRadius = -1;
qreal m_bottomRightRadius = -1;
QList<QPointer<BlobRect> > m_exclude;
static void excludeAppend(QQmlListProperty<BlobRect>* prop, BlobRect* rect);
static qsizetype excludeCount(QQmlListProperty<BlobRect>* prop);
static BlobRect* excludeAt(QQmlListProperty<BlobRect>* prop, qsizetype index);
static void excludeClear(QQmlListProperty<BlobRect>* prop);
static void excludeReplace(QQmlListProperty<BlobRect>* prop, qsizetype index, BlobRect* rect);
static void excludeRemoveLast(QQmlListProperty<BlobRect>* prop);
};
+367
View File
@@ -0,0 +1,367 @@
#include "blobshape.hpp"
#include "blobgroup.hpp"
#include "blobinvertedrect.hpp"
#include <qsggeometry.h>
#include <qsgnode.h>
#include <algorithm>
#include <cmath>
static float deformPadding(const QMatrix4x4& dm, float hw, float hh) {
// Bounding box of the deformed shape: |M * corners|
const float dm00 = dm(0, 0), dm01 = dm(0, 1);
const float dm10 = dm(1, 0), dm11 = dm(1, 1);
const float boundX = std::abs(dm00) * hw + std::abs(dm01) * hh;
const float boundY = std::abs(dm10) * hw + std::abs(dm11) * hh;
const float extraX = std::max(boundX - hw, 0.0f) + std::abs(dm(0, 3));
const float extraY = std::max(boundY - hh, 0.0f) + std::abs(dm(1, 3));
return std::max(extraX, extraY);
}
static float cpuSdBox(float px, float py, float cx, float cy, float hw, float hh) {
const float dx = std::abs(px - cx) - hw;
const float dy = std::abs(py - cy) - hh;
const float mdx = std::max(dx, 0.0f);
const float mdy = std::max(dy, 0.0f);
return std::sqrt(mdx * mdx + mdy * mdy) + std::min(std::max(dx, dy), 0.0f);
}
static float cpuSmoothstep(float edge0, float edge1, float x) {
const float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t);
}
BlobShape::BlobShape(QQuickItem* parent)
: QQuickItem(parent) {
setFlag(ItemHasContents);
}
void BlobShape::setGroup(BlobGroup* g) {
if (m_group == g)
return;
if (m_group && isComponentComplete())
unregisterFromGroup();
m_group = g;
if (m_group && isComponentComplete())
registerWithGroup();
emit groupChanged();
if (m_group)
m_group->markDirty();
}
void BlobShape::setRadius(qreal r) {
if (qFuzzyCompare(m_radius, r))
return;
m_radius = r;
emit radiusChanged();
if (m_group)
m_group->markDirty();
}
void BlobShape::componentComplete() {
QQuickItem::componentComplete();
if (m_group)
registerWithGroup();
}
void BlobShape::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) {
QQuickItem::geometryChange(newGeometry, oldGeometry);
updateCenteredDeformMatrix();
if (m_group) {
// Accumulate sub-pixel drift so slow movements don't desync the shader
m_pendingDx += static_cast<float>(newGeometry.x() - oldGeometry.x());
m_pendingDy += static_cast<float>(newGeometry.y() - oldGeometry.y());
const auto dw = std::abs(newGeometry.width() - oldGeometry.width());
const auto dh = std::abs(newGeometry.height() - oldGeometry.height());
if (std::abs(m_pendingDx) > 0.5f || std::abs(m_pendingDy) > 0.5f || dw > 0.5 || dh > 0.5) {
m_pendingDx = 0;
m_pendingDy = 0;
m_group->markShapeDirty(this);
}
}
}
void BlobShape::updateCenteredDeformMatrix() {
const auto cx = static_cast<float>(width()) * 0.5f;
const auto cy = static_cast<float>(height()) * 0.5f;
QMatrix4x4 result;
result.translate(cx, cy);
result *= m_deformMatrix;
result.translate(-cx, -cy);
if (m_centeredDeformMatrix != result) {
m_centeredDeformMatrix = result;
emit deformMatrixChanged();
}
}
void BlobShape::cornerRadii(float out[4]) const {
const auto r = static_cast<float>(m_radius);
out[0] = r;
out[1] = r;
out[2] = r;
out[3] = r;
}
void BlobShape::registerWithGroup() {
if (m_group)
m_group->addShape(this);
}
void BlobShape::unregisterFromGroup() {
if (m_group)
m_group->removeShape(this);
}
void BlobShape::updatePolish() {
if (!m_group)
return;
// Ensure all shapes have up-to-date physics (only once per frame)
m_group->ensurePhysicsUpdated();
const QPointF scenePos = mapToScene(QPointF(0, 0));
const float pad = static_cast<float>(m_group->smoothing());
if (isInvertedRect()) {
m_cachedPaddedX = static_cast<float>(scenePos.x());
m_cachedPaddedY = static_cast<float>(scenePos.y());
m_cachedPaddedW = static_cast<float>(width());
m_cachedPaddedH = static_cast<float>(height());
m_localPaddedRect = QRectF(0, 0, width(), height());
} else {
const float hw = static_cast<float>(width()) * 0.5f;
const float hh = static_cast<float>(height()) * 0.5f;
const float totalPad = pad + deformPadding(m_deformMatrix, hw, hh);
m_cachedPaddedX = static_cast<float>(scenePos.x()) - totalPad;
m_cachedPaddedY = static_cast<float>(scenePos.y()) - totalPad;
m_cachedPaddedW = static_cast<float>(width()) + 2.0f * totalPad;
m_cachedPaddedH = static_cast<float>(height()) + 2.0f * totalPad;
m_localPaddedRect = QRectF(static_cast<double>(-totalPad), static_cast<double>(-totalPad),
width() + 2.0 * static_cast<double>(totalPad), height() + 2.0 * static_cast<double>(totalPad));
}
// Filter nearby normal rects
m_cachedRects.clear();
m_cachedMyIndex = -2;
const QRectF myPadded(static_cast<double>(m_cachedPaddedX), static_cast<double>(m_cachedPaddedY),
static_cast<double>(m_cachedPaddedW), static_cast<double>(m_cachedPaddedH));
for (BlobShape* other : m_group->shapes()) {
if (other->isInvertedRect())
continue;
// Skip zero-size rects
if (other->width() <= 0 || other->height() <= 0)
continue;
if (isExcluded(other))
continue;
const QPointF otherScene = other->mapToScene(QPointF(0, 0));
bool include = false;
if (isInvertedRect()) {
include = true;
} else {
const float otherHW = static_cast<float>(other->width()) * 0.5f;
const float otherHH = static_cast<float>(other->height()) * 0.5f;
const float otherPad = pad + deformPadding(other->m_deformMatrix, otherHW, otherHH);
const QRectF otherPadded(otherScene.x() - static_cast<double>(otherPad),
otherScene.y() - static_cast<double>(otherPad), other->width() + 2.0 * static_cast<double>(otherPad),
other->height() + 2.0 * static_cast<double>(otherPad));
include = myPadded.intersects(otherPadded);
}
if (include) {
if (other == this)
m_cachedMyIndex = static_cast<int>(m_cachedRects.size());
const QMatrix4x4& dm = other->m_deformMatrix;
const float a = dm(0, 0), b = dm(1, 0);
const float c = dm(0, 1), d = dm(1, 1);
BlobRectData r;
r.cx = static_cast<float>(otherScene.x() + other->width() / 2.0);
r.cy = static_cast<float>(otherScene.y() + other->height() / 2.0);
r.hw = static_cast<float>(other->width() / 2.0);
r.hh = static_cast<float>(other->height() / 2.0);
other->cornerRadii(r.radius);
r.offsetX = dm(0, 3);
r.offsetY = dm(1, 3);
// Pre-compute inverse deformation matrix
const float det = a * d - c * b;
const float invDet = std::abs(det) > 1e-6f ? 1.0f / det : 1.0f;
r.invDeform[0] = d * invDet;
r.invDeform[1] = -b * invDet;
r.invDeform[2] = -c * invDet;
r.invDeform[3] = a * invDet;
// Pre-compute minimum eigenvalue (avoids per-pixel sqrt)
const float halfTr = 0.5f * (a + d);
const float halfDiff = 0.5f * (a - d);
r.minEig = halfTr - std::sqrt(halfDiff * halfDiff + c * c);
// Pre-compute screen-space AABB half-extents
r.screenHalfX = std::abs(a) * r.hw + std::abs(c) * r.hh;
r.screenHalfY = std::abs(b) * r.hw + std::abs(d) * r.hh;
m_cachedRects.append(r);
}
}
if (isInvertedRect())
m_cachedMyIndex = -1;
// Cache inverted rect data
m_cachedHasInverted = false;
m_cachedInvertedRadius = 0;
memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter));
memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner));
auto* inv = m_group->invertedRect();
if (inv) {
const QPointF invScene = inv->mapToScene(QPointF(0, 0));
const float outerCX = static_cast<float>(invScene.x() + inv->width() / 2.0);
const float outerCY = static_cast<float>(invScene.y() + inv->height() / 2.0);
const float outerHW = static_cast<float>(inv->width() / 2.0);
const float outerHH = static_cast<float>(inv->height() / 2.0);
const float innerCX = outerCX + static_cast<float>((inv->borderLeft() - inv->borderRight()) / 2.0);
const float innerCY = outerCY + static_cast<float>((inv->borderTop() - inv->borderBottom()) / 2.0);
const float innerHW = outerHW - static_cast<float>((inv->borderLeft() + inv->borderRight()) / 2.0);
const float innerHH = outerHH - static_cast<float>((inv->borderTop() + inv->borderBottom()) / 2.0);
// Check if this rect is near the border (within 2x smoothing of inner edge)
bool nearBorder = isInvertedRect();
if (!nearBorder) {
const float margin = pad * 2.0f;
const float myCX = m_cachedPaddedX + m_cachedPaddedW * 0.5f;
const float myCY = m_cachedPaddedY + m_cachedPaddedH * 0.5f;
const float myHW = m_cachedPaddedW * 0.5f;
const float myHH = m_cachedPaddedH * 0.5f;
// Near border if any edge of padded rect is within margin of inner edge
nearBorder = (myCX - myHW < innerCX - innerHW + margin) || (myCX + myHW > innerCX + innerHW - margin) ||
(myCY - myHH < innerCY - innerHH + margin) || (myCY + myHH > innerCY + innerHH - margin);
}
if (nearBorder) {
m_cachedHasInverted = true;
m_cachedInvertedRadius = static_cast<float>(inv->radius());
m_cachedInvertedOuter[0] = outerCX;
m_cachedInvertedOuter[1] = outerCY;
m_cachedInvertedOuter[2] = outerHW;
m_cachedInvertedOuter[3] = outerHH;
m_cachedInvertedInner[0] = innerCX;
m_cachedInvertedInner[1] = innerCY;
m_cachedInvertedInner[2] = innerHW;
m_cachedInvertedInner[3] = innerHH;
}
}
// Pre-compute effective per-corner radii (moves O(N²) work from GPU to CPU)
const float smoothFactor = pad;
constexpr float minR = 2.0f;
const auto rectCount = m_cachedRects.size();
for (qsizetype i = 0; i < rectCount; ++i) {
auto& ri = m_cachedRects[i];
float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f;
const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh;
const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh;
const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh;
const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh;
for (qsizetype j = 0; j < rectCount; ++j) {
if (j == i)
continue;
const auto& rj = m_cachedRects[j];
fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh)));
fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh)));
fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh)));
fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh)));
}
if (m_cachedHasInverted) {
const float icx = m_cachedInvertedInner[0];
const float icy = m_cachedInvertedInner[1];
const float ihw = m_cachedInvertedInner[2];
const float ihh = m_cachedInvertedInner[3];
fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh)));
fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh)));
fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh)));
fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh)));
}
// Combine base radii with fill factors into effective per-corner radii
ri.radius[0] = std::max(ri.radius[0] * fTr, minR);
ri.radius[1] = std::max(ri.radius[1] * fBr, minR);
ri.radius[2] = std::max(ri.radius[2] * fBl, minR);
ri.radius[3] = std::max(ri.radius[3] * fTl, minR);
}
}
QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
if (!m_group) {
delete oldNode;
return nullptr;
}
auto* node = static_cast<QSGGeometryNode*>(oldNode);
if (!node) {
node = new QSGGeometryNode;
auto* geometry = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4);
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry);
auto* material = new BlobMaterial;
material->setFlag(QSGMaterial::Blending);
node->setMaterial(material);
node->setFlag(QSGNode::OwnsMaterial);
}
// Update geometry
auto* geometry = node->geometry();
auto* v = geometry->vertexDataAsTexturedPoint2D();
const float x0 = static_cast<float>(m_localPaddedRect.x());
const float y0 = static_cast<float>(m_localPaddedRect.y());
const float x1 = x0 + static_cast<float>(m_localPaddedRect.width());
const float y1 = y0 + static_cast<float>(m_localPaddedRect.height());
v[0].set(x0, y0, 0.0f, 0.0f);
v[1].set(x1, y0, 1.0f, 0.0f);
v[2].set(x0, y1, 0.0f, 1.0f);
v[3].set(x1, y1, 1.0f, 1.0f);
node->markDirty(QSGNode::DirtyGeometry);
// Update material
auto* material = static_cast<BlobMaterial*>(node->material());
material->m_paddedX = m_cachedPaddedX;
material->m_paddedY = m_cachedPaddedY;
material->m_paddedW = m_cachedPaddedW;
material->m_paddedH = m_cachedPaddedH;
material->m_smoothFactor = static_cast<float>(m_group->smoothing());
material->m_myIndex = m_cachedMyIndex;
material->m_color = m_group->color();
material->m_hasInverted = m_cachedHasInverted ? 1 : 0;
material->m_invertedRadius = m_cachedInvertedRadius;
memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter));
memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner));
const int count = static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16)));
material->m_rectCount = count;
for (int i = 0; i < count; ++i)
material->m_rects[i] = m_cachedRects[i];
node->markDirty(QSGNode::DirtyMaterial);
return node;
}
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include "blobmaterial.hpp"
#include <qmatrix4x4.h>
#include <qquickitem.h>
#include <qvector.h>
class BlobGroup;
class BlobShape : public QQuickItem {
Q_OBJECT
Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged)
Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged)
Q_PROPERTY(QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged)
Q_PROPERTY(QMatrix4x4 rawDeformMatrix READ rawDeformMatrix NOTIFY rawDeformMatrixChanged)
friend class BlobGroup;
public:
explicit BlobShape(QQuickItem* parent = nullptr);
~BlobShape() override = default;
BlobGroup* group() const {
return m_group;
}
void setGroup(BlobGroup* g);
qreal radius() const {
return m_radius;
}
void setRadius(qreal r);
QMatrix4x4 deformMatrix() const {
return m_centeredDeformMatrix;
}
QMatrix4x4 rawDeformMatrix() const {
return m_deformMatrix;
}
signals:
void groupChanged();
void radiusChanged();
void deformMatrixChanged();
void rawDeformMatrixChanged();
protected:
void componentComplete() override;
void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override;
void updatePolish() override;
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override;
virtual bool isInvertedRect() const {
return false;
}
virtual bool isExcluded(const BlobShape* /*other*/) const {
return false;
}
virtual void cornerRadii(float out[4]) const;
virtual void updatePhysics() {
}
virtual void registerWithGroup();
virtual void unregisterFromGroup();
void updateCenteredDeformMatrix();
BlobGroup* m_group = nullptr;
qreal m_radius = 0;
QMatrix4x4 m_deformMatrix; // identity by default
QMatrix4x4 m_centeredDeformMatrix;
// Cached data from updatePolish
float m_cachedPaddedX = 0;
float m_cachedPaddedY = 0;
float m_cachedPaddedW = 0;
float m_cachedPaddedH = 0;
QRectF m_localPaddedRect;
QVector<BlobRectData> m_cachedRects;
int m_cachedMyIndex = -2;
float m_pendingDx = 0;
float m_pendingDy = 0;
bool m_cachedHasInverted = false;
float m_cachedInvertedRadius = 0;
float m_cachedInvertedOuter[4] = {};
float m_cachedInvertedInner[4] = {};
};
+302
View File
@@ -0,0 +1,302 @@
#version 440
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float paddedX;
float paddedY;
float paddedW;
float paddedH;
float smoothFactor;
int rectCount;
int myIndex;
vec4 color;
int hasInverted;
float invertedRadius;
vec4 invertedOuter;
vec4 invertedInner;
vec4 rectData[80];
};
float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) {
vec2 d = abs(p - center) - halfSize + vec2(radius);
return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - radius;
}
float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) {
// r = (topRight, bottomRight, bottomLeft, topLeft)
p -= center;
r.xy = (p.x > 0.0) ? r.xy : r.wz;
r.x = (p.y > 0.0) ? r.y : r.x;
vec2 q = abs(p) - halfSize + r.x;
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x;
}
float sdBox(vec2 p, vec2 center, vec2 halfSize) {
vec2 d = abs(p - center) - halfSize;
return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0);
}
float smin(float a, float b, float k) {
// Cubic smooth min (C2 continuous — no curvature kinks at blend boundary)
float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * h * k * (1.0 / 6.0);
}
float smax(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0) / k;
return max(a, b) + h * h * h * k * (1.0 / 6.0);
}
float smaxSharpA(float a, float b, float k) {
// smax variant that keeps a's boundary sharp (no inward rounding at a = 0).
// Used for the frame outer edge so it always fills to the edges.
float h = max(k - abs(a - b), 0.0) / k;
float blend = h * h * h * k * (1.0 / 6.0);
blend *= smoothstep(0.0, k * 0.5, -a);
return max(a, b) + blend;
}
void main() {
vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH);
float mergedSdf = 1e10;
int owner = -2;
float minDist = 1e10;
for (int i = 0; i < rectCount; i++) {
vec4 rect = rectData[i * 5]; // cx, cy, hw, hh
vec4 props = rectData[i * 5 + 1]; // radius, offsetX, offsetY, minEig
vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix
vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0
vec4 radii =
rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl)
// Offset center for asymmetric deformation
vec2 center = rect.xy + props.yz;
// AABB early-out: skip rects far from this pixel
vec2 extent = sh.xy + vec2(smoothFactor * 1.5);
if (abs(pixel.x - center.x) > extent.x ||
abs(pixel.y - center.y) > extent.y)
continue;
// Apply pre-computed inverse deformation to the evaluation point
mat2 invDeform = mat2(invDm.xy, invDm.zw);
vec2 transformedPixel = center + invDeform * (pixel - center);
// Use pre-computed effective per-corner radii
float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii);
// Use pre-computed minimum eigenvalue for SDF correction
d *= max(props.w, 0.01);
// Scale SDF on the axis facing a nearby border to narrow the smin blend
// zone in that direction only, without reducing k (which would cause sharp
// edges).
if (hasInverted != 0) {
vec2 screenHalf = sh.xy;
float distY0 =
(center.y + screenHalf.y) - (invertedInner.y - invertedInner.w);
float distY1 =
(invertedInner.y + invertedInner.w) - (center.y - screenHalf.y);
float distX0 =
(center.x + screenHalf.x) - (invertedInner.x - invertedInner.z);
float distX1 =
(invertedInner.x + invertedInner.z) - (center.x - screenHalf.x);
// 0 = far from border, 1 = at border (max compression)
float yProx = 1.0 - min(smoothstep(0.0, smoothFactor, distY0),
smoothstep(0.0, smoothFactor, distY1));
float xProx = 1.0 - min(smoothstep(0.0, smoothFactor, distX0),
smoothstep(0.0, smoothFactor, distX1));
// Smooth axis weights: gradient-based at corners, face-based inside.
vec2 q = abs(pixel - center) - screenHalf;
vec2 qp = max(q, vec2(0.0));
float cornerLen = length(qp);
// Gradient direction in corner region (smooth 90-degree rotation)
float gradX = qp.x / max(cornerLen, 0.001);
float gradY = qp.y / max(cornerLen, 0.001);
// Smooth face weights for inside/edge (no hard branch)
float faceY = smoothstep(-4.0, 4.0, q.y - q.x);
float faceX = 1.0 - faceY;
// Blend: gradient in corner region, face-based inside
float t = smoothstep(0.0, 2.0, cornerLen);
float xWeight = mix(faceX, gradX, t);
float yWeight = mix(faceY, gradY, t);
float boost = 3.0;
float scale = 1.0 + (xProx * xWeight + yProx * yWeight) * boost;
d *= scale;
}
// Rect-to-rect edge sinks: track the same edge of neighboring rects
{
float rectSinkValue = 0.0;
vec2 iHalf = sh.xy;
float preOff = smoothFactor * (1.0 / 6.0);
for (int j = 0; j < rectCount; j++) {
if (j == i)
continue;
vec4 jRect = rectData[j * 5];
vec4 jProps = rectData[j * 5 + 1];
vec2 jSh = rectData[j * 5 + 3].xy;
vec2 jCtr = jRect.xy + jProps.yz;
// Per-edge containment: the other rect's full span on the
// perpendicular axis must be inside this rect for that edge.
bool hInside = (jCtr.y - jSh.y) >= (center.y - iHalf.y) &&
(jCtr.y + jSh.y) <= (center.y + iHalf.y);
bool vInside = (jCtr.x - jSh.x) >= (center.x - iHalf.x) &&
(jCtr.x + jSh.x) <= (center.x + iHalf.x);
// Top/Bottom: other rect's height must be inside this rect
float topPen =
hInside ? clamp((center.y - iHalf.y) - (jCtr.y - jSh.y) - preOff,
0.0, smoothFactor)
: 0.0;
float botPen =
hInside ? clamp((jCtr.y + jSh.y) - (center.y + iHalf.y) - preOff,
0.0, smoothFactor)
: 0.0;
// Left/Right: other rect's width must be inside this rect
float leftPen =
vInside ? clamp((center.x - iHalf.x) - (jCtr.x - jSh.x) - preOff,
0.0, smoothFactor)
: 0.0;
float rightPen =
vInside ? clamp((jCtr.x + jSh.x) - (center.x + iHalf.x) - preOff,
0.0, smoothFactor)
: 0.0;
// Lateral distance from pixel to other rect's extent along each edge
float hLat = max(abs(pixel.x - jCtr.x) - jSh.x, 0.0);
float vLat = max(abs(pixel.y - jCtr.y) - jSh.y, 0.0);
// Perpendicular proximity: full strength at edge, fade inside
float topZone =
1.0 - smoothstep(center.y - iHalf.y,
center.y - iHalf.y + smoothFactor, pixel.y);
float botZone = smoothstep(center.y + iHalf.y - smoothFactor,
center.y + iHalf.y, pixel.y);
float leftZone =
1.0 - smoothstep(center.x - iHalf.x,
center.x - iHalf.x + smoothFactor, pixel.x);
float rightZone = smoothstep(center.x + iHalf.x - smoothFactor,
center.x + iHalf.x, pixel.x);
float s = smoothFactor * 2.0;
float sink = max(max(topPen * smoothstep(s, 0.0, hLat) * topZone,
botPen * smoothstep(s, 0.0, hLat) * botZone),
max(leftPen * smoothstep(s, 0.0, vLat) * leftZone,
rightPen * smoothstep(s, 0.0, vLat) * rightZone));
rectSinkValue = max(rectSinkValue, sink);
}
d -= rectSinkValue;
}
mergedSdf = smin(mergedSdf, d, smoothFactor);
if (d < smoothFactor && d < minDist) {
minDist = d;
owner = i;
}
}
if (hasInverted != 0) {
float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0;
float dInner =
sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius);
// Border sinks: track the opposite rect edge, clamped to border thickness
float innerTop = invertedInner.y - invertedInner.w;
float innerBot = invertedInner.y + invertedInner.w;
float innerLeft = invertedInner.x - invertedInner.z;
float innerRight = invertedInner.x + invertedInner.z;
float outerTop = invertedOuter.y - invertedOuter.w;
float outerBot = invertedOuter.y + invertedOuter.w;
float outerLeft = invertedOuter.x - invertedOuter.z;
float outerRight = invertedOuter.x + invertedOuter.z;
float sinkValue = 0.0;
for (int i = 0; i < rectCount; i++) {
vec4 rect = rectData[i * 5];
vec4 sinkProps = rectData[i * 5 + 1];
vec2 sinkSh = rectData[i * 5 + 3].xy;
// Screen-space center (with offset) and pre-computed AABB half-extents
vec2 ctr = rect.xy + sinkProps.yz;
// Delay sink to absorb smin blend depth (cubic smin max = k/6)
float preOff = smoothFactor * (1.0 / 6.0);
// Top border: track rect's BOTTOM edge, only within border thickness
float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0,
innerTop - outerTop);
// Bottom border: track rect's TOP edge
float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0,
outerBot - innerBot);
// Left border: track rect's RIGHT edge
float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0,
innerLeft - outerLeft);
// Right border: track rect's LEFT edge
float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0,
outerRight - innerRight);
// Lateral distance from pixel to rect's extent along each edge
float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0);
float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0);
// Perpendicular proximity: full strength in border, fade inside inner
// area
float topZone =
1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y);
float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y);
float leftZone =
1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x);
float rightZone =
smoothstep(innerRight - smoothFactor, innerRight, pixel.x);
float s = smoothFactor * 2.0;
float sink = max(max(topPen * smoothstep(s, 0.0, hLat) * topZone,
botPen * smoothstep(s, 0.0, hLat) * botZone),
max(leftPen * smoothstep(s, 0.0, vLat) * leftZone,
rightPen * smoothstep(s, 0.0, vLat) * rightZone));
sinkValue = max(sinkValue, sink);
}
dInner -= sinkValue;
float dFrame = smaxSharpA(dOuter, -dInner, smoothFactor);
mergedSdf = smin(mergedSdf, dFrame, smoothFactor);
if (dFrame < minDist) {
owner = -1;
}
}
// Each renderer only outputs pixels it owns, but allow rendering
// blend zones to prevent gaps (mergedSdf < smoothFactor means in blend)
// myIndex == -1: inverted rect renders border-owned pixels
// myIndex >= 0: individual rect renders its owned pixels
if (owner != myIndex && mergedSdf > smoothFactor)
discard;
float fw = fwidth(mergedSdf);
float alpha = 1.0 - smoothstep(-fw, fw, mergedSdf);
fragColor = vec4(color.rgb * alpha, alpha) * qt_Opacity;
}
+29
View File
@@ -0,0 +1,29 @@
#version 440
layout(location = 0) in vec4 qt_VertexPosition;
layout(location = 1) in vec2 qt_VertexTexCoord;
layout(location = 0) out vec2 qt_TexCoord0;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float paddedX;
float paddedY;
float paddedW;
float paddedH;
float smoothFactor;
int rectCount;
int myIndex;
vec4 color;
int hasInverted;
float invertedRadius;
vec4 invertedOuter;
vec4 invertedInner;
vec4 rectData[80];
};
void main() {
gl_Position = qt_Matrix * qt_VertexPosition;
qt_TexCoord0 = qt_VertexTexCoord;
}
+2 -1
View File
@@ -1,4 +1,4 @@
find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network DBus) find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
@@ -57,3 +57,4 @@ add_subdirectory(Models)
add_subdirectory(Internal) add_subdirectory(Internal)
add_subdirectory(Services) add_subdirectory(Services)
add_subdirectory(Components) add_subdirectory(Components)
add_subdirectory(Blobs)