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 <noreply@anthropic.com>
This commit is contained in:
JulianandClaude Opus 5 committed 2026-09-11 09:04:56 +02:00
1 parent 1581a760be
commit f28dfec978
1 file changed
+54 -33
+54 -33
View File
@@ -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