have a working edition of the fingerprint visuals
This commit is contained in:
parent
798b6879fd
commit
b39fa30e15
@ -257,5 +257,11 @@
|
||||
<file>ui/devicepages/FingerprintReaderDevicePage.qml</file>
|
||||
<file>ui/images/account.svg</file>
|
||||
<file>ui/images/contact-new.svg</file>
|
||||
<file>ui/components/SegmentedImage.qml</file>
|
||||
<file>ui/components/SegmentRenderer.qml</file>
|
||||
<file>ui/components/SegmentBoundingBoxes.qml</file>
|
||||
<file>ui/components/FingerprintVisual.qml</file>
|
||||
<file>ui/images/fingerprint/fingerprint_boxes.json</file>
|
||||
<file>ui/images/fingerprint/fingerprint_segmented.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
54
nymea-app/ui/components/FingerprintVisual.qml
Normal file
54
nymea-app/ui/components/FingerprintVisual.qml
Normal file
@ -0,0 +1,54 @@
|
||||
import QtQuick 2.9
|
||||
|
||||
SegmentedImage {
|
||||
id: segmentedImage
|
||||
|
||||
property var masks: []
|
||||
|
||||
property bool debug: false
|
||||
|
||||
// http://stackoverflow.com/a/1830844/538866
|
||||
function isNumeric (n) {
|
||||
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||
}
|
||||
|
||||
function getMasksToEnroll () {
|
||||
var outMasks = [];
|
||||
if (masks && masks.length) {
|
||||
masks.forEach(function (mask, i) {
|
||||
// Format is "<source>/[x1,y1,w1,h1],…,[xn,yn,wn,hn]"
|
||||
// If any value is non-numeric, we drop the mask.
|
||||
if (!isNumeric(mask.x) || !isNumeric(mask.y) || !isNumeric(mask.width)
|
||||
|| !isNumeric(mask.height))
|
||||
return;
|
||||
|
||||
// Translate the box so as to mirror the mask
|
||||
mask.x = (1 - (mask.x + mask.width));
|
||||
|
||||
outMasks.push(mask);
|
||||
});
|
||||
}
|
||||
return outMasks;
|
||||
}
|
||||
|
||||
onMasksChanged: segmentedImage.enrollMasks(getMasksToEnroll())
|
||||
|
||||
textureSource: "../images/fingerprint/fingerprint_segmented.png"
|
||||
boxesSource: "../images/fingerprint/fingerprint_boxes.json"
|
||||
|
||||
Repeater {
|
||||
model: segmentedImage.masks
|
||||
|
||||
Rectangle {
|
||||
visible: segmentedImage.debug
|
||||
color: "red"
|
||||
opacity: 0.25
|
||||
x: modelData.x * segmentedImage.implicitWidth
|
||||
y: modelData.y * segmentedImage.implicitHeight
|
||||
width: modelData.width * segmentedImage.implicitWidth
|
||||
height: modelData.height * segmentedImage.implicitHeight
|
||||
|
||||
Component.onCompleted: console.log('Scanner mask (x, y, w, h):', x, y, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
81
nymea-app/ui/components/SegmentBoundingBoxes.qml
Normal file
81
nymea-app/ui/components/SegmentBoundingBoxes.qml
Normal file
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2016 Canonical Ltd.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Authored by Florian Boucault <florian.boucault@canonical.com>
|
||||
*/
|
||||
|
||||
import QtQuick 2.4
|
||||
|
||||
QtObject {
|
||||
id: segmentBoundingBoxes
|
||||
|
||||
property string source
|
||||
onSourceChanged: parseBoundingBoxes(source)
|
||||
|
||||
property var boundingBoxes: []
|
||||
property real width
|
||||
property real height
|
||||
property int count: boundingBoxes.length
|
||||
|
||||
// The API cannot be used reliably before this signal has been emitted.
|
||||
signal ready()
|
||||
|
||||
function parseBoundingBoxes(source) {
|
||||
var xhr = new XMLHttpRequest;
|
||||
xhr.open("GET", source);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == XMLHttpRequest.DONE) {
|
||||
var b = [];
|
||||
var json = JSON.parse(xhr.responseText);
|
||||
boundingBoxes = json["boxes"];
|
||||
width = json["width"];
|
||||
height = json["height"];
|
||||
ready();
|
||||
}
|
||||
}
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function intersects(box1, box2) {
|
||||
// TODO: optimize
|
||||
var x11 = box1[0];
|
||||
var y11 = box1[1];
|
||||
var x12 = box1[0] + box1[2];
|
||||
var y12 = box1[1] + box1[3];
|
||||
var x21 = box2[0];
|
||||
var y21 = box2[1];
|
||||
var x22 = box2[0] + box2[2];
|
||||
var y22 = box2[1] + box2[3];
|
||||
var x_overlap = Math.max(0, Math.min(x12,x22) - Math.max(x11,x21));
|
||||
var y_overlap = Math.max(0, Math.min(y12,y22) - Math.max(y11,y21));
|
||||
return (x_overlap / Math.min(box1[2], box2[2]) > 0.25
|
||||
&& y_overlap / Math.min(box1[3], box2[3]) > 0.25);
|
||||
}
|
||||
|
||||
function computeIntersections(hitBox) {
|
||||
var absoluteHitBox = [hitBox[0] * width, hitBox[1] * height,
|
||||
hitBox[2] * width, hitBox[3] * height];
|
||||
|
||||
var intersections = [];
|
||||
for (var i in boundingBoxes) {
|
||||
var boundingBox = boundingBoxes[i];
|
||||
if (intersects(absoluteHitBox, boundingBox)) {
|
||||
intersections.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
return intersections;
|
||||
}
|
||||
}
|
||||
76
nymea-app/ui/components/SegmentRenderer.qml
Normal file
76
nymea-app/ui/components/SegmentRenderer.qml
Normal file
@ -0,0 +1,76 @@
|
||||
import QtQuick 2.9
|
||||
|
||||
ShaderEffect {
|
||||
id: segmentRenderer
|
||||
|
||||
implicitWidth: texture.width
|
||||
implicitHeight: texture.height
|
||||
|
||||
function animate(segments) {
|
||||
for (var i in segments) {
|
||||
var progressPixel = progressTexture.children[segments[i]];
|
||||
if (progressPixel.progress == 0.0) {
|
||||
progressPixel.animation.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property string source
|
||||
property int segmentsCount
|
||||
property color backgroundColor: app.foregroundColor
|
||||
property color fillColor: app.accentColor
|
||||
property Image texture: Image {
|
||||
source: segmentRenderer.source
|
||||
}
|
||||
property var progressTexture: progressTexture
|
||||
property int progressTextureSize: progressTexture.size
|
||||
|
||||
fragmentShader: "
|
||||
varying mediump vec2 qt_TexCoord0;
|
||||
uniform lowp float qt_Opacity;
|
||||
uniform lowp vec4 backgroundColor;
|
||||
uniform lowp vec4 fillColor;
|
||||
uniform lowp sampler2D texture;
|
||||
uniform lowp sampler2D progressTexture;
|
||||
uniform lowp int progressTextureSize;
|
||||
|
||||
void main() {
|
||||
lowp vec4 p = texture2D(texture, qt_TexCoord0);
|
||||
lowp float segment = p.r * 255.0;
|
||||
lowp vec4 segmentProgress = step(0.9, segment) * texture2D(progressTexture, vec2((segment - 1.0 + 0.5) / float(progressTextureSize), 0.5));
|
||||
lowp vec4 color = mix(fillColor, backgroundColor, step(segmentProgress.r, p.g));
|
||||
gl_FragColor = vec4(color.rgb, 1.0) * p.b * qt_Opacity;
|
||||
}
|
||||
"
|
||||
|
||||
// TODO: not the most efficient; could be replaced with an image provider
|
||||
Row {
|
||||
id: progressTexture
|
||||
|
||||
property int size: 128
|
||||
layer.enabled: true
|
||||
layer.sourceRect: Qt.rect(0, 0, size, 1)
|
||||
layer.textureSize: Qt.size(size, 1)
|
||||
layer.wrapMode: ShaderEffectSource.ClampToEdge
|
||||
visible: false
|
||||
|
||||
Repeater {
|
||||
model: segmentRenderer.segmentsCount
|
||||
Rectangle {
|
||||
id: progressPixel
|
||||
width: 1
|
||||
height: 1
|
||||
color: Qt.rgba(progress, progress, progress, 1.0)
|
||||
property real progress
|
||||
property NumberAnimation animation: NumberAnimation {
|
||||
target: progressPixel
|
||||
property: "progress"
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
duration: 1000
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
nymea-app/ui/components/SegmentedImage.qml
Normal file
52
nymea-app/ui/components/SegmentedImage.qml
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2016 Canonical Ltd.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Authored by Florian Boucault <florian.boucault@canonical.com>
|
||||
*/
|
||||
|
||||
import QtQuick 2.4
|
||||
|
||||
Item {
|
||||
id: segImg
|
||||
property alias textureSource: segmentRenderer.source
|
||||
property alias boxesSource: segmentBoundingBoxes.source
|
||||
|
||||
implicitWidth: segmentRenderer.implicitWidth
|
||||
implicitHeight: segmentRenderer.implicitHeight
|
||||
|
||||
// Ready to enroll.
|
||||
signal ready()
|
||||
|
||||
function enrollMasks(masks) {
|
||||
if (masks && masks.length) {
|
||||
var segments = [];
|
||||
masks.forEach(function (mask, i) {
|
||||
var hitBox = [mask.x, mask.y, mask.width, mask.height];
|
||||
segments = segments.concat(segmentBoundingBoxes.computeIntersections(hitBox));
|
||||
});
|
||||
segmentRenderer.animate(segments);
|
||||
}
|
||||
}
|
||||
|
||||
SegmentRenderer {
|
||||
id: segmentRenderer
|
||||
segmentsCount: segmentBoundingBoxes.count
|
||||
}
|
||||
|
||||
SegmentBoundingBoxes {
|
||||
id: segmentBoundingBoxes
|
||||
onReady: segImg.ready()
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,8 @@ Item {
|
||||
|
||||
property alias delegate: listView.delegate
|
||||
|
||||
property bool autoscroll: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
@ -40,7 +42,11 @@ Item {
|
||||
Layout.fillHeight: true
|
||||
model: logsModel
|
||||
clip: true
|
||||
// onCountChanged: positionViewAtEnd()
|
||||
onCountChanged: {
|
||||
if (root.autoscroll) {
|
||||
positionViewAtEnd()
|
||||
}
|
||||
}
|
||||
|
||||
onContentYChanged: {
|
||||
if (!logsModel.busy && contentY - originY < 5 * height) {
|
||||
|
||||
@ -17,39 +17,9 @@ DevicePageBase {
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
// Item {
|
||||
// Layout.fillWidth: true
|
||||
// Layout.preferredHeight: root.inputVisible ? inputColumn.implicitHeight : 0
|
||||
// Behavior on Layout.preferredHeight { NumberAnimation { duration: 130; easing.type: Easing.InOutQuad } }
|
||||
|
||||
// ColumnLayout {
|
||||
// id: inputColumn
|
||||
// anchors { left: parent.left; bottom: parent.bottom; right: parent.right }
|
||||
|
||||
// TextField {
|
||||
// id: titleTextField
|
||||
// Layout.fillWidth: true
|
||||
// Layout.topMargin: app.margins
|
||||
// Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
// placeholderText: qsTr("Title")
|
||||
// }
|
||||
|
||||
// TextArea {
|
||||
// id: bodyTextField
|
||||
// Layout.fillWidth: true
|
||||
// Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
|
||||
// placeholderText: qsTr("Text")
|
||||
// wrapMode: Text.WordWrap
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: app.margins
|
||||
Layout.leftMargin: app.margins; Layout.topMargin: app.margins; Layout.rightMargin: app.margins
|
||||
text: qsTr("Manage access")
|
||||
onClicked: {
|
||||
pageStack.push(manageUsersComponent)
|
||||
@ -69,12 +39,12 @@ DevicePageBase {
|
||||
live: true
|
||||
Component.onCompleted: update()
|
||||
typeIds: [root.accessGrantedEventType.id, root.accessDeniedEventType.id];
|
||||
|
||||
}
|
||||
|
||||
delegate: MeaListItemDelegate {
|
||||
width: parent.width
|
||||
iconName: "../images/notification.svg"
|
||||
iconName: model.typeId === root.accessGrantedEventType.id ? "../images/tick.svg" : "../images/dialog-error-symbolic.svg"
|
||||
iconColor: model.typeId === root.accessGrantedEventType.id ? "green" : "red"
|
||||
text: model.typeId === root.accessGrantedEventType.id ? qsTr("Access granted for user %1").arg(model.value) : qsTr("Access denied")
|
||||
subText: Qt.formatDateTime(model.timestamp)
|
||||
progressive: false
|
||||
@ -134,6 +104,28 @@ DevicePageBase {
|
||||
engine.deviceManager.executeAction(root.device.id, actionType.id, params)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
width: 200
|
||||
spacing: app.margins * 2
|
||||
visible: parent.count === 0
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width
|
||||
FingerprintVisual {
|
||||
id: fingerprintVisual
|
||||
anchors.centerIn: parent
|
||||
scale: parent.height / implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Add a fingerprint")
|
||||
onClicked: pageStack.push(addUserComponent)
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -153,8 +145,8 @@ DevicePageBase {
|
||||
Connections {
|
||||
target: engine.deviceManager
|
||||
onExecuteActionReply: {
|
||||
addUserPage.error = params["deviceError"] !== DeviceManager.DeviceErrorNoError
|
||||
print("Execute action reply:", params);
|
||||
addUserPage.error = params["deviceError"] !== "DeviceErrorNoError"
|
||||
print("Execute action reply:", params["deviceError"]);
|
||||
addUserSwipeView.currentIndex++
|
||||
}
|
||||
}
|
||||
@ -164,6 +156,7 @@ DevicePageBase {
|
||||
SwipeView {
|
||||
id: addUserSwipeView
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: app.margins * 2
|
||||
Layout.preferredHeight: 200
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Item {
|
||||
@ -182,6 +175,27 @@ DevicePageBase {
|
||||
id: userIdTextField
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Finger")
|
||||
}
|
||||
ComboBox {
|
||||
id: fingerComboBox
|
||||
Layout.fillWidth: true
|
||||
model: ListModel {
|
||||
ListElement { modelData: qsTr("Left thumb"); enumValue: "ThumbLeft" }
|
||||
ListElement { modelData: qsTr("Left index finger"); enumValue: "IndexFingerLeft" }
|
||||
ListElement { modelData: qsTr("Left middle finger"); enumValue: "MiddleFingerLeft" }
|
||||
ListElement { modelData: qsTr("Left ring finger"); enumValue: "RingFingerLeft" }
|
||||
ListElement { modelData: qsTr("Left pinky finger"); enumValue: "PinkyLeft" }
|
||||
ListElement { modelData: qsTr("Right thumb"); enumValue: "ThumbRight" }
|
||||
ListElement { modelData: qsTr("Right index finger"); enumValue: "IndexFingerRight" }
|
||||
ListElement { modelData: qsTr("Right middle finger"); enumValue: "MiddleFingerRight" }
|
||||
ListElement { modelData: qsTr("Right ring finger"); enumValue: "RingFingerRight" }
|
||||
ListElement { modelData: qsTr("Right pinky finger"); enumValue: "PinkyRight" }
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Add user")
|
||||
Layout.fillWidth: true
|
||||
@ -192,6 +206,10 @@ DevicePageBase {
|
||||
titleParam["paramTypeId"] = actionType.paramTypes.findByName("userId").id
|
||||
titleParam["value"] = userIdTextField.displayText
|
||||
params.push(titleParam)
|
||||
var fingerParam = {}
|
||||
fingerParam["paramTypeId"] = actionType.paramTypes.findByName("finger").id
|
||||
fingerParam["value"] = fingerComboBox.model.get(fingerComboBox.currentIndex).enumValue
|
||||
params.push(fingerParam)
|
||||
engine.deviceManager.executeAction(root.device.id, actionType.id, params)
|
||||
addUserSwipeView.currentIndex++
|
||||
}
|
||||
@ -206,9 +224,43 @@ DevicePageBase {
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: app.margins
|
||||
spacing: app.margins * 2
|
||||
Label {
|
||||
text: qsTr("Please scan the fingerprint now")
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: app.largeFont
|
||||
color: app.accentColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 200
|
||||
Layout.preferredHeight: 200
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
FingerprintVisual {
|
||||
id: fingerprintVisual
|
||||
scale: parent.height / implicitHeight
|
||||
|
||||
anchors.centerIn: parent
|
||||
Timer {
|
||||
interval: 500
|
||||
property real position: 0
|
||||
running: addUserSwipeView.currentIndex == 1
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
var masks = [];
|
||||
masks.push({x: 0, y: 0, width: 1, height: position})
|
||||
position += 0.1
|
||||
if (position < 1.1) {
|
||||
fingerprintVisual.masks = masks
|
||||
} else {
|
||||
position = 0
|
||||
fingerprintVisual.masks = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
nymea-app/ui/images/fingerprint/fingerprint_boxes.json
Normal file
1
nymea-app/ui/images/fingerprint/fingerprint_boxes.json
Normal file
@ -0,0 +1 @@
|
||||
{"width": 451, "boxes": [[291, 515, 104, 50], [269, 510, 96, 34], [227, 506, 62, 16], [198, 505, 72, 60], [144, 498, 46, 67], [200, 467, 133, 32], [214, 449, 87, 28], [67, 432, 25, 104], [396, 420, 31, 89], [177, 395, 22, 103], [133, 390, 15, 96], [266, 375, 8, 31], [1, 356, 7, 42], [336, 355, 27, 114], [221, 350, 95, 105], [243, 323, 17, 106], [155, 317, 68, 248], [110, 317, 43, 243], [22, 314, 12, 155], [199, 310, 13, 127], [302, 296, 83, 223], [276, 295, 20, 136], [311, 275, 30, 189], [177, 266, 8, 114], [88, 263, 34, 287], [221, 249, 9, 86], [441, 238, 9, 169], [110, 231, 9, 70], [44, 226, 16, 280], [310, 225, 8, 181], [199, 223, 53, 86], [370, 217, 37, 288], [249, 208, 25, 153], [177, 201, 61, 49], [281, 195, 15, 85], [133, 183, 16, 196], [177, 178, 96, 29], [155, 167, 35, 134], [345, 166, 18, 173], [200, 156, 81, 31], [1, 149, 17, 192], [386, 145, 18, 60], [305, 142, 36, 118], [66, 139, 26, 277], [149, 134, 168, 77], [111, 132, 45, 85], [88, 130, 36, 120], [350, 118, 35, 162], [165, 111, 134, 28], [262, 92, 85, 64], [414, 91, 35, 133], [129, 89, 119, 36], [22, 88, 42, 211], [92, 67, 256, 62], [47, 64, 75, 149], [349, 47, 80, 360], [132, 44, 257, 90], [13, 41, 56, 95], [66, 22, 273, 60], [198, 1, 215, 76], [74, 1, 109, 39]], "height": 566}
|
||||
BIN
nymea-app/ui/images/fingerprint/fingerprint_segmented.png
Normal file
BIN
nymea-app/ui/images/fingerprint/fingerprint_segmented.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
Reference in New Issue
Block a user