]> git.proxmox.com Git - mirror_novnc.git/blame - include/canvas.js
Merge branch 'master' of git@github.com:kanaka/noVNC
[mirror_novnc.git] / include / canvas.js
CommitLineData
c4164bda
JM
1/*
2 * noVNC: HTML5 VNC client
d0c29bb6 3 * Copyright (C) 2011 Joel Martin
5f409eee 4 * Licensed under LGPL-3 (see LICENSE.txt)
c4164bda
JM
5 *
6 * See README.md for usage and integration instructions.
7 */
c4164bda 8
8db09746 9/*jslint browser: true, white: false, bitwise: false */
d3796c14 10/*global Util, Base64, changeCursor */
c4164bda 11
1b097a63 12function Canvas(conf) {
43cf7bd8 13 "use strict";
d93d3e09 14
8db09746
JM
15conf = conf || {}; // Configuration
16var that = {}, // Public API interface
c8460b03 17
8db09746 18 // Private Canvas namespace variables
455e4657 19 c_forceCanvas = false,
d41c33e4 20
8db09746
JM
21 c_width = 0,
22 c_height = 0,
c8460b03 23
8db09746 24 c_prevStyle = "",
48ebcdb1 25
cdb55d26
JM
26 c_webkit_bug = false,
27 c_flush_timer = null;
f272267b 28
8db09746 29// Configuration settings
ff36b127
JM
30function cdef(v, type, defval, desc) {
31 Util.conf_default(conf, that, v, type, defval, desc); }
32
33// Capability settings, default can be overridden
34cdef('prefer_js', 'raw', null, 'Prefer Javascript over canvas methods');
35cdef('cursor_uri', 'raw', null, 'Can we render cursor using data URI');
36
37cdef('target', 'dom', null, 'Canvas element for VNC viewport');
38cdef('focusContainer', 'dom', document, 'DOM element that traps keyboard input');
39cdef('true_color', 'bool', true, 'Request true color pixel data');
ff36b127 40cdef('colourMap', 'raw', [], 'Colour map array (not true color)');
1a2371fc 41cdef('scale', 'float', 1.0, 'Viewport scale factor 0.1 - 1.0');
8cf20615 42
3b20e7a9
JM
43cdef('render_mode', 'str', '', 'Canvas rendering mode (read-only)');
44
8db09746
JM
45// Override some specific getters/setters
46that.set_prefer_js = function(val) {
47 if (val && c_forceCanvas) {
48 Util.Warn("Preferring Javascript to Canvas ops is not supported");
49 return false;
e2e7c224 50 }
8db09746
JM
51 conf.prefer_js = val;
52 return true;
53};
8cf20615 54
8db09746
JM
55that.get_colourMap = function(idx) {
56 if (typeof idx === 'undefined') {
57 return conf.colourMap;
58 } else {
59 return conf.colourMap[idx];
e2e7c224 60 }
8db09746 61};
e2e7c224 62
8db09746
JM
63that.set_colourMap = function(val, idx) {
64 if (typeof idx === 'undefined') {
65 conf.colourMap = val;
66 } else {
67 conf.colourMap[idx] = val;
e2e7c224 68 }
8db09746
JM
69};
70
3b20e7a9
JM
71that.set_render_mode = function () { throw("render_mode is read-only"); };
72
58b4c536
JM
73that.set_scale = function(scale) { that.rescale(scale); };
74
75
8db09746
JM
76// Add some other getters/setters
77that.get_width = function() {
78 return c_width;
79};
80that.get_height = function() {
81 return c_height;
82};
f272267b 83
8db09746
JM
84//
85// Private functions
86//
f272267b 87
8db09746
JM
88// Create the public API interface
89function constructor() {
81e5adaf 90 Util.Debug(">> Canvas.init");
f272267b 91
d3796c14 92 var c, ctx, func, imgTest, tval, i, curDat, curSave,
005d9ee9 93 has_imageData = false, UE = Util.Engine;
8db09746
JM
94
95 if (! conf.target) { throw("target must be set"); }
96
97 if (typeof conf.target === 'string') {
e4671910 98 throw("target must be a DOM element");
8db09746
JM
99 }
100
101 c = conf.target;
d93d3e09 102
8db09746 103 if (! c.getContext) { throw("no getContext method"); }
d93d3e09 104
8db09746
JM
105 if (! conf.ctx) { conf.ctx = c.getContext('2d'); }
106 ctx = conf.ctx;
107
9b940131 108 Util.Debug("User Agent: " + navigator.userAgent);
005d9ee9
JM
109 if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); }
110 if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); }
455e4657
JM
111 if (UE.trident) { Util.Debug("Browser: trident " + UE.trident); }
112 if (UE.presto) { Util.Debug("Browser: presto " + UE.presto); }
005d9ee9 113
8db09746 114 that.clear();
d93d3e09 115
d93d3e09 116 /*
48eed1ac
JM
117 * Determine browser Canvas feature support
118 * and select fastest rendering methods
d93d3e09
JM
119 */
120 tval = 0;
121 try {
8db09746 122 imgTest = ctx.getImageData(0, 0, 1,1);
d93d3e09
JM
123 imgTest.data[0] = 123;
124 imgTest.data[3] = 255;
8db09746
JM
125 ctx.putImageData(imgTest, 0, 0);
126 tval = ctx.getImageData(0, 0, 1, 1).data[0];
48eed1ac 127 if (tval === 123) {
8db09746 128 has_imageData = true;
48eed1ac 129 }
8db09746 130 } catch (exc1) {}
48eed1ac 131
8db09746 132 if (has_imageData) {
81e5adaf 133 Util.Info("Canvas supports imageData");
8db09746
JM
134 c_forceCanvas = false;
135 if (ctx.createImageData) {
d93d3e09 136 // If it's there, it's faster
81e5adaf 137 Util.Info("Using Canvas createImageData");
3b20e7a9 138 conf.render_mode = "createImageData rendering";
8db09746
JM
139 that.imageData = that.imageDataCreate;
140 } else if (ctx.getImageData) {
3b20e7a9 141 // I think this is mostly just Opera
81e5adaf 142 Util.Info("Using Canvas getImageData");
3b20e7a9 143 conf.render_mode = "getImageData rendering";
8db09746 144 that.imageData = that.imageDataGet;
d93d3e09 145 }
81e5adaf 146 Util.Info("Prefering javascript operations");
8db09746
JM
147 if (conf.prefer_js === null) {
148 conf.prefer_js = true;
149 }
150 that.rgbxImage = that.rgbxImageData;
151 that.cmapImage = that.cmapImageData;
d93d3e09 152 } else {
81e5adaf 153 Util.Warn("Canvas lacks imageData, using fillRect (slow)");
3b20e7a9 154 conf.render_mode = "fillRect rendering (slow)";
8db09746
JM
155 c_forceCanvas = true;
156 conf.prefer_js = false;
157 that.rgbxImage = that.rgbxImageFill;
158 that.cmapImage = that.cmapImageFill;
d93d3e09 159 }
532a9fd9 160
cdb55d26
JM
161 if (UE.webkit && UE.webkit >= 534.7 && UE.webkit <= 534.9) {
162 // Workaround WebKit canvas rendering bug #46319
163 conf.render_mode += ", webkit bug workaround";
164 Util.Debug("Working around WebKit bug #46319");
165 c_webkit_bug = true;
166 for (func in {"fillRect":1, "copyImage":1, "rgbxImage":1,
167 "cmapImage":1, "blitStringImage":1}) {
168 that[func] = (function() {
169 var myfunc = that[func]; // Save original function
170 //Util.Debug("Wrapping " + func);
171 return function() {
172 myfunc.apply(this, arguments);
173 if (!c_flush_timer) {
174 c_flush_timer = setTimeout(that.flush, 100);
175 }
176 };
43cf7bd8 177 }());
cdb55d26
JM
178 }
179 }
180
2c2b492c
JM
181 /*
182 * Determine browser support for setting the cursor via data URI
183 * scheme
184 */
185 curDat = [];
8db09746 186 for (i=0; i < 8 * 8 * 4; i += 1) {
2c2b492c
JM
187 curDat.push(255);
188 }
8171f4d8
JM
189 try {
190 curSave = c.style.cursor;
9a23006e 191 changeCursor(conf.target, curDat, curDat, 2, 2, 8, 8);
8171f4d8 192 if (c.style.cursor) {
8db09746
JM
193 if (conf.cursor_uri === null) {
194 conf.cursor_uri = true;
195 }
8171f4d8
JM
196 Util.Info("Data URI scheme cursor supported");
197 } else {
8db09746
JM
198 if (conf.cursor_uri === null) {
199 conf.cursor_uri = false;
200 }
8171f4d8
JM
201 Util.Warn("Data URI scheme cursor not supported");
202 }
203 c.style.cursor = curSave;
8db09746 204 } catch (exc2) {
8171f4d8
JM
205 Util.Error("Data URI scheme cursor test exception: " + exc2);
206 conf.cursor_uri = false;
2c2b492c 207 }
2c2b492c 208
81e5adaf 209 Util.Debug("<< Canvas.init");
8db09746
JM
210 return that ;
211}
212
8db09746
JM
213//
214// Public API interface functions
215//
216
217that.getContext = function () {
218 return conf.ctx;
219};
220
8db09746 221that.rescale = function(factor) {
125d8bbb
JM
222 var c, tp, x, y,
223 properties = ['transform', 'WebkitTransform', 'MozTransform', null];
8db09746
JM
224 c = conf.target;
225 tp = properties.shift();
226 while (tp) {
227 if (typeof c.style[tp] !== 'undefined') {
125d8bbb
JM
228 break;
229 }
8db09746 230 tp = properties.shift();
125d8bbb
JM
231 }
232
233 if (tp === null) {
234 Util.Debug("No scaling support");
235 return;
236 }
237
1a2371fc
JM
238 if (factor > 1.0) {
239 factor = 1.0;
240 } else if (factor < 0.1) {
241 factor = 0.1;
242 }
243
8db09746 244 if (conf.scale === factor) {
125d8bbb
JM
245 //Util.Debug("Canvas already scaled to '" + factor + "'");
246 return;
247 }
248
8db09746 249 conf.scale = factor;
125d8bbb
JM
250 x = c.width - c.width * factor;
251 y = c.height - c.height * factor;
8db09746
JM
252 c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)";
253};
254
255that.resize = function(width, height, true_color) {
256 var c = conf.target;
257
258 if (typeof true_color !== "undefined") {
259 conf.true_color = true_color;
260 }
65bca0c9 261 c_prevStyle = "";
8db09746
JM
262
263 c.width = width;
264 c.height = height;
265
266 c_width = c.offsetWidth;
267 c_height = c.offsetHeight;
268
269 that.rescale(conf.scale);
270};
271
272that.clear = function() {
273 that.resize(640, 20);
274 conf.ctx.clearRect(0, 0, c_width, c_height);
1b097a63
JM
275
276 // No benefit over default ("source-over") in Chrome and firefox
277 //conf.ctx.globalCompositeOperation = "copy";
8db09746
JM
278};
279
cdb55d26
JM
280that.flush = function() {
281 var old_val;
282 //Util.Debug(">> flush");
283 // Force canvas redraw (for webkit bug #46319 workaround)
284 old_val = conf.target.style.marginRight;
c1d008f1 285 conf.target.style.marginRight = "1px";
cdb55d26
JM
286 c_flush_timer = null;
287 setTimeout(function () {
288 conf.target.style.marginRight = old_val;
289 }, 1);
290};
291
65bca0c9 292that.setFillColor = function(color) {
8db09746
JM
293 var rgb, newStyle;
294 if (conf.true_color) {
295 rgb = color;
296 } else {
297 rgb = conf.colourMap[color[0]];
298 }
65bca0c9 299 newStyle = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
8db09746 300 if (newStyle !== c_prevStyle) {
8db09746
JM
301 conf.ctx.fillStyle = newStyle;
302 c_prevStyle = newStyle;
303 }
304};
8db09746 305
65bca0c9
JM
306that.fillRect = function(x, y, width, height, color) {
307 that.setFillColor(color);
8db09746
JM
308 conf.ctx.fillRect(x, y, width, height);
309};
8db09746
JM
310
311that.copyImage = function(old_x, old_y, new_x, new_y, width, height) {
312 conf.ctx.drawImage(conf.target, old_x, old_y, width, height,
313 new_x, new_y, width, height);
314};
532a9fd9 315
3875f847
JM
316/*
317 * Tile rendering functions optimized for rendering engines.
318 *
319 * - In Chrome/webkit, Javascript image data array manipulations are
320 * faster than direct Canvas fillStyle, fillRect rendering. In
321 * gecko, Javascript array handling is much slower.
322 */
8db09746 323that.getTile = function(x, y, width, height, color) {
d3796c14 324 var img, data = [], rgb, red, green, blue, i;
c4164bda 325 img = {'x': x, 'y': y, 'width': width, 'height': height,
65bca0c9 326 'data': data};
8db09746 327 if (conf.prefer_js) {
8db09746 328 if (conf.true_color) {
d41c33e4
JM
329 rgb = color;
330 } else {
8db09746 331 rgb = conf.colourMap[color[0]];
d41c33e4
JM
332 }
333 red = rgb[0];
334 green = rgb[1];
335 blue = rgb[2];
65bca0c9
JM
336 for (i = 0; i < (width * height * 4); i+=4) {
337 data[i ] = red;
338 data[i + 1] = green;
339 data[i + 2] = blue;
340 }
3875f847 341 } else {
65bca0c9 342 that.fillRect(x, y, width, height, color);
3875f847
JM
343 }
344 return img;
8db09746 345};
3875f847 346
8db09746 347that.setSubTile = function(img, x, y, w, h, color) {
65bca0c9 348 var data, p, rgb, red, green, blue, width, j, i, xend, yend;
8db09746 349 if (conf.prefer_js) {
97763d0e 350 data = img.data;
c4164bda 351 width = img.width;
8db09746 352 if (conf.true_color) {
d41c33e4
JM
353 rgb = color;
354 } else {
8db09746 355 rgb = conf.colourMap[color[0]];
d41c33e4
JM
356 }
357 red = rgb[0];
358 green = rgb[1];
359 blue = rgb[2];
65bca0c9
JM
360 xend = x + w;
361 yend = y + h;
362 for (j = y; j < yend; j += 1) {
363 for (i = x; i < xend; i += 1) {
364 p = (i + (j * width) ) * 4;
365 data[p ] = red;
97763d0e
JM
366 data[p + 1] = green;
367 data[p + 2] = blue;
3875f847
JM
368 }
369 }
370 } else {
65bca0c9 371 that.fillRect(img.x + x, img.y + y, w, h, color);
3875f847 372 }
8db09746 373};
3875f847 374
8db09746
JM
375that.putTile = function(img) {
376 if (conf.prefer_js) {
377 that.rgbxImage(img.x, img.y, img.width, img.height, img.data, 0);
3875f847 378 }
d3796c14 379 // else: No-op, under gecko already done by setSubTile
8db09746 380};
3875f847 381
8db09746
JM
382that.imageDataGet = function(width, height) {
383 return conf.ctx.getImageData(0, 0, width, height);
384};
385that.imageDataCreate = function(width, height) {
386 return conf.ctx.createImageData(width, height);
387};
3875f847 388
8db09746 389that.rgbxImageData = function(x, y, width, height, arr, offset) {
7f4f41b0 390 var img, i, j, data;
8db09746 391 img = that.imageData(width, height);
97763d0e 392 data = img.data;
d41c33e4 393 for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) {
7f4f41b0
JM
394 data[i + 0] = arr[j + 0];
395 data[i + 1] = arr[j + 1];
396 data[i + 2] = arr[j + 2];
397 data[i + 3] = 255; // Set Alpha
64ab5c4d 398 }
8db09746
JM
399 conf.ctx.putImageData(img, x, y);
400};
64ab5c4d 401
d93d3e09 402// really slow fallback if we don't have imageData
8db09746 403that.rgbxImageFill = function(x, y, width, height, arr, offset) {
a7a89626 404 var i, j, sx = 0, sy = 0;
d93d3e09 405 for (i=0, j=offset; i < (width * height); i+=1, j+=4) {
65bca0c9 406 that.fillRect(x+sx, y+sy, 1, 1, [arr[j+0], arr[j+1], arr[j+2]]);
d93d3e09
JM
407 sx += 1;
408 if ((sx % width) === 0) {
409 sx = 0;
410 sy += 1;
411 }
412 }
8db09746 413};
d93d3e09 414
8db09746 415that.cmapImageData = function(x, y, width, height, arr, offset) {
15046f00 416 var img, i, j, data, rgb, cmap;
8db09746 417 img = that.imageData(width, height);
d41c33e4 418 data = img.data;
8db09746 419 cmap = conf.colourMap;
d93d3e09 420 for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) {
d41c33e4
JM
421 rgb = cmap[arr[j]];
422 data[i + 0] = rgb[0];
423 data[i + 1] = rgb[1];
424 data[i + 2] = rgb[2];
425 data[i + 3] = 255; // Set Alpha
426 }
8db09746
JM
427 conf.ctx.putImageData(img, x, y);
428};
d41c33e4 429
8db09746 430that.cmapImageFill = function(x, y, width, height, arr, offset) {
a7a89626 431 var i, j, sx = 0, sy = 0, cmap;
8db09746 432 cmap = conf.colourMap;
d93d3e09 433 for (i=0, j=offset; i < (width * height); i+=1, j+=1) {
65bca0c9 434 that.fillRect(x+sx, y+sy, 1, 1, [arr[j]]);
d93d3e09
JM
435 sx += 1;
436 if ((sx % width) === 0) {
437 sx = 0;
438 sy += 1;
439 }
440 }
8db09746 441};
d93d3e09
JM
442
443
8db09746
JM
444that.blitImage = function(x, y, width, height, arr, offset) {
445 if (conf.true_color) {
446 that.rgbxImage(x, y, width, height, arr, offset);
d41c33e4 447 } else {
8db09746 448 that.cmapImage(x, y, width, height, arr, offset);
d41c33e4 449 }
8db09746 450};
d9cbdc7d 451
8db09746 452that.blitStringImage = function(str, x, y) {
d93d3e09 453 var img = new Image();
8db09746 454 img.onload = function () { conf.ctx.drawImage(img, x, y); };
d93d3e09 455 img.src = str;
8db09746 456};
f272267b 457
8db09746 458that.changeCursor = function(pixels, mask, hotx, hoty, w, h) {
8db09746 459 if (conf.cursor_uri === false) {
da6dd893 460 Util.Warn("changeCursor called but no cursor data URI support");
2c2b492c
JM
461 return;
462 }
463
9a23006e
JM
464 if (conf.true_color) {
465 changeCursor(conf.target, pixels, mask, hotx, hoty, w, h);
466 } else {
467 changeCursor(conf.target, pixels, mask, hotx, hoty, w, h, conf.colourMap);
468 }
43cf7bd8 469};
9a23006e 470
d3796c14
JM
471that.defaultCursor = function() {
472 conf.target.style.cursor = "default";
473};
474
9a23006e
JM
475return constructor(); // Return the public API interface
476
477} // End of Canvas()
478
479
480/* Set CSS cursor property using data URI encoded cursor file */
481function changeCursor(target, pixels, mask, hotx, hoty, w, h, cmap) {
482 var cur = [], rgb, IHDRsz, RGBsz, ANDsz, XORsz, url, idx, alpha, x, y;
483 //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h);
484
67b4e987
JM
485 // Push multi-byte little-endian values
486 cur.push16le = function (num) {
487 this.push((num ) & 0xFF,
488 (num >> 8) & 0xFF );
489 };
490 cur.push32le = function (num) {
491 this.push((num ) & 0xFF,
492 (num >> 8) & 0xFF,
493 (num >> 16) & 0xFF,
494 (num >> 24) & 0xFF );
495 };
496
2c2b492c 497 IHDRsz = 40;
9a23006e 498 RGBsz = w * h * 4;
2c2b492c 499 XORsz = Math.ceil( (w * h) / 8.0 );
9a23006e 500 ANDsz = Math.ceil( (w * h) / 8.0 );
2c2b492c
JM
501
502 // Main header
9a23006e
JM
503 cur.push16le(0); // 0: Reserved
504 cur.push16le(2); // 2: .CUR type
505 cur.push16le(1); // 4: Number of images, 1 for non-animated ico
506
507 // Cursor #1 header (ICONDIRENTRY)
508 cur.push(w); // 6: width
509 cur.push(h); // 7: height
510 cur.push(0); // 8: colors, 0 -> true-color
511 cur.push(0); // 9: reserved
512 cur.push16le(hotx); // 10: hotspot x coordinate
513 cur.push16le(hoty); // 12: hotspot y coordinate
514 cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
515 // 14: cursor data byte size
516 cur.push32le(22); // 18: offset of cursor data in the file
517
518
519 // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
520 cur.push32le(IHDRsz); // 22: Infoheader size
521 cur.push32le(w); // 26: Cursor width
522 cur.push32le(h*2); // 30: XOR+AND height
523 cur.push16le(1); // 34: number of planes
524 cur.push16le(32); // 36: bits per pixel
525 cur.push32le(0); // 38: Type of compression
526
527 cur.push32le(XORsz + ANDsz); // 43: Size of Image
528 // Gimp leaves this as 0
529
530 cur.push32le(0); // 46: reserved
531 cur.push32le(0); // 50: reserved
532 cur.push32le(0); // 54: reserved
533 cur.push32le(0); // 58: reserved
534
535 // 62: color data (RGBQUAD icColors[])
8db09746
JM
536 for (y = h-1; y >= 0; y -= 1) {
537 for (x = 0; x < w; x += 1) {
2c2b492c
JM
538 idx = y * Math.ceil(w / 8) + Math.floor(x/8);
539 alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
540
9a23006e 541 if (cmap) {
2c2b492c
JM
542 idx = (w * y) + x;
543 rgb = cmap[pixels[idx]];
544 cur.push(rgb[2]); // blue
545 cur.push(rgb[1]); // green
546 cur.push(rgb[0]); // red
547 cur.push(alpha); // alpha
9a23006e
JM
548 } else {
549 idx = ((w * y) + x) * 4;
550 cur.push(pixels[idx + 2]); // blue
551 cur.push(pixels[idx + 1]); // green
552 cur.push(pixels[idx + 0]); // red
553 cur.push(alpha); // alpha
2c2b492c
JM
554 }
555 }
556 }
557
9a23006e
JM
558 // XOR/bitmask data (BYTE icXOR[])
559 // (ignored, just needs to be right size)
560 for (y = 0; y < h; y += 1) {
561 for (x = 0; x < Math.ceil(w / 8); x += 1) {
562 cur.push(0x00);
563 }
564 }
565
566 // AND/bitmask data (BYTE icAND[])
567 // (ignored, just needs to be right size)
8db09746
JM
568 for (y = 0; y < h; y += 1) {
569 for (x = 0; x < Math.ceil(w / 8); x += 1) {
2c2b492c
JM
570 cur.push(0x00);
571 }
572 }
573
574 url = "data:image/x-icon;base64," + Base64.encode(cur);
9a23006e 575 target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default";
da6dd893 576 //Util.Debug("<< changeCursor, cur.length: " + cur.length);
43cf7bd8 577}