blob: d89a0e4bf1d3df31c111362bc000fbd5890ace2d [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;
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 */
113Webplot.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 */
158Webplot.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 */
174Webplot.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 */
190Webplot.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 */
200Webplot.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 */
223Webplot.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 */
280function 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 */
290function 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 */
314function 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 */
373function 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}