From f28dfec9786cb10a0e7b52f94dcb16166bca4457 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 11 Sep 2026 09:04:56 +0200 Subject: [PATCH] Settle the eyes down while walking Two things made the eyes jump. The body bobbed two units per step and the head rode along with it, and each eye ran its own random blink timer, so the cat winked one eye at a time at intervals of its own choosing. The bob is now a property on the cat rather than an animation owning torso.y, the amplitude is softer, and the head translates against it by 75 percent, the way a walking cat keeps its head level. Measured on a 240 px cat, the eye's vertical travel per step went from 4.0 px to 0.8 px. Both eyes now share one eyelid, driven by a single timer and a single animation. That also removes the Behavior that was fighting the blink animation for the same property, and the mid-blink flicker to the drawn closed-eye arch, which now only appears when the eyes are really shut. Co-Authored-By: Claude Opus 5 --- qml/Cat.qml | 87 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/qml/Cat.qml b/qml/Cat.qml index 454e12d..12e94a4 100644 --- a/qml/Cat.qml +++ b/qml/Cat.qml @@ -16,6 +16,11 @@ Item { property bool showCodes: true property real sipDip: 0 + // Vertical travel of the body during a step. The head cancels most of it + // again, because a walking cat keeps its head almost level and the eyes + // are what the eye follows. + property real bob: 0 + // The cat announces what it is doing in the machine's own language. readonly property string moodCode: mood === "running" || mood === "chasing" ? "G00" : mood === "walking" ? "G01" @@ -98,6 +103,7 @@ Item { id: torso width: 100 height: 90 + y: cat.bob // tail Shape { @@ -238,6 +244,7 @@ Item { x: 46; y: 12 width: 50; height: 44 transformOrigin: Item.Bottom + transform: Translate { y: -cat.bob * 0.75 } Behavior on x { NumberAnimation { duration: 450; easing.type: Easing.InOutQuad } } Behavior on y { NumberAnimation { duration: 450; easing.type: Easing.InOutQuad } } @@ -311,34 +318,31 @@ Item { Item { id: eye required property int index - property real open: 1 x: (index === 0 ? 12 : 29) + cat.look.x * 2.2 y: 17 + cat.look.y * 1.6 width: 9.5; height: 12 - Behavior on open { NumberAnimation { duration: 110 } } - Rectangle { id: eyeBall anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter width: eye.width * (cat.mood === "alert" || cat.mood === "batting" ? 1.15 : 1) - height: eye.height * eye.open * (cat.mood === "alert" || cat.mood === "batting" ? 1.15 : 1) + height: eye.height * cat.eyeOpen * (cat.mood === "alert" || cat.mood === "batting" ? 1.15 : 1) radius: width / 2 color: cat.eyeColor - visible: eye.open > 0.08 + visible: !cat.eyesShut && cat.eyeOpen > 0.02 antialiasing: true Rectangle { // highlight x: parent.width * 0.5; y: parent.height * 0.14 - width: 3.4; height: 3.4 * eye.open + width: 3.4; height: 3.4 * cat.eyeOpen radius: 1.7 color: "white" antialiasing: true } Rectangle { x: parent.width * 0.22; y: parent.height * 0.6 - width: 1.6; height: 1.6 * eye.open + width: 1.6; height: 1.6 * cat.eyeOpen radius: 0.8 color: "white" antialiasing: true @@ -347,7 +351,7 @@ Item { Shape { // closed eye: happy arch or sleepy curve anchors.fill: parent - visible: eye.open <= 0.08 + visible: cat.eyesShut preferredRendererType: Shape.CurveRenderer ShapePath { strokeColor: cat.ink @@ -363,23 +367,6 @@ Item { } } - Timer { // blink - interval: 2600 + Math.random() * 3800 - running: !cat.sleeping && !cat.happy && cat.mood !== "alert" - repeat: true - onTriggered: { interval = 2600 + Math.random() * 3800; blinkAnim.start() } - } - SequentialAnimation { - id: blinkAnim - NumberAnimation { target: eye; property: "open"; to: 0; duration: 80 } - NumberAnimation { target: eye; property: "open"; to: 1; duration: 120 } - } - - states: State { - name: "closed" - when: cat.sleeping || cat.happy - PropertyChanges { eye.open: 0 } - } } } @@ -450,14 +437,6 @@ Item { } } - // walking bob - SequentialAnimation on y { - running: cat.moving - loops: Animation.Infinite - alwaysRunToEnd: true - NumberAnimation { to: cat.running ? -4 : -2; duration: cat.running ? 130 : 240; easing.type: Easing.InOutSine } - NumberAnimation { to: 0; duration: cat.running ? 130 : 240; easing.type: Easing.InOutSine } - } // purr wiggle while petted / loving SequentialAnimation on rotation { @@ -543,6 +522,48 @@ Item { onMoodCodeChanged: if (showCodes && moodCode.length > 0) codeFlash.restart() + SequentialAnimation { + running: cat.moving + loops: Animation.Infinite + alwaysRunToEnd: true + NumberAnimation { target: cat; property: "bob"; to: cat.running ? -3 : -1.6; duration: cat.running ? 130 : 240; easing.type: Easing.InOutSine } + NumberAnimation { target: cat; property: "bob"; to: 0; duration: cat.running ? 130 : 240; easing.type: Easing.InOutSine } + } + + // One pair of eyelids for both eyes. Two independent blink timers made the + // cat wink at random, which read as the eyes jittering. + readonly property bool eyesShut: sleeping || happy + property real eyeOpen: 1 + + onEyesShutChanged: { + blinkSeq.stop() + lidAnim.to = eyesShut ? 0 : 1 + lidAnim.restart() + } + + NumberAnimation { + id: lidAnim + target: cat + property: "eyeOpen" + duration: 140 + easing.type: Easing.InOutQuad + } + + SequentialAnimation { + id: blinkSeq + NumberAnimation { target: cat; property: "eyeOpen"; to: 0.06; duration: 90; easing.type: Easing.InQuad } + NumberAnimation { target: cat; property: "eyeOpen"; to: 1; duration: 140; easing.type: Easing.OutQuad } + } + + Timer { + id: blinkTimer + interval: 3600 + running: !cat.eyesShut && !cat.lifted + repeat: true + onTriggered: { interval = 3200 + Math.random() * 4200; blinkSeq.start() } + Component.onCompleted: interval = 2000 + Math.random() * 4000 + } + // Head dips into the mug, holds, comes back up. SequentialAnimation { running: cat.mood === "drinking" && !cat.lifted