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:
1 parent
0f1df75a1d
commit
403ac30b3a
14 files changed
+283
-18
No files matched your search
+24
-2
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user