diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 0d95075..22c82ed 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -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] + } } ''; };