// Used to hold mouse coordinates class MousePos { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } toString(): string { return "{" + this.x + "," + this.y + "}"; } } // used to track mouse movement while mouse is down var mouseIsDown: boolean = false; // The main drawing canvas var canvas: HTMLCanvasElement; var ctx: CanvasRenderingContext2D; // Used to store gesture points while being drawn var stack: Array = []; // Get the mouse position on the canvas function getMousePos(evt: MouseEvent): MousePos { var rect = canvas.getBoundingClientRect(); return new MousePos( evt.clientX - rect.left, evt.clientY - rect.top ); } // Start stroke tracking function mouseDown(me: MouseEvent) { stack = []; mouseIsDown = true; var pos: MousePos = getMousePos(me); stack.push(pos); ctx.beginPath(); ctx.moveTo(pos.x, pos.y); me.preventDefault(); } // Stop if leave canvas area function mouseLeave(me: MouseEvent) { mouseIsDown = false; me.preventDefault(); } // The gesture is finished, attempt to match and display results function mouseUp(me: MouseEvent) { mouseIsDown = false; var m: Array> = match(center(rescaleByDistance(stack, 40))); drawPatterns(m); me.preventDefault(); } // Rescaling by time is the easiest. Not used this demonstration function rescaleByTime(arr: Array, length: number): Array { var r: Array = []; r.push(arr[0]); for (var i: number = 1; i < length - 1; ++i) { var index = i / length * arr.length; var start = Math.floor(index); var end = start + 1; var fraction = index - start; var x = (arr[end].x - arr[start].x) * fraction + arr[start].x; var y = (arr[end].y - arr[start].y) * fraction + arr[start].y; r.push(new MousePos(x, y)); } r.push(arr[arr.length - 1]); return r; } // Binary Search that gives us fractional index function binarySearch(arr: Array, d: number): number { var startIndex: number = 0; var stopIndex: number = arr.length - 1; var middle: number = Math.floor((stopIndex + startIndex) / 2); while (arr[middle] != d && startIndex < stopIndex) { if (d < arr[middle]) { stopIndex = middle - 1; } else if (d > arr[middle]) { startIndex = middle + 1; } middle = Math.floor((stopIndex + startIndex) / 2); } return (d - arr[middle]) / (arr[middle + 1] - arr[middle]) + middle; } // Calculate the distance between two mouse points function dist(a: MousePos, b: MousePos): number { var dx = (b.x - a.x); var dy = (b.y - a.y); return Math.sqrt(dx * dx + dy * dy); } // Rescale the gesture to fixed number of points function rescaleByDistance(arr: Array, length: number): Array { var distArr: Array = []; var tot = 0; var cur = arr[0]; distArr.push(0); for (var i = 1; i < arr.length; ++i) { tot += dist(arr[i], arr[i - 1]); distArr.push(tot); } var r: Array = []; r.push(arr[0]); var old: MousePos = arr[0]; for (var i: number = 1; i < length - 2; ++i) { // TODO // There is an error here somewhere, but haven't found it yet so do try/catch for now try { var d = i / (length - 1) * tot; var index = binarySearch(distArr, d); var start = Math.floor(index); var end = start + 1; var fraction = index - start; var x = (arr[end].x - arr[start].x) * fraction + arr[start].x; var y = (arr[end].y - arr[start].y) * fraction + arr[start].y; var p = new MousePos(x, y); r.push(p); old = p; } catch (error) { r.push(old); } } r.push(arr[arr.length - 1]); return r; } // Utility to print out gesture points for debuggin purposes (not used in demonstration) function stackToString(arr: Array): string { var sep: string = ""; var str: string = ""; str += "{"; for (var x in arr) { str += sep; str += arr[x].toString(); sep = ","; } str += "}"; return str; } // Clear the canvas function clear() { ctx.clearRect(0, 0, canvas.width, canvas.height); } // Add a point to gesture line if mouse is down function mouseMove(me: MouseEvent) { if (mouseIsDown) { var pos: MousePos = getMousePos(me); stack.push(pos); ctx.lineTo(pos.x, pos.y); ctx.stroke(); } } // Perform matching between to MousePos arrays // this is done as if the MousePos was a complex number // with x being real and y being imaginary // Mathematically this is a.Conjugate(b) function cross(a: Array, b: Array): Array { var match: Array = [0, 0]; for (var i = 0; i < a.length; ++i) { match[0] += a[i].x * b[i].x + a[i].y * b[i].y; match[1] += a[i].y * b[i].x - a[i].x * b[i].y; } return match; } // Try to match gesture against known gestures function match(a: Array): Array> { var match: Array> = []; for (var i = 0; i < pattern.length; ++i) { var b: Array = pattern[i]; var aa: Array = cross(a, a); var bb: Array = cross(b, b); var m: Array = cross(a, b); var r: number = Math.sqrt(aa[0] * bb[0]); m[0] /= r; m[1] /= r; match.push([m[0], m[1]]); } return match; } // Center a mouse array around the Complex(0,0) point by // removing average value function center(arr: Array): Array { var r: Array = []; var x: number = 0; var y: number = 0; for (var i = 0; i < arr.length; ++i) { x += arr[i].x; y += arr[i].y; } x /= arr.length; y /= arr.length; for (var i = 0; i < arr.length; ++i) { r.push(new MousePos(arr[i].x - x, arr[i].y - y)); } return r; } // Plot a stored pattern in a given position and scale function plotPattern(ctx: CanvasRenderingContext2D, n: number, xoff: number, scale: number) { var b = pattern[n]; ctx.beginPath(); ctx.arc(b[0].x / scale + xoff, b[0].y / scale + 40, 4, 0, 2 * Math.PI); ctx.stroke(); if (n == 0 || n == 8) { ctx.beginPath(); ctx.arc(b[b.length - 3].x / scale + xoff, b[b.length - 3].y / scale + 40, 2, 0, 2 * Math.PI); ctx.fill(); } else { ctx.beginPath(); ctx.arc(b[b.length - 1].x / scale + xoff, b[b.length - 1].y / scale + 40, 2, 0, 2 * Math.PI); ctx.fill(); } ctx.beginPath(); ctx.moveTo(b[0].x / scale + xoff, b[0].y / scale + 40); for (var i = 1; i < b.length; ++i) { ctx.lineTo(b[i].x / scale + xoff, b[i].y / scale + 40); } ctx.stroke(); } // Show real number to 2 decimal points function formatNumber(n: number): string { return n.toFixed(2); } var first = true; // Draw the patterns and indicate a match if there is one function drawPatterns(match: Array>): void { var ind = -1; if (!first) { var max = 0; for (var i = 0; i < match.length; ++i) { var r: number = match[i][0] * match[i][0] + match[i][1] * match[i][1]; if (r > max) { ind = i; max = r; } } // If 6 or 9 need to take angle into account if (ind == 6 || ind == 9) { if (match[6][0] > match[9][0]) { ind = 6; } else { ind = 9; } } } first = false; var canvas = document.getElementById("gestures"); var ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); for (var i = 0; i < pattern.length; ++i) { console.log("Drawing pattern " + i); var off = i * 75 + 50; if (i == ind) { ctx.strokeStyle = "#f00"; ctx.fillStyle = "#f00"; } else { ctx.strokeStyle = "#000"; ctx.fillStyle = "#000"; } plotPattern(ctx, i, off, 300); ctx.fillText("Cor: " + formatNumber((match[i][0] * match[i][0] + match[i][1] * match[i][1])), off - 30, 90); ctx.fillText("Deg: " + formatNumber(Math.atan2(match[i][1], match[i][0]) * 180 / Math.PI), off - 30, 110); } } // The starting function that executes when webpage is loaded function main(): void { canvas = document.getElementById("main"); ctx = canvas.getContext("2d"); ctx.strokeStyle = "#88f"; canvas.onpointerdown = mouseDown; canvas.onpointerup = mouseUp; canvas.onpointerleave = mouseLeave; canvas.onpointermove = mouseMove; document.getElementById("clear")!.onclick = clear; for (var i = 0; i < patternArr.length; ++i) { var list: Array = []; var x: Array> = patternArr[i]; for (var j = 0; j < x.length; ++j) { var pair: Array = x[j]; list.push(new MousePos(pair[0], pair[1])); } pattern.push(center(list)); } var match: Array> = []; for (var i = 0; i < patternArr.length; ++i) { match.push([0, 0]); } drawPatterns(match); } document.body.onload = main; // Used to store patterns var pattern: Array> = []; // Compact representation of known patterns an array of arrays of arrays var patternArr: Array>> = [[[3493, 0], [2812, 0], [2161, 208], [1653, 595], [1244, 1102], [923, 1663], [642, 2272], [441, 2880], [361, 3592], [281, 4304], [160, 4981], [0, 5683], [0, 6402], [120, 7100], [361, 7686], [602, 8402], [1004, 8833], [1412, 9284], [1799, 9799], [2451, 9999], [3142, 9919], [3812, 9680], [4404, 9290], [4985, 8869], [5535, 8399], [5892, 7802], [6104, 7161], [6305, 6536], [6465, 5783], [6546, 5099], [6586, 4327], [6557, 3596], [6425, 2886], [6265, 2180], [6109, 1496], [5781, 881], [5280, 441], [4759, 200], [4111, 40], [3413, 0]], [[107, 0], [199, 214], [249, 448], [249, 713], [249, 977], [249, 1241], [249, 1506], [249, 1770], [249, 2035], [249, 2299], [249, 2563], [249, 2828], [249, 3078], [249, 3357], [249, 3623], [214, 3877], [178, 4136], [142, 4394], [137, 4658], [107, 4917], [90, 5085], [71, 5428], [71, 5705], [71, 5969], [35, 6228], [35, 6488], [35, 6756], [13, 7029], [0, 7244], [0, 7517], [46, 7796], [107, 8041], [157, 8295], [178, 8556], [214, 8814], [214, 9079], [214, 9343], [213, 9607], [142, 9842], [107, 10000]], [[506, 1239], [1140, 940], [1745, 555], [2404, 213], [3141, 42], [3981, 0], [4820, 42], [5633, 170], [6323, 474], [6812, 940], [7087, 1623], [7216, 2402], [7216, 3260], [7130, 4082], [6955, 4842], [6703, 5675], [6437, 6428], [6088, 7169], [5581, 7746], [5059, 8395], [4609, 8941], [3991, 9293], [3360, 9625], [2683, 9914], [1861, 10000], [1089, 9914], [387, 9700], [0, 9188], [79, 8371], [614, 7948], [1436, 7863], [2205, 8066], [2885, 8446], [3602, 8803], [4267, 9096], [5015, 9230], [5720, 9534], [6334, 9914], [7059, 10000], [7771, 9914]], [[159, 477], [605, 331], [968, 95], [1475, 0], [2016, 31], [2544, 92], [3052, 254], [3471, 501], [3821, 799], [3980, 1287], [4114, 1783], [4203, 2285], [4203, 2859], [4108, 3373], [3990, 3875], [3821, 4345], [3504, 4713], [3168, 5016], [2763, 5286], [2267, 5382], [1751, 5476], [1242, 5540], [1495, 5541], [2005, 5604], [2456, 5764], [2866, 6020], [3210, 6305], [3521, 6624], [3630, 7106], [3566, 7634], [3503, 8144], [3309, 8598], [3152, 9033], [2898, 9426], [2468, 9665], [2054, 9887], [1528, 9981], [987, 10000], [445, 9999], [0, 9936]], [[3229, 10000], [3293, 9407], [3326, 8771], [3391, 8153], [3520, 7546], [3520, 6908], [3520, 6270], [3520, 5632], [3552, 4998], [3585, 4366], [3617, 3745], [3714, 3108], [3801, 2511], [3876, 1900], [3908, 1247], [3973, 674], [3939, 63], [3749, 0], [3455, 381], [3154, 819], [2753, 1252], [2367, 1754], [1992, 2239], [1678, 2682], [1297, 3171], [942, 3581], [716, 4130], [406, 4602], [0, 4976], [43, 5145], [654, 5145], [1251, 5273], [1893, 5307], [2522, 5307], [3159, 5307], [3771, 5307], [4435, 5307], [5073, 5307], [5697, 5275], [6303, 5307]], [[6480, 0], [5784, 0], [5177, 214], [4554, 343], [3873, 386], [3177, 386], [2499, 343], [1803, 343], [1122, 300], [643, 308], [600, 961], [557, 1639], [472, 2275], [472, 2971], [386, 3606], [379, 4163], [928, 3991], [1625, 3991], [2260, 3948], [2994, 3948], [3674, 4034], [4247, 4204], [4954, 4396], [5555, 4635], [6035, 5005], [6383, 5599], [6480, 6271], [6523, 6949], [6437, 7617], [6102, 8181], [5654, 8680], [5141, 9150], [4572, 9547], [3988, 9785], [3326, 9906], [2658, 9986], [1949, 10000], [1306, 9871], [653, 9828], [0, 9785]], [[4296, 0], [3851, 256], [3391, 571], [2909, 941], [2444, 1319], [2037, 1764], [1777, 2343], [1444, 2818], [1011, 3210], [656, 3676], [518, 4327], [444, 4997], [430, 5694], [259, 6328], [37, 6915], [0, 7548], [148, 8172], [444, 8657], [686, 9242], [1131, 9666], [1733, 9851], [2329, 9996], [3030, 10000], [3701, 9928], [4222, 9682], [4703, 9326], [5082, 8851], [5370, 8331], [5555, 7750], [5444, 7139], [5185, 6610], [4792, 6259], [4242, 6000], [3696, 5733], [3038, 5629], [2383, 5727], [1735, 5814], [1191, 6030], [654, 6222], [222, 6481]], [[0, 128], [514, 171], [1028, 214], [1542, 257], [2057, 214], [2510, 85], [2982, 0], [3471, 42], [3960, 85], [4432, 171], [4964, 171], [5496, 171], [6010, 214], [6507, 284], [6780, 386], [6480, 643], [6180, 925], [5922, 1326], [5579, 1640], [5255, 1974], [4921, 2331], [4678, 2762], [4472, 3209], [4163, 3538], [3844, 3837], [3497, 4163], [3186, 4453], [2961, 4892], [2767, 5344], [2575, 5771], [2317, 6171], [2103, 6539], [1888, 6882], [1716, 7292], [1545, 7703], [1401, 8126], [1268, 8602], [1201, 9107], [1201, 9639], [1201, 10000]], [[3262, 0], [2519, 0], [1829, 211], [1231, 466], [805, 941], [593, 1606], [466, 2332], [508, 3142], [720, 3832], [987, 4491], [1407, 5135], [1960, 5677], [2700, 5889], [3238, 6289], [3678, 6864], [4125, 7303], [4194, 8017], [4110, 8785], [3867, 9437], [3255, 9838], [2479, 10000], [1664, 9957], [894, 9830], [256, 9491], [0, 8846], [0, 8019], [84, 7219], [254, 6461], [561, 5752], [1059, 5205], [1610, 4804], [2180, 4364], [2754, 3970], [3262, 3445], [3644, 2842], [3983, 2252], [4194, 1540], [4110, 772], [3728, 301], [3177, 84]], [[526, 10000], [889, 9677], [1147, 9257], [1369, 8765], [1677, 8281], [2035, 7883], [2334, 7409], [2578, 6908], [2759, 6372], [2938, 5857], [3140, 5315], [3337, 4765], [3519, 4255], [3677, 3709], [3824, 3161], [4020, 2606], [4210, 2105], [4291, 1562], [4251, 1004], [3978, 577], [3542, 364], [3074, 161], [2526, 40], [1992, 0], [1470, 155], [945, 337], [518, 615], [283, 963], [161, 1481], [40, 1967], [0, 2525], [0, 3093], [162, 3522], [439, 3880], [704, 4267], [1130, 4493], [1617, 4574], [2184, 4574], [2711, 4615], [3238, 4655]]];