quickshell: inchworm gait, and the dropdown outline is what shoves him
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3eb1d242fa
commit
75a31a7695
1 changed files with 130 additions and 44 deletions
|
|
@ -845,8 +845,20 @@ 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: 10
|
||||
readonly property int lag: 3 // ticks of trail per segment
|
||||
readonly property int segments: 9
|
||||
|
||||
// ── Inchworm gait ───────────────────────────────────────
|
||||
// A looper does not glide: it plants its tail, arches the
|
||||
// middle into a hoop, throws the head forward, then drags
|
||||
// the tail up to meet it. So the body is the rail interval
|
||||
// [tailD, headD] and the two ends take turns moving — the
|
||||
// span oscillating between spanMin and spanMax IS the walk
|
||||
// cycle, and the arch is just how tall the hoop has to be
|
||||
// to absorb the slack.
|
||||
readonly property real spanMin: 26
|
||||
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 rail ────────────────────────────────────────────
|
||||
// The worm rides the chrome cutout: the exact rounded rect
|
||||
|
|
@ -896,30 +908,53 @@ in
|
|||
+ Math.max(0, Math.min(runX, railR - rad - x));
|
||||
}
|
||||
|
||||
// Inward unit normal at d — which way "up off the surface"
|
||||
// points. Taken from the tangent rather than cased per edge,
|
||||
// so it swings round the corners for free: the path runs
|
||||
// clockwise, so rotating the tangent by +90° in screen
|
||||
// coordinates (y down) always aims into the screen.
|
||||
function railNormal(d) {
|
||||
let a = railPoint(d - 2), b = railPoint(d + 2);
|
||||
let tx = b.x - a.x, ty = b.y - a.y;
|
||||
let len = Math.hypot(tx, ty) || 1;
|
||||
return Qt.point(-ty / len, tx / len);
|
||||
}
|
||||
|
||||
// ── Worm ────────────────────────────────────────────────
|
||||
QtObject {
|
||||
id: worm
|
||||
property real d: Math.random() * 4000 // distance along the rail
|
||||
property real hx: 0
|
||||
property real hy: 0
|
||||
property bool loose: false // knocked off, ballistic
|
||||
property real tailD: Math.random() * 4000 // rail distance
|
||||
property real headD: tailD + weird.spanMin
|
||||
property bool reaching: true // head moving, tail planted
|
||||
property bool loose: false // shoved off, ballistic
|
||||
property real vx: 0
|
||||
property real vy: 0
|
||||
property var trail: []
|
||||
}
|
||||
|
||||
// A card in flight punts him off the rail. Head only — a
|
||||
// per-segment hit test buys nothing you can see.
|
||||
function checkHit() {
|
||||
for (let c of pile) {
|
||||
if (!c.inFlight) continue; // a landed card is scenery
|
||||
if (worm.hx < c.x - 6 || worm.hx > c.x + c.width + 6) continue;
|
||||
if (worm.hy < c.y - 6 || worm.hy > c.y + c.height + 6) continue;
|
||||
worm.loose = true;
|
||||
worm.vx = (worm.hx < c.x + c.width / 2 ? -1 : 1) * (2 + Math.random() * 2);
|
||||
worm.vy = -3; // popped up first, then gravity
|
||||
return;
|
||||
// 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
|
||||
// and handing over plain points is far less machinery than
|
||||
// making both reactive.
|
||||
property var body: []
|
||||
|
||||
// Body laid along the rail, hooped by however much slack the
|
||||
// current compression leaves. index 0 is the head.
|
||||
function railBody() {
|
||||
let span = worm.headD - worm.tailD;
|
||||
let squash = (spanMax - span) / (spanMax - spanMin);
|
||||
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 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);
|
||||
let n = railNormal(d);
|
||||
out.push(Qt.point(base.x + n.x * lift, base.y + n.y * lift));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Timer {
|
||||
|
|
@ -927,34 +962,51 @@ in
|
|||
onTriggered: {
|
||||
if (worm.loose) {
|
||||
worm.vy += 0.6; // gravity, px/tick²
|
||||
worm.hx += worm.vx;
|
||||
worm.hy += worm.vy;
|
||||
let hx = body.length ? body[0].x : 0;
|
||||
let hy = body.length ? body[0].y : 0;
|
||||
hx += worm.vx; hy += worm.vy;
|
||||
// The rail doubles as the walls — he cannot leave
|
||||
// the screen he was knocked off.
|
||||
if (worm.hx < weird.railL) { worm.hx = weird.railL; worm.vx = -worm.vx * 0.5; }
|
||||
if (worm.hx > weird.railR) { worm.hx = weird.railR; worm.vx = -worm.vx * 0.5; }
|
||||
if (worm.hy >= weird.railB) {
|
||||
worm.hy = weird.railB;
|
||||
// the screen he was shoved off.
|
||||
if (hx < railL) { hx = railL; worm.vx = -worm.vx * 0.5; }
|
||||
if (hx > railR) { hx = railR; worm.vx = -worm.vx * 0.5; }
|
||||
if (hy >= railB) {
|
||||
hy = railB;
|
||||
// Still moving: bounce. Spent: he is back on
|
||||
// the floor, which is just the bottom run.
|
||||
if (worm.vy > 3) { worm.vy = -worm.vy * 0.45; worm.vx *= 0.7; }
|
||||
else { worm.loose = false; worm.d = weird.railAtBottom(worm.hx); }
|
||||
else {
|
||||
worm.loose = false;
|
||||
worm.tailD = railAtBottom(hx);
|
||||
worm.headD = worm.tailD + spanMin;
|
||||
worm.reaching = true;
|
||||
}
|
||||
}
|
||||
if (worm.loose) {
|
||||
// Flying, he keeps whatever shape he was
|
||||
// caught in and simply travels — a curled-up
|
||||
// worm mid-air, not a swimming one.
|
||||
let dx = hx - body[0].x, dy = hy - body[0].y;
|
||||
body = body.map(p => Qt.point(p.x + dx, p.y + dy));
|
||||
} else {
|
||||
body = railBody();
|
||||
}
|
||||
} else {
|
||||
worm.d += 2.6;
|
||||
let p = weird.railPoint(worm.d);
|
||||
worm.hx = p.x; worm.hy = p.y;
|
||||
weird.checkHit();
|
||||
// Take turns: throw the head out to full stretch,
|
||||
// then drag the tail up to it.
|
||||
if (worm.reaching) {
|
||||
worm.headD += crawl;
|
||||
if (worm.headD - worm.tailD >= spanMax) worm.reaching = false;
|
||||
} else {
|
||||
worm.tailD += crawl;
|
||||
if (worm.headD - worm.tailD <= spanMin) worm.reaching = true;
|
||||
}
|
||||
body = railBody();
|
||||
checkShove();
|
||||
}
|
||||
let t = worm.trail;
|
||||
t.unshift(Qt.point(worm.hx, worm.hy));
|
||||
if (t.length > weird.segments * weird.lag) t.pop();
|
||||
// slice(), not `t`: a var property assigned the array
|
||||
// it already holds emits no change signal, so the
|
||||
// segments would bind once to an empty trail and never
|
||||
// hear about a single tick. Copy = new reference =
|
||||
// notification. 30 short arrays a second is nothing.
|
||||
worm.trail = t.slice();
|
||||
// Snapshot last, so a rect is always compared against
|
||||
// where it was exactly one tick ago. vector4d is a
|
||||
// value type, so this copies numbers, not handles.
|
||||
prevRects = pushers.slice();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -963,7 +1015,7 @@ in
|
|||
Rectangle {
|
||||
id: seg
|
||||
required property int index
|
||||
readonly property var p: worm.trail[index * weird.lag]
|
||||
readonly property var p: weird.body[index]
|
||||
readonly property int size: weird.cell * (index < weird.segments - 3 ? 2 : 1)
|
||||
visible: p !== undefined
|
||||
width: size; height: size
|
||||
|
|
@ -984,6 +1036,37 @@ in
|
|||
}
|
||||
}
|
||||
|
||||
// ── Shoved by the panels ────────────────────────────────
|
||||
// Bound to the very rects the frame shader melts into the
|
||||
// chrome (cx, cy, hw, hh; hw <= 0 means "not on screen"), so
|
||||
// what sweeps him aside is the panel outline you can actually
|
||||
// see growing, not the cards riding inside it.
|
||||
property var pushers: []
|
||||
property var prevRects: []
|
||||
|
||||
function checkShove() {
|
||||
let hx = body[0].x, hy = body[0].y;
|
||||
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 (!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
|
||||
// grows rightward out of the bar, the launcher grows
|
||||
// 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;
|
||||
worm.loose = true;
|
||||
// Clamped: the panel's first frame of OutExpo covers
|
||||
// most of its width, which would fire him off screen.
|
||||
worm.vx = Math.max(-22, Math.min(22, dx * 1.4));
|
||||
worm.vy = Math.max(-22, Math.min(22, dy * 1.4)) - 3; // -3: a little air
|
||||
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
|
||||
|
|
@ -1020,9 +1103,6 @@ in
|
|||
textureSize: Qt.size(width / 3, height / 3)
|
||||
smooth: false
|
||||
transformOrigin: Item.Center
|
||||
// Only a card still in the air can knock the worm off
|
||||
// his rail; one sitting in the pile is scenery.
|
||||
readonly property bool inFlight: fall.running
|
||||
|
||||
function drop() {
|
||||
fallX.to = Math.max(Theme.barWidth, Math.min(weird.width - width, x + (Math.random() - 0.5) * 240));
|
||||
|
|
@ -1767,6 +1847,7 @@ in
|
|||
// junction fillet by construction, so all of the previous
|
||||
// ears / border-gaps / melt geometry lives in the math now.
|
||||
ShaderEffect {
|
||||
id: chromeSdf
|
||||
anchors.fill: parent
|
||||
// Panels grow rightward out of the bar column, so the
|
||||
// cross-axis is now vertical: the 8px inset that used to
|
||||
|
|
@ -6716,7 +6797,12 @@ 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 }
|
||||
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]
|
||||
}
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue