quickshell: geometric melt into the frame column — one shape, cubic-morphed right side

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-06-12 09:21:02 +01:00
parent dcf31fbe63
commit 1a71f2c07b

View file

@ -514,26 +514,22 @@ in
} }
} }
// Frame right-column inner border starts below a flush-right // Frame right-column inner border always full: when a panel
// dropdown's bottom curve and follows the morph. The short y // is merged, its own right-edge stroke sits exactly on this
// animation softens the dock/undock jump. // line, so they coincide instead of needing a gap.
Rectangle { Rectangle {
x: bar.width - Theme.frameWidth - Theme.borderWidth / 2 x: bar.width - Theme.frameWidth - Theme.borderWidth / 2
y: chrome.mergedRight ? 30 + chrome.height + 8 : 38 y: 38
width: Theme.borderWidth width: Theme.borderWidth
height: Math.max(0, bar.height - Theme.frameWidth - 8 - y) height: Math.max(0, bar.height - Theme.frameWidth - 8 - y)
color: Theme.base03 color: Theme.base03
Behavior on y {
NumberAnimation { duration: 90; easing.type: Easing.OutCubic }
}
} }
// Frame top-right inner corner fades out while a flush-right // Frame top-right inner corner fades in sync with the
// dropdown is merged into the column there. // panel's geometric melt into the column.
Shape { Shape {
opacity: chrome.mergedRight ? 0 : 1 opacity: 1 - chrome.mergeP
visible: opacity > 0.01 visible: opacity > 0.01
Behavior on opacity { NumberAnimation { duration: 90 } }
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
ShapePath { ShapePath {
fillColor: "transparent" fillColor: "transparent"
@ -1209,15 +1205,29 @@ in
} }
} }
// Panel silhouette (caelestia-inspired): fill and border are // Panel silhouette: fill and border are each ONE continuous
// each ONE continuous path concave ear, side, rounded bottom // path concave ear, side, rounded bottom corners, side,
// corners, side, ear so there are no seams or junctions at // ear so there are no seams or junctions at all. The border
// all. The border path is open at the top where the panel // path is open at the top where the panel joins the bar.
// joins the bar; the bar's own border strips meet it there. //
// `merge` (0..1) continuously morphs the RIGHT side between
// floating (concave top ear, convex bottom corner at an 8px
// inset) and merged-into-the-frame-column (edge flush, ear
// collapsed, bottom corner flipped into a concave flare).
// The right side is built from cubics (kappa 0.5523) because
// they degrade gracefully through the zero-radius midpoint,
// where arcs would degenerate.
component PanelShape: Shape { component PanelShape: Shape {
id: pshape id: pshape
property real merge: 0
readonly property real ear: 8 readonly property real ear: 8
readonly property real rr: Math.min(8, Math.max(1, height / 2)) readonly property real rr: Math.min(8, Math.max(1, height / 2))
// Right-side morph geometry
readonly property real xr: width - ear * (1 - merge) // right edge x
readonly property real re: ear * (1 - merge) // top ear radius
readonly property real rek: re * 0.5523
readonly property real bey: height - ear + 2 * ear * merge // bottom feature endpoint y
readonly property real bck: ear * 0.5523 * (1 - 2 * merge) // endpoint control offset; sign flips at the melt point
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
ShapePath { ShapePath {
@ -1227,10 +1237,18 @@ in
PathArc { x: pshape.ear; y: Math.min(pshape.ear, pshape.height); radiusX: pshape.ear; radiusY: pshape.ear; direction: PathArc.Clockwise } PathArc { x: pshape.ear; y: Math.min(pshape.ear, pshape.height); radiusX: pshape.ear; radiusY: pshape.ear; direction: PathArc.Clockwise }
PathLine { x: pshape.ear; y: pshape.height - pshape.rr } PathLine { x: pshape.ear; y: pshape.height - pshape.rr }
PathArc { x: pshape.ear + pshape.rr; y: pshape.height; radiusX: pshape.rr; radiusY: pshape.rr; direction: PathArc.Counterclockwise } PathArc { x: pshape.ear + pshape.rr; y: pshape.height; radiusX: pshape.rr; radiusY: pshape.rr; direction: PathArc.Counterclockwise }
PathLine { x: pshape.width - pshape.ear - pshape.rr; y: pshape.height } PathLine { x: pshape.xr - pshape.ear; y: pshape.height }
PathArc { x: pshape.width - pshape.ear; y: pshape.height - pshape.rr; radiusX: pshape.rr; radiusY: pshape.rr; direction: PathArc.Counterclockwise } PathCubic {
PathLine { x: pshape.width - pshape.ear; y: Math.min(pshape.ear, pshape.height) } x: pshape.xr; y: pshape.bey
PathArc { x: pshape.width; y: 0; radiusX: pshape.ear; radiusY: pshape.ear; direction: PathArc.Clockwise } control1X: pshape.xr - pshape.ear + 4.42; control1Y: pshape.height
control2X: pshape.xr; control2Y: pshape.bey + pshape.bck
}
PathLine { x: pshape.xr; y: Math.min(pshape.re, pshape.height) }
PathCubic {
x: pshape.xr + pshape.re; y: 0
control1X: pshape.xr; control1Y: Math.max(0, Math.min(pshape.re, pshape.height) - pshape.rek)
control2X: pshape.xr + pshape.re - pshape.rek; control2Y: 0
}
PathLine { x: 0; y: 0 } PathLine { x: 0; y: 0 }
} }
@ -1243,45 +1261,18 @@ in
PathArc { x: pshape.ear; y: Math.min(pshape.ear, pshape.height); radiusX: pshape.ear; radiusY: pshape.ear; direction: PathArc.Clockwise } PathArc { x: pshape.ear; y: Math.min(pshape.ear, pshape.height); radiusX: pshape.ear; radiusY: pshape.ear; direction: PathArc.Clockwise }
PathLine { x: pshape.ear; y: pshape.height - pshape.rr } PathLine { x: pshape.ear; y: pshape.height - pshape.rr }
PathArc { x: pshape.ear + pshape.rr; y: pshape.height; radiusX: pshape.rr; radiusY: pshape.rr; direction: PathArc.Counterclockwise } PathArc { x: pshape.ear + pshape.rr; y: pshape.height; radiusX: pshape.rr; radiusY: pshape.rr; direction: PathArc.Counterclockwise }
PathLine { x: pshape.width - pshape.ear - pshape.rr; y: pshape.height } PathLine { x: pshape.xr - pshape.ear; y: pshape.height }
PathArc { x: pshape.width - pshape.ear; y: pshape.height - pshape.rr; radiusX: pshape.rr; radiusY: pshape.rr; direction: PathArc.Counterclockwise } PathCubic {
PathLine { x: pshape.width - pshape.ear; y: Math.min(pshape.ear, pshape.height) } x: pshape.xr; y: pshape.bey
PathArc { x: pshape.width; y: 0; radiusX: pshape.ear; radiusY: pshape.ear; direction: PathArc.Clockwise } control1X: pshape.xr - pshape.ear + 4.42; control1Y: pshape.height
control2X: pshape.xr; control2Y: pshape.bey + pshape.bck
} }
PathLine { x: pshape.xr; y: Math.min(pshape.re, pshape.height) }
PathCubic {
x: pshape.xr + pshape.re; y: 0
control1X: pshape.xr; control1Y: Math.max(0, Math.min(pshape.re, pshape.height) - pshape.rek)
control2X: pshape.xr + pshape.re - pshape.rek; control2Y: 0
} }
// Flush-right variant: no right ear; the bottom-right curve
// merges the panel into the screen frame's right column.
component PanelShapeFlush: Shape {
id: pshapeF
readonly property real ear: 8
readonly property real rr: Math.min(8, Math.max(1, height / 2))
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.barBg
strokeWidth: -1
startX: 0; startY: 0
PathArc { x: pshapeF.ear; y: Math.min(pshapeF.ear, pshapeF.height); radiusX: pshapeF.ear; radiusY: pshapeF.ear; direction: PathArc.Clockwise }
PathLine { x: pshapeF.ear; y: pshapeF.height - pshapeF.rr }
PathArc { x: pshapeF.ear + pshapeF.rr; y: pshapeF.height; radiusX: pshapeF.rr; radiusY: pshapeF.rr; direction: PathArc.Counterclockwise }
PathLine { x: pshapeF.width - pshapeF.ear; y: pshapeF.height }
PathArc { x: pshapeF.width; y: pshapeF.height + pshapeF.ear; radiusX: pshapeF.ear; radiusY: pshapeF.ear; direction: PathArc.Clockwise }
PathLine { x: pshapeF.width; y: 0 }
PathLine { x: 0; y: 0 }
}
ShapePath {
fillColor: "transparent"
strokeColor: Theme.base03
strokeWidth: Theme.borderWidth
capStyle: ShapePath.FlatCap
startX: 0; startY: 0
PathArc { x: pshapeF.ear; y: Math.min(pshapeF.ear, pshapeF.height); radiusX: pshapeF.ear; radiusY: pshapeF.ear; direction: PathArc.Clockwise }
PathLine { x: pshapeF.ear; y: pshapeF.height - pshapeF.rr }
PathArc { x: pshapeF.ear + pshapeF.rr; y: pshapeF.height; radiusX: pshapeF.rr; radiusY: pshapeF.rr; direction: PathArc.Counterclockwise }
PathLine { x: pshapeF.width - pshapeF.ear; y: pshapeF.height }
PathArc { x: pshapeF.width; y: pshapeF.height + pshapeF.ear; radiusX: pshapeF.ear; radiusY: pshapeF.ear; direction: PathArc.Clockwise }
} }
} }
@ -1365,22 +1356,19 @@ in
NumberAnimation { duration: 280; easing.type: Easing.OutExpo } NumberAnimation { duration: 280; easing.type: Easing.OutExpo }
} }
// Crossfade between the floating and column-merged // Continuous geometric melt instead of a shape swap: on
// silhouettes at dock/undock; the brief double-draw of // dock the ear collapses, the edge slides the last 8px
// the (near-identical) bodies is imperceptible over blur. // into the column and the bottom corner flips into the
PanelShape { // flare one shape the whole way.
opacity: chrome.mergedRight ? 0 : 1 property real mergeP: mergedRight ? 1 : 0
visible: opacity > 0.01 Behavior on mergeP {
Behavior on opacity { NumberAnimation { duration: 90 } } NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
width: chrome.width
height: chrome.height
} }
PanelShapeFlush {
opacity: chrome.mergedRight ? 1 : 0 PanelShape {
visible: opacity > 0.01
Behavior on opacity { NumberAnimation { duration: 90 } }
width: chrome.width width: chrome.width
height: chrome.height height: chrome.height
merge: chrome.mergeP
} }
} }