pragma ComponentBehavior: Bound import QtQuick import QtQuick.Shapes // A chibi cat drawn from primitives and vector paths. // Design space is 100 x 90 units, ground line at y = 84, cat faces right. Item { id: cat property color coat: "#f4b183" property bool facingRight: true property string mood: "walking" // walking running sitting grooming stretching sleeping loving alert property bool lifted: false property bool petted: false property point look: Qt.point(0, 0) property bool showCodes: true // 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" : mood === "sitting" ? "M00" : mood === "grooming" ? "M08" : mood === "stretching" ? "G28" : mood === "sleeping" ? "M30" : "" readonly property bool dark: coat.hslLightness < 0.45 readonly property color coatShade: Qt.darker(coat, 1.12) readonly property color coatLight: Qt.lighter(coat, dark ? 1.35 : 1.16) readonly property color ink: dark ? Qt.lighter(coat, 1.9) : Qt.darker(coat, 1.7) readonly property color eyeColor: dark ? "#f7d67c" : "#2f2a36" readonly property color pink: "#f2929f" readonly property color blush: "#f77d95" readonly property bool walking: mood === "walking" 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 real unit: width / 100 // tail pose (design units); swish is added on top by the idle animation property real tailC1X: 14 property real tailC1Y: 66 property real tailC2X: 8 property real tailC2Y: 46 property real tailX: 13 property real tailY: 34 property real tailSwish: 0 Behavior on tailC1X { NumberAnimation { duration: 500; easing.type: Easing.InOutQuad } } Behavior on tailC1Y { NumberAnimation { duration: 500; easing.type: Easing.InOutQuad } } Behavior on tailC2X { NumberAnimation { duration: 500; easing.type: Easing.InOutQuad } } Behavior on tailC2Y { NumberAnimation { duration: 500; easing.type: Easing.InOutQuad } } Behavior on tailX { NumberAnimation { duration: 500; easing.type: Easing.InOutQuad } } Behavior on tailY { NumberAnimation { duration: 500; easing.type: Easing.InOutQuad } } implicitWidth: 100 implicitHeight: 90 transform: Scale { origin.x: cat.width / 2 xScale: cat.facingRight ? 1 : -1 } Item { id: art width: 100 height: 90 transformOrigin: Item.TopLeft scale: cat.unit // ground shadow Rectangle { id: shadow x: 24; y: 79 width: 56; height: 8 radius: 4 color: "#000000" opacity: 0.16 antialiasing: true scale: 1 - torso.y * -0.02 } Item { id: torso width: 100 height: 90 // tail Shape { id: tailShape z: -1 anchors.fill: parent preferredRendererType: Shape.CurveRenderer ShapePath { id: tail strokeColor: cat.coatShade strokeWidth: 7 capStyle: ShapePath.RoundCap fillColor: "transparent" startX: 30; startY: 60 PathCubic { control1X: cat.tailC1X; control1Y: cat.tailC1Y control2X: cat.tailC2X + cat.tailSwish * 0.5; control2Y: cat.tailC2Y x: cat.tailX + cat.tailSwish; y: cat.tailY } } SequentialAnimation { running: !cat.sleeping && !cat.lifted loops: Animation.Infinite alwaysRunToEnd: true NumberAnimation { target: cat; property: "tailSwish"; to: 6; duration: 900; easing.type: Easing.InOutSine } NumberAnimation { target: cat; property: "tailSwish"; to: -4; duration: 900; easing.type: Easing.InOutSine } } } // legs — the far pair sits behind the body, the near pair in front Repeater { id: legs model: 4 Rectangle { id: leg required property int index readonly property bool front: index >= 2 readonly property bool far: index % 2 === 0 readonly property real swing: cat.running ? 38 : 24 readonly property real restRotation: front ? (cat.mood === "stretching" ? (far ? 48 : 40) : cat.lifted ? (far ? -12 : -18) : 0) : (cat.mood === "stretching" ? -6 : 0) z: far ? 0 : 2 x: front ? (cat.seated && !cat.lifted ? 60 : 56) + (far ? 0 : 8) : (cat.seated && !cat.lifted ? 36 : 30) + (far ? 0 : 8) y: cat.sleeping && !cat.lifted ? 74 : (front ? (cat.seated && !cat.lifted ? 60 : 63) : (cat.seated && !cat.lifted ? 72 : 63)) width: 10 height: cat.sleeping && !cat.lifted ? 8 : (front ? (cat.seated && !cat.lifted ? 24 : 20) : (cat.seated && !cat.lifted ? 12 : 20)) radius: 5 color: far ? cat.coatShade : cat.coat antialiasing: true transformOrigin: Item.Top Behavior on x { NumberAnimation { duration: 400; easing.type: Easing.InOutQuad } } Behavior on y { NumberAnimation { duration: 400; easing.type: Easing.InOutQuad } } Behavior on height { NumberAnimation { duration: 400; easing.type: Easing.InOutQuad } } // paw Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom width: parent.width height: 6.5 radius: 3.25 color: cat.coatLight antialiasing: true Rectangle { // toe line anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: 1.2 width: 4.5 height: 1 radius: 0.5 color: cat.coatShade opacity: 0.7 visible: !leg.far && !cat.sleeping } } // While walking the gait animation owns rotation; otherwise the pose does. states: State { name: "resting" when: !cat.moving PropertyChanges { leg.rotation: leg.restRotation } } transitions: Transition { NumberAnimation { property: "rotation"; duration: 400; easing.type: Easing.OutBack } } SequentialAnimation on rotation { running: cat.moving loops: Animation.Infinite alwaysRunToEnd: true NumberAnimation { to: leg.far ? leg.swing : -leg.swing; duration: cat.running ? 130 : 240; easing.type: Easing.InOutSine } NumberAnimation { to: leg.far ? -leg.swing : leg.swing; duration: cat.running ? 130 : 240; easing.type: Easing.InOutSine } } } } // body Rectangle { id: body z: 1 x: 24; y: 46 width: 50; height: 32 radius: 16 color: cat.coat antialiasing: true Behavior on x { NumberAnimation { duration: 450; easing.type: Easing.InOutQuad } } Behavior on y { NumberAnimation { duration: 450; easing.type: Easing.InOutQuad } } Behavior on width { NumberAnimation { duration: 450; easing.type: Easing.InOutQuad } } Behavior on height { NumberAnimation { duration: 450; easing.type: Easing.InOutQuad } } Behavior on rotation { NumberAnimation { duration: 450; easing.type: Easing.InOutQuad } } Rectangle { // belly anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: 3 width: parent.width * 0.5 height: parent.height * 0.42 radius: height / 2 color: cat.coatLight antialiasing: true } } // head Item { id: head z: 3 x: 46; y: 12 width: 50; height: 44 transformOrigin: Item.Bottom Behavior on x { NumberAnimation { duration: 450; easing.type: Easing.InOutQuad } } Behavior on y { NumberAnimation { duration: 450; easing.type: Easing.InOutQuad } } Behavior on rotation { NumberAnimation { duration: 450; easing.type: Easing.InOutBack } } // ears Shape { anchors.fill: parent preferredRendererType: Shape.CurveRenderer ShapePath { // left ear fillColor: cat.coat strokeColor: "transparent" startX: 6; startY: 14 PathQuad { controlX: 2; controlY: -4; x: 8; y: -4 } PathQuad { controlX: 14; controlY: -2; x: 24; y: 5 } PathLine { x: 6; y: 14 } } ShapePath { // left inner fillColor: cat.pink strokeColor: "transparent" startX: 9; startY: 11 PathQuad { controlX: 6; controlY: 2; x: 9; y: 1 } PathQuad { controlX: 14; controlY: 3; x: 19; y: 6 } PathLine { x: 9; y: 11 } } ShapePath { // right ear fillColor: cat.coat strokeColor: "transparent" startX: 44; startY: 14 PathQuad { controlX: 48; controlY: -4; x: 42; y: -4 } PathQuad { controlX: 36; controlY: -2; x: 26; y: 5 } PathLine { x: 44; y: 14 } } ShapePath { // right inner fillColor: cat.pink strokeColor: "transparent" startX: 41; startY: 11 PathQuad { controlX: 44; controlY: 2; x: 41; y: 1 } PathQuad { controlX: 36; controlY: 3; x: 31; y: 6 } PathLine { x: 41; y: 11 } } } Rectangle { // face x: 0; y: 4 width: 50; height: 40 radius: 21 color: cat.coat antialiasing: true } // cheeks Repeater { model: 2 Rectangle { required property int index x: index === 0 ? 5 : 35 y: 30 width: 10; height: 5.5 radius: 3 color: cat.blush opacity: cat.happy ? 0.75 : 0.4 antialiasing: true Behavior on opacity { NumberAnimation { duration: 300 } } } } // eyes Repeater { model: 2 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) radius: width / 2 color: cat.eyeColor visible: eye.open > 0.08 antialiasing: true Rectangle { // highlight x: parent.width * 0.5; y: parent.height * 0.14 width: 3.4; height: 3.4 * eye.open 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 radius: 0.8 color: "white" antialiasing: true } } Shape { // closed eye: happy arch or sleepy curve anchors.fill: parent visible: eye.open <= 0.08 preferredRendererType: Shape.CurveRenderer ShapePath { strokeColor: cat.ink strokeWidth: 1.8 capStyle: ShapePath.RoundCap fillColor: "transparent" startX: 0.5; startY: 7 PathQuad { controlX: 4.75 controlY: cat.happy ? 2 : 10 x: 9; y: 7 } } } 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 } } } } // nose Rectangle { x: 22.5; y: 28 width: 5; height: 3.6 radius: 1.8 color: cat.pink antialiasing: true } // mouth "ω" Shape { anchors.fill: parent preferredRendererType: Shape.CurveRenderer ShapePath { strokeColor: cat.ink strokeWidth: 1.1 capStyle: ShapePath.RoundCap fillColor: "transparent" startX: 20.5; startY: 32 PathQuad { controlX: 22.5; controlY: 35.2; x: 25; y: 32.4 } PathQuad { controlX: 27.5; controlY: 35.2; x: 29.5; y: 32 } } } // whiskers Repeater { model: 4 Rectangle { required property int index readonly property bool leftSide: index < 2 x: leftSide ? -7 : 45 y: 25 + (index % 2) * 4.5 width: 12 height: 1 radius: 0.5 color: cat.ink opacity: 0.55 rotation: (leftSide ? 1 : -1) * ((index % 2) === 0 ? 7 : -7) antialiasing: true } } // raised paw while grooming Rectangle { x: 8; y: 26 width: 10; height: 16 radius: 5 color: cat.coat antialiasing: true visible: cat.mood === "grooming" transformOrigin: Item.Bottom SequentialAnimation on rotation { running: cat.mood === "grooming" loops: Animation.Infinite NumberAnimation { to: 14; duration: 260; easing.type: Easing.InOutSine } NumberAnimation { to: -6; duration: 260; easing.type: Easing.InOutSine } } Rectangle { anchors.horizontalCenter: parent.horizontalCenter y: 0 width: parent.width; height: 6 radius: 3 color: cat.coatLight } } } // 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 { running: cat.petted || cat.mood === "loving" loops: Animation.Infinite alwaysRunToEnd: true NumberAnimation { to: 2.5; duration: 180; easing.type: Easing.InOutSine } NumberAnimation { to: -2.5; duration: 180; easing.type: Easing.InOutSine } } } // speech bubble: "!" / "z" / "♥" Text { id: bubble readonly property string glyph: cat.mood === "alert" || cat.mood === "batting" ? "!" : cat.sleeping ? "z" : (cat.mood === "loving" || cat.petted) ? "♥" : "" x: 82; y: 4 text: glyph visible: glyph.length > 0 font.pixelSize: cat.mood === "alert" || cat.mood === "batting" ? 22 : 16 font.bold: true font.family: "Helvetica Neue" color: cat.mood === "alert" || cat.mood === "batting" ? "#f2a33c" : (glyph === "♥" ? "#ff5b7f" : cat.ink) transform: Scale { xScale: cat.facingRight ? 1 : -1; origin.x: bubble.width / 2 } Accessible.ignored: true SequentialAnimation on y { running: bubble.visible loops: Animation.Infinite NumberAnimation { from: 10; to: -8; duration: cat.sleeping ? 1800 : 900; easing.type: Easing.OutQuad } } SequentialAnimation on opacity { running: bubble.visible loops: Animation.Infinite NumberAnimation { from: 0; to: 1; duration: cat.sleeping ? 500 : 250 } NumberAnimation { to: 0; duration: cat.sleeping ? 1300 : 650 } } } } // Machine code tag above the head, flashed whenever the mood changes Item { id: codeTag x: 30; y: 2 width: codeBg.width height: codeBg.height opacity: 0 visible: cat.showCodes && opacity > 0.01 transformOrigin: Item.Center transform: Scale { xScale: cat.facingRight ? 1 : -1; origin.x: codeTag.width / 2 } z: 10 scale: cat.unit Accessible.ignored: true Rectangle { id: codeBg width: codeText.implicitWidth + 10 height: codeText.implicitHeight + 6 radius: 4 color: "#242833" opacity: 0.92 antialiasing: true Text { id: codeText anchors.centerIn: parent text: cat.moodCode color: "#8fd6ff" font.pixelSize: 11 font.bold: true font.family: "Menlo" } } SequentialAnimation { id: codeFlash NumberAnimation { target: codeTag; property: "opacity"; to: 1; duration: 160 } PauseAnimation { duration: 1500 } NumberAnimation { target: codeTag; property: "opacity"; to: 0; duration: 500 } } } onMoodCodeChanged: if (showCodes && moodCode.length > 0) codeFlash.restart() states: [ State { name: "running" when: cat.running && !cat.lifted PropertyChanges { body.rotation: -5; head.y: 15; head.x: 48 } PropertyChanges { cat.tailC1X: 8; cat.tailC1Y: 60; cat.tailC2X: 4; cat.tailC2Y: 56; cat.tailX: 0; cat.tailY: 52 } }, State { name: "sitting" when: (cat.mood === "sitting" || cat.mood === "loving" || cat.mood === "alert") && !cat.lifted PropertyChanges { body.x: 34; body.y: 40; body.width: 40; body.height: 40 } PropertyChanges { head.y: 8; head.x: 40 } PropertyChanges { cat.tailC1X: 26; cat.tailC1Y: 84; cat.tailC2X: 40; cat.tailC2Y: 88; cat.tailX: 52; cat.tailY: 80 } }, State { name: "grooming" when: cat.mood === "grooming" && !cat.lifted PropertyChanges { body.x: 34; body.y: 40; body.width: 40; body.height: 40 } PropertyChanges { head.y: 10; head.x: 38; head.rotation: -10 } PropertyChanges { cat.tailC1X: 26; cat.tailC1Y: 84; cat.tailC2X: 40; cat.tailC2Y: 88; cat.tailX: 52; cat.tailY: 80 } }, State { name: "stretching" when: cat.mood === "stretching" && !cat.lifted PropertyChanges { body.x: 16; body.y: 50; body.width: 64; body.height: 26; body.rotation: -8 } PropertyChanges { head.y: 32; head.x: 52; head.rotation: 7 } PropertyChanges { cat.tailC1X: 10; cat.tailC1Y: 48; cat.tailC2X: 6; cat.tailC2Y: 30; cat.tailX: 14; cat.tailY: 22 } }, State { name: "sleeping" when: cat.sleeping && !cat.lifted PropertyChanges { body.x: 22; body.y: 54; body.width: 56; body.height: 26 } 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: "batting" when: cat.mood === "batting" && !cat.lifted PropertyChanges { body.rotation: -10; body.x: 26 } PropertyChanges { head.y: 10; head.x: 50 } }, State { name: "lifted" when: cat.lifted PropertyChanges { torso.rotation: 8; torso.y: -6 } PropertyChanges { body.x: 28; body.y: 40; body.width: 42; body.height: 40 } PropertyChanges { head.y: 8; head.x: 40 } PropertyChanges { cat.tailC1X: 22; cat.tailC1Y: 74; cat.tailC2X: 18; cat.tailC2Y: 86; cat.tailX: 24; cat.tailY: 92 } PropertyChanges { shadow.opacity: 0.06; shadow.scale: 0.7 } } ] transitions: Transition { NumberAnimation { target: torso; properties: "rotation,y"; duration: 260; easing.type: Easing.OutBack } NumberAnimation { target: shadow; properties: "opacity,scale"; duration: 260 } } readonly property bool seated: mood === "sitting" || mood === "loving" || mood === "alert" || mood === "grooming" }