Add a coffee break

Every so often a mug appears at the cat's nose, it sits down, dips its head in
and takes a few slow sips while steam rises. M01, optional stop.

The mug lives in its own click-through window like the chip, or inside the
overlay on Wayland. It goes away as soon as the cat is petted, dragged, sent to
sleep or moves on to something else, so no mug is ever left standing.

Also renames OverlayWindow's mode flag, which shadowed QWindow::active.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
JulianandClaude Opus 5 committed 2026-09-10 23:00:22 +02:00
1 parent 0f1df75a1d
commit 403ac30b3a
14 files changed
+283 -18

No files matched your search

+24 -2
View File
@@ -14,6 +14,7 @@ Item {
property bool petted: false
property point look: Qt.point(0, 0)
property bool showCodes: true
property real sipDip: 0
// The cat announces what it is doing in the machine's own language.
readonly property string moodCode: mood === "running" || mood === "chasing" ? "G00"
@@ -22,6 +23,7 @@ Item {
: mood === "grooming" ? "M08"
: mood === "stretching" ? "G28"
: mood === "sleeping" ? "M30"
: mood === "drinking" ? "M01"
: ""
readonly property bool dark: coat.hslLightness < 0.45
@@ -36,7 +38,8 @@ Item {
readonly property bool running: mood === "running" || mood === "chasing"
readonly property bool moving: (walking || running) && !lifted
readonly property bool sleeping: mood === "sleeping"
readonly property bool happy: mood === "loving" || mood === "grooming" || mood === "stretching" || petted
readonly property bool happy: mood === "loving" || mood === "grooming" || mood === "stretching"
|| mood === "drinking" || petted
readonly property real unit: width / 100
// tail pose (design units); swish is added on top by the idle animation
@@ -531,6 +534,17 @@ Item {
onMoodCodeChanged: if (showCodes && moodCode.length > 0) codeFlash.restart()
// Head dips into the mug, holds, comes back up.
SequentialAnimation {
running: cat.mood === "drinking" && !cat.lifted
loops: Animation.Infinite
alwaysRunToEnd: true
NumberAnimation { target: cat; property: "sipDip"; to: 13; duration: 700; easing.type: Easing.InOutQuad }
PauseAnimation { duration: 900 }
NumberAnimation { target: cat; property: "sipDip"; to: 0; duration: 600; easing.type: Easing.InOutQuad }
PauseAnimation { duration: 1400 }
}
states: [
State {
name: "running"
@@ -566,6 +580,13 @@ Item {
PropertyChanges { head.y: 24; head.x: 44; head.rotation: 4 }
PropertyChanges { cat.tailC1X: 24; cat.tailC1Y: 86; cat.tailC2X: 60; cat.tailC2Y: 90; cat.tailX: 76; cat.tailY: 78 }
},
State {
name: "drinking"
when: cat.mood === "drinking" && !cat.lifted
PropertyChanges { body.x: 32; body.y: 42; body.width: 42; body.height: 38 }
PropertyChanges { head.x: 50; head.y: 20 + cat.sipDip; head.rotation: 16 }
PropertyChanges { cat.tailC1X: 26; cat.tailC1Y: 84; cat.tailC2X: 40; cat.tailC2Y: 88; cat.tailX: 52; cat.tailY: 80 }
},
State {
name: "batting"
when: cat.mood === "batting" && !cat.lifted
@@ -588,5 +609,6 @@ Item {
NumberAnimation { target: shadow; properties: "opacity,scale"; duration: 260 }
}
readonly property bool seated: mood === "sitting" || mood === "loving" || mood === "alert" || mood === "grooming"
readonly property bool seated: mood === "sitting" || mood === "loving" || mood === "alert"
|| mood === "grooming" || mood === "drinking"
}
+1 -1
View File
@@ -5,7 +5,7 @@ import TNCat
// Maps CatController::Mood onto the strings Cat.qml understands.
QtObject {
readonly property var names: ["walking", "running", "sitting", "grooming", "stretching",
"sleeping", "loving", "alert", "chasing", "batting"]
"sleeping", "loving", "alert", "chasing", "batting", "drinking"]
function name(mood) {
return names[mood] !== undefined ? names[mood] : "sitting"
+109
View File
@@ -0,0 +1,109 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Shapes
// A mug of coffee with steam. Design space is 44 x 52 units, mug on the ground.
Item {
id: cup
readonly property real unit: width / 44
implicitWidth: 44
implicitHeight: 52
Item {
width: 44
height: 52
transformOrigin: Item.TopLeft
scale: cup.unit
Rectangle { // ground shadow
x: 8; y: 46
width: 26; height: 5
radius: 2.5
color: "#000000"
opacity: 0.18
antialiasing: true
}
// steam
Repeater {
model: 3
Shape {
id: wisp
required property int index
x: 10 + index * 7
y: 2
width: 8
height: 16
opacity: 0
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeColor: "#ffffff"
strokeWidth: 1.8
capStyle: ShapePath.RoundCap
fillColor: "transparent"
startX: 4; startY: 15
PathQuad { controlX: 0; controlY: 10; x: 4; y: 7 }
PathQuad { controlX: 8; controlY: 4; x: 4; y: 0 }
}
SequentialAnimation {
running: true
loops: Animation.Infinite
PauseAnimation { duration: wisp.index * 600 }
ParallelAnimation {
NumberAnimation { target: wisp; property: "opacity"; from: 0; to: 0.55; duration: 700 }
NumberAnimation { target: wisp; property: "y"; from: 8; to: 2; duration: 700 }
}
ParallelAnimation {
NumberAnimation { target: wisp; property: "opacity"; to: 0; duration: 1000 }
NumberAnimation { target: wisp; property: "y"; to: -6; duration: 1000 }
}
PauseAnimation { duration: 1800 - wisp.index * 600 }
}
}
}
// handle
Shape {
x: 28; y: 24
width: 14; height: 18
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeColor: "#e8e3da"
strokeWidth: 3.5
capStyle: ShapePath.RoundCap
fillColor: "transparent"
startX: 1; startY: 2
PathCubic { control1X: 12; control1Y: 0; control2X: 12; control2Y: 14; x: 1; y: 13 }
}
}
// mug
Rectangle {
x: 9; y: 20
width: 22; height: 27
radius: 4
color: "#f7f4ee"
antialiasing: true
Rectangle { // band
x: 0; y: 13
width: parent.width
height: 5
color: "#4d7ea8"
}
}
// coffee surface
Rectangle {
x: 10.5; y: 19
width: 19; height: 5
radius: 2.5
color: "#4a2c17"
antialiasing: true
}
}
}
+28
View File
@@ -0,0 +1,28 @@
pragma ComponentBehavior: Bound
import QtQuick
import TNCat
// The mug stands on the ground next to the cat, click-through like the chip.
Window {
id: win
required property CatController cat
required property AppSettings settings
readonly property int cupSize: Math.round(settings.catSize * 0.38)
width: cupSize
height: Math.round(cupSize * 1.18)
x: cat.cupX - width / 2
y: Screen.desktopAvailableHeight - height - settings.bottomMargin
color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
| Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus
| Qt.WindowTransparentForInput
| (Platform.bypassWindowManager ? Qt.X11BypassWindowManagerHint : 0)
visible: cat.cupVisible && !Platform.opaqueBackground
Cup {
anchors.fill: parent
}
}
+7 -1
View File
@@ -28,10 +28,16 @@ QtObject {
visible: !Platform.overlayMode && app.cat.chipVisible
}
readonly property CupWindow cupWindow: CupWindow {
cat: app.cat
settings: app.settings
visible: !Platform.overlayMode && app.cat.cupVisible
}
readonly property OverlayWindow overlayWindow: OverlayWindow {
cat: app.cat
settings: app.settings
active: Platform.overlayMode
overlayActive: Platform.overlayMode
onSettingsRequested: app.openSettings()
}
+13 -5
View File
@@ -15,7 +15,7 @@ Window {
// Fullscreen is a visibility, not a visible flag, so the mode switch has to
// go through visibility or the two fight each other.
property bool active: false
property bool overlayActive: false
readonly property int catW: settings.catSize
readonly property int catH: Math.round(settings.catSize * 0.9)
@@ -26,14 +26,14 @@ Window {
Platform.setInputRegion(win, Qt.rect(Math.round(cat.x), Math.round(groundY), catW, catH))
}
visibility: active ? Window.FullScreen : Window.Hidden
visibility: overlayActive ? Window.FullScreen : Window.Hidden
color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
| Qt.NoDropShadowWindowHint | Qt.WindowDoesNotAcceptFocus
title: qsTr("TNCat")
onActiveChanged: {
if (!active)
onOverlayActiveChanged: {
if (!overlayActive)
return
cat.stageWidth = width
cat.catWidth = catW
@@ -51,7 +51,7 @@ Window {
// Coalesce mask updates: one X11 round trip per frame of walking is plenty.
Timer {
interval: 33
running: win.active
running: win.overlayActive
repeat: true
onTriggered: win.refreshInputRegion()
}
@@ -69,6 +69,14 @@ Window {
}
}
Cup {
x: win.cat.cupX - width / 2
y: win.height - height - win.settings.bottomMargin
width: Math.round(win.settings.catSize * 0.38)
height: Math.round(width * 1.18)
visible: win.cat.cupVisible
}
Item {
id: catItem
x: win.cat.x
+8
View File
@@ -163,6 +163,13 @@ Window {
onToggled: win.settings.chipsEnabled = checked
}
Label { text: qsTr("Coffee") }
Switch {
text: qsTr("Take a coffee break now and then")
checked: win.settings.coffeeEnabled
onToggled: win.settings.coffeeEnabled = checked
}
Label { text: qsTr("Cat") }
Switch {
text: checked ? qsTr("Napping") : qsTr("Awake")
@@ -183,6 +190,7 @@ Window {
Button { text: qsTr("Dash"); onClicked: win.cat.doTrick(CatController.Running) }
Button { text: qsTr("Nap"); onClicked: win.cat.doTrick(CatController.Sleeping) }
Button { text: qsTr("Toss a chip"); onClicked: win.cat.tossChip() }
Button { text: qsTr("Coffee"); onClicked: win.cat.coffeeBreak() }
}
}