From d7ea0978ec5fd9abc4230f777fc9133ed8ac7d7e Mon Sep 17 00:00:00 2001 From: rope Date: Wed, 19 Aug 2026 12:59:19 +0100 Subject: [PATCH] quickshell: drop the worm; only feed the spectrum ring while the popup is open --- settings/quickshell.nix | 319 ++-------------------------------------- 1 file changed, 12 insertions(+), 307 deletions(-) diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 68dfd48..39be0dc 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -347,8 +347,9 @@ in # cava in raw mode: one newline-terminated frame per tick, bars as # semicolon-separated 0-100 ints — trivially parsed by SplitParser. - # Drives the media card's spectrum ring. Only spawned while the - # calendar popup is open, so nothing captures audio at idle. + # Drives the media card's spectrum ring. Spawned while something is + # playing, so nothing captures audio at idle; the frames are only fed + # to the ring while the calendar popup is actually open. cavaConfig = pkgs.writeText "cava-quickshell.conf" '' [general] framerate = 30 @@ -374,7 +375,6 @@ in Bar 1.0 Bar.qml Lock 1.0 Lock.qml Cheatsheet 1.0 Cheatsheet.qml - Weird 1.0 Weird.qml Archie 1.0 Archie.qml ''; }; @@ -842,296 +842,6 @@ in ''; }; - "quickshell/Weird.qml" = { - onChange = qsRestart; - text = '' - import QtQuick - - // 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 - anchors.fill: parent - - // 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: 11 - - // ── 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 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 - // 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)); - } - - // 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 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 - property real spin: 0 // rad/tick while airborne - } - - - // 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. `span` is - // signed, so everything below works crawling either way. - function railBody() { - 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 = from + u * (to - from); - let base = railPoint(d); - // 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)); - } - return out; - } - - Timer { - interval: 33; running: true; repeat: true - onTriggered: { - if (worm.loose) { - worm.vy += 0.6; // gravity, px/tick² - 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 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.backD = railAtBottom(hx); - worm.frontD = worm.backD + Math.sign(worm.step) * spanMin; - worm.reaching = true; - } - } - if (worm.loose) { - // Airborne he keeps the shape he was caught - // in and tumbles about his own head: map() - // reads the old array throughout, so body[0] - // stays the pivot and the head lands exactly - // on the new position. - let c = Math.cos(worm.spin), s = Math.sin(worm.spin); - let px = body[0].x, py = body[0].y; - body = body.map(p => { - let ox = p.x - px, oy = p.y - py; - return Qt.point(hx + ox * c - oy * s, hy + ox * s + oy * c); - }); - } else { - body = railBody(); - } - } else { - // Take turns: throw the front out to full stretch, - // then drag the back up to it. - if (worm.reaching) { - worm.frontD += worm.step; - if (Math.abs(worm.frontD - worm.backD) >= spanMax) worm.reaching = false; - } else { - worm.backD += worm.step; - if (Math.abs(worm.frontD - worm.backD) <= spanMin) worm.reaching = true; - } - body = railBody(); - checkShove(); - } - // 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.map(p => p.rect); - } - } - - Repeater { - model: weird.segments - Rectangle { - id: seg - required property int index - 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 - // Snap to the grid — sub-pixel motion would sand the - // pixels back off. - x: p ? Math.round((p.x - size / 2) / weird.cell) * weird.cell : 0 - y: p ? Math.round((p.y - size / 2) / weird.cell) * weird.cell : 0 - color: Theme.base0D - opacity: 1 - index * 0.05 - antialiasing: false - - Row { // eyes, head segment only - visible: seg.index === 0 - anchors.centerIn: parent - spacing: 4 - Repeater { model: 2; Rectangle { width: 2; height: 2; color: Theme.base00 } } - } - } - } - - // ── Shoved by the panels ──────────────────────────────── - // Each pusher is { rect, push }: `rect` is the very vector4d - // 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. `push` is the unit - // direction that panel slides out in — a constant of the - // design, and not something worth deriving: the morph is - // 280ms of OutExpo, nearly all of it landing inside a single - // 33ms tick, so there is rarely a partly-grown rect to read a - // direction off of. - property var pushers: [] - property var prevRects: [] - readonly property real shoveSpeed: 26 - // Ticks before a panel may launch him again. Without it he can - // land back inside the same rect with his head over the edge - // and get booted forever. - property int shoveCool: 0 - - function launch(p) { - worm.loose = true; - worm.vx = p.push.x * shoveSpeed; - worm.vy = p.push.y * shoveSpeed - 8; // -8: sent up, not slid along - worm.spin = (0.04 + Math.random() * 0.05) * (p.push.x >= 0 ? 1 : -1); - shoveCool = 45; - } - - 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() { - if (shoveCool > 0) { shoveCool--; return; } - let tail = body[segments - 1]; - for (let i = 0; i < pushers.length; i++) { - let r = pushers[i].rect, q = prevRects[i]; - if (!r || r.z < 0.5) continue; - // Any part of him in the way is enough to be caught. - let touched = false; - for (let s = 0; s < body.length && !touched; s++) - touched = inRect(body[s], r); - if (!touched) continue; - // A panel popping out on top of him and him crawling - // into one already open both end the same way now: - // launched. Only the first is the moment you asked - // for, but a dropdown sits open for minutes while he - // crawls at well under a pixel a tick, so contact is - // the event you actually see — and having that merely - // turn him round is what read as never being knocked. - // - // The exception is his whole body being inside, which - // means he landed in there: walking out is the way - // out, and kicking him again would only pin him. - if (!inRect(tail, r)) launch(pushers[i]); - return; - } - } - } - ''; - }; - "quickshell/Archie.qml" = { onChange = qsRestart; text = '' @@ -6247,7 +5957,15 @@ in if (parts[i] === "") continue; out.push(parseInt(parts[i]) || 0); } - if (out.length > 0) calPopup.levels = out; + // Only publish while the popup is on screen. + // The bar's surface is the whole screen with a + // full-screen SDF shader filling it, so every + // levels write repaints all of it — 30 times a + // second, for a ring nobody can see, whenever + // anything plays audio. cava itself keeps + // running so the ring is still mid-song when + // the popup opens. + if (out.length > 0 && calPopup.open) calPopup.levels = out; } } } @@ -7318,20 +7036,7 @@ in } } - // 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, paired with the way each - // one slides out: dropdowns and toasts rightward out of the - // bar column, the launcher up out of the bottom frame band. ${lib.optionalString (!isMacbook) '' - Weird { - pushers: [ - { rect: chromeSdf.panel, push: Qt.point(1, 0) }, - { rect: chromeSdf.toast, push: Qt.point(1, 0) }, - { rect: chromeSdf.launcher, push: Qt.point(0, -1) } - ] - } - // Archie. One dog, primary screen only. Archie { id: archieDog; primary: bar.screen === Quickshell.screens[0] } ''}