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 import * as Log
from './util/logging.js';
11 import Base64
from "./base64.js";
13 export default function Display(target
) {
15 this._c_forceCanvas
= false;
17 this._renderQ
= []; // queue drawing actions for in-oder rendering
18 this._flushing
= false;
20 // the full frame buffer (logical canvas) size
24 this._prevDrawStyle
= "";
26 this._tile16x16
= null;
30 Log
.Debug(">> Display.constructor");
33 this._target
= target
;
36 throw new Error("Target must be set");
39 if (typeof this._target
=== 'string') {
40 throw new Error('target must be a DOM element');
43 if (!this._target
.getContext
) {
44 throw new Error("no getContext method");
47 this._targetCtx
= this._target
.getContext('2d');
49 // the visible canvas viewport (i.e. what actually gets seen)
50 this._viewportLoc
= { 'x': 0, 'y': 0, 'w': this._target
.width
, 'h': this._target
.height
};
52 // The hidden canvas, where we do the actual rendering
53 this._backbuffer
= document
.createElement('canvas');
54 this._drawCtx
= this._backbuffer
.getContext('2d');
56 this._damageBounds
= { left
:0, top
:0,
57 right
: this._backbuffer
.width
,
58 bottom
: this._backbuffer
.height
};
60 Log
.Debug("User Agent: " + navigator
.userAgent
);
64 // Check canvas features
65 if (!('createImageData' in this._drawCtx
)) {
66 throw new Error("Canvas does not support createImageData");
69 this._tile16x16
= this._drawCtx
.createImageData(16, 16);
70 Log
.Debug("<< Display.constructor");
73 var SUPPORTS_IMAGEDATA_CONSTRUCTOR
= false;
75 new ImageData(new Uint8ClampedArray(4), 1, 1);
76 SUPPORTS_IMAGEDATA_CONSTRUCTOR
= true;
82 // ===== PROPERTIES =====
85 get scale() { return this._scale
; },
91 get clipViewport() { return this._clipViewport
; },
92 set clipViewport(viewport
) {
93 this._clipViewport
= viewport
;
94 // May need to readjust the viewport dimensions
95 var vp
= this._viewportLoc
;
96 this.viewportChangeSize(vp
.w
, vp
.h
);
97 this.viewportChangePos(0, 0);
101 return this._fb_width
;
104 return this._fb_height
;
109 // ===== EVENT HANDLERS =====
111 onflush: function () {}, // A flush request has finished
113 // ===== PUBLIC METHODS =====
115 viewportChangePos: function (deltaX
, deltaY
) {
116 var vp
= this._viewportLoc
;
117 deltaX
= Math
.floor(deltaX
);
118 deltaY
= Math
.floor(deltaY
);
120 if (!this._clipViewport
) {
121 deltaX
= -vp
.w
; // clamped later of out of bounds
125 var vx2
= vp
.x
+ vp
.w
- 1;
126 var vy2
= vp
.y
+ vp
.h
- 1;
130 if (deltaX
< 0 && vp
.x
+ deltaX
< 0) {
133 if (vx2
+ deltaX
>= this._fb_width
) {
134 deltaX
-= vx2
+ deltaX
- this._fb_width
+ 1;
137 if (vp
.y
+ deltaY
< 0) {
140 if (vy2
+ deltaY
>= this._fb_height
) {
141 deltaY
-= (vy2
+ deltaY
- this._fb_height
+ 1);
144 if (deltaX
=== 0 && deltaY
=== 0) {
147 Log
.Debug("viewportChange deltaX: " + deltaX
+ ", deltaY: " + deltaY
);
152 this._damage(vp
.x
, vp
.y
, vp
.w
, vp
.h
);
157 viewportChangeSize: function(width
, height
) {
159 if (!this._clipViewport
||
160 typeof(width
) === "undefined" ||
161 typeof(height
) === "undefined") {
163 Log
.Debug("Setting viewport to full display region");
164 width
= this._fb_width
;
165 height
= this._fb_height
;
168 if (width
> this._fb_width
) {
169 width
= this._fb_width
;
171 if (height
> this._fb_height
) {
172 height
= this._fb_height
;
175 var vp
= this._viewportLoc
;
176 if (vp
.w
!== width
|| vp
.h
!== height
) {
180 var canvas
= this._target
;
181 canvas
.width
= width
;
182 canvas
.height
= height
;
184 // The position might need to be updated if we've grown
185 this.viewportChangePos(0, 0);
187 this._damage(vp
.x
, vp
.y
, vp
.w
, vp
.h
);
190 // Update the visible size of the target canvas
191 this._rescale(this._scale
);
196 return x
/ this._scale
+ this._viewportLoc
.x
;
200 return y
/ this._scale
+ this._viewportLoc
.y
;
203 resize: function (width
, height
) {
204 this._prevDrawStyle
= "";
206 this._fb_width
= width
;
207 this._fb_height
= height
;
209 var canvas
= this._backbuffer
;
210 if (canvas
.width
!== width
|| canvas
.height
!== height
) {
212 // We have to save the canvas data since changing the size will clear it
214 if (canvas
.width
> 0 && canvas
.height
> 0) {
215 saveImg
= this._drawCtx
.getImageData(0, 0, canvas
.width
, canvas
.height
);
218 if (canvas
.width
!== width
) {
219 canvas
.width
= width
;
221 if (canvas
.height
!== height
) {
222 canvas
.height
= height
;
226 this._drawCtx
.putImageData(saveImg
, 0, 0);
230 // Readjust the viewport as it may be incorrectly sized
232 var vp
= this._viewportLoc
;
233 this.viewportChangeSize(vp
.w
, vp
.h
);
234 this.viewportChangePos(0, 0);
237 // Track what parts of the visible canvas that need updating
238 _damage: function(x
, y
, w
, h
) {
239 if (x
< this._damageBounds
.left
) {
240 this._damageBounds
.left
= x
;
242 if (y
< this._damageBounds
.top
) {
243 this._damageBounds
.top
= y
;
245 if ((x
+ w
) > this._damageBounds
.right
) {
246 this._damageBounds
.right
= x
+ w
;
248 if ((y
+ h
) > this._damageBounds
.bottom
) {
249 this._damageBounds
.bottom
= y
+ h
;
253 // Update the visible canvas with the contents of the
255 flip: function(from_queue
) {
256 if (this._renderQ
.length
!== 0 && !from_queue
) {
261 var x
, y
, vx
, vy
, w
, h
;
263 x
= this._damageBounds
.left
;
264 y
= this._damageBounds
.top
;
265 w
= this._damageBounds
.right
- x
;
266 h
= this._damageBounds
.bottom
- y
;
268 vx
= x
- this._viewportLoc
.x
;
269 vy
= y
- this._viewportLoc
.y
;
282 if ((vx
+ w
) > this._viewportLoc
.w
) {
283 w
= this._viewportLoc
.w
- vx
;
285 if ((vy
+ h
) > this._viewportLoc
.h
) {
286 h
= this._viewportLoc
.h
- vy
;
289 if ((w
> 0) && (h
> 0)) {
290 // FIXME: We may need to disable image smoothing here
291 // as well (see copyImage()), but we haven't
292 // noticed any problem yet.
293 this._targetCtx
.drawImage(this._backbuffer
,
298 this._damageBounds
.left
= this._damageBounds
.top
= 65535;
299 this._damageBounds
.right
= this._damageBounds
.bottom
= 0;
305 this.resize(this._logo
.width
, this._logo
.height
);
306 this.imageRect(0, 0, this._logo
.type
, this._logo
.data
);
308 this.resize(240, 20);
309 this._drawCtx
.clearRect(0, 0, this._fb_width
, this._fb_height
);
314 pending: function() {
315 return this._renderQ
.length
> 0;
319 if (this._renderQ
.length
=== 0) {
322 this._flushing
= true;
326 fillRect: function (x
, y
, width
, height
, color
, from_queue
) {
327 if (this._renderQ
.length
!== 0 && !from_queue
) {
337 this._setFillColor(color
);
338 this._drawCtx
.fillRect(x
, y
, width
, height
);
339 this._damage(x
, y
, width
, height
);
343 copyImage: function (old_x
, old_y
, new_x
, new_y
, w
, h
, from_queue
) {
344 if (this._renderQ
.length
!== 0 && !from_queue
) {
355 // Due to this bug among others [1] we need to disable the image-smoothing to
356 // avoid getting a blur effect when copying data.
358 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
360 // We need to set these every time since all properties are reset
361 // when the the size is changed
362 this._drawCtx
.mozImageSmoothingEnabled
= false;
363 this._drawCtx
.webkitImageSmoothingEnabled
= false;
364 this._drawCtx
.msImageSmoothingEnabled
= false;
365 this._drawCtx
.imageSmoothingEnabled
= false;
367 this._drawCtx
.drawImage(this._backbuffer
,
370 this._damage(new_x
, new_y
, w
, h
);
374 imageRect: function(x
, y
, mime
, arr
) {
375 var img
= new Image();
376 img
.src
= "data: " + mime
+ ";base64," + Base64
.encode(arr
);
385 // start updating a tile
386 startTile: function (x
, y
, width
, height
, color
) {
389 if (width
=== 16 && height
=== 16) {
390 this._tile
= this._tile16x16
;
392 this._tile
= this._drawCtx
.createImageData(width
, height
);
396 var green
= color
[1];
399 var data
= this._tile
.data
;
400 for (var i
= 0; i
< width
* height
* 4; i
+= 4) {
408 // update sub-rectangle of the current tile
409 subTile: function (x
, y
, w
, h
, color
) {
411 var green
= color
[1];
416 var data
= this._tile
.data
;
417 var width
= this._tile
.width
;
418 for (var j
= y
; j
< yend
; j
++) {
419 for (var i
= x
; i
< xend
; i
++) {
420 var p
= (i
+ (j
* width
)) * 4;
429 // draw the current tile to the screen
430 finishTile: function () {
431 this._drawCtx
.putImageData(this._tile
, this._tile_x
, this._tile_y
);
432 this._damage(this._tile_x
, this._tile_y
,
433 this._tile
.width
, this._tile
.height
);
436 blitImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
437 if (this._renderQ
.length
!== 0 && !from_queue
) {
438 // NB(directxman12): it's technically more performant here to use preallocated arrays,
439 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
440 // this probably isn't getting called *nearly* as much
441 var new_arr
= new Uint8Array(width
* height
* 4);
442 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
452 this._bgrxImageData(x
, y
, width
, height
, arr
, offset
);
456 blitRgbImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
457 if (this._renderQ
.length
!== 0 && !from_queue
) {
458 // NB(directxman12): it's technically more performant here to use preallocated arrays,
459 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
460 // this probably isn't getting called *nearly* as much
461 var new_arr
= new Uint8Array(width
* height
* 3);
462 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
472 this._rgbImageData(x
, y
, width
, height
, arr
, offset
);
476 blitRgbxImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
477 if (this._renderQ
.length
!== 0 && !from_queue
) {
478 // NB(directxman12): it's technically more performant here to use preallocated arrays,
479 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
480 // this probably isn't getting called *nearly* as much
481 var new_arr
= new Uint8Array(width
* height
* 4);
482 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
492 this._rgbxImageData(x
, y
, width
, height
, arr
, offset
);
496 drawImage: function (img
, x
, y
) {
497 this._drawCtx
.drawImage(img
, x
, y
);
498 this._damage(x
, y
, img
.width
, img
.height
);
501 changeCursor: function (pixels
, mask
, hotx
, hoty
, w
, h
) {
502 Display
.changeCursor(this._target
, pixels
, mask
, hotx
, hoty
, w
, h
);
505 defaultCursor: function () {
506 this._target
.style
.cursor
= "default";
509 disableLocalCursor: function () {
510 this._target
.style
.cursor
= "none";
513 autoscale: function (containerWidth
, containerHeight
) {
514 var vp
= this._viewportLoc
;
515 var targetAspectRatio
= containerWidth
/ containerHeight
;
516 var fbAspectRatio
= vp
.w
/ vp
.h
;
519 if (fbAspectRatio
>= targetAspectRatio
) {
520 scaleRatio
= containerWidth
/ vp
.w
;
522 scaleRatio
= containerHeight
/ vp
.h
;
525 this._rescale(scaleRatio
);
528 // ===== PRIVATE METHODS =====
530 _rescale: function (factor
) {
531 this._scale
= factor
;
532 var vp
= this._viewportLoc
;
534 // NB(directxman12): If you set the width directly, or set the
535 // style width to a number, the canvas is cleared.
536 // However, if you set the style width to a string
537 // ('NNNpx'), the canvas is scaled without clearing.
538 var width
= Math
.round(factor
* vp
.w
) + 'px';
539 var height
= Math
.round(factor
* vp
.h
) + 'px';
541 if ((this._target
.style
.width
!== width
) ||
542 (this._target
.style
.height
!== height
)) {
543 this._target
.style
.width
= width
;
544 this._target
.style
.height
= height
;
548 _setFillColor: function (color
) {
549 var newStyle
= 'rgb(' + color
[2] + ',' + color
[1] + ',' + color
[0] + ')';
550 if (newStyle
!== this._prevDrawStyle
) {
551 this._drawCtx
.fillStyle
= newStyle
;
552 this._prevDrawStyle
= newStyle
;
556 _rgbImageData: function (x
, y
, width
, height
, arr
, offset
) {
557 var img
= this._drawCtx
.createImageData(width
, height
);
559 for (var i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 3) {
561 data
[i
+ 1] = arr
[j
+ 1];
562 data
[i
+ 2] = arr
[j
+ 2];
563 data
[i
+ 3] = 255; // Alpha
565 this._drawCtx
.putImageData(img
, x
, y
);
566 this._damage(x
, y
, img
.width
, img
.height
);
569 _bgrxImageData: function (x
, y
, width
, height
, arr
, offset
) {
570 var img
= this._drawCtx
.createImageData(width
, height
);
572 for (var i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 4) {
573 data
[i
] = arr
[j
+ 2];
574 data
[i
+ 1] = arr
[j
+ 1];
575 data
[i
+ 2] = arr
[j
];
576 data
[i
+ 3] = 255; // Alpha
578 this._drawCtx
.putImageData(img
, x
, y
);
579 this._damage(x
, y
, img
.width
, img
.height
);
582 _rgbxImageData: function (x
, y
, width
, height
, arr
, offset
) {
583 // NB(directxman12): arr must be an Type Array view
585 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR
) {
586 img
= new ImageData(new Uint8ClampedArray(arr
.buffer
, arr
.byteOffset
, width
* height
* 4), width
, height
);
588 img
= this._drawCtx
.createImageData(width
, height
);
589 img
.data
.set(new Uint8ClampedArray(arr
.buffer
, arr
.byteOffset
, width
* height
* 4));
591 this._drawCtx
.putImageData(img
, x
, y
);
592 this._damage(x
, y
, img
.width
, img
.height
);
595 _renderQ_push: function (action
) {
596 this._renderQ
.push(action
);
597 if (this._renderQ
.length
=== 1) {
598 // If this can be rendered immediately it will be, otherwise
599 // the scanner will wait for the relevant event
600 this._scan_renderQ();
604 _resume_renderQ: function() {
605 // "this" is the object that is ready, not the
607 this.removeEventListener('load', this._noVNC_display
._resume_renderQ
);
608 this._noVNC_display
._scan_renderQ();
611 _scan_renderQ: function () {
613 while (ready
&& this._renderQ
.length
> 0) {
614 var a
= this._renderQ
[0];
620 this.copyImage(a
.old_x
, a
.old_y
, a
.x
, a
.y
, a
.width
, a
.height
, true);
623 this.fillRect(a
.x
, a
.y
, a
.width
, a
.height
, a
.color
, true);
626 this.blitImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
629 this.blitRgbImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
632 this.blitRgbxImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
635 if (a
.img
.complete
) {
636 this.drawImage(a
.img
, a
.x
, a
.y
);
638 a
.img
._noVNC_display
= this;
639 a
.img
.addEventListener('load', this._resume_renderQ
);
640 // We need to wait for this image to 'load'
641 // to keep things in-order
648 this._renderQ
.shift();
652 if (this._renderQ
.length
=== 0 && this._flushing
) {
653 this._flushing
= false;
660 Display
.changeCursor = function (target
, pixels
, mask
, hotx
, hoty
, w
, h
) {
661 if ((w
=== 0) || (h
=== 0)) {
662 target
.style
.cursor
= 'none';
668 for (y
= 0; y
< h
; y
++) {
669 for (x
= 0; x
< w
; x
++) {
670 var idx
= y
* Math
.ceil(w
/ 8) + Math
.floor(x
/ 8);
671 var alpha
= (mask
[idx
] << (x
% 8)) & 0x80 ? 255 : 0;
672 idx
= ((w
* y
) + x
) * 4;
673 cur
.push(pixels
[idx
+ 2]); // red
674 cur
.push(pixels
[idx
+ 1]); // green
675 cur
.push(pixels
[idx
]); // blue
676 cur
.push(alpha
); // alpha
680 var canvas
= document
.createElement('canvas');
681 var ctx
= canvas
.getContext('2d');
687 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR
) {
688 img
= new ImageData(new Uint8ClampedArray(cur
), w
, h
);
690 img
= ctx
.createImageData(w
, h
);
691 img
.data
.set(new Uint8ClampedArray(cur
));
693 ctx
.clearRect(0, 0, w
, h
);
694 ctx
.putImageData(img
, 0, 0);
696 var url
= canvas
.toDataURL();
697 target
.style
.cursor
= 'url(' + url
+ ')' + hotx
+ ' ' + hoty
+ ', default';