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)
7 * See README.md for usage and integration instructions.
10 /*jslint browser: true, white: false */
11 /*global Util, Base64, changeCursor */
13 import { set_defaults
, make_properties
} from './util/properties.js';
14 import * as Log
from './util/logging.js';
15 import Base64
from "./base64.js";
17 export default function Display(target
, defaults
) {
19 this._c_forceCanvas
= false;
21 this._renderQ
= []; // queue drawing actions for in-oder rendering
22 this._flushing
= false;
24 // the full frame buffer (logical canvas) size
28 this._prevDrawStyle
= "";
30 this._tile16x16
= null;
34 set_defaults(this, defaults
, {
37 "onFlush": function () {},
40 Log
.Debug(">> Display.constructor");
43 this._target
= target
;
46 throw new Error("Target must be set");
49 if (typeof this._target
=== 'string') {
50 throw new Error('target must be a DOM element');
53 if (!this._target
.getContext
) {
54 throw new Error("no getContext method");
57 this._targetCtx
= this._target
.getContext('2d');
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
};
62 // The hidden canvas, where we do the actual rendering
63 this._backbuffer
= document
.createElement('canvas');
64 this._drawCtx
= this._backbuffer
.getContext('2d');
66 this._damageBounds
= { left
:0, top
:0,
67 right
: this._backbuffer
.width
,
68 bottom
: this._backbuffer
.height
};
70 Log
.Debug("User Agent: " + navigator
.userAgent
);
74 // Check canvas features
75 if (!('createImageData' in this._drawCtx
)) {
76 throw new Error("Canvas does not support createImageData");
79 Log
.Debug("<< Display.constructor");
82 var SUPPORTS_IMAGEDATA_CONSTRUCTOR
= false;
84 new ImageData(new Uint8ClampedArray(4), 1, 1);
85 SUPPORTS_IMAGEDATA_CONSTRUCTOR
= true;
92 viewportChangePos: function (deltaX
, deltaY
) {
93 var vp
= this._viewportLoc
;
94 deltaX
= Math
.floor(deltaX
);
95 deltaY
= Math
.floor(deltaY
);
97 if (!this._viewport
) {
98 deltaX
= -vp
.w
; // clamped later of out of bounds
102 var vx2
= vp
.x
+ vp
.w
- 1;
103 var vy2
= vp
.y
+ vp
.h
- 1;
107 if (deltaX
< 0 && vp
.x
+ deltaX
< 0) {
110 if (vx2
+ deltaX
>= this._fb_width
) {
111 deltaX
-= vx2
+ deltaX
- this._fb_width
+ 1;
114 if (vp
.y
+ deltaY
< 0) {
117 if (vy2
+ deltaY
>= this._fb_height
) {
118 deltaY
-= (vy2
+ deltaY
- this._fb_height
+ 1);
121 if (deltaX
=== 0 && deltaY
=== 0) {
124 Log
.Debug("viewportChange deltaX: " + deltaX
+ ", deltaY: " + deltaY
);
129 this._damage(vp
.x
, vp
.y
, vp
.w
, vp
.h
);
134 viewportChangeSize: function(width
, height
) {
136 if (!this._viewport
||
137 typeof(width
) === "undefined" ||
138 typeof(height
) === "undefined") {
140 Log
.Debug("Setting viewport to full display region");
141 width
= this._fb_width
;
142 height
= this._fb_height
;
145 if (width
> this._fb_width
) {
146 width
= this._fb_width
;
148 if (height
> this._fb_height
) {
149 height
= this._fb_height
;
152 var vp
= this._viewportLoc
;
153 if (vp
.w
!== width
|| vp
.h
!== height
) {
157 var canvas
= this._target
;
158 canvas
.width
= width
;
159 canvas
.height
= height
;
161 // The position might need to be updated if we've grown
162 this.viewportChangePos(0, 0);
164 this._damage(vp
.x
, vp
.y
, vp
.w
, vp
.h
);
167 // Update the visible size of the target canvas
168 this._rescale(this._scale
);
173 return x
/ this._scale
+ this._viewportLoc
.x
;
177 return y
/ this._scale
+ this._viewportLoc
.y
;
180 resize: function (width
, height
) {
181 this._prevDrawStyle
= "";
183 this._fb_width
= width
;
184 this._fb_height
= height
;
186 var canvas
= this._backbuffer
;
187 if (canvas
.width
!== width
|| canvas
.height
!== height
) {
189 // We have to save the canvas data since changing the size will clear it
191 if (canvas
.width
> 0 && canvas
.height
> 0) {
192 saveImg
= this._drawCtx
.getImageData(0, 0, canvas
.width
, canvas
.height
);
195 if (canvas
.width
!== width
) {
196 canvas
.width
= width
;
198 if (canvas
.height
!== height
) {
199 canvas
.height
= height
;
203 this._drawCtx
.putImageData(saveImg
, 0, 0);
207 // Readjust the viewport as it may be incorrectly sized
209 var vp
= this._viewportLoc
;
210 this.viewportChangeSize(vp
.w
, vp
.h
);
211 this.viewportChangePos(0, 0);
214 // Track what parts of the visible canvas that need updating
215 _damage: function(x
, y
, w
, h
) {
216 if (x
< this._damageBounds
.left
) {
217 this._damageBounds
.left
= x
;
219 if (y
< this._damageBounds
.top
) {
220 this._damageBounds
.top
= y
;
222 if ((x
+ w
) > this._damageBounds
.right
) {
223 this._damageBounds
.right
= x
+ w
;
225 if ((y
+ h
) > this._damageBounds
.bottom
) {
226 this._damageBounds
.bottom
= y
+ h
;
230 // Update the visible canvas with the contents of the
232 flip: function(from_queue
) {
233 if (this._renderQ
.length
!== 0 && !from_queue
) {
238 var x
, y
, vx
, vy
, w
, h
;
240 x
= this._damageBounds
.left
;
241 y
= this._damageBounds
.top
;
242 w
= this._damageBounds
.right
- x
;
243 h
= this._damageBounds
.bottom
- y
;
245 vx
= x
- this._viewportLoc
.x
;
246 vy
= y
- this._viewportLoc
.y
;
259 if ((vx
+ w
) > this._viewportLoc
.w
) {
260 w
= this._viewportLoc
.w
- vx
;
262 if ((vy
+ h
) > this._viewportLoc
.h
) {
263 h
= this._viewportLoc
.h
- vy
;
266 if ((w
> 0) && (h
> 0)) {
267 // FIXME: We may need to disable image smoothing here
268 // as well (see copyImage()), but we haven't
269 // noticed any problem yet.
270 this._targetCtx
.drawImage(this._backbuffer
,
275 this._damageBounds
.left
= this._damageBounds
.top
= 65535;
276 this._damageBounds
.right
= this._damageBounds
.bottom
= 0;
282 this.resize(this._logo
.width
, this._logo
.height
);
283 this.imageRect(0, 0, this._logo
.type
, this._logo
.data
);
285 this.resize(240, 20);
286 this._drawCtx
.clearRect(0, 0, this._fb_width
, this._fb_height
);
291 pending: function() {
292 return this._renderQ
.length
> 0;
296 if (this._renderQ
.length
=== 0) {
299 this._flushing
= true;
303 fillRect: function (x
, y
, width
, height
, color
, from_queue
) {
304 if (this._renderQ
.length
!== 0 && !from_queue
) {
314 this._setFillColor(color
);
315 this._drawCtx
.fillRect(x
, y
, width
, height
);
316 this._damage(x
, y
, width
, height
);
320 copyImage: function (old_x
, old_y
, new_x
, new_y
, w
, h
, from_queue
) {
321 if (this._renderQ
.length
!== 0 && !from_queue
) {
332 // Due to this bug among others [1] we need to disable the image-smoothing to
333 // avoid getting a blur effect when copying data.
335 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
337 // We need to set these every time since all properties are reset
338 // when the the size is changed
339 this._drawCtx
.mozImageSmoothingEnabled
= false;
340 this._drawCtx
.webkitImageSmoothingEnabled
= false;
341 this._drawCtx
.msImageSmoothingEnabled
= false;
342 this._drawCtx
.imageSmoothingEnabled
= false;
344 this._drawCtx
.drawImage(this._backbuffer
,
347 this._damage(new_x
, new_y
, w
, h
);
351 imageRect: function(x
, y
, mime
, arr
) {
352 var img
= new Image();
353 img
.src
= "data: " + mime
+ ";base64," + Base64
.encode(arr
);
362 // start updating a tile
363 startTile: function (x
, y
, width
, height
, color
) {
366 if (width
=== 16 && height
=== 16) {
367 this._tile
= this._tile16x16
;
369 this._tile
= this._drawCtx
.createImageData(width
, height
);
373 var green
= color
[1];
376 var data
= this._tile
.data
;
377 for (var i
= 0; i
< width
* height
* 4; i
+= 4) {
385 // update sub-rectangle of the current tile
386 subTile: function (x
, y
, w
, h
, color
) {
388 var green
= color
[1];
393 var data
= this._tile
.data
;
394 var width
= this._tile
.width
;
395 for (var j
= y
; j
< yend
; j
++) {
396 for (var i
= x
; i
< xend
; i
++) {
397 var p
= (i
+ (j
* width
)) * 4;
406 // draw the current tile to the screen
407 finishTile: function () {
408 this._drawCtx
.putImageData(this._tile
, this._tile_x
, this._tile_y
);
409 this._damage(this._tile_x
, this._tile_y
,
410 this._tile
.width
, this._tile
.height
);
413 blitImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
414 if (this._renderQ
.length
!== 0 && !from_queue
) {
415 // NB(directxman12): it's technically more performant here to use preallocated arrays,
416 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
417 // this probably isn't getting called *nearly* as much
418 var new_arr
= new Uint8Array(width
* height
* 4);
419 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
429 this._bgrxImageData(x
, y
, width
, height
, arr
, offset
);
433 blitRgbImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
434 if (this._renderQ
.length
!== 0 && !from_queue
) {
435 // NB(directxman12): it's technically more performant here to use preallocated arrays,
436 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
437 // this probably isn't getting called *nearly* as much
438 var new_arr
= new Uint8Array(width
* height
* 3);
439 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
449 this._rgbImageData(x
, y
, width
, height
, arr
, offset
);
453 blitRgbxImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
454 if (this._renderQ
.length
!== 0 && !from_queue
) {
455 // NB(directxman12): it's technically more performant here to use preallocated arrays,
456 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
457 // this probably isn't getting called *nearly* as much
458 var new_arr
= new Uint8Array(width
* height
* 4);
459 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
469 this._rgbxImageData(x
, y
, width
, height
, arr
, offset
);
473 drawImage: function (img
, x
, y
) {
474 this._drawCtx
.drawImage(img
, x
, y
);
475 this._damage(x
, y
, img
.width
, img
.height
);
478 changeCursor: function (pixels
, mask
, hotx
, hoty
, w
, h
) {
479 Display
.changeCursor(this._target
, pixels
, mask
, hotx
, hoty
, w
, h
);
482 defaultCursor: function () {
483 this._target
.style
.cursor
= "default";
486 disableLocalCursor: function () {
487 this._target
.style
.cursor
= "none";
490 clippingDisplay: function () {
491 var vp
= this._viewportLoc
;
492 return this._fb_width
> vp
.w
|| this._fb_height
> vp
.h
;
495 // Overridden getters/setters
496 set_scale: function (scale
) {
497 this._rescale(scale
);
500 set_viewport: function (viewport
) {
501 this._viewport
= viewport
;
502 // May need to readjust the viewport dimensions
503 var vp
= this._viewportLoc
;
504 this.viewportChangeSize(vp
.w
, vp
.h
);
505 this.viewportChangePos(0, 0);
508 get_width: function () {
509 return this._fb_width
;
511 get_height: function () {
512 return this._fb_height
;
515 autoscale: function (containerWidth
, containerHeight
, downscaleOnly
) {
516 var vp
= this._viewportLoc
;
517 var targetAspectRatio
= containerWidth
/ containerHeight
;
518 var fbAspectRatio
= vp
.w
/ vp
.h
;
521 if (fbAspectRatio
>= targetAspectRatio
) {
522 scaleRatio
= containerWidth
/ vp
.w
;
524 scaleRatio
= containerHeight
/ vp
.h
;
527 if (scaleRatio
> 1.0 && downscaleOnly
) {
531 this._rescale(scaleRatio
);
535 _rescale: function (factor
) {
536 this._scale
= factor
;
537 var vp
= this._viewportLoc
;
539 // NB(directxman12): If you set the width directly, or set the
540 // style width to a number, the canvas is cleared.
541 // However, if you set the style width to a string
542 // ('NNNpx'), the canvas is scaled without clearing.
543 var width
= Math
.round(factor
* vp
.w
) + 'px';
544 var height
= Math
.round(factor
* vp
.h
) + 'px';
546 if ((this._target
.style
.width
!== width
) ||
547 (this._target
.style
.height
!== height
)) {
548 this._target
.style
.width
= width
;
549 this._target
.style
.height
= height
;
553 _setFillColor: function (color
) {
554 var newStyle
= 'rgb(' + color
[2] + ',' + color
[1] + ',' + color
[0] + ')';
555 if (newStyle
!== this._prevDrawStyle
) {
556 this._drawCtx
.fillStyle
= newStyle
;
557 this._prevDrawStyle
= newStyle
;
561 _rgbImageData: function (x
, y
, width
, height
, arr
, offset
) {
562 var img
= this._drawCtx
.createImageData(width
, height
);
564 for (var i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 3) {
566 data
[i
+ 1] = arr
[j
+ 1];
567 data
[i
+ 2] = arr
[j
+ 2];
568 data
[i
+ 3] = 255; // Alpha
570 this._drawCtx
.putImageData(img
, x
, y
);
571 this._damage(x
, y
, img
.width
, img
.height
);
574 _bgrxImageData: function (x
, y
, width
, height
, arr
, offset
) {
575 var img
= this._drawCtx
.createImageData(width
, height
);
577 for (var i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 4) {
578 data
[i
] = arr
[j
+ 2];
579 data
[i
+ 1] = arr
[j
+ 1];
580 data
[i
+ 2] = arr
[j
];
581 data
[i
+ 3] = 255; // Alpha
583 this._drawCtx
.putImageData(img
, x
, y
);
584 this._damage(x
, y
, img
.width
, img
.height
);
587 _rgbxImageData: function (x
, y
, width
, height
, arr
, offset
) {
588 // NB(directxman12): arr must be an Type Array view
590 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR
) {
591 img
= new ImageData(new Uint8ClampedArray(arr
.buffer
, arr
.byteOffset
, width
* height
* 4), width
, height
);
593 img
= this._drawCtx
.createImageData(width
, height
);
594 img
.data
.set(new Uint8ClampedArray(arr
.buffer
, arr
.byteOffset
, width
* height
* 4));
596 this._drawCtx
.putImageData(img
, x
, y
);
597 this._damage(x
, y
, img
.width
, img
.height
);
600 _renderQ_push: function (action
) {
601 this._renderQ
.push(action
);
602 if (this._renderQ
.length
=== 1) {
603 // If this can be rendered immediately it will be, otherwise
604 // the scanner will wait for the relevant event
605 this._scan_renderQ();
609 _resume_renderQ: function() {
610 // "this" is the object that is ready, not the
612 this.removeEventListener('load', this._noVNC_display
._resume_renderQ
);
613 this._noVNC_display
._scan_renderQ();
616 _scan_renderQ: function () {
618 while (ready
&& this._renderQ
.length
> 0) {
619 var a
= this._renderQ
[0];
625 this.copyImage(a
.old_x
, a
.old_y
, a
.x
, a
.y
, a
.width
, a
.height
, true);
628 this.fillRect(a
.x
, a
.y
, a
.width
, a
.height
, a
.color
, true);
631 this.blitImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
634 this.blitRgbImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
637 this.blitRgbxImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
640 if (a
.img
.complete
) {
641 this.drawImage(a
.img
, a
.x
, a
.y
);
643 a
.img
._noVNC_display
= this;
644 a
.img
.addEventListener('load', this._resume_renderQ
);
645 // We need to wait for this image to 'load'
646 // to keep things in-order
653 this._renderQ
.shift();
657 if (this._renderQ
.length
=== 0 && this._flushing
) {
658 this._flushing
= false;
664 make_properties(Display
, [
665 ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only)
666 ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "type": mime-type, "data": data}
667 ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0
668 ['viewport', 'rw', 'bool'], // Use viewport clipping
669 ['width', 'ro', 'int'], // Display area width
670 ['height', 'ro', 'int'], // Display area height
672 ['onFlush', 'rw', 'func'], // onFlush(): A flush request has finished
676 Display
.changeCursor = function (target
, pixels
, mask
, hotx
, hoty
, w
, h
) {
677 if ((w
=== 0) || (h
=== 0)) {
678 target
.style
.cursor
= 'none';
684 for (y
= 0; y
< h
; y
++) {
685 for (x
= 0; x
< w
; x
++) {
686 var idx
= y
* Math
.ceil(w
/ 8) + Math
.floor(x
/ 8);
687 var alpha
= (mask
[idx
] << (x
% 8)) & 0x80 ? 255 : 0;
688 idx
= ((w
* y
) + x
) * 4;
689 cur
.push(pixels
[idx
+ 2]); // red
690 cur
.push(pixels
[idx
+ 1]); // green
691 cur
.push(pixels
[idx
]); // blue
692 cur
.push(alpha
); // alpha
696 var canvas
= document
.createElement('canvas');
697 var ctx
= canvas
.getContext('2d');
703 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR
) {
704 img
= new ImageData(new Uint8ClampedArray(cur
), w
, h
);
706 img
= ctx
.createImageData(w
, h
);
707 img
.data
.set(new Uint8ClampedArray(cur
));
709 ctx
.clearRect(0, 0, w
, h
);
710 ctx
.putImageData(img
, 0, 0);
712 var url
= canvas
.toDataURL();
713 target
.style
.cursor
= 'url(' + url
+ ')' + hotx
+ ' ' + hoty
+ ', default';