From 36360544ee5f7f5ed83cf29137f407960a7e9b0e Mon Sep 17 00:00:00 2001 From: rope Date: Tue, 4 Aug 2026 13:21:13 +0100 Subject: [PATCH] quickshell: redraw Archie at 2px, fix his paws, add a throwable ball and a bed Co-Authored-By: Claude Opus 5 --- settings/quickshell.nix | 471 +++++++++++++++++++++++++++++----------- 1 file changed, 346 insertions(+), 125 deletions(-) diff --git a/settings/quickshell.nix b/settings/quickshell.nix index d252911..007532b 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -1125,12 +1125,13 @@ in import QtQuick // Archie — a black and tan dachshund with a red collar, living - // along the bottom of the screen. He trots up and down, stops to - // wag, and every so often remembers his bowl and goes for a snack. + // along the bottom of the screen. He trots about, wags, naps in + // his bed, visits his bowl, and fetches a ball you can pick up + // and fling with the mouse. // // The art is plain text, one character per pixel, so he can be // redrawn by editing the picture instead of editing code. He walks - // on all fours and never limps; that was deliberate. + // evenly on all fours and never limps; that was deliberate. Item { id: archie anchors.fill: parent @@ -1138,190 +1139,369 @@ in property bool primary: true visible: primary - // 4px pixels, not the worm's 6: he reads better with more - // pixels in him, and at 112x48 he takes up a lot less of the - // screen than the chunkier first pass did. - readonly property int cell: 4 - readonly property int cols: 28 - readonly property int rows: 12 + // 2px pixels: plenty of resolution for a proper pointed + // dachshund head, and he still lands smaller on screen than + // the chunkier drawing did. + readonly property int cell: 2 + readonly property int cols: 44 + readonly property int rows: 19 // Not themed — he looked how he looked. readonly property var pal: ({ "K": "#2b2523", // black "T": "#a86a30", // tan "R": "#b3322c", // collar, and his bowl - "N": "#14100f", // nose - "W": "#efe7dd", // catchlight - "B": "#96602f" // kibble + "N": "#14100f", // nose and eye + "B": "#96602f", // kibble + "G": "#5a667a", // bed + "D": "#3a4454", // bed cushion + "Y": "#c8d84a", // ball + "L": "#e4ee8c" // ball highlight }) - // Trotting: legs out at full stride, tail up. + // Trotting: front leg forward, hind leg back, tail up. Paws + // point the way he is going — drawn trailing behind the leg + // they read as feet on backwards, which is exactly how the + // first pass looked. readonly property var walkA: [ - "....................KKKKK...", - "...................KKTKKKK..", - "KK.................KKNTTKKN.", - ".KKKKKKKKKKKKKKKKKRKKTTTTTN.", - "..KKKKKKKKKKKKKKKKRKKTTTT...", - "..KKKKKKKKKKKKKKKKRKKTT.....", - "...KKKKKKKKKKKKKKTTT........", - "....TKKKKKKKKKKKTTT.........", - "....TTKKKKKKKKKTTT..........", - "....TT..........TT..........", - "....TT..........TT..........", - "...TTT.........TTT.........." + "...............................KKKKKKKK.....", + "KK............................KKKKKKKKKK....", + "KKKK..........................KKKKKKKKKKK...", + ".KKKK.........................KKKKK.KKKKK...", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKK..TTKKKKK.", + ".KKKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTTT.", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTT..", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTT.....", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTKKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTTKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKTTTT.............", + ".....KKKKKKKKKKKKKKKKKKKKKTTTT..............", + "....TTKKKKKKKKKKKKKKKKKKKTTTT...............", + ".....TTTTT..............TTTT................", + "......TTTT..............TTT.................", + ".....TTT................TTT.................", + ".....TTT................TTT.................", + ".....TTT................TTT.................", + ".....TTTTT..............TTTTT..............." ] - // Legs gathered underneath, tail level. + // Legs gathered under him, tail level. readonly property var walkB: [ - "....................KKKKK...", - "...................KKTKKKK..", - "...................KKNTTKKN.", - "KKKKKKKKKKKKKKKKKKRKKTTTTTN.", - "..KKKKKKKKKKKKKKKKRKKTTTT...", - "..KKKKKKKKKKKKKKKKRKKTT.....", - "...KKKKKKKKKKKKKKTTT........", - "....TKKKKKKKKKKKTTT.........", - "....TTKKKKKKKKKTTT..........", - "......TT......TT............", - "......TT......TT............", - ".....TTT.....TTT............" + "...............................KKKKKKKK.....", + "..............................KKKKKKKKKK....", + "KKKK..........................KKKKKKKKKKK...", + "KKKK..........................KKKKK.KKKKK...", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKK..TTKKKKK.", + ".KKKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTTT.", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTT..", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTT.....", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTKKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTTKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKTTTT.............", + ".....KKKKKKKKKKKKKKKKKKKKKTTTT..............", + "....TTKKKKKKKKKKKKKKKKKKKTTTT...............", + "........TTTTT........TTTT...................", + ".........TTTT........TTT....................", + "........TTT..........TTT....................", + "........TTT..........TTT....................", + "........TTT..........TTT....................", + "........TTTTT........TTTTT.................." ] - // Standing still: walkA's planted legs, walkB's tail. Flipping + // Standing: walkA's planted legs, walkB's tail. Flipping // between this and walkA is the wag. readonly property var wag: [ - "....................KKKKK...", - "...................KKTKKKK..", - "...................KKNTTKKN.", - "KKKKKKKKKKKKKKKKKKRKKTTTTTN.", - "..KKKKKKKKKKKKKKKKRKKTTTT...", - "..KKKKKKKKKKKKKKKKRKKTT.....", - "...KKKKKKKKKKKKKKTTT........", - "....TKKKKKKKKKKKTTT.........", - "....TTKKKKKKKKKTTT..........", - "....TT..........TT..........", - "....TT..........TT..........", - "...TTT.........TTT.........." + "...............................KKKKKKKK.....", + "..............................KKKKKKKKKK....", + "KKKK..........................KKKKKKKKKKK...", + "KKKK..........................KKKKK.KKKKK...", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKK..TTKKKKK.", + ".KKKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTTT.", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTT..", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTT.....", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTKKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTTKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKTTTT.............", + ".....KKKKKKKKKKKKKKKKKKKKKTTTT..............", + "....TTKKKKKKKKKKKKKKKKKKKTTTT...............", + ".....TTTTT..............TTTT................", + "......TTTT..............TTT.................", + ".....TTT................TTT.................", + ".....TTT................TTT.................", + ".....TTT................TTT.................", + ".....TTTTT..............TTTTT..............." ] // Head down in the bowl. readonly property var eat: [ - "............................", - "............................", - "KK..........................", - ".KKKKKKKKKKKKKKKKKRKK.......", - "..KKKKKKKKKKKKKKKKRKKK......", - "..KKKKKKKKKKKKKKKKRKKKK.....", - "...KKKKKKKKKKKKKKTTKKKKK....", - "....TKKKKKKKKKKKTTKKTKKK....", - "....TTKKKKKKKKKTTTKKNTTKN...", - "....TT..........TT.KKTTTTN..", - "....TT..........TT..KTTN....", - "...TTT.........TTT.........." + "............................................", + "KK..........................................", + "KKKK........................................", + ".KKKK.......................................", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKKKKK............", + ".KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK...........", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK..........", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTKKKK..........", + "...KKKKKKKKKKKKKKKKKKKKKKKKKTTTKKKK.........", + "...KKKKKKKKKKKKKKKKKKKKKKKKTTTTKKKKKKKK.....", + ".....KKKKKKKKKKKKKKKKKKKKKTTTTKKKKKKKKKK....", + "....TTKKKKKKKKKKKKKKKKKKKTTTT.KKKKK.KKKK....", + ".....TTTTT..............TTTT..KKKKK.TTTKK...", + "......TTTT..............TTT...KKKKTTTTTTTTT.", + ".....TTT................TTT...KKKKKTTTTTTT..", + ".....TTT................TTT....KKKKTTTT.....", + ".....TTT................TTT.................", + ".....TTTTT..............TTTTT..............." + ] + // Flat out in his bed, muzzle on the cushion. + readonly property var sleepArt: [ + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "............................................", + "...............................KKKKKKKK.....", + "..............................KKKKKKKKKKK...", + "KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK.KKKKK...", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKTTTTTKKK..", + "..KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKTTTTTTTTT.", + "...KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKTTTTTTT...", + "...KKKKKKKKKKKKKKKKKKKKKKKKKKTTTT...........", + "....KKKKKKKKKKKKKKKKKKKKKKKKTTTT............", + ".....TTTTTTTTKKKKKKKKKKKKTTTT...............", + "......TTTTTT..........TTTTTT................" ] readonly property var bowlArt: [ - "...BBB..", - "..BBBBB.", - ".RBBBBBR", - ".RRRRRR.", - "..RRRR.." + ".....BBBBBB.....", + "...BBBBBBBBBB...", + ".BBBBBBBBBBBBBB.", + "RRRRRRRRRRRRRRRR", + ".RRRRRRRRRRRRRR.", + "..RRRRRRRRRRRR..", + "...RRRRRRRRRR...", + "....RRRRRRRR....", + ".....RRRRRR....." + ] + readonly property var bedArt: [ + "GGGGGGG........................................GGGGGGG", + "GGGGGGGGG....................................GGGGGGGGG", + "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", + "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", + "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", + "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", + "..GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG.." + ] + readonly property var ballArt: [ + "..YYY..", + ".YLYYY.", + "YLYYYYY", + "YYYYYYY", + "YYYYYYY", + ".YYYYY.", + "..YYY.." ] property var frame: walkA - // ── Where he lives: the strip along the bottom, the same - // floor line the worm lands on. + // ── His patch of floor ────────────────────────────────── readonly property real floorY: archie.height - Theme.frameWidth readonly property real roamL: Theme.barWidth + 12 readonly property real roamR: Math.max(roamL, archie.width - Theme.frameWidth - cols * cell - 12) - // Bowl parked over toward the right, clear of the launcher. - readonly property real bowlX: Math.max(roamL, roamR - 150) - // Stand so his muzzle lands in it rather than his shoulder: - // head-down, his nose is about 22 cells along from his tail. - readonly property real bowlStandX: bowlX - 18 * cell + readonly property real bowlX: Math.max(roamL, roamR - 120) + readonly property real bedX: Math.max(roamL, Math.min(bowlX - 500, roamL + 300)) + // Stand so his muzzle lands in the bowl, not his shoulder. + readonly property real bowlStandX: bowlX - 32 * cell + // Lying down, centred on the cushion. + readonly property real bedLieX: bedX + 5 * cell - property real px: roamL + property real px: 0 property int facing: 1 // 1 = right, -1 = left property string mode: "walk" property real targetX: 0 - property bool toBowl: false + property string errand: "" // "" | "bowl" | "bed" | "ball" property int hold: 0 - property int tick: 0 + // Legs swap on distance covered, not on a tick count, so the + // gait stays in step whether he is ambling or running for the + // ball. 3 cells is exactly how far the feet move in the art. + property real stride: 0 + property bool stepped: false + // ── The ball ──────────────────────────────────────────── + readonly property int ballSize: 7 * cell + property real ballX: 0 + property real ballY: 0 + property real ballVX: 0 + property real ballVY: 0 + property bool ballHeld: false // in your hand + property bool ballCarried: false // in his mouth + property bool ballThrown: false // he only fetches a fresh throw + property real lastBX: 0 + property real lastBY: 0 + property int carry: 0 + + // Where his mouth is, mirrored with him. + readonly property real mouthX: px + (facing > 0 ? 41 : 3) * cell + readonly property real mouthY: floorY - 13 * cell + + // Exposed so Bar.qml can punch the ball out of the window's + // input mask — without it the whole surface is click-through + // here and you could never grab the thing. + readonly property real hitX: ballX - 6 + readonly property real hitY: ballY - 6 + readonly property real hitW: primary ? ballSize + 12 : 0 + + // At rest on the floor after a throw: that is his cue. + readonly property bool ballLoose: ballThrown && !ballHeld && !ballCarried + && Math.abs(ballVX) < 0.3 && Math.abs(ballVY) < 0.3 + && ballY >= floorY - ballSize - 1 + + function ballTick() { + if (ballCarried) { + ballX = mouthX - ballSize / 2; + ballY = mouthY - ballSize / 2; + return; + } + if (ballHeld) { + // Velocity comes from how fast you are dragging it, so + // letting go mid-swing throws it. + ballVX = Math.max(-45, Math.min(45, ballX - lastBX)); + ballVY = Math.max(-45, Math.min(45, ballY - lastBY)); + return; + } + ballVY += 0.55; + ballX += ballVX; + ballY += ballVY; + let ground = floorY - ballSize; + if (ballY >= ground) { + ballY = ground; + if (Math.abs(ballVY) > 1.2) ballVY = -ballVY * 0.55; + else ballVY = 0; + ballVX *= 0.92; // rolling friction + if (Math.abs(ballVX) < 0.08) ballVX = 0; + } + if (ballY < 0) { ballY = 0; ballVY = Math.abs(ballVY) * 0.4; } + let bl = Theme.barWidth; + let br = archie.width - Theme.frameWidth - ballSize; + if (ballX < bl) { ballX = bl; ballVX = -ballVX * 0.6; } + if (ballX > br) { ballX = br; ballVX = -ballVX * 0.6; } + } + + // ── What he does next ─────────────────────────────────── function wander() { mode = "walk"; - // Not if he is already standing at it — otherwise he can - // roll "go to the bowl" while stood in it and eat twice - // over without moving. - // Near enough to be worth it, but not already stood in it - // — otherwise he either marches half a minute across the - // screen for a snack, or rolls "go to the bowl" while his - // head is in it and eats twice over without moving. + errand = ""; let dBowl = Math.abs(px - bowlStandX); - toBowl = Math.random() < 0.3 && dBowl > 80 && dBowl < 900; - // A short trip from wherever he is, not a random point on - // the whole screen: picking uniformly meant single walks - // most of a minute long across a wide monitor, which reads - // as a patrol. Short hops read as pottering about. + let dBed = Math.abs(px - bedLieX); + // Near enough to be worth it, and not already stood there + // — otherwise he marches half a minute for a snack, or + // rolls "go to the bowl" with his head already in it. No + // errands while he has the ball; he is busy. + if (!ballCarried && Math.random() < 0.3 && dBowl > 80 && dBowl < 900) errand = "bowl"; + else if (!ballCarried && Math.random() < 0.25 && dBed > 80 && dBed < 900) errand = "bed"; + + if (errand === "bowl") { targetX = bowlStandX; return; } + if (errand === "bed") { targetX = bedLieX; return; } + let dir = Math.random() < 0.5 ? -1 : 1; - // Turn away from an end he is already near. Clamping the - // target instead just parks him against the wall, picking - // the same spot over and over. + // Turn away from an end he is already near; clamping the + // target instead just parks him against the wall. if (dir < 0 && px - roamL < 220) dir = 1; if (dir > 0 && roamR - px < 220) dir = -1; - targetX = toBowl ? bowlStandX - : Math.max(roamL, Math.min(roamR, - px + dir * (150 + Math.random() * 550))); + targetX = Math.max(roamL, Math.min(roamR, + px + dir * (150 + Math.random() * 550))); + } + + function chase() { + mode = "walk"; + errand = "ball"; + // Line his mouth up with it, approaching from whichever + // side he is already on. + let bc = ballX + ballSize / 2; + targetX = (px + cols * cell / 2 < bc) ? bc - 41 * cell : bc - 3 * cell; + targetX = Math.max(roamL - 60, Math.min(roamR + 60, targetX)); } function arrive() { - if (toBowl) { + if (errand === "ball") { + ballCarried = true; + ballThrown = false; + carry = 400 + Math.random() * 500; // 13-30s of pride + mode = "idle"; + hold = 40; + } else if (errand === "bowl") { mode = "eat"; hold = 100 + Math.random() * 80; - facing = 1; // the bowl is off his nose + facing = 1; + } else if (errand === "bed") { + mode = "sleep"; + hold = 600 + Math.random() * 900; + facing = 1; } else { mode = "idle"; hold = 45 + Math.random() * 130; } } - // Placed on the first real width, not on completion: anchors - // have not resolved yet at that point, so roamR collapses to - // roamL and he spawns pinned to the left edge. + // Placed on the first real width: anchors have not resolved at + // Component.onCompleted, so roamR collapses to roamL and he + // spawns pinned to the left edge. property bool placed: false onWidthChanged: { if (placed || width <= 0) return; placed = true; px = roamL + Math.random() * (roamR - roamL); + ballX = Math.min(roamR, px + 300); + ballY = floorY - ballSize; wander(); } Timer { interval: 33; running: archie.primary; repeat: true onTriggered: { - archie.tick++; + archie.ballTick(); + archie.lastBX = archie.ballX; + archie.lastBY = archie.ballY; + + // A thrown ball beats whatever else he was doing. + if (archie.ballLoose && archie.errand !== "ball") archie.chase(); + + if (archie.ballCarried && --archie.carry <= 0) { + archie.ballCarried = false; + archie.ballVX = 0; + archie.ballVY = 0; + } + if (archie.mode === "walk") { // Arrival first: checking after the step makes him - // take a stride backwards when he is already there. - if (Math.abs(archie.targetX - archie.px) < 2) { + // stride backwards when he is already there. + if (Math.abs(archie.targetX - archie.px) < 2.5) { archie.arrive(); } else { let dir = archie.targetX > archie.px ? 1 : -1; + let sp = archie.errand === "ball" ? 3.0 : 1.5; archie.facing = dir; - archie.px += dir * 1.6; - // Swap legs every 5 ticks: at 1.6px a tick - // that is exactly the 8px his feet move in the - // art (2 cells), so he does not moonwalk. - archie.frame = archie.tick % 10 < 5 ? archie.walkA : archie.walkB; + archie.px += dir * sp; + archie.stride += sp; + if (archie.stride >= 3 * archie.cell) { + archie.stride = 0; + archie.stepped = !archie.stepped; + } + archie.frame = archie.stepped ? archie.walkB : archie.walkA; } } else { archie.frame = archie.mode === "eat" ? archie.eat - : (archie.tick % 26 < 13 ? archie.walkA : archie.wag); + : archie.mode === "sleep" ? archie.sleepArt + : (archie.hold % 26 < 13 ? archie.walkA : archie.wag); if (--archie.hold <= 0) archie.wander(); } } } - // ── Sprite. One Rectangle per pixel, colour looked up per - // frame; switching frames just rebinds them. + // ── Sprite: one Rectangle per pixel, colour looked up per + // frame, so switching frames just rebinds them. component Sprite: Item { id: sp property var art: [] @@ -1349,32 +1529,65 @@ in } } + // Bed first, so he lies in it rather than behind it. + Sprite { + art: archie.bedArt; w: 54; h: 7 + x: Math.round(archie.bedX / archie.cell) * archie.cell + y: archie.floorY - height + } + Sprite { art: archie.frame w: archie.cols h: archie.rows - // Snap to the pixel grid, same as the worm, or the art - // shimmers as he walks. + // Snap x to the pixel grid or the art shimmers as he + // walks. y is exact: he never moves vertically, and + // rounding sank his paws through the floor. x: Math.round(archie.px / archie.cell) * archie.cell - // Exact, not grid-snapped: he never moves vertically, so - // there is nothing to shimmer, and rounding sank his paws - // a couple of pixels through the floor. y: archie.floorY - height // Mirror him when he heads back the other way. transform: Scale { origin.x: archie.cols * archie.cell / 2; xScale: archie.facing } } - // Drawn after him, so at the bowl his nose goes behind it. + // Bowl after him, so at dinner his nose goes behind it. Sprite { - art: archie.bowlArt - w: 8 - h: 5 + art: archie.bowlArt; w: 16; h: 9 x: Math.round(archie.bowlX / archie.cell) * archie.cell - // Exact, not grid-snapped: he never moves vertically, so - // there is nothing to shimmer, and rounding sank his paws - // a couple of pixels through the floor. y: archie.floorY - height } + + Sprite { + art: archie.ballArt; w: 7; h: 7 + x: Math.round(archie.ballX / archie.cell) * archie.cell + y: Math.round(archie.ballY / archie.cell) * archie.cell + } + + // Grab handle for the ball, a little larger than the ball so + // it is catchable while it rolls. + MouseArea { + x: archie.hitX; y: archie.hitY + width: archie.hitW; height: archie.hitW + cursorShape: Qt.PointingHandCursor + property real grabDX: 0 + property real grabDY: 0 + onPressed: (m) => { + let p = mapToItem(archie, m.x, m.y); + grabDX = p.x - archie.ballX; + grabDY = p.y - archie.ballY; + archie.ballCarried = false; // you may take it off him + archie.ballHeld = true; + } + onPositionChanged: (m) => { + if (!archie.ballHeld) return; + let p = mapToItem(archie, m.x, m.y); + archie.ballX = p.x - grabDX; + archie.ballY = p.y - grabDY; + } + onReleased: { + archie.ballHeld = false; + archie.ballThrown = true; + } + } } ''; }; @@ -1694,6 +1907,14 @@ in width: launcherPanel.visible ? launcherPanel.width : 0 height: launcherPanel.visible ? launcherPanel.height : 0 } + // Archie's ball. The rest of this surface is click-through, + // so without a hole here you could never pick it up. + Region { + x: archieDog.hitX + y: archieDog.hitY + width: archieDog.hitW + height: archieDog.hitW + } } Item { @@ -7035,7 +7256,7 @@ in } // Archie. One dog, primary screen only. - Archie { primary: bar.screen === Quickshell.screens[0] } + Archie { id: archieDog; primary: bar.screen === Quickshell.screens[0] } } ''; };