blob: ad14e0a5b744ea5696c75e67eacda485ca73e894 [file] [log] [blame]
// 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} touchMinX the min x value of the touch device.
* @param {int} touchMaxX the max x value of the touch device.
* @param {int} touchMinY the min y value of the touch device.
* @param {int} touchMaxY the max y value of the touch device.
* @param {int} touchMinPressure the min pressure value of the touch device.
* @param {int} touchMaxPressure the max pressure value of the touch device.
* @param {int} tiltMinX the min tilt x value of the touch device.
* @param {int} tiltMaxX the max tilt x value of the touch device.
* @param {int} tiltMinY the min tilt y value of the touch device.
* @param {int} tiltMaxY the max tilt y value of the touch device.
*/
function Webplot(canvas, touchMinX, touchMaxX, touchMinY, touchMaxY,
touchMinPressure, touchMaxPressure, tiltMinX, tiltMaxX,
tiltMinY, tiltMaxY) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.color = new Color();
this.minX = touchMinX;
this.maxX = touchMaxX;
this.minY = touchMinY;
this.maxY = touchMaxY;
this.minPressure = touchMinPressure;
this.maxPressure = touchMaxPressure;
this.tiltMinX = tiltMinX;
this.tiltMaxX = tiltMaxX;
this.tiltMinY = tiltMinY;
this.tiltMaxY = tiltMaxY;
this.showTilt = ! ((tiltMinX == tiltMaxX) && (tiltMinY == tiltMaxY))
this.maxRadiusRatio = 0.03;
this.maxRadius = null;
this.clickEdge = null;
this.clickDown = false;
this.pressureMode = true;
this.pointRadius = 2;
this.saved_events = '/tmp/webplot.dat';
this.saved_image = '/tmp/webplot.png';
}
/**
* 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) {
var deviceRatio = (this.maxY - this.minY) / (this.maxX - this.minX);
var canvasRatio = (newHeight / newWidth);
// The actual dimension of the viewport.
this.canvas.width = newWidth;
this.canvas.height = newHeight;
// Calculate the inner area of the viewport on which to draw finger traces.
// This inner area has the same height/width ratio as the touch device.
if (deviceRatio >= canvasRatio) {
this.canvas.innerWidth = Math.round(newHeight / deviceRatio);
this.canvas.innerHeight = newHeight;
this.canvas.innerOffsetLeft = Math.round(
(newWidth - this.canvas.innerWidth) / 2);
this.canvas.innerOffsetTop = 0;
} else {
this.canvas.innerWidth = newWidth;
this.canvas.innerHeight = Math.round(newWidth * deviceRatio);
this.canvas.innerOffsetLeft = 0;
this.canvas.innerOffsetTop = Math.round(
(newHeight - this.canvas.innerHeight) / 2);
}
this.maxRadius = Math.min(this.canvas.innerWidth, this.canvas.innerHeight) *
this.maxRadiusRatio;
this.clickEdge = (this.pressureMode ? this.maxRadius : this.maxRadius / 2);
}
this.drawRect(this.canvas.innerOffsetLeft, this.canvas.innerOffsetTop,
this.canvas.innerWidth, this.canvas.innerHeight,
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();
}
/**
* Fill text.
* @param {string} text the text to display
* @param {int} x the x coordinate of upper left corner of the text.
* @param {int} y the y coordinate of upper left corner of the text.
* @param {string} font the size and the font
*/
Webplot.prototype.fillText = function(text, x, y, font) {
this.ctx.font = font;
this.ctx.fillText(text, x, y);
}
/**
* Capture the canvas image.
* @return {string} the image represented in base64 text
*/
Webplot.prototype.captureCanvasImage = function() {
var imageData = this.canvas.toDataURL('image/png');
// Strip off the header.
return imageData.replace(/^data:image\/png;base64,/, '');
}
/**
* 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 =[
* MtFinger(tid=13, slot=0, syn_time=1420522152.269537, x=440, y=277,
* pressure=33),
* MtFinger(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;
}
// Calculate (x, y) based on the inner width/height which has the same
// dimension ratio as the touch device.
var x = (finger.x - this.minX) / (this.maxX - this.minX) *
this.canvas.innerWidth + this.canvas.innerOffsetLeft;
var y = (finger.y - this.minY) / (this.maxY - this.minY) *
this.canvas.innerHeight + this.canvas.innerOffsetTop;
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.innerWidth +
this.canvas.innerOffsetLeft;
var y = Math.random() * this.canvas.innerHeight +
this.canvas.innerOffsetTop;
this.drawRect(x, y, edge, edge, this.color.getRectColor());
this.clickDown = true;
}
if (snapshot.button_pressed == 0) {
this.clickDown = false;
}
}
Webplot.quitFlag = false;
/**
* An handler for onresize event to update the canvas dimensions.
*/
function resizeCanvas() {
webplot.updateCanvasDimension();
}
/**
* Send a 'quit' message to the server and display the event file name
* on the canvas.
* @param {boolean} closed_by_server True if this is requested by the server.
*/
function quit(closed_by_server) {
var canvas = document.getElementById('canvasWebplot');
var webplot = window.webplot;
var startX = 100;
var startY = 100;
var font = '30px Verdana';
// Capture the image before clearing the canvas and send it to the server,
// and notify the server that this client quits.
if (!Webplot.quitFlag) {
Webplot.quitFlag = true;
window.ws.send('quit');
clear(false);
if (closed_by_server) {
webplot.fillText('The python server has quit.', startX, startY, font);
}
webplot.fillText('Events are saved in "' + webplot.saved_events + '"',
startX, startY + 100, font);
webplot.fillText('The image is saved in "' + webplot.saved_image + '"',
startX, startY + 200, font);
}
}
function save() {
window.ws.send('save:' + webplot.captureCanvasImage());
}
/**
* A handler for keyup events to handle user hot keys.
*/
function keyupHandler() {
var webplot = window.webplot;
var key = String.fromCharCode(event.which).toLowerCase();
var ESC = String.fromCharCode(27);
switch(String.fromCharCode(event.which).toLowerCase()) {
// ESC: clearing the canvas
case ESC:
clear(true);
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 (and save the plot and logs first)
case 'q':
save();
quit(false);
break;
// 's': Tell the server to save the touch events and a png of the plot
case 's':
save();
break;
}
}
function clear(should_redraw_border) {
var canvas = document.getElementById('canvasWebplot');
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
if (should_redraw_border) {
window.webplot.updateCanvasDimension();
}
}
/**
* Create a web socket and a new webplot object.
*/
function createWS() {
var websocket = document.getElementById('websocketUrl').innerText;
var touchMinX = document.getElementById('touchMinX').innerText;
var touchMaxX = document.getElementById('touchMaxX').innerText;
var touchMinY = document.getElementById('touchMinY').innerText;
var touchMaxY = document.getElementById('touchMaxY').innerText;
var touchMinPressure = document.getElementById('touchMinPressure').innerText;
var touchMaxPressure = document.getElementById('touchMaxPressure').innerText;
var tiltMinX = document.getElementById('tiltMinX').innerText;
var tiltMaxX = document.getElementById('tiltMaxX').innerText;
var tiltMinY = document.getElementById('tiltMinY').innerText;
var tiltMaxY = document.getElementById('tiltMaxY').innerText;
if (window.WebSocket) {
ws = new WebSocket(websocket);
ws.addEventListener("message", function(event) {
if (event.data == 'quit') {
save();
quit(true);
} else if (event.data == 'clear') {
clear(true);
} else if (event.data == 'save') {
save();
} else {
var snapshot = JSON.parse(event.data);
webplot.processSnapshot(snapshot);
}
});
} else {
alert('WebSocket is not supported on this browser!')
}
webplot = new Webplot(document.getElementById('canvasWebplot'),
touchMinX, touchMaxX, touchMinY, touchMaxY,
touchMinPressure, touchMaxPressure);
webplot.updateCanvasDimension();
}