feat: swing the pill's padlock open when it unlocks

This commit is contained in:
Felitendo committed 2026-09-23 00:17:34 +02:00
1 parent 2ab4122c05
commit aeb9aa89d5
3 files changed
+70 -35

No files matched your search

+1
View File
@@ -231,6 +231,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
x: 16 x: 16
open: root.shownPhase === "success" open: root.shownPhase === "success"
pace: root.pace
color: root.shownPhase === "failure" || root.shownPhase === "lockout" ? Theme.failure : Theme.textPrimary color: root.shownPhase === "failure" || root.shownPhase === "lockout" ? Theme.failure : Theme.textPrimary
} }
+1
View File
@@ -411,6 +411,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width * 0.42 width: parent.width * 0.42
height: width height: width
pace: root.pace
color: root.color color: root.color
opacity: root.mode === "lockout" ? 1 : 0 opacity: root.mode === "lockout" ? 1 : 0
scale: root.mode === "lockout" ? 1 : 0.7 scale: root.mode === "lockout" ? 1 : 0.7
+68 -35
View File
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-or-later // 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
import QtQuick.Shapes import QtQuick.Shapes
@@ -10,9 +12,64 @@ Item {
property color color: Theme.textPrimary property color color: Theme.textPrimary
property bool open: false property bool open: false
property real pace: 1
readonly property real u: width / 100 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 // Body
Rectangle { Rectangle {
x: 14 * root.u x: 14 * root.u
@@ -23,41 +80,17 @@ Item {
color: root.color color: root.color
} }
// Shackle: a U standing on the body. Opening lifts it and swings it about Shape {
// its right leg. anchors.fill: parent
Item { preferredRendererType: Shape.CurveRenderer
id: shackle
x: 26 * root.u
y: 4 * root.u
width: 48 * root.u
height: 52 * root.u
transformOrigin: Item.BottomRight
property real lift: root.open ? 7 * root.u : 0 ShapePath {
Behavior on lift { NumberAnimation { duration: 260; easing.type: Easing.OutBack } } strokeColor: root.color
rotation: root.open ? -28 : 0 strokeWidth: 11 * root.u
Behavior on rotation { NumberAnimation { duration: 320; easing.type: Easing.OutBack } } fillColor: "transparent"
transform: Translate { y: -shackle.lift } capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
Shape { PathPolyline { path: root.shackle() }
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 }
}
} }
} }
} }