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