diff --git a/settings/quickshell.nix b/settings/quickshell.nix index d50eec0..0d95075 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -848,29 +848,106 @@ in readonly property int segments: 10 readonly property int lag: 3 // ticks of trail per segment + // ── The rail ──────────────────────────────────────────── + // The worm rides the chrome cutout: the exact rounded rect + // the frame shader carves out of the shell, so he crawls the + // real screen edge, corners and all. Keep these in step with + // the ShaderEffect's `cutout` in Bar.qml. + readonly property real railL: Theme.barWidth + readonly property real railT: Theme.frameWidth + readonly property real railR: weird.width - Theme.frameWidth + readonly property real railB: weird.height - Theme.frameWidth + readonly property real rad: Math.min(Theme.radius, (railR - railL) / 2, (railB - railT) / 2) + readonly property real runX: railR - railL - 2 * rad // straight top/bottom run + readonly property real runY: railB - railT - 2 * rad // straight left/right run + readonly property real arc: Math.PI * rad / 2 + readonly property real perim: 2 * (runX + runY) + 4 * arc + + // Point at distance d clockwise along that outline, starting + // where the top-left corner lets go of the top edge: run, + // quarter arc, run, quarter arc, … Each leg hands its end + // point to the next, so the path closes exactly. + function railPoint(d) { + d = ((d % perim) + perim) % perim; + let r = rad; + if (d < runX) return Qt.point(railL + r + d, railT); + d -= runX; + if (d < arc) { let a = -Math.PI / 2 + d / r; return Qt.point(railR - r + Math.cos(a) * r, railT + r + Math.sin(a) * r); } + d -= arc; + if (d < runY) return Qt.point(railR, railT + r + d); + d -= runY; + if (d < arc) { let a = d / r; return Qt.point(railR - r + Math.cos(a) * r, railB - r + Math.sin(a) * r); } + d -= arc; + if (d < runX) return Qt.point(railR - r - d, railB); + d -= runX; + if (d < arc) { let a = Math.PI / 2 + d / r; return Qt.point(railL + r + Math.cos(a) * r, railB - r + Math.sin(a) * r); } + d -= arc; + if (d < runY) return Qt.point(railL, railB - r - d); + d -= runY; + let a = Math.PI + d / r; + return Qt.point(railL + r + Math.cos(a) * r, railT + r + Math.sin(a) * r); + } + + // Where a landing at x sits on the bottom run — he always + // comes down onto the floor, so that is the only leg that + // ever needs solving backwards. + function railAtBottom(x) { + return runX + arc + runY + arc + + Math.max(0, Math.min(runX, railR - rad - x)); + } + // ── Worm ──────────────────────────────────────────────── QtObject { id: worm - property real hx: weird.width / 2 - property real hy: weird.height / 2 - property real dir: Math.random() * 2 * Math.PI + 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 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; + } + } + Timer { interval: 33; running: true; repeat: true onTriggered: { - worm.dir += (Math.random() - 0.5) * 0.45; - let m = weird.cell * 3; - let nx = worm.hx + Math.cos(worm.dir) * 2.4; - let ny = worm.hy + Math.sin(worm.dir) * 2.4; - // Reflect the heading at the edges. Clamping alone - // leaves it grinding along the border forever. - if (nx < m || nx > weird.width - m) { worm.dir = Math.PI - worm.dir; nx = worm.hx; } - if (ny < m || ny > weird.height - m) { worm.dir = -worm.dir; ny = worm.hy; } - worm.hx = nx; worm.hy = ny; + if (worm.loose) { + worm.vy += 0.6; // gravity, px/tick² + worm.hx += worm.vx; + worm.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; + // 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.d += 2.6; + let p = weird.railPoint(worm.d); + worm.hx = p.x; worm.hy = p.y; + weird.checkHit(); + } let t = worm.trail; - t.unshift(Qt.point(nx, ny)); + 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 @@ -943,6 +1020,9 @@ 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));