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. |
| 87 | */ |
| 88 | function Webplot(canvas, touchMinX, touchMaxX, touchMinY, touchMaxY, |
| 89 | touchMinPressure, touchMaxPressure) { |
| 90 | this.canvas = canvas; |
| 91 | this.ctx = canvas.getContext('2d'); |
| 92 | this.color = new Color(); |
| 93 | this.minX = touchMinX; |
| 94 | this.maxX = touchMaxX; |
| 95 | this.minY = touchMinY; |
| 96 | this.maxY = touchMaxY; |
| 97 | this.minPressure = touchMinPressure; |
| 98 | this.maxPressure = touchMaxPressure; |
| 99 | this.maxRadiusRatio = 0.03; |
| 100 | this.maxRadius = null; |
| 101 | this.clickEdge = null; |
| 102 | this.clickDown = false; |
| 103 | this.pressureMode = true; |
| 104 | this.pointRadius = 2; |
| 105 | this.saved_file = '/tmp/webplot.dat'; |
| 106 | } |
| 107 | |
| 108 | |
| 109 | /** |
| 110 | * Update the width and height of the canvas, the max radius of circles, |
| 111 | * and the edge of click rectangles. |
| 112 | */ |
| 113 | Webplot.prototype.updateCanvasDimension = function() { |
| 114 | var newWidth = document.body.clientWidth; |
| 115 | var newHeight = document.body.clientHeight; |
| 116 | |
| 117 | if (this.canvas.width != newWidth || this.canvas.height != newHeight) { |
| 118 | var deviceRatio = (this.maxY - this.minY) / (this.maxX - this.minX); |
| 119 | var canvasRatio = (newHeight / newWidth); |
| 120 | |
| 121 | // The actual dimension of the viewport. |
| 122 | this.canvas.width = newWidth; |
| 123 | this.canvas.height = newHeight; |
| 124 | |
| 125 | // Calculate the inner area of the viewport on which to draw finger traces. |
| 126 | // This inner area has the same height/width ratio as the touch device. |
| 127 | if (deviceRatio >= canvasRatio) { |
| 128 | this.canvas.innerWidth = Math.round(newHeight / deviceRatio); |
| 129 | this.canvas.innerHeight = newHeight; |
| 130 | this.canvas.innerOffsetLeft = Math.round( |
| 131 | (newWidth - this.canvas.innerWidth) / 2); |
| 132 | this.canvas.innerOffsetTop = 0; |
| 133 | } else { |
| 134 | this.canvas.innerWidth = newWidth; |
| 135 | this.canvas.innerHeight = Math.round(newWidth * deviceRatio); |
| 136 | this.canvas.innerOffsetLeft = 0; |
| 137 | this.canvas.innerOffsetTop = Math.round( |
| 138 | (newHeight - this.canvas.innerHeight) / 2); |
| 139 | } |
| 140 | |
| 141 | this.maxRadius = Math.min(this.canvas.innerWidth, this.canvas.innerHeight) * |
| 142 | this.maxRadiusRatio; |
| 143 | this.clickEdge = (this.pressureMode ? this.maxRadius : this.maxRadius / 2); |
| 144 | } |
| 145 | this.drawRect(this.canvas.innerOffsetLeft, this.canvas.innerOffsetTop, |
| 146 | this.canvas.innerWidth, this.canvas.innerHeight, |
| 147 | this.color.COLOR_FRAME); |
| 148 | } |
| 149 | |
| 150 | |
| 151 | /** |
| 152 | * Draw a circle. |
| 153 | * @param {int} x the x coordinate of the circle. |
| 154 | * @param {int} y the y coordinate of the circle. |
| 155 | * @param {int} r the radius of the circle. |
| 156 | * @param {string} colorName |
| 157 | */ |
| 158 | Webplot.prototype.drawCircle = function(x, y, r, colorName) { |
| 159 | this.ctx.beginPath(); |
| 160 | this.ctx.fillStyle = colorName; |
| 161 | this.ctx.arc(x, y, r, 0, 2 * Math.PI); |
| 162 | this.ctx.fill(); |
| 163 | } |
| 164 | |
| 165 | |
| 166 | /** |
| 167 | * Draw a rectangle. |
| 168 | * @param {int} x the x coordinate of upper left corner of the rectangle. |
| 169 | * @param {int} y the y coordinate of upper left corner of the rectangle. |
| 170 | * @param {int} width the width of the rectangle. |
| 171 | * @param {int} height the height of the rectangle. |
| 172 | * @param {string} colorName |
| 173 | */ |
| 174 | Webplot.prototype.drawRect = function(x, y, width, height, colorName) { |
| 175 | this.ctx.beginPath(); |
| 176 | this.ctx.lineWidth = "4"; |
| 177 | this.ctx.strokeStyle = colorName; |
| 178 | this.ctx.rect(x, y, width, height); |
| 179 | this.ctx.stroke(); |
| 180 | } |
| 181 | |
| 182 | |
| 183 | /** |
| 184 | * Fill text. |
| 185 | * @param {string} text the text to display |
| 186 | * @param {int} x the x coordinate of upper left corner of the text. |
| 187 | * @param {int} y the y coordinate of upper left corner of the text. |
| 188 | * @param {string} font the size and the font |
| 189 | */ |
| 190 | Webplot.prototype.fillText = function(text, x, y, font) { |
| 191 | this.ctx.font = font; |
| 192 | this.ctx.fillText(text, x, y); |
| 193 | } |
| 194 | |
| 195 | |
| 196 | /** |
| 197 | * Capture the canvas image. |
| 198 | * @return {string} the image represented in base64 text |
| 199 | */ |
| 200 | Webplot.prototype.captureCanvasImage = function() { |
| 201 | var imageData = this.canvas.toDataURL('image/png'); |
| 202 | // Strip off the header. |
| 203 | return imageData.replace(/^data:image\/png;base64,/, ''); |
| 204 | } |
| 205 | |
| 206 | |
| 207 | /** |
| 208 | * Process an incoming snapshot. |
| 209 | * @param {object} snapshot |
| 210 | * |
| 211 | * A 2f snapshot received from the python server looks like: |
| 212 | * MtbSnapshot( |
| 213 | * syn_time=1420522152.269537, |
| 214 | * button_pressed=False, |
| 215 | * fingers=[ |
| 216 | * MtbFinger(tid=13, slot=0, syn_time=1420522152.269537, x=440, y=277, |
| 217 | * pressure=33), |
| 218 | * MtbFinger(tid=14, slot=1, syn_time=1420522152.269537, x=271, y=308, |
| 219 | * pressure=38) |
| 220 | * ] |
| 221 | * ) |
| 222 | */ |
| 223 | Webplot.prototype.processSnapshot = function(snapshot) { |
| 224 | var edge = this.clickEdge; |
| 225 | |
| 226 | for (var i = 0; i < snapshot.fingers.length; i++) { |
| 227 | var finger = snapshot.fingers[i]; |
| 228 | |
| 229 | // Update the color object if the finger is leaving. |
| 230 | if (finger.leaving) { |
| 231 | this.color.remove(finger.tid); |
| 232 | continue; |
| 233 | } |
| 234 | |
| 235 | // Calculate (x, y) based on the inner width/height which has the same |
| 236 | // dimension ratio as the touch device. |
| 237 | var x = (finger.x - this.minX) / (this.maxX - this.minX) * |
| 238 | this.canvas.innerWidth + this.canvas.innerOffsetLeft; |
| 239 | var y = (finger.y - this.minY) / (this.maxY - this.minY) * |
| 240 | this.canvas.innerHeight + this.canvas.innerOffsetTop; |
| 241 | if (this.pressureMode) |
| 242 | var r = (finger.pressure - this.minPressure) / |
| 243 | (this.maxPressure - this.minPressure) * this.maxRadius; |
| 244 | else |
| 245 | var r = this.pointRadius; |
| 246 | |
| 247 | this.drawCircle(x, y, r, this.color.getCircleColor(finger.tid)); |
| 248 | |
| 249 | // If there is a click, draw the click with finger 0. |
| 250 | // The flag clickDown is used to draw the click exactly once |
| 251 | // during the click down period. |
| 252 | if (snapshot.button_pressed == 1 && i == 0 && !this.clickDown) { |
| 253 | this.drawRect(x, y, edge, edge, this.color.getRectColor()); |
| 254 | this.clickDown = true; |
| 255 | } |
| 256 | } |
| 257 | |
| 258 | // In some special situation, the click comes with no fingers. |
| 259 | // This may happen if an insulated object is used to click the touchpad. |
| 260 | // Just draw the click at a random position. |
| 261 | if (snapshot.fingers.length == 0 && snapshot.button_pressed == 1 && |
| 262 | !this.clickDown) { |
| 263 | var x = Math.random() * this.canvas.innerWidth + |
| 264 | this.canvas.innerOffsetLeft; |
| 265 | var y = Math.random() * this.canvas.innerHeight + |
| 266 | this.canvas.innerOffsetTop; |
| 267 | this.drawRect(x, y, edge, edge, this.color.getRectColor()); |
| 268 | this.clickDown = true; |
| 269 | } |
| 270 | |
| 271 | if (snapshot.button_pressed == 0) { |
| 272 | this.clickDown = false; |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | |
| 277 | /** |
| 278 | * An handler for onresize event to update the canvas dimensions. |
| 279 | */ |
| 280 | function resizeCanvas() { |
| 281 | webplot.updateCanvasDimension(); |
| 282 | } |
| 283 | |
| 284 | |
| 285 | /** |
| 286 | * Send a 'quit' message to the server and display the event file name |
| 287 | * on the canvas. |
| 288 | * @param {boolean} closed_by_server True if this is requested by the server. |
| 289 | */ |
| 290 | function quit(closed_by_server) { |
| 291 | var canvas = document.getElementById('canvasWebplot'); |
| 292 | var webplot = window.webplot; |
| 293 | var startX = 100; |
| 294 | var startY = 100; |
| 295 | var font = '30px Verdana'; |
| 296 | |
| 297 | // Capture the image before clearing the canvas and send it to the server, |
| 298 | // and notify the server that this client quits. |
| 299 | window.ws.send('quit:' + webplot.captureCanvasImage()); |
| 300 | |
| 301 | canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); |
| 302 | if (closed_by_server) { |
| 303 | webplot.fillText('The python server has quit.', startX, startY, font); |
| 304 | startY += 100; |
| 305 | } |
| 306 | webplot.fillText('Events are saved in "' + webplot.saved_file + '"', |
| 307 | startX, startY, font); |
| 308 | } |
| 309 | |
| 310 | |
| 311 | /** |
| 312 | * A handler for keyup events to handle user hot keys. |
| 313 | */ |
| 314 | function keyupHandler() { |
| 315 | var webplot = window.webplot; |
| 316 | var canvas = document.getElementById('canvasWebplot'); |
| 317 | var key = String.fromCharCode(event.which).toLowerCase(); |
| 318 | var ESC = String.fromCharCode(27); |
| 319 | |
| 320 | switch(String.fromCharCode(event.which).toLowerCase()) { |
| 321 | // ESC: clearing the canvas |
| 322 | case ESC: |
| 323 | canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); |
| 324 | webplot.updateCanvasDimension(); |
| 325 | break; |
| 326 | |
| 327 | // 'b': toggle the background color between black and white |
| 328 | // default: black |
| 329 | case 'b': |
| 330 | document.bgColor = (document.bgColor == 'Black' ? 'White' : 'Black'); |
| 331 | break; |
| 332 | |
| 333 | // 'f': toggle full screen |
| 334 | // default: non-full screen |
| 335 | // Note: entering or leaving full screen will trigger onresize events. |
| 336 | case 'f': |
| 337 | if (document.documentElement.webkitRequestFullscreen) { |
| 338 | if (document.webkitFullscreenElement) |
| 339 | document.webkitCancelFullScreen(); |
| 340 | else |
| 341 | document.documentElement.webkitRequestFullscreen( |
| 342 | Element.ALLOW_KEYBOARD_INPUT); |
| 343 | } |
| 344 | webplot.updateCanvasDimension(); |
| 345 | break; |
| 346 | |
| 347 | // 'p': toggle between pressure mode and point mode. |
| 348 | // pressure mode: the circle radius corresponds to the pressure |
| 349 | // point mode: the circle radius is fixed and small |
| 350 | // default: pressure mode |
| 351 | case 'p': |
| 352 | webplot.pressureMode = webplot.pressureMode ? false : true; |
| 353 | webplot.updateCanvasDimension(); |
| 354 | break; |
| 355 | |
| 356 | // 'q': Quit the server |
| 357 | case 'q': |
| 358 | quit(false); |
| 359 | break; |
| 360 | |
| 361 | // 's': save the touch events in a specified file name. |
| 362 | // default: /tmp/webplot.dat |
| 363 | case 's': |
| 364 | window.ws.send('save:' + webplot.saved_file); |
| 365 | break; |
| 366 | } |
| 367 | } |
| 368 | |
| 369 | |
| 370 | /** |
| 371 | * Create a web socket and a new webplot object. |
| 372 | */ |
| 373 | function createWS() { |
| 374 | var websocket = document.getElementById('websocketUrl').innerText; |
| 375 | var touchMinX = document.getElementById('touchMinX').innerText; |
| 376 | var touchMaxX = document.getElementById('touchMaxX').innerText; |
| 377 | var touchMinY = document.getElementById('touchMinY').innerText; |
| 378 | var touchMaxY = document.getElementById('touchMaxY').innerText; |
| 379 | var touchMinPressure = document.getElementById('touchMinPressure').innerText; |
| 380 | var touchMaxPressure = document.getElementById('touchMaxPressure').innerText; |
| 381 | if (window.WebSocket) { |
| 382 | ws = new WebSocket(websocket); |
| 383 | ws.addEventListener("message", function(event) { |
| 384 | if (event.data == 'quit') { |
| 385 | quit(true); |
| 386 | } else { |
| 387 | var snapshot = JSON.parse(event.data); |
| 388 | webplot.processSnapshot(snapshot); |
| 389 | } |
| 390 | }); |
| 391 | } else { |
| 392 | alert('WebSocket is not supported on this browser!') |
| 393 | } |
| 394 | |
| 395 | webplot = new Webplot(document.getElementById('canvasWebplot'), |
| 396 | touchMinX, touchMaxX, touchMinY, touchMaxY, |
| 397 | touchMinPressure, touchMaxPressure); |
| 398 | webplot.updateCanvasDimension(); |
| 399 | } |