diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 7427e92..ec5a7bd 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -375,6 +375,7 @@ in Lock 1.0 Lock.qml Cheatsheet 1.0 Cheatsheet.qml Weird 1.0 Weird.qml + Archie 1.0 Archie.qml ''; }; @@ -1118,6 +1119,256 @@ in ''; }; + "quickshell/Archie.qml" = { + onChange = qsRestart; + text = '' + 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. + // + // 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. + Item { + id: archie + anchors.fill: parent + // One dog. The bar is instantiated per monitor. + property bool primary: true + visible: primary + + readonly property int cell: 6 + readonly property int cols: 26 + readonly property int rows: 13 + + // 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 + }) + + // Trotting: legs out at full stride, tail up. + readonly property var walkA: [ + "....................KKKK..", + "...................KKKKKK.", + "...................KKTKKK.", + "KK.................KKWTTK.", + ".KK................KKTTTN.", + "..KKKKKKKKKKKKKKKKRKKTTTN.", + "..KKKKKKKKKKKKKKKKRKKTTT..", + "...KKKKKKKKKKKKKKKRKKT....", + "...KKKKKKKKKKKKKTTK.......", + "....TKKKKKKKKKKKTT........", + "....TT.........TTT........", + "....TT..........TT........", + "...TTT..........TTT......." + ] + // Legs gathered underneath, tail level. + readonly property var walkB: [ + "....................KKKK..", + "...................KKKKKK.", + "...................KKTKKK.", + "...................KKWTTK.", + "KKK................KKTTTN.", + "..KKKKKKKKKKKKKKKKRKKTTTN.", + "..KKKKKKKKKKKKKKKKRKKTTT..", + "...KKKKKKKKKKKKKKKRKKT....", + "...KKKKKKKKKKKKKTTK.......", + "....TKKKKKKKKKKKTT........", + "......TT......TT..........", + "......TT......TT..........", + ".....TTT.....TTT.........." + ] + // Standing still: walkA's planted legs, walkB's tail. Flipping + // between this and walkA is the wag. + readonly property var wag: [ + "....................KKKK..", + "...................KKKKKK.", + "...................KKTKKK.", + "...................KKWTTK.", + "KKK................KKTTTN.", + "..KKKKKKKKKKKKKKKKRKKTTTN.", + "..KKKKKKKKKKKKKKKKRKKTTT..", + "...KKKKKKKKKKKKKKKRKKT....", + "...KKKKKKKKKKKKKTTK.......", + "....TKKKKKKKKKKKTT........", + "....TT.........TTT........", + "....TT..........TT........", + "...TTT..........TTT......." + ] + // Head down in the bowl. + readonly property var eat: [ + "..........................", + "..........................", + "..........................", + "KK........................", + ".KK.......................", + "..KKKKKKKKKKKKKKKKRK......", + "..KKKKKKKKKKKKKKKKRKK.....", + "...KKKKKKKKKKKKKKKRKKKK...", + "...KKKKKKKKKKKKKTTKKTKKK..", + "....TKKKKKKKKKKKTTKKWTTK..", + "....TT.........TT..KKTTTN.", + "....TT.........TT...KTTN..", + "...TTT........TTT........." + ] + readonly property var bowlArt: [ + "...BBB..", + "..BBBBB.", + ".RBBBBBR", + ".RRRRRR.", + "..RRRR.." + ] + + property var frame: walkA + + // ── Where he lives: the strip along the bottom, the same + // floor line the worm lands on. + 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. + readonly property real bowlStandX: bowlX - 18 * cell + + property real px: roamL + property int facing: 1 // 1 = right, -1 = left + property string mode: "walk" + property real targetX: 0 + property bool toBowl: false + property int hold: 0 + property int tick: 0 + + 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. + toBowl = Math.random() < 0.3 && Math.abs(px - bowlStandX) > 80; + // 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 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. + 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))); + } + + function arrive() { + if (toBowl) { + mode = "eat"; + hold = 100 + Math.random() * 80; + facing = 1; // the bowl is off his nose + } 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. + property bool placed: false + onWidthChanged: { + if (placed || width <= 0) return; + placed = true; + px = roamL + Math.random() * (roamR - roamL); + wander(); + } + + Timer { + interval: 33; running: archie.primary; repeat: true + onTriggered: { + archie.tick++; + 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) { + archie.arrive(); + } else { + let dir = archie.targetX > archie.px ? 1 : -1; + archie.facing = dir; + archie.px += dir * 1.8; + // Swap legs every 6 ticks: at 1.8px a tick + // that is ~11px of ground per half-stride, + // against the 12px the feet actually move in + // the art, so he does not moonwalk. + archie.frame = archie.tick % 12 < 6 ? archie.walkA : archie.walkB; + } + } else { + archie.frame = archie.mode === "eat" ? archie.eat + : (archie.tick % 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. + component Sprite: Item { + id: sp + property var art: [] + property int w: 0 + property int h: 0 + width: w * archie.cell + height: h * archie.cell + Repeater { + // Delegates parent to the Repeater's PARENT, so `sp` + // is the only reliable way back to the sprite here. + model: sp.w * sp.h + Rectangle { + required property int index + readonly property int col: index % sp.w + readonly property int row: Math.floor(index / sp.w) + readonly property string ch: sp.art[row][col] + visible: ch !== "." + color: archie.pal[ch] || "transparent" + x: col * archie.cell + y: row * archie.cell + width: archie.cell + height: archie.cell + antialiasing: false + } + } + } + + 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. + x: Math.round(archie.px / archie.cell) * archie.cell + y: Math.round((archie.floorY - height) / archie.cell) * archie.cell + // 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. + Sprite { + art: archie.bowlArt + w: 8 + h: 5 + x: Math.round(archie.bowlX / archie.cell) * archie.cell + y: Math.round((archie.floorY - height) / archie.cell) * archie.cell + } + } + ''; + }; + "quickshell/Commands.qml" = { onChange = qsRestart; text = '' @@ -6772,6 +7023,9 @@ in { rect: chromeSdf.launcher, push: Qt.point(0, -1) } ] } + + // Archie. One dog, primary screen only. + Archie { primary: bar.screen === Quickshell.screens[0] } } ''; };