]> git.proxmox.com Git - mirror_novnc.git/blob - core/display.js
Fix missing semicolons
[mirror_novnc.git] / core / display.js
1 /*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2012 Joel Martin
4 * Copyright (C) 2015 Samuel Mannehed for Cendio AB
5 * Licensed under MPL 2.0 (see LICENSE.txt)
6 *
7 * See README.md for usage and integration instructions.
8 */
9
10 /*jslint browser: true, white: false */
11 /*global Util, Base64, changeCursor */
12
13 import { browserSupportsCursorURIs as cursorURIsSupported } from './util/browsers.js';
14 import { set_defaults, make_properties } from './util/properties.js';
15 import * as Log from './util/logging.js';
16 import Base64 from "./base64.js";
17
18 export default function Display(defaults) {
19 this._drawCtx = null;
20 this._c_forceCanvas = false;
21
22 this._renderQ = []; // queue drawing actions for in-oder rendering
23 this._flushing = false;
24
25 // the full frame buffer (logical canvas) size
26 this._fb_width = 0;
27 this._fb_height = 0;
28
29 this._prevDrawStyle = "";
30 this._tile = null;
31 this._tile16x16 = null;
32 this._tile_x = 0;
33 this._tile_y = 0;
34
35 set_defaults(this, defaults, {
36 'scale': 1.0,
37 'viewport': false,
38 'render_mode': '',
39 "onFlush": function () {},
40 });
41
42 Log.Debug(">> Display.constructor");
43
44 // The visible canvas
45 if (!this._target) {
46 throw new Error("Target must be set");
47 }
48
49 if (typeof this._target === 'string') {
50 throw new Error('target must be a DOM element');
51 }
52
53 if (!this._target.getContext) {
54 throw new Error("no getContext method");
55 }
56
57 this._targetCtx = this._target.getContext('2d');
58
59 // the visible canvas viewport (i.e. what actually gets seen)
60 this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };
61
62 // The hidden canvas, where we do the actual rendering
63 this._backbuffer = document.createElement('canvas');
64 this._drawCtx = this._backbuffer.getContext('2d');
65
66 this._damageBounds = { left:0, top:0,
67 right: this._backbuffer.width,
68 bottom: this._backbuffer.height };
69
70 Log.Debug("User Agent: " + navigator.userAgent);
71
72 this.clear();
73
74 // Check canvas features
75 if ('createImageData' in this._drawCtx) {
76 this._render_mode = 'canvas rendering';
77 } else {
78 throw new Error("Canvas does not support createImageData");
79 }
80
81 if (this._prefer_js === null) {
82 Log.Info("Prefering javascript operations");
83 this._prefer_js = true;
84 }
85
86 // Determine browser support for setting the cursor via data URI scheme
87 if (this._cursor_uri || this._cursor_uri === null ||
88 this._cursor_uri === undefined) {
89 this._cursor_uri = cursorURIsSupported();
90 }
91
92 Log.Debug("<< Display.constructor");
93 };
94
95 var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false;
96 try {
97 new ImageData(new Uint8ClampedArray(4), 1, 1);
98 SUPPORTS_IMAGEDATA_CONSTRUCTOR = true;
99 } catch (ex) {
100 // ignore failure
101 }
102
103 Display.prototype = {
104 // Public methods
105 viewportChangePos: function (deltaX, deltaY) {
106 var vp = this._viewportLoc;
107 deltaX = Math.floor(deltaX);
108 deltaY = Math.floor(deltaY);
109
110 if (!this._viewport) {
111 deltaX = -vp.w; // clamped later of out of bounds
112 deltaY = -vp.h;
113 }
114
115 var vx2 = vp.x + vp.w - 1;
116 var vy2 = vp.y + vp.h - 1;
117
118 // Position change
119
120 if (deltaX < 0 && vp.x + deltaX < 0) {
121 deltaX = -vp.x;
122 }
123 if (vx2 + deltaX >= this._fb_width) {
124 deltaX -= vx2 + deltaX - this._fb_width + 1;
125 }
126
127 if (vp.y + deltaY < 0) {
128 deltaY = -vp.y;
129 }
130 if (vy2 + deltaY >= this._fb_height) {
131 deltaY -= (vy2 + deltaY - this._fb_height + 1);
132 }
133
134 if (deltaX === 0 && deltaY === 0) {
135 return;
136 }
137 Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
138
139 vp.x += deltaX;
140 vp.y += deltaY;
141
142 this._damage(vp.x, vp.y, vp.w, vp.h);
143
144 this.flip();
145 },
146
147 viewportChangeSize: function(width, height) {
148
149 if (!this._viewport ||
150 typeof(width) === "undefined" ||
151 typeof(height) === "undefined") {
152
153 Log.Debug("Setting viewport to full display region");
154 width = this._fb_width;
155 height = this._fb_height;
156 }
157
158 if (width > this._fb_width) {
159 width = this._fb_width;
160 }
161 if (height > this._fb_height) {
162 height = this._fb_height;
163 }
164
165 var vp = this._viewportLoc;
166 if (vp.w !== width || vp.h !== height) {
167 vp.w = width;
168 vp.h = height;
169
170 var canvas = this._target;
171 canvas.width = width;
172 canvas.height = height;
173
174 // The position might need to be updated if we've grown
175 this.viewportChangePos(0, 0);
176
177 this._damage(vp.x, vp.y, vp.w, vp.h);
178 this.flip();
179
180 // Update the visible size of the target canvas
181 this._rescale(this._scale);
182 }
183 },
184
185 absX: function (x) {
186 return x / this._scale + this._viewportLoc.x;
187 },
188
189 absY: function (y) {
190 return y / this._scale + this._viewportLoc.y;
191 },
192
193 resize: function (width, height) {
194 this._prevDrawStyle = "";
195
196 this._fb_width = width;
197 this._fb_height = height;
198
199 var canvas = this._backbuffer;
200 if (canvas.width !== width || canvas.height !== height) {
201
202 // We have to save the canvas data since changing the size will clear it
203 var saveImg = null;
204 if (canvas.width > 0 && canvas.height > 0) {
205 saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);
206 }
207
208 if (canvas.width !== width) {
209 canvas.width = width;
210 }
211 if (canvas.height !== height) {
212 canvas.height = height;
213 }
214
215 if (saveImg) {
216 this._drawCtx.putImageData(saveImg, 0, 0);
217 }
218 }
219
220 // Readjust the viewport as it may be incorrectly sized
221 // and positioned
222 var vp = this._viewportLoc;
223 this.viewportChangeSize(vp.w, vp.h);
224 this.viewportChangePos(0, 0);
225 },
226
227 // Track what parts of the visible canvas that need updating
228 _damage: function(x, y, w, h) {
229 if (x < this._damageBounds.left) {
230 this._damageBounds.left = x;
231 }
232 if (y < this._damageBounds.top) {
233 this._damageBounds.top = y;
234 }
235 if ((x + w) > this._damageBounds.right) {
236 this._damageBounds.right = x + w;
237 }
238 if ((y + h) > this._damageBounds.bottom) {
239 this._damageBounds.bottom = y + h;
240 }
241 },
242
243 // Update the visible canvas with the contents of the
244 // rendering canvas
245 flip: function(from_queue) {
246 if (this._renderQ.length !== 0 && !from_queue) {
247 this._renderQ_push({
248 'type': 'flip'
249 });
250 } else {
251 var x, y, vx, vy, w, h;
252
253 x = this._damageBounds.left;
254 y = this._damageBounds.top;
255 w = this._damageBounds.right - x;
256 h = this._damageBounds.bottom - y;
257
258 vx = x - this._viewportLoc.x;
259 vy = y - this._viewportLoc.y;
260
261 if (vx < 0) {
262 w += vx;
263 x -= vx;
264 vx = 0;
265 }
266 if (vy < 0) {
267 h += vy;
268 y -= vy;
269 vy = 0;
270 }
271
272 if ((vx + w) > this._viewportLoc.w) {
273 w = this._viewportLoc.w - vx;
274 }
275 if ((vy + h) > this._viewportLoc.h) {
276 h = this._viewportLoc.h - vy;
277 }
278
279 if ((w > 0) && (h > 0)) {
280 // FIXME: We may need to disable image smoothing here
281 // as well (see copyImage()), but we haven't
282 // noticed any problem yet.
283 this._targetCtx.drawImage(this._backbuffer,
284 x, y, w, h,
285 vx, vy, w, h);
286 }
287
288 this._damageBounds.left = this._damageBounds.top = 65535;
289 this._damageBounds.right = this._damageBounds.bottom = 0;
290 }
291 },
292
293 clear: function () {
294 if (this._logo) {
295 this.resize(this._logo.width, this._logo.height);
296 this.imageRect(0, 0, this._logo.type, this._logo.data);
297 } else {
298 this.resize(240, 20);
299 this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height);
300 }
301 this.flip();
302 },
303
304 pending: function() {
305 return this._renderQ.length > 0;
306 },
307
308 flush: function() {
309 if (this._renderQ.length === 0) {
310 this._onFlush();
311 } else {
312 this._flushing = true;
313 }
314 },
315
316 fillRect: function (x, y, width, height, color, from_queue) {
317 if (this._renderQ.length !== 0 && !from_queue) {
318 this._renderQ_push({
319 'type': 'fill',
320 'x': x,
321 'y': y,
322 'width': width,
323 'height': height,
324 'color': color
325 });
326 } else {
327 this._setFillColor(color);
328 this._drawCtx.fillRect(x, y, width, height);
329 this._damage(x, y, width, height);
330 }
331 },
332
333 copyImage: function (old_x, old_y, new_x, new_y, w, h, from_queue) {
334 if (this._renderQ.length !== 0 && !from_queue) {
335 this._renderQ_push({
336 'type': 'copy',
337 'old_x': old_x,
338 'old_y': old_y,
339 'x': new_x,
340 'y': new_y,
341 'width': w,
342 'height': h,
343 });
344 } else {
345 // Due to this bug among others [1] we need to disable the image-smoothing to
346 // avoid getting a blur effect when copying data.
347 //
348 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
349 //
350 // We need to set these every time since all properties are reset
351 // when the the size is changed
352 this._drawCtx.mozImageSmoothingEnabled = false;
353 this._drawCtx.webkitImageSmoothingEnabled = false;
354 this._drawCtx.msImageSmoothingEnabled = false;
355 this._drawCtx.imageSmoothingEnabled = false;
356
357 this._drawCtx.drawImage(this._backbuffer,
358 old_x, old_y, w, h,
359 new_x, new_y, w, h);
360 this._damage(new_x, new_y, w, h);
361 }
362 },
363
364 imageRect: function(x, y, mime, arr) {
365 var img = new Image();
366 img.src = "data: " + mime + ";base64," + Base64.encode(arr);
367 this._renderQ_push({
368 'type': 'img',
369 'img': img,
370 'x': x,
371 'y': y
372 });
373 },
374
375 // start updating a tile
376 startTile: function (x, y, width, height, color) {
377 this._tile_x = x;
378 this._tile_y = y;
379 if (width === 16 && height === 16) {
380 this._tile = this._tile16x16;
381 } else {
382 this._tile = this._drawCtx.createImageData(width, height);
383 }
384
385 if (this._prefer_js) {
386 var red = color[2];
387 var green = color[1];
388 var blue = color[0];
389
390 var data = this._tile.data;
391 for (var i = 0; i < width * height * 4; i += 4) {
392 data[i] = red;
393 data[i + 1] = green;
394 data[i + 2] = blue;
395 data[i + 3] = 255;
396 }
397 } else {
398 this.fillRect(x, y, width, height, color, true);
399 }
400 },
401
402 // update sub-rectangle of the current tile
403 subTile: function (x, y, w, h, color) {
404 if (this._prefer_js) {
405 var red = color[2];
406 var green = color[1];
407 var blue = color[0];
408 var xend = x + w;
409 var yend = y + h;
410
411 var data = this._tile.data;
412 var width = this._tile.width;
413 for (var j = y; j < yend; j++) {
414 for (var i = x; i < xend; i++) {
415 var p = (i + (j * width)) * 4;
416 data[p] = red;
417 data[p + 1] = green;
418 data[p + 2] = blue;
419 data[p + 3] = 255;
420 }
421 }
422 } else {
423 this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color, true);
424 }
425 },
426
427 // draw the current tile to the screen
428 finishTile: function () {
429 if (this._prefer_js) {
430 this._drawCtx.putImageData(this._tile, this._tile_x, this._tile_y);
431 this._damage(this._tile_x, this._tile_y,
432 this._tile.width, this._tile.height);
433 }
434 // else: No-op -- already done by setSubTile
435 },
436
437 blitImage: function (x, y, width, height, arr, offset, from_queue) {
438 if (this._renderQ.length !== 0 && !from_queue) {
439 // NB(directxman12): it's technically more performant here to use preallocated arrays,
440 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
441 // this probably isn't getting called *nearly* as much
442 var new_arr = new Uint8Array(width * height * 4);
443 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
444 this._renderQ_push({
445 'type': 'blit',
446 'data': new_arr,
447 'x': x,
448 'y': y,
449 'width': width,
450 'height': height,
451 });
452 } else {
453 this._bgrxImageData(x, y, width, height, arr, offset);
454 }
455 },
456
457 blitRgbImage: function (x, y , width, height, arr, offset, from_queue) {
458 if (this._renderQ.length !== 0 && !from_queue) {
459 // NB(directxman12): it's technically more performant here to use preallocated arrays,
460 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
461 // this probably isn't getting called *nearly* as much
462 var new_arr = new Uint8Array(width * height * 3);
463 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
464 this._renderQ_push({
465 'type': 'blitRgb',
466 'data': new_arr,
467 'x': x,
468 'y': y,
469 'width': width,
470 'height': height,
471 });
472 } else {
473 this._rgbImageData(x, y, width, height, arr, offset);
474 }
475 },
476
477 blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) {
478 if (this._renderQ.length !== 0 && !from_queue) {
479 // NB(directxman12): it's technically more performant here to use preallocated arrays,
480 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
481 // this probably isn't getting called *nearly* as much
482 var new_arr = new Uint8Array(width * height * 4);
483 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
484 this._renderQ_push({
485 'type': 'blitRgbx',
486 'data': new_arr,
487 'x': x,
488 'y': y,
489 'width': width,
490 'height': height,
491 });
492 } else {
493 this._rgbxImageData(x, y, width, height, arr, offset);
494 }
495 },
496
497 drawImage: function (img, x, y) {
498 this._drawCtx.drawImage(img, x, y);
499 this._damage(x, y, img.width, img.height);
500 },
501
502 changeCursor: function (pixels, mask, hotx, hoty, w, h) {
503 if (this._cursor_uri === false) {
504 Log.Warn("changeCursor called but no cursor data URI support");
505 return;
506 }
507
508 Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h);
509 },
510
511 defaultCursor: function () {
512 this._target.style.cursor = "default";
513 },
514
515 disableLocalCursor: function () {
516 this._target.style.cursor = "none";
517 },
518
519 clippingDisplay: function () {
520 var vp = this._viewportLoc;
521 return this._fb_width > vp.w || this._fb_height > vp.h;
522 },
523
524 // Overridden getters/setters
525 set_scale: function (scale) {
526 this._rescale(scale);
527 },
528
529 set_viewport: function (viewport) {
530 this._viewport = viewport;
531 // May need to readjust the viewport dimensions
532 var vp = this._viewportLoc;
533 this.viewportChangeSize(vp.w, vp.h);
534 this.viewportChangePos(0, 0);
535 },
536
537 get_width: function () {
538 return this._fb_width;
539 },
540 get_height: function () {
541 return this._fb_height;
542 },
543
544 autoscale: function (containerWidth, containerHeight, downscaleOnly) {
545 var vp = this._viewportLoc;
546 var targetAspectRatio = containerWidth / containerHeight;
547 var fbAspectRatio = vp.w / vp.h;
548
549 var scaleRatio;
550 if (fbAspectRatio >= targetAspectRatio) {
551 scaleRatio = containerWidth / vp.w;
552 } else {
553 scaleRatio = containerHeight / vp.h;
554 }
555
556 if (scaleRatio > 1.0 && downscaleOnly) {
557 scaleRatio = 1.0;
558 }
559
560 this._rescale(scaleRatio);
561 },
562
563 // Private Methods
564 _rescale: function (factor) {
565 this._scale = factor;
566 var vp = this._viewportLoc;
567
568 // NB(directxman12): If you set the width directly, or set the
569 // style width to a number, the canvas is cleared.
570 // However, if you set the style width to a string
571 // ('NNNpx'), the canvas is scaled without clearing.
572 var width = Math.round(factor * vp.w) + 'px';
573 var height = Math.round(factor * vp.h) + 'px';
574
575 if ((this._target.style.width !== width) ||
576 (this._target.style.height !== height)) {
577 this._target.style.width = width;
578 this._target.style.height = height;
579 }
580 },
581
582 _setFillColor: function (color) {
583 var newStyle = 'rgb(' + color[2] + ',' + color[1] + ',' + color[0] + ')';
584 if (newStyle !== this._prevDrawStyle) {
585 this._drawCtx.fillStyle = newStyle;
586 this._prevDrawStyle = newStyle;
587 }
588 },
589
590 _rgbImageData: function (x, y, width, height, arr, offset) {
591 var img = this._drawCtx.createImageData(width, height);
592 var data = img.data;
593 for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) {
594 data[i] = arr[j];
595 data[i + 1] = arr[j + 1];
596 data[i + 2] = arr[j + 2];
597 data[i + 3] = 255; // Alpha
598 }
599 this._drawCtx.putImageData(img, x, y);
600 this._damage(x, y, img.width, img.height);
601 },
602
603 _bgrxImageData: function (x, y, width, height, arr, offset) {
604 var img = this._drawCtx.createImageData(width, height);
605 var data = img.data;
606 for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) {
607 data[i] = arr[j + 2];
608 data[i + 1] = arr[j + 1];
609 data[i + 2] = arr[j];
610 data[i + 3] = 255; // Alpha
611 }
612 this._drawCtx.putImageData(img, x, y);
613 this._damage(x, y, img.width, img.height);
614 },
615
616 _rgbxImageData: function (x, y, width, height, arr, offset) {
617 // NB(directxman12): arr must be an Type Array view
618 var img;
619 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) {
620 img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height);
621 } else {
622 img = this._drawCtx.createImageData(width, height);
623 img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4));
624 }
625 this._drawCtx.putImageData(img, x, y);
626 this._damage(x, y, img.width, img.height);
627 },
628
629 _renderQ_push: function (action) {
630 this._renderQ.push(action);
631 if (this._renderQ.length === 1) {
632 // If this can be rendered immediately it will be, otherwise
633 // the scanner will wait for the relevant event
634 this._scan_renderQ();
635 }
636 },
637
638 _resume_renderQ: function() {
639 // "this" is the object that is ready, not the
640 // display object
641 this.removeEventListener('load', this._noVNC_display._resume_renderQ);
642 this._noVNC_display._scan_renderQ();
643 },
644
645 _scan_renderQ: function () {
646 var ready = true;
647 while (ready && this._renderQ.length > 0) {
648 var a = this._renderQ[0];
649 switch (a.type) {
650 case 'flip':
651 this.flip(true);
652 break;
653 case 'copy':
654 this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true);
655 break;
656 case 'fill':
657 this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
658 break;
659 case 'blit':
660 this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
661 break;
662 case 'blitRgb':
663 this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true);
664 break;
665 case 'blitRgbx':
666 this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true);
667 break;
668 case 'img':
669 if (a.img.complete) {
670 this.drawImage(a.img, a.x, a.y);
671 } else {
672 a.img._noVNC_display = this;
673 a.img.addEventListener('load', this._resume_renderQ);
674 // We need to wait for this image to 'load'
675 // to keep things in-order
676 ready = false;
677 }
678 break;
679 }
680
681 if (ready) {
682 this._renderQ.shift();
683 }
684 }
685
686 if (this._renderQ.length === 0 && this._flushing) {
687 this._flushing = false;
688 this._onFlush();
689 }
690 },
691 };
692
693 make_properties(Display, [
694 ['target', 'wo', 'dom'], // Canvas element for rendering
695 ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only)
696 ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "type": mime-type, "data": data}
697 ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0
698 ['viewport', 'rw', 'bool'], // Use viewport clipping
699 ['width', 'ro', 'int'], // Display area width
700 ['height', 'ro', 'int'], // Display area height
701
702 ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only)
703
704 ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods
705 ['cursor_uri', 'rw', 'raw'], // Can we render cursor using data URI
706
707 ['onFlush', 'rw', 'func'], // onFlush(): A flush request has finished
708 ]);
709
710 // Class Methods
711 Display.changeCursor = function (target, pixels, mask, hotx, hoty, w, h) {
712 if ((w === 0) || (h === 0)) {
713 target.style.cursor = 'none';
714 return;
715 }
716
717 var cur = []
718 var y, x;
719 for (y = 0; y < h; y++) {
720 for (x = 0; x < w; x++) {
721 var idx = y * Math.ceil(w / 8) + Math.floor(x / 8);
722 var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
723 idx = ((w * y) + x) * 4;
724 cur.push(pixels[idx + 2]); // red
725 cur.push(pixels[idx + 1]); // green
726 cur.push(pixels[idx]); // blue
727 cur.push(alpha); // alpha
728 }
729 }
730
731 var canvas = document.createElement('canvas');
732 var ctx = canvas.getContext('2d');
733
734 canvas.width = w;
735 canvas.height = h;
736
737 var img;
738 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) {
739 img = new ImageData(new Uint8ClampedArray(cur), w, h);
740 } else {
741 img = ctx.createImageData(w, h);
742 img.data.set(new Uint8ClampedArray(cur));
743 }
744 ctx.clearRect(0, 0, w, h);
745 ctx.putImageData(img, 0, 0);
746
747 var url = canvas.toDataURL();
748 target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
749 };