blob: 03020a58c08d4c7859171b9521f26bb7995b1898 [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.
Jingkui Wang55115ef2017-06-17 13:44:33 -070087 * @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 Mooneybbc05f52015-03-24 13:36:22 -070091 */
92function Webplot(canvas, touchMinX, touchMaxX, touchMinY, touchMaxY,
Jingkui Wang55115ef2017-06-17 13:44:33 -070093 touchMinPressure, touchMaxPressure, tiltMinX, tiltMaxX,
94 tiltMinY, tiltMaxY) {
Charlie Mooneybbc05f52015-03-24 13:36:22 -070095 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 Wang55115ef2017-06-17 13:44:33 -0700104 this.tiltMinX = tiltMinX;
105 this.tiltMaxX = tiltMaxX;
106 this.tiltMinY = tiltMinY;
107 this.tiltMaxY = tiltMaxY;
108 this.showTilt = ! ((tiltMinX == tiltMaxX) && (tiltMinY == tiltMaxY))
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700109 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 Hwang95bf52a2015-04-14 13:09:39 +0800115 this.saved_events = '/tmp/webplot.dat';
116 this.saved_image = '/tmp/webplot.png';
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700117}
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 */
124Webplot.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 */
169Webplot.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 */
185Webplot.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 */
201Webplot.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 */
211Webplot.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 Wang55115ef2017-06-17 13:44:33 -0700226 * points =[
227 * TouchPoint(tid=13, slot=0, syn_time=1420522152.269537, x=440, y=277,
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700228 * pressure=33),
Jingkui Wang55115ef2017-06-17 13:44:33 -0700229 * TouchPoint(tid=14, slot=1, syn_time=1420522152.269537, x=271, y=308,
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700230 * pressure=38)
231 * ]
232 * )
233 */
234Webplot.prototype.processSnapshot = function(snapshot) {
235 var edge = this.clickEdge;
236
Jingkui Wang55115ef2017-06-17 13:44:33 -0700237 for (var i = 0; i < snapshot.points.length; i++) {
238 var finger = snapshot.points[i];
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700239
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 Wang55115ef2017-06-17 13:44:33 -0700272 if (snapshot.points.length == 0 && snapshot.button_pressed == 1 &&
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700273 !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 Hwang95bf52a2015-04-14 13:09:39 +0800288Webplot.quitFlag = false;
289
290
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700291/**
292 * An handler for onresize event to update the canvas dimensions.
293 */
294function 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 */
304function 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 Hwang95bf52a2015-04-14 13:09:39 +0800313 if (!Webplot.quitFlag) {
314 Webplot.quitFlag = true;
315 window.ws.send('quit');
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700316
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800317 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 Mooneybbc05f52015-03-24 13:36:22 -0700325 }
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700326}
327
Charlie Mooneyb476e892015-04-02 13:25:49 -0700328function save() {
329 window.ws.send('save:' + webplot.captureCanvasImage());
330}
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700331
332/**
333 * A handler for keyup events to handle user hot keys.
334 */
335function keyupHandler() {
336 var webplot = window.webplot;
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700337 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 Mooney8bd87382015-04-02 14:06:59 -0700343 clear(true);
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700344 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 Mooneyb476e892015-04-02 13:25:49 -0700375 // 'q': Quit the server (and save the plot and logs first)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700376 case 'q':
Charlie Mooneyb476e892015-04-02 13:25:49 -0700377 save();
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700378 quit(false);
379 break;
380
Charlie Mooneyb476e892015-04-02 13:25:49 -0700381 // 's': Tell the server to save the touch events and a png of the plot
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700382 case 's':
Charlie Mooneyb476e892015-04-02 13:25:49 -0700383 save();
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700384 break;
385 }
386}
387
388
Charlie Mooney8bd87382015-04-02 14:06:59 -0700389function 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 Mooneybbc05f52015-03-24 13:36:22 -0700397/**
398 * Create a web socket and a new webplot object.
399 */
400function 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 Wang55115ef2017-06-17 13:44:33 -0700408 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 Mooneybbc05f52015-03-24 13:36:22 -0700413 if (window.WebSocket) {
414 ws = new WebSocket(websocket);
415 ws.addEventListener("message", function(event) {
416 if (event.data == 'quit') {
Charlie Mooneyb476e892015-04-02 13:25:49 -0700417 save();
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700418 quit(true);
Charlie Mooney8bd87382015-04-02 14:06:59 -0700419 } else if (event.data == 'clear') {
420 clear(true);
Charlie Mooney68b9d772015-04-02 14:15:51 -0700421 } else if (event.data == 'save') {
422 save();
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700423 } 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}