Add the html and javascript

A html template is provided to serve as default index.html.
A javascript file is used to draw finger traces on the document
canvas which basically mimics the old mtplot. It also supports
some hot keys:

  ESC: the clear the screen
  'b': toggle the background color
  'f': toggle between full and non-full screen
  'p': toggle between pressure mode and point mode
  'q': notify the server that this client quits
  's': notify the server to save the event file

BUG=chromium:443539
TEST=Manually test.
1. Scp the program to a chromebook.
2. Launch a web server to serve touchpad data on the chromebook.
   $ python webplot.py
3. Switch to the chrome browser. Type "localhost" in the omnibox.
   It will display a dark background. Fingers move on the touchpad
   and observe the corresponding finger traces shown on the
   browser tab.

Change-Id: I5d654080d4cf93565db4ef0e1b9c7d44ceabcc5f
Reviewed-on: https://chromium-review.googlesource.com/237525
Reviewed-by: Charlie Mooney <[email protected]>
Commit-Queue: Shyh-In Hwang <[email protected]>
Tested-by: Shyh-In Hwang <[email protected]>
diff --git a/webplot.html b/webplot.html
new file mode 100644
index 0000000..235e6ef
--- /dev/null
+++ b/webplot.html
@@ -0,0 +1,24 @@
+<!--
+Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<html>
+<head>
+  <script src="webplot.js"></script>
+</head>
+
+<body style="margin:0; padding:0; background-color:black"
+  onbeforeunload="quit()"
+  onload="createWS()"
+  onkeyup="keyupHandler()"
+  onresize="resizeCanvas()">
+
+  <div id="websocketUrl" hidden>%(websocketUrl)s</div>
+  <div id="touchMaxX" hidden>%(touchMaxX)s</div>
+  <div id="touchMaxY" hidden>%(touchMaxY)s</div>
+  <canvas id="canvasWebplot"></canvas>
+
+</body>
+</html>
diff --git a/webplot.js b/webplot.js
new file mode 100644
index 0000000..274daa8
--- /dev/null
+++ b/webplot.js
@@ -0,0 +1,317 @@
+// Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+/**
+ * Choose finger colors for circles and the click color for rectangles.
+ * @constructor
+ */
+function Color() {
+  this.tids = [];
+  this.lastIndex = -1;
+  this.COLOR_TABLE = [
+    'Blue', 'Gold', 'LimeGreen', 'Red', 'Cyan',
+    'Magenta', 'Brown', 'Wheat', 'DarkGreen', 'Coral',
+  ];
+  this.length = this.COLOR_TABLE.length;
+  this.COLOR_CLICK = 'Gray';
+  this.COLOR_FRAME = 'Gray';
+
+  for (var i = 0; i < this.length; i++) {
+    this.tids[i] = -1;
+  }
+}
+
+
+/**
+ * Get the color to draw a circle for a given Tracking ID (tid).
+ * @param {int} tid
+ * @return {string}
+ */
+Color.prototype.getCircleColor = function(tid) {
+  index = this.tids.indexOf(tid);
+  // Find next color for this new tid.
+  if (index == -1) {
+    var i = (this.lastIndex + 1) % this.length;
+    while (i != this.lastIndex) {
+      if (this.tids[i] == -1) {
+        this.tids[i] = tid;
+        this.lastIndex = i;
+        return this.COLOR_TABLE[i];
+      }
+      i = (i + 1) % this.length;
+    }
+
+    // It is very unlikely that all slots in this.tids have been occupied.
+    // Should it happen, just assign a color to it.
+    return this.COLOR_TABLE[0];
+  } else {
+    return this.COLOR_TABLE[index];
+  }
+}
+
+
+/**
+ * Get the color to draw a rectangle for a given Tracking ID (tid).
+ * @param {int} tid
+ * @return {string}
+ */
+Color.prototype.getRectColor = function(tid) {
+  return this.COLOR_CLICK;
+}
+
+
+/**
+ * Remove the Tracking ID (tid) from the tids array.
+ * @param {int} tid
+ */
+Color.prototype.remove = function(tid) {
+  index = this.tids.indexOf(tid);
+  if (index >= 0) {
+    this.tids[index] = -1;
+  }
+}
+
+
+/**
+ * Pick up colors for circles and rectangles.
+ * @constructor
+ * @param {Element} canvas the canvas to draw circles and clicks.
+ * @param {int} touchMaxX the max x value of the touch device.
+ * @param {int} touchMaxY the max y value of the touch device.
+ */
+function Webplot(canvas, touchMaxX, touchMaxY) {
+  this.canvas = canvas;
+  this.ctx = canvas.getContext('2d');
+  this.color = new Color();
+  this.minX = 0;
+  this.maxX = touchMaxX;
+  this.minY = 0;
+  this.maxY = touchMaxY;
+  this.minPressure = 0;
+  // TODO: it is better to get maxPressure from a touch device.
+  //       This requires a change in remote._GetDimensions() in
+  //       touch_firmware_test.
+  this.maxPressure = 255;
+  this.maxRadiusRatio = 0.03;
+  this.maxRadius = null;
+  this.clickEdge = null;
+  this.clickDown = false;
+  this.pressureMode = true;
+  this.pointRadius = 2;
+}
+
+
+/**
+ * Update the width and height of the canvas, the max radius of circles,
+ * and the edge of click rectangles.
+ */
+Webplot.prototype.updateCanvasDimension = function() {
+  var newWidth = document.body.clientWidth;
+  var newHeight = document.body.clientHeight;
+
+  if (this.canvas.width != newWidth || this.canvas.height != newHeight) {
+    this.canvas.width = newWidth;
+    this.canvas.height = newHeight;
+    this.maxRadius = Math.min(newWidth, newHeight) * this.maxRadiusRatio;
+    this.clickEdge = (this.pressureMode ? this.maxRadius : this.maxRadius / 2);
+  }
+  this.drawRect(0, 0, newWidth, newHeight, this.color.COLOR_FRAME);
+}
+
+
+/**
+ * Draw a circle.
+ * @param {int} x the x coordinate of the circle.
+ * @param {int} y the y coordinate of the circle.
+ * @param {int} r the radius of the circle.
+ * @param {string} colorName
+ */
+Webplot.prototype.drawCircle = function(x, y, r, colorName) {
+  this.ctx.beginPath();
+  this.ctx.fillStyle = colorName;
+  this.ctx.arc(x, y, r, 0, 2 * Math.PI);
+  this.ctx.fill();
+}
+
+
+/**
+ * Draw a rectangle.
+ * @param {int} x the x coordinate of upper left corner of the rectangle.
+ * @param {int} y the y coordinate of upper left corner of the rectangle.
+ * @param {int} width the width of the rectangle.
+ * @param {int} height the height of the rectangle.
+ * @param {string} colorName
+ */
+Webplot.prototype.drawRect = function(x, y, width, height, colorName) {
+  this.ctx.beginPath();
+  this.ctx.lineWidth = "4";
+  this.ctx.strokeStyle = colorName;
+  this.ctx.rect(x, y, width, height);
+  this.ctx.stroke();
+}
+
+
+/**
+ * Process an incoming snapshot.
+ * @param {object} snapshot
+ *
+ * A 2f snapshot received from the python server looks like:
+ *   MtbSnapshot(
+ *     syn_time=1420522152.269537,
+ *     button_pressed=False,
+ *     fingers=[
+ *       MtbFinger(tid=13, slot=0, syn_time=1420522152.269537, x=440, y=277,
+ *                 pressure=33),
+ *       MtbFinger(tid=14, slot=1, syn_time=1420522152.269537, x=271, y=308,
+ *                 pressure=38)
+ *     ]
+ *   )
+ */
+Webplot.prototype.processSnapshot = function(snapshot) {
+  var edge = this.clickEdge;
+
+  for (var i = 0; i < snapshot.fingers.length; i++) {
+    var finger = snapshot.fingers[i];
+
+    // Update the color object if the finger is leaving.
+    if (finger.leaving) {
+      this.color.remove(finger.tid);
+      continue;
+    }
+
+    var x = (finger.x - this.minX) / (this.maxX - this.minX) *
+            this.canvas.width;
+    var y = (finger.y - this.minY) / (this.maxY - this.minY) *
+            this.canvas.height;
+    if (this.pressureMode)
+      var r = (finger.pressure - this.minPressure) /
+              (this.maxPressure - this.minPressure) * this.maxRadius;
+    else
+      var r = this.pointRadius;
+
+    this.drawCircle(x, y, r, this.color.getCircleColor(finger.tid));
+
+    // If there is a click, draw the click with finger 0.
+    // The flag clickDown is used to draw the click exactly once
+    // during the click down period.
+    if (snapshot.button_pressed == 1 && i == 0 && !this.clickDown) {
+      this.drawRect(x, y, edge, edge, this.color.getRectColor());
+      this.clickDown = true;
+    }
+  }
+
+  // In some special situation, the click comes with no fingers.
+  // This may happen if an insulated object is used to click the touchpad.
+  // Just draw the click at a random position.
+  if (snapshot.fingers.length == 0 && snapshot.button_pressed == 1 &&
+      !this.clickDown) {
+    var x = Math.random() * this.canvas.width;
+    var y = Math.random() * this.canvas.height;
+    this.drawRect(x, y, edge, edge, this.color.getRectColor());
+    this.clickDown = true;
+  }
+
+  if (snapshot.button_pressed == 0) {
+    this.clickDown = false;
+  }
+}
+
+
+/**
+ * An handler for onresize event to update the canvas dimensions.
+ */
+function resizeCanvas() {
+  webplot.updateCanvasDimension();
+}
+
+
+/**
+ * Send a 'quit' message to the server.
+ */
+function quit() {
+  window.ws.send('quit');
+}
+
+
+/**
+ * A handler for keyup events to handle user hot keys.
+ */
+function keyupHandler() {
+  var webplot = window.webplot;
+  var canvas = document.getElementById('canvasWebplot');
+  var key = String.fromCharCode(event.which).toLowerCase();
+  var ESC = String.fromCharCode(27);
+
+  switch(String.fromCharCode(event.which).toLowerCase()) {
+    // ESC: clearing the canvas
+    case ESC:
+      canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
+      webplot.updateCanvasDimension();
+      break;
+
+    // 'b': toggle the background color between black and white
+    //      default: black
+    case 'b':
+      document.bgColor = (document.bgColor == 'Black' ? 'White' : 'Black');
+      break;
+
+    // 'f': toggle full screen
+    //      default: non-full screen
+    //      Note: entering or leaving full screen will trigger onresize events.
+    case 'f':
+      if (document.documentElement.webkitRequestFullscreen) {
+        if (document.webkitFullscreenElement)
+          document.webkitCancelFullScreen();
+        else
+          document.documentElement.webkitRequestFullscreen(
+              Element.ALLOW_KEYBOARD_INPUT);
+      }
+      webplot.updateCanvasDimension();
+      break;
+
+    // 'p': toggle between pressure mode and point mode.
+    //      pressure mode: the circle radius corresponds to the pressure
+    //      point mode: the circle radius is fixed and small
+    //      default: pressure mode
+    case 'p':
+      webplot.pressureMode = webplot.pressureMode ? false : true;
+      webplot.updateCanvasDimension();
+      break;
+
+    // 'q': Quit the server
+    case 'q':
+      quit();
+      break;
+
+    // 's': save the touch events in a specified file name.
+    //      default: /tmp/webplot.dat
+    case 's':
+      window.ws.send('save:/tmp/webplot.dat')
+      break;
+  }
+}
+
+
+/**
+ * Create a web socket and a new webplot object.
+ */
+function createWS() {
+  var websocket = document.getElementById('websocketUrl').innerText;
+  var touchMaxX = document.getElementById('touchMaxX').innerText;
+  var touchMaxY = document.getElementById('touchMaxY').innerText;
+  if (window.WebSocket) {
+    ws = new WebSocket(websocket);
+    ws.addEventListener("message", function(event) {
+      var snapshot = JSON.parse(event.data);
+      webplot.processSnapshot(snapshot);
+    });
+  } else {
+    alert('WebSocket is not supported on this browser!')
+  }
+
+  webplot = new Webplot(document.getElementById('canvasWebplot'),
+                        touchMaxX, touchMaxY);
+  webplot.updateCanvasDimension();
+}