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:
parent
75a31a7695
commit
25819bd901
1 changed files with 69 additions and 117 deletions
|
|
@ -833,10 +833,9 @@ in
|
||||||
text = ''
|
text = ''
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
// Decorative only. A pixel worm wanders the screen, and
|
// Decorative only: a pixel inchworm loops around the screen edge
|
||||||
// middle-clicking any Card tips it out of its panel to land in a
|
// and gets shoved about by the panels sliding out of the bar.
|
||||||
// pile at the bottom. Nothing here is load-bearing: delete the
|
// Nothing here is load-bearing — delete the file, its qmldir line
|
||||||
// file, its qmldir line, the objectName on Bar's Card component
|
|
||||||
// and the one instantiation at the end of Bar.qml.
|
// and the one instantiation at the end of Bar.qml.
|
||||||
Item {
|
Item {
|
||||||
id: weird
|
id: weird
|
||||||
|
|
@ -845,7 +844,7 @@ in
|
||||||
// Pixel grid. Every worm segment snaps to it, which IS the
|
// Pixel grid. Every worm segment snaps to it, which IS the
|
||||||
// pixel-art look — there are no sprites involved.
|
// pixel-art look — there are no sprites involved.
|
||||||
readonly property int cell: 6
|
readonly property int cell: 6
|
||||||
readonly property int segments: 9
|
readonly property int segments: 11
|
||||||
|
|
||||||
// ── Inchworm gait ───────────────────────────────────────
|
// ── Inchworm gait ───────────────────────────────────────
|
||||||
// A looper does not glide: it plants its tail, arches the
|
// A looper does not glide: it plants its tail, arches the
|
||||||
|
|
@ -859,6 +858,12 @@ in
|
||||||
readonly property real spanMax: 66
|
readonly property real spanMax: 66
|
||||||
readonly property real crawl: 2.2 // px per tick, whichever end is moving
|
readonly property real crawl: 2.2 // px per tick, whichever end is moving
|
||||||
readonly property real archMax: 26 // hoop height at full compression
|
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 rail ────────────────────────────────────────────
|
||||||
// The worm rides the chrome cutout: the exact rounded rect
|
// The worm rides the chrome cutout: the exact rounded rect
|
||||||
|
|
@ -923,14 +928,24 @@ in
|
||||||
// ── Worm ────────────────────────────────────────────────
|
// ── Worm ────────────────────────────────────────────────
|
||||||
QtObject {
|
QtObject {
|
||||||
id: worm
|
id: worm
|
||||||
property real tailD: Math.random() * 4000 // rail distance
|
property real backD: Math.random() * 4000 // rail distance
|
||||||
property real headD: tailD + weird.spanMin
|
property real frontD: backD + weird.spanMin
|
||||||
property bool reaching: true // head moving, tail planted
|
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 bool loose: false // shoved off, ballistic
|
||||||
property real vx: 0
|
property real vx: 0
|
||||||
property real vy: 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
|
// Every segment's position, rebuilt each tick. One array
|
||||||
// instead of per-segment bindings: the gait and the ballistic
|
// instead of per-segment bindings: the gait and the ballistic
|
||||||
// flight are different enough that computing them in the tick
|
// flight are different enough that computing them in the tick
|
||||||
|
|
@ -939,18 +954,26 @@ in
|
||||||
property var body: []
|
property var body: []
|
||||||
|
|
||||||
// Body laid along the rail, hooped by however much slack the
|
// 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() {
|
function railBody() {
|
||||||
let span = worm.headD - worm.tailD;
|
let span = worm.frontD - worm.backD;
|
||||||
let squash = (spanMax - span) / (spanMax - spanMin);
|
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 = [];
|
let out = [];
|
||||||
for (let i = 0; i < segments; i++) {
|
for (let i = 0; i < segments; i++) {
|
||||||
let u = 1 - i / (segments - 1); // 1 at head, 0 at tail
|
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);
|
let base = railPoint(d);
|
||||||
// sin() pins both ends to the surface and lifts the
|
// Lift only across the arching middle: sin() pins the
|
||||||
// middle: the feet stay planted, the back arches.
|
// hoop's own two ends down, and beyond them the neck
|
||||||
let lift = archMax * squash * Math.sin(Math.PI * u);
|
// 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);
|
let n = railNormal(d);
|
||||||
out.push(Qt.point(base.x + n.x * lift, base.y + n.y * lift));
|
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; }
|
if (worm.vy > 3) { worm.vy = -worm.vy * 0.45; worm.vx *= 0.7; }
|
||||||
else {
|
else {
|
||||||
worm.loose = false;
|
worm.loose = false;
|
||||||
worm.tailD = railAtBottom(hx);
|
worm.backD = railAtBottom(hx);
|
||||||
worm.headD = worm.tailD + spanMin;
|
worm.frontD = worm.backD + Math.sign(worm.step) * spanMin;
|
||||||
worm.reaching = true;
|
worm.reaching = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -991,14 +1014,14 @@ in
|
||||||
body = railBody();
|
body = railBody();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Take turns: throw the head out to full stretch,
|
// Take turns: throw the front out to full stretch,
|
||||||
// then drag the tail up to it.
|
// then drag the back up to it.
|
||||||
if (worm.reaching) {
|
if (worm.reaching) {
|
||||||
worm.headD += crawl;
|
worm.frontD += worm.step;
|
||||||
if (worm.headD - worm.tailD >= spanMax) worm.reaching = false;
|
if (Math.abs(worm.frontD - worm.backD) >= spanMax) worm.reaching = false;
|
||||||
} else {
|
} else {
|
||||||
worm.tailD += crawl;
|
worm.backD += worm.step;
|
||||||
if (worm.headD - worm.tailD <= spanMin) worm.reaching = true;
|
if (Math.abs(worm.frontD - worm.backD) <= spanMin) worm.reaching = true;
|
||||||
}
|
}
|
||||||
body = railBody();
|
body = railBody();
|
||||||
checkShove();
|
checkShove();
|
||||||
|
|
@ -1044,12 +1067,16 @@ in
|
||||||
property var pushers: []
|
property var pushers: []
|
||||||
property var prevRects: []
|
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() {
|
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++) {
|
for (let i = 0; i < pushers.length; i++) {
|
||||||
let r = pushers[i], q = prevRects[i];
|
let r = pushers[i], q = prevRects[i];
|
||||||
if (!r || r.z < 0.5) continue;
|
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
|
if (!q || q.z < 0.5) { q = r; } // appeared already overlapping him
|
||||||
// Only an edge that is travelling outward carries him,
|
// Only an edge that is travelling outward carries him,
|
||||||
// and it carries him the way it is going: the dropdown
|
// 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.
|
// 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 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));
|
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;
|
worm.loose = true;
|
||||||
// Clamped: the panel's first frame of OutExpo covers
|
// Clamped: the panel's first frame of OutExpo covers
|
||||||
// most of its width, which would fire him off screen.
|
// most of its width, which would fire him off screen.
|
||||||
|
|
@ -1066,87 +1104,6 @@ in
|
||||||
return;
|
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 {
|
component Card: Rectangle {
|
||||||
default property alias cardData: _cardCol.data
|
default property alias cardData: _cardCol.data
|
||||||
property alias cardSpacing: _cardCol.spacing
|
property alias cardSpacing: _cardCol.spacing
|
||||||
// Weird.qml hit-tests for this tag on middle-click.
|
|
||||||
objectName: "card"
|
|
||||||
radius: Theme.radiusCard
|
radius: Theme.radiusCard
|
||||||
color: Theme.cardBg
|
color: Theme.cardBg
|
||||||
implicitHeight: _cardCol.height + 2 * Theme.cardPad
|
implicitHeight: _cardCol.height + 2 * Theme.cardPad
|
||||||
|
|
@ -6795,14 +6750,11 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last child = on top of every panel, which is what the
|
// Last child, so he crawls over the panels rather than under
|
||||||
// card hit-test needs. Purely decorative, see Weird.qml.
|
// them. Purely decorative, see Weird.qml. The pushers are the
|
||||||
Weird {
|
// same rects the frame shader draws, so what shoves him is the
|
||||||
resetOn: bar.activeDropdown
|
// panel outline itself as it grows.
|
||||||
// The same rects the frame shader draws, so the worm is
|
Weird { pushers: [chromeSdf.panel, chromeSdf.launcher] }
|
||||||
// swept by the panel outline itself as it grows.
|
|
||||||
pushers: [chromeSdf.panel, chromeSdf.launcher]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue