Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 1 | // Copyright (c) 2014 The Chromium OS Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | |
| 6 | /** |
| 7 | * Choose finger colors for circles and the click color for rectangles. |
| 8 | * @constructor |
| 9 | */ |
| 10 | function Color() { |
| 11 | this.tids = []; |
| 12 | this.lastIndex = -1; |
| 13 | this.COLOR_TABLE = [ |
| 14 | 'Blue', 'Gold', 'LimeGreen', 'Red', 'Cyan', |
| 15 | 'Magenta', 'Brown', 'Wheat', 'DarkGreen', 'Coral', |
| 16 | ]; |
| 17 | this.length = this.COLOR_TABLE.length; |
| 18 | this.COLOR_CLICK = 'Gray'; |
| 19 | this.COLOR_FRAME = 'Gray'; |
| 20 | |
| 21 | for (var i = 0; i < this.length; i++) { |
| 22 | this.tids[i] = -1; |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | |
| 27 | /** |
| 28 | * Get the color to draw a circle for a given Tracking ID (tid). |
| 29 | * @param {int} tid |
| 30 | * @return {string} |
| 31 | */ |
| 32 | Color.prototype.getCircleColor = function(tid) { |
| 33 | index = this.tids.indexOf(tid); |
| 34 | // Find next color for this new tid. |
| 35 | if (index == -1) { |
| 36 | var i = (this.lastIndex + 1) % this.length; |
| 37 | while (i != this.lastIndex) { |
| 38 | if (this.tids[i] == -1) { |
| 39 | this.tids[i] = tid; |
| 40 | this.lastIndex = i; |
| 41 | return this.COLOR_TABLE[i]; |
| 42 | } |
| 43 | i = (i + 1) % this.length; |
| 44 | } |
| 45 | |
| 46 | // It is very unlikely that all slots in this.tids have been occupied. |
| 47 | // Should it happen, just assign a color to it. |
| 48 | return this.COLOR_TABLE[0]; |
| 49 | } else { |
| 50 | return this.COLOR_TABLE[index]; |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | |
| 55 | /** |
| 56 | * Get the color to draw a rectangle for a given Tracking ID (tid). |
| 57 | * @param {int} tid |
| 58 | * @return {string} |
| 59 | */ |
| 60 | Color.prototype.getRectColor = function(tid) { |
| 61 | return this.COLOR_CLICK; |
| 62 | } |
| 63 | |
| 64 | |
| 65 | /** |
| 66 | * Remove the Tracking ID (tid) from the tids array. |
| 67 | * @param {int} tid |
| 68 | */ |
| 69 | Color.prototype.remove = function(tid) { |
| 70 | index = this.tids.indexOf(tid); |
| 71 | if (index >= 0) { |
| 72 | this.tids[index] = -1; |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | |
| 77 | /** |
| 78 | * Pick up colors for circles and rectangles. |
| 79 | * @constructor |
| 80 | * @param {Element} canvas the canvas to draw circles and clicks. |
| 81 | * @param {int} touchMinX the min x value of the touch device. |
| 82 | * @param {int} touchMaxX the max x value of the touch device. |
| 83 | * @param {int} touchMinY the min y value of the touch device. |
| 84 | * @param {int} touchMaxY the max y value of the touch device. |
| 85 | * @param {int} touchMinPressure the min pressure value of the touch device. |
| 86 | * @param {int} touchMaxPressure the max pressure value of the touch device. |
Jingkui Wang | 55115ef | 2017-06-17 13:44:33 -0700 | [diff] [blame^] | 87 | * @param {int} tiltMinX the min tilt x value of the touch device. |
| 88 | * @param {int} tiltMaxX the max tilt x value of the touch device. |
| 89 | * @param {int} tiltMinY the min tilt y value of the touch device. |
| 90 | * @param {int} tiltMaxY the max tilt y value of the touch device. |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 91 | */ |
| 92 | function Webplot(canvas, touchMinX, touchMaxX, touchMinY, touchMaxY, |
Jingkui Wang | 55115ef | 2017-06-17 13:44:33 -0700 | [diff] [blame^] | 93 | touchMinPressure, touchMaxPressure, tiltMinX, tiltMaxX, |
| 94 | tiltMinY, tiltMaxY) { |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 95 | this.canvas = canvas; |
| 96 | this.ctx = canvas.getContext('2d'); |
| 97 | this.color = new Color(); |
| 98 | this.minX = touchMinX; |
| 99 | this.maxX = touchMaxX; |
| 100 | this.minY = touchMinY; |
| 101 | this.maxY = touchMaxY; |
| 102 | this.minPressure = touchMinPressure; |
| 103 | this.maxPressure = touchMaxPressure; |
Jingkui Wang | 55115ef | 2017-06-17 13:44:33 -0700 | [diff] [blame^] | 104 | this.tiltMinX = tiltMinX; |
| 105 | this.tiltMaxX = tiltMaxX; |
| 106 | this.tiltMinY = tiltMinY; |
| 107 | this.tiltMaxY = tiltMaxY; |
| 108 | this.showTilt = ! ((tiltMinX == tiltMaxX) && (tiltMinY == tiltMaxY)) |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 109 | this.maxRadiusRatio = 0.03; |
| 110 | this.maxRadius = null; |
| 111 | this.clickEdge = null; |
| 112 | this.clickDown = false; |
| 113 | this.pressureMode = true; |
| 114 | this.pointRadius = 2; |
Joseph Hwang | 95bf52a | 2015-04-14 13:09:39 +0800 | [diff] [blame] | 115 | this.saved_events = '/tmp/webplot.dat'; |
| 116 | this.saved_image = '/tmp/webplot.png'; |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 117 | } |
| 118 | |
| 119 | |
| 120 | /** |
| 121 | * Update the width and height of the canvas, the max radius of circles, |
| 122 | * and the edge of click rectangles. |
| 123 | */ |
| 124 | Webplot.prototype.updateCanvasDimension = function() { |
| 125 | var newWidth = document.body.clientWidth; |
| 126 | var newHeight = document.body.clientHeight; |
| 127 | |
| 128 | if (this.canvas.width != newWidth || this.canvas.height != newHeight) { |
| 129 | var deviceRatio = (this.maxY - this.minY) / (this.maxX - this.minX); |
| 130 | var canvasRatio = (newHeight / newWidth); |
| 131 | |
| 132 | // The actual dimension of the viewport. |
| 133 | this.canvas.width = newWidth; |
| 134 | this.canvas.height = newHeight; |
| 135 | |
| 136 | // Calculate the inner area of the viewport on which to draw finger traces. |
| 137 | // This inner area has the same height/width ratio as the touch device. |
| 138 | if (deviceRatio >= canvasRatio) { |
| 139 | this.canvas.innerWidth = Math.round(newHeight / deviceRatio); |
| 140 | this.canvas.innerHeight = newHeight; |
| 141 | this.canvas.innerOffsetLeft = Math.round( |
| 142 | (newWidth - this.canvas.innerWidth) / 2); |
| 143 | this.canvas.innerOffsetTop = 0; |
| 144 | } else { |
| 145 | this.canvas.innerWidth = newWidth; |
| 146 | this.canvas.innerHeight = Math.round(newWidth * deviceRatio); |
| 147 | this.canvas.innerOffsetLeft = 0; |
| 148 | this.canvas.innerOffsetTop = Math.round( |
| 149 | (newHeight - this.canvas.innerHeight) / 2); |
| 150 | } |
| 151 | |
| 152 | this.maxRadius = Math.min(this.canvas.innerWidth, this.canvas.innerHeight) * |
| 153 | this.maxRadiusRatio; |
| 154 | this.clickEdge = (this.pressureMode ? this.maxRadius : this.maxRadius / 2); |
| 155 | } |
| 156 | this.drawRect(this.canvas.innerOffsetLeft, this.canvas.innerOffsetTop, |
| 157 | this.canvas.innerWidth, this.canvas.innerHeight, |
| 158 | this.color.COLOR_FRAME); |
| 159 | } |
| 160 | |
| 161 | |
| 162 | /** |
| 163 | * Draw a circle. |
| 164 | * @param {int} x the x coordinate of the circle. |
| 165 | * @param {int} y the y coordinate of the circle. |
| 166 | * @param {int} r the radius of the circle. |
| 167 | * @param {string} colorName |
| 168 | */ |
| 169 | Webplot.prototype.drawCircle = function(x, y, r, colorName) { |
| 170 | this.ctx.beginPath(); |
| 171 | this.ctx.fillStyle = colorName; |
| 172 | this.ctx.arc(x, y, r, 0, 2 * Math.PI); |
| 173 | this.ctx.fill(); |
| 174 | } |
| 175 | |
| 176 | |
| 177 | /** |
| 178 | * Draw a rectangle. |
| 179 | * @param {int} x the x coordinate of upper left corner of the rectangle. |
| 180 | * @param {int} y the y coordinate of upper left corner of the rectangle. |
| 181 | * @param {int} width the width of the rectangle. |
| 182 | * @param {int} height the height of the rectangle. |
| 183 | * @param {string} colorName |
| 184 | */ |
| 185 | Webplot.prototype.drawRect = function(x, y, width, height, colorName) { |
| 186 | this.ctx.beginPath(); |
| 187 | this.ctx.lineWidth = "4"; |
| 188 | this.ctx.strokeStyle = colorName; |
| 189 | this.ctx.rect(x, y, width, height); |
| 190 | this.ctx.stroke(); |
| 191 | } |
| 192 | |
| 193 | |
| 194 | /** |
| 195 | * Fill text. |
| 196 | * @param {string} text the text to display |
| 197 | * @param {int} x the x coordinate of upper left corner of the text. |
| 198 | * @param {int} y the y coordinate of upper left corner of the text. |
| 199 | * @param {string} font the size and the font |
| 200 | */ |
| 201 | Webplot.prototype.fillText = function(text, x, y, font) { |
| 202 | this.ctx.font = font; |
| 203 | this.ctx.fillText(text, x, y); |
| 204 | } |
| 205 | |
| 206 | |
| 207 | /** |
| 208 | * Capture the canvas image. |
| 209 | * @return {string} the image represented in base64 text |
| 210 | */ |
| 211 | Webplot.prototype.captureCanvasImage = function() { |
| 212 | var imageData = this.canvas.toDataURL('image/png'); |
| 213 | // Strip off the header. |
| 214 | return imageData.replace(/^data:image\/png;base64,/, ''); |
| 215 | } |
| 216 | |
| 217 | |
| 218 | /** |
| 219 | * Process an incoming snapshot. |
| 220 | * @param {object} snapshot |
| 221 | * |
| 222 | * A 2f snapshot received from the python server looks like: |
| 223 | * MtbSnapshot( |
| 224 | * syn_time=1420522152.269537, |
| 225 | * button_pressed=False, |
Jingkui Wang | 55115ef | 2017-06-17 13:44:33 -0700 | [diff] [blame^] | 226 | * points =[ |
| 227 | * TouchPoint(tid=13, slot=0, syn_time=1420522152.269537, x=440, y=277, |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 228 | * pressure=33), |
Jingkui Wang | 55115ef | 2017-06-17 13:44:33 -0700 | [diff] [blame^] | 229 | * TouchPoint(tid=14, slot=1, syn_time=1420522152.269537, x=271, y=308, |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 230 | * pressure=38) |
| 231 | * ] |
| 232 | * ) |
| 233 | */ |
| 234 | Webplot.prototype.processSnapshot = function(snapshot) { |
| 235 | var edge = this.clickEdge; |
| 236 | |
Jingkui Wang | 55115ef | 2017-06-17 13:44:33 -0700 | [diff] [blame^] | 237 | for (var i = 0; i < snapshot.points.length; i++) { |
| 238 | var finger = snapshot.points[i]; |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 239 | |
| 240 | // Update the color object if the finger is leaving. |
| 241 | if (finger.leaving) { |
| 242 | this.color.remove(finger.tid); |
| 243 | continue; |
| 244 | } |
| 245 | |
| 246 | // Calculate (x, y) based on the inner width/height which has the same |
| 247 | // dimension ratio as the touch device. |
| 248 | var x = (finger.x - this.minX) / (this.maxX - this.minX) * |
| 249 | this.canvas.innerWidth + this.canvas.innerOffsetLeft; |
| 250 | var y = (finger.y - this.minY) / (this.maxY - this.minY) * |
| 251 | this.canvas.innerHeight + this.canvas.innerOffsetTop; |
| 252 | if (this.pressureMode) |
| 253 | var r = (finger.pressure - this.minPressure) / |
| 254 | (this.maxPressure - this.minPressure) * this.maxRadius; |
| 255 | else |
| 256 | var r = this.pointRadius; |
| 257 | |
| 258 | this.drawCircle(x, y, r, this.color.getCircleColor(finger.tid)); |
| 259 | |
| 260 | // If there is a click, draw the click with finger 0. |
| 261 | // The flag clickDown is used to draw the click exactly once |
| 262 | // during the click down period. |
| 263 | if (snapshot.button_pressed == 1 && i == 0 && !this.clickDown) { |
| 264 | this.drawRect(x, y, edge, edge, this.color.getRectColor()); |
| 265 | this.clickDown = true; |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | // In some special situation, the click comes with no fingers. |
| 270 | // This may happen if an insulated object is used to click the touchpad. |
| 271 | // Just draw the click at a random position. |
Jingkui Wang | 55115ef | 2017-06-17 13:44:33 -0700 | [diff] [blame^] | 272 | if (snapshot.points.length == 0 && snapshot.button_pressed == 1 && |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 273 | !this.clickDown) { |
| 274 | var x = Math.random() * this.canvas.innerWidth + |
| 275 | this.canvas.innerOffsetLeft; |
| 276 | var y = Math.random() * this.canvas.innerHeight + |
| 277 | this.canvas.innerOffsetTop; |
| 278 | this.drawRect(x, y, edge, edge, this.color.getRectColor()); |
| 279 | this.clickDown = true; |
| 280 | } |
| 281 | |
| 282 | if (snapshot.button_pressed == 0) { |
| 283 | this.clickDown = false; |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | |
Joseph Hwang | 95bf52a | 2015-04-14 13:09:39 +0800 | [diff] [blame] | 288 | Webplot.quitFlag = false; |
| 289 | |
| 290 | |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 291 | /** |
| 292 | * An handler for onresize event to update the canvas dimensions. |
| 293 | */ |
| 294 | function resizeCanvas() { |
| 295 | webplot.updateCanvasDimension(); |
| 296 | } |
| 297 | |
| 298 | |
| 299 | /** |
| 300 | * Send a 'quit' message to the server and display the event file name |
| 301 | * on the canvas. |
| 302 | * @param {boolean} closed_by_server True if this is requested by the server. |
| 303 | */ |
| 304 | function quit(closed_by_server) { |
| 305 | var canvas = document.getElementById('canvasWebplot'); |
| 306 | var webplot = window.webplot; |
| 307 | var startX = 100; |
| 308 | var startY = 100; |
| 309 | var font = '30px Verdana'; |
| 310 | |
| 311 | // Capture the image before clearing the canvas and send it to the server, |
| 312 | // and notify the server that this client quits. |
Joseph Hwang | 95bf52a | 2015-04-14 13:09:39 +0800 | [diff] [blame] | 313 | if (!Webplot.quitFlag) { |
| 314 | Webplot.quitFlag = true; |
| 315 | window.ws.send('quit'); |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 316 | |
Joseph Hwang | 95bf52a | 2015-04-14 13:09:39 +0800 | [diff] [blame] | 317 | clear(false); |
| 318 | if (closed_by_server) { |
| 319 | webplot.fillText('The python server has quit.', startX, startY, font); |
| 320 | } |
| 321 | webplot.fillText('Events are saved in "' + webplot.saved_events + '"', |
| 322 | startX, startY + 100, font); |
| 323 | webplot.fillText('The image is saved in "' + webplot.saved_image + '"', |
| 324 | startX, startY + 200, font); |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 325 | } |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 326 | } |
| 327 | |
Charlie Mooney | b476e89 | 2015-04-02 13:25:49 -0700 | [diff] [blame] | 328 | function save() { |
| 329 | window.ws.send('save:' + webplot.captureCanvasImage()); |
| 330 | } |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 331 | |
| 332 | /** |
| 333 | * A handler for keyup events to handle user hot keys. |
| 334 | */ |
| 335 | function keyupHandler() { |
| 336 | var webplot = window.webplot; |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 337 | var key = String.fromCharCode(event.which).toLowerCase(); |
| 338 | var ESC = String.fromCharCode(27); |
| 339 | |
| 340 | switch(String.fromCharCode(event.which).toLowerCase()) { |
| 341 | // ESC: clearing the canvas |
| 342 | case ESC: |
Charlie Mooney | 8bd8738 | 2015-04-02 14:06:59 -0700 | [diff] [blame] | 343 | clear(true); |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 344 | break; |
| 345 | |
| 346 | // 'b': toggle the background color between black and white |
| 347 | // default: black |
| 348 | case 'b': |
| 349 | document.bgColor = (document.bgColor == 'Black' ? 'White' : 'Black'); |
| 350 | break; |
| 351 | |
| 352 | // 'f': toggle full screen |
| 353 | // default: non-full screen |
| 354 | // Note: entering or leaving full screen will trigger onresize events. |
| 355 | case 'f': |
| 356 | if (document.documentElement.webkitRequestFullscreen) { |
| 357 | if (document.webkitFullscreenElement) |
| 358 | document.webkitCancelFullScreen(); |
| 359 | else |
| 360 | document.documentElement.webkitRequestFullscreen( |
| 361 | Element.ALLOW_KEYBOARD_INPUT); |
| 362 | } |
| 363 | webplot.updateCanvasDimension(); |
| 364 | break; |
| 365 | |
| 366 | // 'p': toggle between pressure mode and point mode. |
| 367 | // pressure mode: the circle radius corresponds to the pressure |
| 368 | // point mode: the circle radius is fixed and small |
| 369 | // default: pressure mode |
| 370 | case 'p': |
| 371 | webplot.pressureMode = webplot.pressureMode ? false : true; |
| 372 | webplot.updateCanvasDimension(); |
| 373 | break; |
| 374 | |
Charlie Mooney | b476e89 | 2015-04-02 13:25:49 -0700 | [diff] [blame] | 375 | // 'q': Quit the server (and save the plot and logs first) |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 376 | case 'q': |
Charlie Mooney | b476e89 | 2015-04-02 13:25:49 -0700 | [diff] [blame] | 377 | save(); |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 378 | quit(false); |
| 379 | break; |
| 380 | |
Charlie Mooney | b476e89 | 2015-04-02 13:25:49 -0700 | [diff] [blame] | 381 | // 's': Tell the server to save the touch events and a png of the plot |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 382 | case 's': |
Charlie Mooney | b476e89 | 2015-04-02 13:25:49 -0700 | [diff] [blame] | 383 | save(); |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 384 | break; |
| 385 | } |
| 386 | } |
| 387 | |
| 388 | |
Charlie Mooney | 8bd8738 | 2015-04-02 14:06:59 -0700 | [diff] [blame] | 389 | function clear(should_redraw_border) { |
| 390 | var canvas = document.getElementById('canvasWebplot'); |
| 391 | canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); |
| 392 | if (should_redraw_border) { |
| 393 | window.webplot.updateCanvasDimension(); |
| 394 | } |
| 395 | } |
| 396 | |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 397 | /** |
| 398 | * Create a web socket and a new webplot object. |
| 399 | */ |
| 400 | function createWS() { |
| 401 | var websocket = document.getElementById('websocketUrl').innerText; |
| 402 | var touchMinX = document.getElementById('touchMinX').innerText; |
| 403 | var touchMaxX = document.getElementById('touchMaxX').innerText; |
| 404 | var touchMinY = document.getElementById('touchMinY').innerText; |
| 405 | var touchMaxY = document.getElementById('touchMaxY').innerText; |
| 406 | var touchMinPressure = document.getElementById('touchMinPressure').innerText; |
| 407 | var touchMaxPressure = document.getElementById('touchMaxPressure').innerText; |
Jingkui Wang | 55115ef | 2017-06-17 13:44:33 -0700 | [diff] [blame^] | 408 | var tiltMinX = document.getElementById('tiltMinX').innerText; |
| 409 | var tiltMaxX = document.getElementById('tiltMaxX').innerText; |
| 410 | var tiltMinY = document.getElementById('tiltMinY').innerText; |
| 411 | var tiltMaxY = document.getElementById('tiltMaxY').innerText; |
| 412 | |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 413 | if (window.WebSocket) { |
| 414 | ws = new WebSocket(websocket); |
| 415 | ws.addEventListener("message", function(event) { |
| 416 | if (event.data == 'quit') { |
Charlie Mooney | b476e89 | 2015-04-02 13:25:49 -0700 | [diff] [blame] | 417 | save(); |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 418 | quit(true); |
Charlie Mooney | 8bd8738 | 2015-04-02 14:06:59 -0700 | [diff] [blame] | 419 | } else if (event.data == 'clear') { |
| 420 | clear(true); |
Charlie Mooney | 68b9d77 | 2015-04-02 14:15:51 -0700 | [diff] [blame] | 421 | } else if (event.data == 'save') { |
| 422 | save(); |
Charlie Mooney | bbc05f5 | 2015-03-24 13:36:22 -0700 | [diff] [blame] | 423 | } else { |
| 424 | var snapshot = JSON.parse(event.data); |
| 425 | webplot.processSnapshot(snapshot); |
| 426 | } |
| 427 | }); |
| 428 | } else { |
| 429 | alert('WebSocket is not supported on this browser!') |
| 430 | } |
| 431 | |
| 432 | webplot = new Webplot(document.getElementById('canvasWebplot'), |
| 433 | touchMinX, touchMaxX, touchMinY, touchMaxY, |
| 434 | touchMinPressure, touchMaxPressure); |
| 435 | webplot.updateCanvasDimension(); |
| 436 | } |