forked from s3lph/matemat
Touchkey documentation & cleanup.
This commit is contained in:
parent
be09ea1ee7
commit
61649657b0
4 changed files with 214 additions and 102 deletions
|
@ -1,9 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Initialize the touchkey setup.
|
||||||
|
*
|
||||||
|
* Requires an empty SVG container, and a HTML input tag (recommended: type="hidden") to write the string representation
|
||||||
|
* to. Can additionally be provided with the ID of a HTML form which should be auto-submitted after touchkey entry.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* <form id="touchkey-form" method="post" action="/login">
|
||||||
|
* <svg id="touchkey-svg"></svg>
|
||||||
|
* <input type="hidden" name="touchkey" id="touchkey-field" />
|
||||||
|
* </form>
|
||||||
|
* <script>
|
||||||
|
* initTouchkey(false, "touchkey-svg", "touchkey-form", "touchkey-field");
|
||||||
|
* </script>
|
||||||
|
*
|
||||||
|
* @param {boolean} keepPattern: Whether to keep the pattern on screen, or to clear it after the end event.
|
||||||
|
* @param {string} svgid: HTML id of the SVG container the touchkey is drawn in.
|
||||||
|
* @param {string} formid: HTML id of the form that should be submitted after touchkey entry. null to disable
|
||||||
|
* auto-submit.
|
||||||
|
* @param {string} formfieldid: ID of the input object that should receive the entered touchkey as its value.
|
||||||
|
*/
|
||||||
|
initTouchkey = (keepPattern, svgid, formid, formfieldid) => {
|
||||||
|
|
||||||
|
// Define forEach (iteration) and slice (abused for shallow copying) on HTMLCollections (reuse the Array methods)
|
||||||
HTMLCollection.prototype.forEach = Array.prototype.forEach;
|
HTMLCollection.prototype.forEach = Array.prototype.forEach;
|
||||||
HTMLCollection.prototype.slice = Array.prototype.slice;
|
HTMLCollection.prototype.slice = Array.prototype.slice;
|
||||||
|
|
||||||
initTouchkey = (keepPattern, svgid, formid, formfieldid) => {
|
// Get the DOM objects for the SVG, the form and the input field
|
||||||
|
|
||||||
let svg = document.getElementById(svgid);
|
let svg = document.getElementById(svgid);
|
||||||
let form;
|
let form;
|
||||||
if (formid !== null) {
|
if (formid !== null) {
|
||||||
|
@ -11,122 +34,231 @@ initTouchkey = (keepPattern, svgid, formid, formfieldid) => {
|
||||||
}
|
}
|
||||||
let formfield = document.getElementById(formfieldid);
|
let formfield = document.getElementById(formfieldid);
|
||||||
|
|
||||||
|
// Reference to the SVG line object that's currently being drawn by the user, or null, if none
|
||||||
let currentStroke = null;
|
let currentStroke = null;
|
||||||
|
// ID generator for the SVG line objects drawn by the user
|
||||||
let strokeId = 0;
|
let strokeId = 0;
|
||||||
|
// Set of touchkey pattern nodes that have already been visited by a users pattern, and may not be reused again
|
||||||
let doneMap = {};
|
let doneMap = {};
|
||||||
|
// The string representation of the touchkey entered by the user.
|
||||||
let enteredKey = '';
|
let enteredKey = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create a new stroke after the user completed one stroke by connecting two pattern nodes.
|
||||||
|
*
|
||||||
|
* @param {number} fromX: X coordinate of the starting point of this line.
|
||||||
|
* @param {number} fromY: Y coordinate of the starting point of this line.
|
||||||
|
* @param {number} toX: X coordinate of the ending point of this line.
|
||||||
|
* @param {number} toY: Y coordinate of the ending point of this line.
|
||||||
|
* @returns {string} The ID of the generated line object.
|
||||||
|
*/
|
||||||
let drawLine = (fromX, fromY, toX, toY) => {
|
let drawLine = (fromX, fromY, toX, toY) => {
|
||||||
|
// Create a new SVG line object
|
||||||
let line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
let line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||||
let id = 'l-' + (strokeId++);
|
// Generate and set an unique ID for the line object
|
||||||
let idAttr = document.createAttribute('id');
|
let id = 'touchkey-svg-stroke-' + (strokeId++);
|
||||||
let classAttr = document.createAttribute('class');
|
line.setAttribute('id', id);
|
||||||
let x1attr = document.createAttribute('x1');
|
// Create and set the HTML class attribute
|
||||||
let y1attr = document.createAttribute('y1');
|
line.setAttribute('class', 'touchkey-svg-stroke');
|
||||||
let x2attr = document.createAttribute('x2');
|
// Create and set the coordinate attributes
|
||||||
let y2attr = document.createAttribute('y2');
|
line.setAttribute('x1', fromX.toString());
|
||||||
let styleAttr = document.createAttribute('style');
|
line.setAttribute('y1', fromY.toString());
|
||||||
idAttr.value = id;
|
line.setAttribute('x2', toX.toString());
|
||||||
classAttr.value = 'l';
|
line.setAttribute('y2', toY.toString());
|
||||||
x1attr.value = fromX;
|
// Create and set the style attribute (grey, circular ends, 5% width)
|
||||||
y1attr.value = fromY;
|
line.setAttribute('style', 'stroke: grey; stroke-width: 5%; stroke-linecap: round');
|
||||||
x2attr.value = toX;
|
// Add the line to the SVG
|
||||||
y2attr.value = toY;
|
|
||||||
styleAttr.value = 'stroke: grey; stroke-width: 5%; stroke-linecap: round';
|
|
||||||
line.setAttributeNode(idAttr);
|
|
||||||
line.setAttributeNode(classAttr);
|
|
||||||
line.setAttributeNode(x1attr);
|
|
||||||
line.setAttributeNode(y1attr);
|
|
||||||
line.setAttributeNode(x2attr);
|
|
||||||
line.setAttributeNode(y2attr);
|
|
||||||
line.setAttributeNode(styleAttr);
|
|
||||||
svg.appendChild(line);
|
svg.appendChild(line);
|
||||||
|
// Return the previously generated ID to identify the line object by
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function used to remove the "trailing stroke" (i.e. after the user let go, there is a dangling stroke from
|
||||||
|
* the node that was hit last to the mouse pointer/finger).
|
||||||
|
*/
|
||||||
let endPath = () => {
|
let endPath = () => {
|
||||||
|
// Remove the current stroke ...
|
||||||
svg.removeChild(svg.getElementById(currentStroke));
|
svg.removeChild(svg.getElementById(currentStroke));
|
||||||
|
// ... and set its reference to null
|
||||||
currentStroke = null;
|
currentStroke = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function used to clear the touchkey pattern drawn by the user.
|
||||||
|
*/
|
||||||
let clearTouchkey = () => {
|
let clearTouchkey = () => {
|
||||||
|
// Reset the set of visited pattern nodes
|
||||||
doneMap = {};
|
doneMap = {};
|
||||||
|
// Reset the touchkey string representation
|
||||||
enteredKey = '';
|
enteredKey = '';
|
||||||
svg.getElementsByClassName('l').slice().reverse().forEach((line) => {
|
// Remove all line objects. Create a shallow copy of the list first to retain deletion order.
|
||||||
|
svg.getElementsByClassName('touchkey-svg-stroke').slice().forEach((line) => {
|
||||||
svg.removeChild(line);
|
svg.removeChild(line);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
svg.ontouchstart = svg.onmousedown = (ev) => {
|
/**
|
||||||
clearTouchkey();
|
* Helper function to read and convert the event coordinates of a MouseEvent or TouchEvent to coordinates relative
|
||||||
const svgrect = svg.getBoundingClientRect();
|
* to the SVG's origin.
|
||||||
|
*
|
||||||
|
* @param {(MouseEvent|TouchEvent)} ev: The event to get the X and Y coordinates from.
|
||||||
|
* @param {(ClientRect|DOMRect)} svgrect: Bounds rectangle of the SVG container.
|
||||||
|
* @returns {Array} The X and Y coordinates relative to the SVG's origin.
|
||||||
|
*/
|
||||||
|
let getEventCoordinates = (ev, svgrect) => {
|
||||||
|
// Check for existence of the "touches" property to distinguish between touch and mouse events
|
||||||
|
// For a touch event, take the page coordinates of the first touch
|
||||||
|
// For a mouse event, take the event coordinates
|
||||||
|
// Then subtract the SVG's origin from the page-relative coordinates to obtain the translated coordinates
|
||||||
const trX = (typeof ev.touches !== 'undefined' ? ev.touches[0].pageX : ev.x) - svgrect.left;
|
const trX = (typeof ev.touches !== 'undefined' ? ev.touches[0].pageX : ev.x) - svgrect.left;
|
||||||
const trY = (typeof ev.touches !== 'undefined' ? ev.touches[0].pageY : ev.y) - svgrect.top;
|
const trY = (typeof ev.touches !== 'undefined' ? ev.touches[0].pageY : ev.y) - svgrect.top;
|
||||||
|
return [trX, trY]
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the pattern node closest to a coordinate.
|
||||||
|
*
|
||||||
|
* @param {number} evX: X coordinate of the point to search the closest node for.
|
||||||
|
* @param {number} evY: Y coordinate of the point to search the closest node for.
|
||||||
|
* @param {(ClientRect|DOMRect)} svgrect: Bounds rectangle of the SVG container.
|
||||||
|
* @returns {Array} The node's ID, the squared distance, the X and Y coordinate of the node's center.
|
||||||
|
*/
|
||||||
|
let getNearestPatternNode = (evX, evY, svgrect) => {
|
||||||
|
// Initialize nearest neighbors search variables
|
||||||
let minId = '';
|
let minId = '';
|
||||||
let minDist = Infinity;
|
let minDist2 = Infinity; // Squared distance
|
||||||
let minx = 0;
|
let minX = 0;
|
||||||
let miny = 0;
|
let minY = 0;
|
||||||
doneMap = {};
|
// Iterate the pattern nodes for nearest neighbors search
|
||||||
document.getElementsByClassName('c').forEach((circle) => {
|
document.getElementsByClassName('touchkey-svg-node').forEach((node) => {
|
||||||
let x = parseFloat(circle.getAttribute('cx')) / 100.0 * svgrect.width;
|
// Get the position of a node's center, converted from ratio into absolute pixel count
|
||||||
let y = parseFloat(circle.getAttribute('cy')) / 100.0 * svgrect.height;
|
let x = parseFloat(node.getAttribute('cx')) / 100.0 * svgrect.width;
|
||||||
let dist = Math.pow(trX - x, 2) + Math.pow(trY - y, 2);
|
let y = parseFloat(node.getAttribute('cy')) / 100.0 * svgrect.height;
|
||||||
if (dist < minDist) {
|
// Compute the squared distance from the event coordinate to the node's center
|
||||||
minDist = dist;
|
let dist2 = Math.pow(evX - x, 2) + Math.pow(evY - y, 2);
|
||||||
minId = circle.id;
|
// Keep the properties of the closest node
|
||||||
minx = x;
|
if (dist2 < minDist2) {
|
||||||
miny = y;
|
minDist2 = dist2;
|
||||||
|
minId = node.dataset.stringRepresentation;
|
||||||
|
minX = x;
|
||||||
|
minY = y;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
currentStroke = drawLine(minx, miny, trX, trY);
|
|
||||||
|
return [minId, minDist2, minX, minY];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for "mouse down" / "touch down" events.
|
||||||
|
*
|
||||||
|
* Selects an "anchor node", i.e. the node where the pattern path started, and draws a line from there to the event
|
||||||
|
* coordinates.
|
||||||
|
*/
|
||||||
|
svg.ontouchstart = svg.onmousedown = (ev) => {
|
||||||
|
// Remove any previous strokes that may still be there if "keepPattern" was set to true in the init call
|
||||||
|
clearTouchkey();
|
||||||
|
// Get the SVG container's rectangle
|
||||||
|
const svgrect = svg.getBoundingClientRect();
|
||||||
|
// Get the event coordinates relative to the SVG container's origin
|
||||||
|
const [trX, trY] = getEventCoordinates(ev, svgrect);
|
||||||
|
// Get the closest pattern node
|
||||||
|
const [minId, _, minX, minY] = getNearestPatternNode(trX, trY, svgrect);
|
||||||
|
// Create the line from the anchor node to the event position
|
||||||
|
currentStroke = drawLine(minX, minY, trX, trY);
|
||||||
|
// Mark the anchor node as visited
|
||||||
doneMap[minId] = 1;
|
doneMap[minId] = 1;
|
||||||
|
// Add the anchor node's string representation to the touchkey string representation
|
||||||
enteredKey += minId;
|
enteredKey += minId;
|
||||||
};
|
};
|
||||||
|
|
||||||
svg.ontouchend = svg.onmouseup = (ev) => {
|
/**
|
||||||
|
* Event handler for "mouse move" / "touch move" events.
|
||||||
|
*/
|
||||||
|
svg.ontouchmove = svg.onmousemove = (ev) => {
|
||||||
|
// Only act if the user started is drawing a pattern (only relevant for mouse input)
|
||||||
|
if (currentStroke != null) {
|
||||||
|
// Get the SVG container's rectangle
|
||||||
|
const svgrect = svg.getBoundingClientRect();
|
||||||
|
// Get the event coordinates relative to the SVG container's origin
|
||||||
|
const [trX, trY] = getEventCoordinates(ev, svgrect);
|
||||||
|
// Get the closest pattern node
|
||||||
|
const [minId, minDist2, minX, minY] = getNearestPatternNode(trX, trY, svgrect);
|
||||||
|
// If the closest node is not visited yet, and the event coordinate is less than ~44px from the node's
|
||||||
|
// center, snap the current stroke to the node, and create a new stroke starting from this node
|
||||||
|
if (minDist2 < 2000 && !(minId in doneMap)) {
|
||||||
|
// Snap the current stroke to the node
|
||||||
|
let line = svg.getElementById(currentStroke);
|
||||||
|
line.setAttribute('x2', minX.toString());
|
||||||
|
line.setAttribute('y2', minY.toString());
|
||||||
|
// Create a new line object from the closest node to the event position
|
||||||
|
currentStroke = drawLine(minX, minY, trX, trY);
|
||||||
|
// Mark the closest node as visited
|
||||||
|
doneMap[minId] = 1;
|
||||||
|
// Append its string representation to the touchkey string representation
|
||||||
|
enteredKey += minId;
|
||||||
|
} else {
|
||||||
|
// If the stroke was not snapped to the closest node, update its end position
|
||||||
|
let line = svg.getElementById(currentStroke);
|
||||||
|
line.setAttribute('x2', trX);
|
||||||
|
line.setAttribute('y2', trY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for "mouse up" / "touch end" events.
|
||||||
|
*
|
||||||
|
* Sets the input object value, and optionally clears the SVG path and submits the form.
|
||||||
|
*/
|
||||||
|
svg.ontouchend = svg.onmouseup = () => {
|
||||||
|
// Remove the trailing, unfinished stroke
|
||||||
endPath();
|
endPath();
|
||||||
|
// Write the touchkey string representation to the input field
|
||||||
formfield.value = enteredKey;
|
formfield.value = enteredKey;
|
||||||
|
// Erase the touchkey pattern, if requested in the init call
|
||||||
if (keepPattern !== true) {
|
if (keepPattern !== true) {
|
||||||
clearTouchkey();
|
clearTouchkey();
|
||||||
}
|
}
|
||||||
|
// Submit the HTML form, if requested in the init call
|
||||||
if (formid !== null) {
|
if (formid !== null) {
|
||||||
form.submit();
|
form.submit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
svg.ontouchmove = svg.onmousemove = (ev) => {
|
/*
|
||||||
const svgrect = svg.getBoundingClientRect();
|
* Create the SVG touchkey nodes
|
||||||
const trX = (typeof ev.touches !== 'undefined' ? ev.touches[0].pageX : ev.x) - svgrect.left;
|
*/
|
||||||
const trY = (typeof ev.touches !== 'undefined' ? ev.touches[0].pageY : ev.y) - svgrect.top;
|
|
||||||
console.log(trY);
|
// Node ID provider
|
||||||
if (currentStroke != null) {
|
let touchkey_node_counter = 0;
|
||||||
let minId = '';
|
// Percentages for centers of quarters of the container's width and height
|
||||||
let minDist = Infinity;
|
['12.5%', '37.5%', '62.5%', '87.5%'].forEach((y) => {
|
||||||
let minx = 0;
|
['12.5%', '37.5%', '62.5%', '87.5%'].forEach((x) => {
|
||||||
let miny = 0;
|
// Create a new pattern node
|
||||||
document.getElementsByClassName('c').forEach((circle) => {
|
let node = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||||
let x = parseFloat(circle.getAttribute('cx')) / 100.0 * svgrect.width;
|
|
||||||
let y = parseFloat(circle.getAttribute('cy')) / 100.0 * svgrect.height;
|
// Generate a new ID (and the touchkey string representation from it)
|
||||||
let dist = Math.pow(trX - x, 2) + Math.pow(trY - y, 2);
|
let id = touchkey_node_counter++;
|
||||||
if (dist < minDist) {
|
node.dataset.stringRepresentation = id.toString(16).toLowerCase();
|
||||||
minDist = dist;
|
|
||||||
minId = circle.id;
|
// Class
|
||||||
minx = x;
|
node.setAttribute('class', 'touchkey-svg-node');
|
||||||
miny = y;
|
// Center
|
||||||
}
|
node.setAttribute('cx', x);
|
||||||
|
node.setAttribute('cy', y);
|
||||||
|
// Radius
|
||||||
|
node.setAttribute('r', '10%');
|
||||||
|
// Center color
|
||||||
|
node.setAttribute('fill', 'white');
|
||||||
|
// Circle color
|
||||||
|
node.setAttribute('stroke', 'grey');
|
||||||
|
// Circle width
|
||||||
|
node.setAttribute('stroke-width', '2%');
|
||||||
|
|
||||||
|
// Add the node to the SVG container
|
||||||
|
svg.appendChild(node);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
if (minDist < 2000 && !(minId in doneMap)) {
|
|
||||||
let line = svg.getElementById(currentStroke);
|
|
||||||
line.setAttribute('x2', minx);
|
|
||||||
line.setAttribute('y2', miny);
|
|
||||||
currentStroke = drawLine(minx, miny, trX, trY);
|
|
||||||
doneMap[minId] = 1;
|
|
||||||
enteredKey += minId;
|
|
||||||
}
|
|
||||||
let line = svg.getElementById(currentStroke);
|
|
||||||
line.setAttribute('x2', trX);
|
|
||||||
line.setAttribute('y2', trY);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
<form id="admin-touchkey-form" method="post" action="/admin?change=touchkey" accept-charset="UTF-8">
|
<form id="admin-touchkey-form" method="post" action="/admin?change=touchkey" accept-charset="UTF-8">
|
||||||
Draw a new touchkey (leave empty to disable):
|
Draw a new touchkey (leave empty to disable):
|
||||||
<br/>
|
<br/>
|
||||||
{% include "touchkey.svg" %}
|
<svg id="touchkey-svg" width="400" height="400"></svg>
|
||||||
<br/>
|
<br/>
|
||||||
<input id="admin-touchkey-touchkey" type="hidden" name="touchkey" value="" />
|
<input id="admin-touchkey-touchkey" type="hidden" name="touchkey" value="" />
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
{% include "touchkey.svg" %}
|
|
||||||
|
<svg id="touchkey-svg" width="400" height="400"></svg>
|
||||||
|
|
||||||
<form method="post" action="/touchkey" id="loginform" accept-charset="UTF-8">
|
<form method="post" action="/touchkey" id="loginform" accept-charset="UTF-8">
|
||||||
<input type="hidden" name="uid" value="{{ uid }}" />
|
<input type="hidden" name="uid" value="{{ uid }}" />
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
<svg id="touchkey-svg" width="400" height="400">
|
|
||||||
<circle class="c" id="0" cx="12.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
<circle class="c" id="1" cx="37.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
<circle class="c" id="2" cx="62.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
<circle class="c" id="3" cx="87.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
|
|
||||||
<circle class="c" id="4" cx="12.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
<circle class="c" id="5" cx="37.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
<circle class="c" id="6" cx="62.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
<circle class="c" id="7" cx="87.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
|
|
||||||
<circle class="c" id="8" cx="12.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
<circle class="c" id="9" cx="37.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
<circle class="c" id="a" cx="62.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
<circle class="c" id="b" cx="87.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
|
|
||||||
<circle class="c" id="c" cx="12.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
<circle class="c" id="d" cx="37.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
<circle class="c" id="e" cx="62.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
<circle class="c" id="f" cx="87.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.7 KiB |
Loading…
Reference in a new issue