From aeb9aa89d5ae1c3e65f2da87da242e1563a52be7 Mon Sep 17 00:00:00 2001 From: Felitendo Date: Wed, 23 Sep 2026 00:17:34 +0200 Subject: [PATCH] feat: swing the pill's padlock open when it unlocks --- src/agent/qml/Bubble.qml | 1 + src/agent/qml/FaceGlyph.qml | 1 + src/agent/qml/LockGlyph.qml | 103 ++++++++++++++++++++++++------------ 3 files changed, 70 insertions(+), 35 deletions(-) diff --git a/src/agent/qml/Bubble.qml b/src/agent/qml/Bubble.qml index 8bbfd6a..977a379 100644 --- a/src/agent/qml/Bubble.qml +++ b/src/agent/qml/Bubble.qml @@ -231,6 +231,7 @@ Item { anchors.verticalCenter: parent.verticalCenter x: 16 open: root.shownPhase === "success" + pace: root.pace color: root.shownPhase === "failure" || root.shownPhase === "lockout" ? Theme.failure : Theme.textPrimary } diff --git a/src/agent/qml/FaceGlyph.qml b/src/agent/qml/FaceGlyph.qml index 52f0562..a6b5369 100644 --- a/src/agent/qml/FaceGlyph.qml +++ b/src/agent/qml/FaceGlyph.qml @@ -411,6 +411,7 @@ Item { anchors.centerIn: parent width: parent.width * 0.42 height: width + pace: root.pace color: root.color opacity: root.mode === "lockout" ? 1 : 0 scale: root.mode === "lockout" ? 1 : 0.7 diff --git a/src/agent/qml/LockGlyph.qml b/src/agent/qml/LockGlyph.qml index 9d42013..62c60c7 100644 --- a/src/agent/qml/LockGlyph.qml +++ b/src/agent/qml/LockGlyph.qml @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later // -// A padlock whose shackle lifts and swings open. +// A padlock that opens the way a real one does: the shackle jumps up, its +// left leg coming out of the body, and swings round its right leg to the +// other side. import QtQuick import QtQuick.Shapes @@ -10,9 +12,64 @@ Item { property color color: Theme.textPrimary property bool open: false + property real pace: 1 readonly property real u: width / 100 + // 0 shut, 1 open. Everything below follows from it. Only opening is + // animated: it is shut again for the next scan, and the screen was + // locked all along, so showing it lock would make no sense. + property real openness: 0 + onOpenChanged: { + if (open) { + opening.restart() + } else { + opening.stop() + openness = 0 + } + } + NumberAnimation { + id: opening + target: root + property: "openness" + to: 1 + duration: 560 * root.pace + } + + function smooth(from, to, p) { + const x = Math.max(0, Math.min(1, (p - from) / (to - from))) + return x * x * (3 - 2 * x) + } + // Up first, then round, a little past the other side and back. + readonly property real lift: 12 * smooth(0, 0.3, openness) + readonly property real swing: { + const p = Math.max(0, Math.min(1, (openness - 0.22) / 0.78)) + const s = 1.4 + return 180 * (1 + (s + 1) * Math.pow(p - 1, 3) + s * Math.pow(p - 1, 2)) + } + // A small beat as it comes open. + scale: 1 + 0.14 * Math.sin(Math.PI * smooth(0.55, 1, openness)) + + // The shackle as points: up the left leg, over the top, down the right + // leg, which stays in the body. Swung about that leg, everything else + // narrows towards it and comes out on the other side. + function shackle() { + const right = 68.5, radius = 18.5 + const top = 28 - lift + const narrow = Math.cos(swing * Math.PI / 180) + const points = [] + const add = (x, y) => points.push(Qt.point((right + (x - right) * narrow) * u, y * u)) + add(31.5, 56 - 18 * smooth(0, 0.3, openness)) + add(31.5, top) + for (let i = 1; i < 24; ++i) { + const a = Math.PI + Math.PI * i / 24 + add(50 + radius * Math.cos(a), top + radius * Math.sin(a)) + } + add(right, top) + add(right, 56) + return points + } + // Body Rectangle { x: 14 * root.u @@ -23,41 +80,17 @@ Item { color: root.color } - // Shackle: a U standing on the body. Opening lifts it and swings it about - // its right leg. - Item { - id: shackle - x: 26 * root.u - y: 4 * root.u - width: 48 * root.u - height: 52 * root.u - transformOrigin: Item.BottomRight + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer - property real lift: root.open ? 7 * root.u : 0 - Behavior on lift { NumberAnimation { duration: 260; easing.type: Easing.OutBack } } - rotation: root.open ? -28 : 0 - Behavior on rotation { NumberAnimation { duration: 320; easing.type: Easing.OutBack } } - transform: Translate { y: -shackle.lift } - - Shape { - anchors.fill: parent - preferredRendererType: Shape.CurveRenderer - ShapePath { - strokeColor: root.color - strokeWidth: 11 * root.u - fillColor: "transparent" - capStyle: ShapePath.FlatCap - startX: 5.5 * root.u - startY: shackle.height - PathLine { x: 5.5 * root.u; y: 24 * root.u } - PathArc { - x: shackle.width - 5.5 * root.u - y: 24 * root.u - radiusX: (shackle.width - 11 * root.u) / 2 - radiusY: radiusX - } - PathLine { x: shackle.width - 5.5 * root.u; y: shackle.height } - } + ShapePath { + strokeColor: root.color + strokeWidth: 11 * root.u + fillColor: "transparent" + capStyle: ShapePath.RoundCap + joinStyle: ShapePath.RoundJoin + PathPolyline { path: root.shackle() } } } }