quickshell: flat head and tail past the hoop, solid panels, drop card knock-off

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-08-04 10:50:56 +01:00
parent 75a31a7695
commit 25819bd901

View file

@ -833,10 +833,9 @@ in
text = ''
import QtQuick
// Decorative only. A pixel worm wanders the screen, and
// middle-clicking any Card tips it out of its panel to land in a
// pile at the bottom. Nothing here is load-bearing: delete the
// file, its qmldir line, the objectName on Bar's Card component
// Decorative only: a pixel inchworm loops around the screen edge
// and gets shoved about by the panels sliding out of the bar.
// Nothing here is load-bearing delete the file, its qmldir line
// and the one instantiation at the end of Bar.qml.
Item {
id: weird
@ -845,7 +844,7 @@ in
// Pixel grid. Every worm segment snaps to it, which IS the
// pixel-art look there are no sprites involved.
readonly property int cell: 6
readonly property int segments: 9
readonly property int segments: 11
// Inchworm gait
// A looper does not glide: it plants its tail, arches the
@ -859,6 +858,12 @@ in
readonly property real spanMax: 66
readonly property real crawl: 2.2 // px per tick, whichever end is moving
readonly property real archMax: 26 // hoop height at full compression
// The hoop is only his middle. The face carries on ahead of
// it and the tail trails behind, both flat on the rail, so
// what you read is a body pumping between two planted ends
// rather than the whole animal rippling.
readonly property real headLead: 16
readonly property real tailDrag: 16
// The rail
// The worm rides the chrome cutout: the exact rounded rect
@ -923,14 +928,24 @@ in
// Worm
QtObject {
id: worm
property real tailD: Math.random() * 4000 // rail distance
property real headD: tailD + weird.spanMin
property bool reaching: true // head moving, tail planted
property real backD: Math.random() * 4000 // rail distance
property real frontD: backD + weird.spanMin
property real step: weird.crawl // sign = direction of travel
property bool reaching: true // front moving, back planted
property bool loose: false // shoved off, ballistic
property real vx: 0
property real vy: 0
}
// Turn around: the two ends swap roles, so the face leads in
// the new direction instead of the worm moonwalking.
function reverse() {
let f = worm.frontD;
worm.frontD = worm.backD;
worm.backD = f;
worm.step = -worm.step;
}
// Every segment's position, rebuilt each tick. One array
// instead of per-segment bindings: the gait and the ballistic
// flight are different enough that computing them in the tick
@ -939,18 +954,26 @@ in
property var body: []
// Body laid along the rail, hooped by however much slack the
// current compression leaves. index 0 is the head.
// current compression leaves. index 0 is the head. `span` is
// signed, so everything below works crawling either way.
function railBody() {
let span = worm.headD - worm.tailD;
let squash = (spanMax - span) / (spanMax - spanMin);
let span = worm.frontD - worm.backD;
let dir = span >= 0 ? 1 : -1;
let squash = (spanMax - Math.abs(span)) / (spanMax - spanMin);
// He reaches past the hoop at both ends.
let from = worm.backD - dir * tailDrag;
let to = worm.frontD + dir * headLead;
let out = [];
for (let i = 0; i < segments; i++) {
let u = 1 - i / (segments - 1); // 1 at head, 0 at tail
let d = worm.tailD + u * span;
let d = from + u * (to - from);
let base = railPoint(d);
// sin() pins both ends to the surface and lifts the
// middle: the feet stay planted, the back arches.
let lift = archMax * squash * Math.sin(Math.PI * u);
// Lift only across the arching middle: sin() pins the
// hoop's own two ends down, and beyond them the neck
// and tail lie flat on the rail.
let v = (d - worm.backD) / span;
let lift = v > 0 && v < 1
? archMax * squash * Math.sin(Math.PI * v) : 0;
let n = railNormal(d);
out.push(Qt.point(base.x + n.x * lift, base.y + n.y * lift));
}
@ -976,8 +999,8 @@ in
if (worm.vy > 3) { worm.vy = -worm.vy * 0.45; worm.vx *= 0.7; }
else {
worm.loose = false;
worm.tailD = railAtBottom(hx);
worm.headD = worm.tailD + spanMin;
worm.backD = railAtBottom(hx);
worm.frontD = worm.backD + Math.sign(worm.step) * spanMin;
worm.reaching = true;
}
}
@ -991,14 +1014,14 @@ in
body = railBody();
}
} else {
// Take turns: throw the head out to full stretch,
// then drag the tail up to it.
// Take turns: throw the front out to full stretch,
// then drag the back up to it.
if (worm.reaching) {
worm.headD += crawl;
if (worm.headD - worm.tailD >= spanMax) worm.reaching = false;
worm.frontD += worm.step;
if (Math.abs(worm.frontD - worm.backD) >= spanMax) worm.reaching = false;
} else {
worm.tailD += crawl;
if (worm.headD - worm.tailD <= spanMin) worm.reaching = true;
worm.backD += worm.step;
if (Math.abs(worm.frontD - worm.backD) <= spanMin) worm.reaching = true;
}
body = railBody();
checkShove();
@ -1044,12 +1067,16 @@ in
property var pushers: []
property var prevRects: []
function inRect(p, r) {
return Math.abs(p.x - r.x) <= r.z + 6 && Math.abs(p.y - r.y) <= r.w + 6;
}
function checkShove() {
let hx = body[0].x, hy = body[0].y;
let head = body[0], tail = body[segments - 1];
for (let i = 0; i < pushers.length; i++) {
let r = pushers[i], q = prevRects[i];
if (!r || r.z < 0.5) continue;
if (Math.abs(hx - r.x) > r.z + 6 || Math.abs(hy - r.y) > r.w + 6) continue;
if (!inRect(head, r)) continue;
if (!q || q.z < 0.5) { q = r; } // appeared already overlapping him
// Only an edge that is travelling outward carries him,
// and it carries him the way it is going: the dropdown
@ -1057,7 +1084,18 @@ in
// up out of the floor. A panel closing just lets go.
let dx = Math.max(0, (r.x + r.z) - (q.x + q.z)) + Math.min(0, (r.x - r.z) - (q.x - q.z));
let dy = Math.max(0, (r.y + r.w) - (q.y + q.w)) + Math.min(0, (r.y - r.w) - (q.y - q.w));
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) continue;
// Sitting still, it is simply solid: he walks into the
// panel and turns around rather than crawling through
// it, which is what made an open dropdown feel like it
// was not there at all. Only turn him when he is
// ENTERING head in, tail still out. Whole body
// inside (he landed there) means walking on is the way
// out, and reversing on that would just buzz him in
// place against both edges.
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
if (!inRect(tail, r)) reverse();
return;
}
worm.loose = true;
// Clamped: the panel's first frame of OutExpo covers
// most of its width, which would fire him off screen.
@ -1066,87 +1104,6 @@ in
return;
}
}
// Knocked-off cards
// hideSource pulls the panel's own copy out of the render
// pass without touching a single one of its bindings, so
// destroying the chunk puts the card straight back.
property var pile: []
property var resetOn: null // bind to whatever sweeps the pile
onResetOnChanged: sweep()
function knock(card) {
for (let d of pile) if (d.card === card) return;
let p = card.mapToItem(weird, 0, 0);
let c = chunk.createObject(weird, { card: card, x: p.x, y: p.y });
pile.push(c);
c.drop();
}
function sweep() {
for (let d of pile) d.destroy();
pile = [];
}
Component {
id: chunk
ShaderEffectSource {
id: bit
property Item card: null
sourceItem: card
hideSource: true
width: card ? card.width : 0
height: card ? card.height : 0
// Deliberately under-resolved so the card lands as
// chunky pixels like the worm. Raise the divisor for
// a cleaner-looking chunk.
textureSize: Qt.size(width / 3, height / 3)
smooth: false
transformOrigin: Item.Center
function drop() {
fallX.to = Math.max(Theme.barWidth, Math.min(weird.width - width, x + (Math.random() - 0.5) * 240));
fallY.to = weird.height - height - Theme.frameWidth;
spin.to = (Math.random() - 0.5) * 60;
fall.start();
}
ParallelAnimation {
id: fall
NumberAnimation { id: fallY; target: bit; property: "y"; duration: 1100; easing.type: Easing.OutBounce }
NumberAnimation { id: fallX; target: bit; property: "x"; duration: 1100; easing.type: Easing.OutQuad }
NumberAnimation { id: spin; target: bit; property: "rotation"; duration: 1100; easing.type: Easing.OutQuad }
}
}
}
// Sits above every panel, so it sees the press first. A press
// that is not on a card is handed straight back the volume
// widget's own middle-click still mutes.
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.MiddleButton
onPressed: (mouse) => {
let c = weird.cardAt(weird.parent, mouse.x, mouse.y);
if (c) weird.knock(c); else mouse.accepted = false;
}
}
// Deepest item tagged objectName "card" under the point.
// childAt() is no use: this overlay is the topmost child of
// the window, so it would only ever find itself.
function cardAt(item, x, y) {
for (let i = item.children.length - 1; i >= 0; i--) {
let c = item.children[i];
if (c === weird || !c.visible) continue;
let p = item.mapToItem(c, x, y);
if (p.x < 0 || p.y < 0 || p.x > c.width || p.y > c.height) continue;
let hit = cardAt(c, p.x, p.y);
if (hit) return hit;
if (c.objectName === "card") return c;
}
return null;
}
}
'';
};
@ -1586,8 +1543,6 @@ in
component Card: Rectangle {
default property alias cardData: _cardCol.data
property alias cardSpacing: _cardCol.spacing
// Weird.qml hit-tests for this tag on middle-click.
objectName: "card"
radius: Theme.radiusCard
color: Theme.cardBg
implicitHeight: _cardCol.height + 2 * Theme.cardPad
@ -6795,14 +6750,11 @@ in
}
}
// Last child = on top of every panel, which is what the
// card hit-test needs. Purely decorative, see Weird.qml.
Weird {
resetOn: bar.activeDropdown
// The same rects the frame shader draws, so the worm is
// swept by the panel outline itself as it grows.
pushers: [chromeSdf.panel, chromeSdf.launcher]
}
// Last child, so he crawls over the panels rather than under
// them. Purely decorative, see Weird.qml. The pushers are the
// same rects the frame shader draws, so what shoves him is the
// panel outline itself as it grows.
Weird { pushers: [chromeSdf.panel, chromeSdf.launcher] }
}
'';
};