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