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 let 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 const 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 const 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 const vx2
= vp
.x
+ vp
.w
- 1;
126 const 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 const vp
= this._viewportLoc
;
176 if (vp
.w
!== width
|| vp
.h
!== height
) {
180 const 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 const 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 const 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 let x
= this._damageBounds
.left
;
262 let y
= this._damageBounds
.top
;
263 let w
= this._damageBounds
.right
- x
;
264 let h
= this._damageBounds
.bottom
- y
;
266 let vx
= x
- this._viewportLoc
.x
;
267 let vy
= y
- this._viewportLoc
.y
;
280 if ((vx
+ w
) > this._viewportLoc
.w
) {
281 w
= this._viewportLoc
.w
- vx
;
283 if ((vy
+ h
) > this._viewportLoc
.h
) {
284 h
= this._viewportLoc
.h
- vy
;
287 if ((w
> 0) && (h
> 0)) {
288 // FIXME: We may need to disable image smoothing here
289 // as well (see copyImage()), but we haven't
290 // noticed any problem yet.
291 this._targetCtx
.drawImage(this._backbuffer
,
296 this._damageBounds
.left
= this._damageBounds
.top
= 65535;
297 this._damageBounds
.right
= this._damageBounds
.bottom
= 0;
303 this.resize(this._logo
.width
, this._logo
.height
);
304 this.imageRect(0, 0, this._logo
.type
, this._logo
.data
);
306 this.resize(240, 20);
307 this._drawCtx
.clearRect(0, 0, this._fb_width
, this._fb_height
);
312 pending: function() {
313 return this._renderQ
.length
> 0;
317 if (this._renderQ
.length
=== 0) {
320 this._flushing
= true;
324 fillRect: function (x
, y
, width
, height
, color
, from_queue
) {
325 if (this._renderQ
.length
!== 0 && !from_queue
) {
335 this._setFillColor(color
);
336 this._drawCtx
.fillRect(x
, y
, width
, height
);
337 this._damage(x
, y
, width
, height
);
341 copyImage: function (old_x
, old_y
, new_x
, new_y
, w
, h
, from_queue
) {
342 if (this._renderQ
.length
!== 0 && !from_queue
) {
353 // Due to this bug among others [1] we need to disable the image-smoothing to
354 // avoid getting a blur effect when copying data.
356 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
358 // We need to set these every time since all properties are reset
359 // when the the size is changed
360 this._drawCtx
.mozImageSmoothingEnabled
= false;
361 this._drawCtx
.webkitImageSmoothingEnabled
= false;
362 this._drawCtx
.msImageSmoothingEnabled
= false;
363 this._drawCtx
.imageSmoothingEnabled
= false;
365 this._drawCtx
.drawImage(this._backbuffer
,
368 this._damage(new_x
, new_y
, w
, h
);
372 imageRect: function(x
, y
, mime
, arr
) {
373 const img
= new Image();
374 img
.src
= "data: " + mime
+ ";base64," + Base64
.encode(arr
);
383 // start updating a tile
384 startTile: function (x
, y
, width
, height
, color
) {
387 if (width
=== 16 && height
=== 16) {
388 this._tile
= this._tile16x16
;
390 this._tile
= this._drawCtx
.createImageData(width
, height
);
393 const red
= color
[2];
394 const green
= color
[1];
395 const blue
= color
[0];
397 const data
= this._tile
.data
;
398 for (let i
= 0; i
< width
* height
* 4; i
+= 4) {
406 // update sub-rectangle of the current tile
407 subTile: function (x
, y
, w
, h
, color
) {
408 const red
= color
[2];
409 const green
= color
[1];
410 const blue
= color
[0];
414 const data
= this._tile
.data
;
415 const width
= this._tile
.width
;
416 for (let j
= y
; j
< yend
; j
++) {
417 for (let i
= x
; i
< xend
; i
++) {
418 const p
= (i
+ (j
* width
)) * 4;
427 // draw the current tile to the screen
428 finishTile: function () {
429 this._drawCtx
.putImageData(this._tile
, this._tile_x
, this._tile_y
);
430 this._damage(this._tile_x
, this._tile_y
,
431 this._tile
.width
, this._tile
.height
);
434 blitImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
435 if (this._renderQ
.length
!== 0 && !from_queue
) {
436 // NB(directxman12): it's technically more performant here to use preallocated arrays,
437 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
438 // this probably isn't getting called *nearly* as much
439 const new_arr
= new Uint8Array(width
* height
* 4);
440 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
450 this._bgrxImageData(x
, y
, width
, height
, arr
, offset
);
454 blitRgbImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
455 if (this._renderQ
.length
!== 0 && !from_queue
) {
456 // NB(directxman12): it's technically more performant here to use preallocated arrays,
457 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
458 // this probably isn't getting called *nearly* as much
459 const new_arr
= new Uint8Array(width
* height
* 3);
460 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
470 this._rgbImageData(x
, y
, width
, height
, arr
, offset
);
474 blitRgbxImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
475 if (this._renderQ
.length
!== 0 && !from_queue
) {
476 // NB(directxman12): it's technically more performant here to use preallocated arrays,
477 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
478 // this probably isn't getting called *nearly* as much
479 const new_arr
= new Uint8Array(width
* height
* 4);
480 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
490 this._rgbxImageData(x
, y
, width
, height
, arr
, offset
);
494 drawImage: function (img
, x
, y
) {
495 this._drawCtx
.drawImage(img
, x
, y
);
496 this._damage(x
, y
, img
.width
, img
.height
);
499 changeCursor: function (pixels
, mask
, hotx
, hoty
, w
, h
) {
500 Display
.changeCursor(this._target
, pixels
, mask
, hotx
, hoty
, w
, h
);
503 defaultCursor: function () {
504 this._target
.style
.cursor
= "default";
507 disableLocalCursor: function () {
508 this._target
.style
.cursor
= "none";
511 autoscale: function (containerWidth
, containerHeight
) {
512 const vp
= this._viewportLoc
;
513 const targetAspectRatio
= containerWidth
/ containerHeight
;
514 const fbAspectRatio
= vp
.w
/ vp
.h
;
517 if (fbAspectRatio
>= targetAspectRatio
) {
518 scaleRatio
= containerWidth
/ vp
.w
;
520 scaleRatio
= containerHeight
/ vp
.h
;
523 this._rescale(scaleRatio
);
526 // ===== PRIVATE METHODS =====
528 _rescale: function (factor
) {
529 this._scale
= factor
;
530 const vp
= this._viewportLoc
;
532 // NB(directxman12): If you set the width directly, or set the
533 // style width to a number, the canvas is cleared.
534 // However, if you set the style width to a string
535 // ('NNNpx'), the canvas is scaled without clearing.
536 const width
= Math
.round(factor
* vp
.w
) + 'px';
537 const height
= Math
.round(factor
* vp
.h
) + 'px';
539 if ((this._target
.style
.width
!== width
) ||
540 (this._target
.style
.height
!== height
)) {
541 this._target
.style
.width
= width
;
542 this._target
.style
.height
= height
;
546 _setFillColor: function (color
) {
547 const newStyle
= 'rgb(' + color
[2] + ',' + color
[1] + ',' + color
[0] + ')';
548 if (newStyle
!== this._prevDrawStyle
) {
549 this._drawCtx
.fillStyle
= newStyle
;
550 this._prevDrawStyle
= newStyle
;
554 _rgbImageData: function (x
, y
, width
, height
, arr
, offset
) {
555 const img
= this._drawCtx
.createImageData(width
, height
);
556 const data
= img
.data
;
557 for (let i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 3) {
559 data
[i
+ 1] = arr
[j
+ 1];
560 data
[i
+ 2] = arr
[j
+ 2];
561 data
[i
+ 3] = 255; // Alpha
563 this._drawCtx
.putImageData(img
, x
, y
);
564 this._damage(x
, y
, img
.width
, img
.height
);
567 _bgrxImageData: function (x
, y
, width
, height
, arr
, offset
) {
568 const img
= this._drawCtx
.createImageData(width
, height
);
569 const data
= img
.data
;
570 for (let i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 4) {
571 data
[i
] = arr
[j
+ 2];
572 data
[i
+ 1] = arr
[j
+ 1];
573 data
[i
+ 2] = arr
[j
];
574 data
[i
+ 3] = 255; // Alpha
576 this._drawCtx
.putImageData(img
, x
, y
);
577 this._damage(x
, y
, img
.width
, img
.height
);
580 _rgbxImageData: function (x
, y
, width
, height
, arr
, offset
) {
581 // NB(directxman12): arr must be an Type Array view
583 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR
) {
584 img
= new ImageData(new Uint8ClampedArray(arr
.buffer
, arr
.byteOffset
, width
* height
* 4), width
, height
);
586 img
= this._drawCtx
.createImageData(width
, height
);
587 img
.data
.set(new Uint8ClampedArray(arr
.buffer
, arr
.byteOffset
, width
* height
* 4));
589 this._drawCtx
.putImageData(img
, x
, y
);
590 this._damage(x
, y
, img
.width
, img
.height
);
593 _renderQ_push: function (action
) {
594 this._renderQ
.push(action
);
595 if (this._renderQ
.length
=== 1) {
596 // If this can be rendered immediately it will be, otherwise
597 // the scanner will wait for the relevant event
598 this._scan_renderQ();
602 _resume_renderQ: function() {
603 // "this" is the object that is ready, not the
605 this.removeEventListener('load', this._noVNC_display
._resume_renderQ
);
606 this._noVNC_display
._scan_renderQ();
609 _scan_renderQ: function () {
611 while (ready
&& this._renderQ
.length
> 0) {
612 const a
= this._renderQ
[0];
618 this.copyImage(a
.old_x
, a
.old_y
, a
.x
, a
.y
, a
.width
, a
.height
, true);
621 this.fillRect(a
.x
, a
.y
, a
.width
, a
.height
, a
.color
, true);
624 this.blitImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
627 this.blitRgbImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
630 this.blitRgbxImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
633 if (a
.img
.complete
) {
634 this.drawImage(a
.img
, a
.x
, a
.y
);
636 a
.img
._noVNC_display
= this;
637 a
.img
.addEventListener('load', this._resume_renderQ
);
638 // We need to wait for this image to 'load'
639 // to keep things in-order
646 this._renderQ
.shift();
650 if (this._renderQ
.length
=== 0 && this._flushing
) {
651 this._flushing
= false;
658 Display
.changeCursor = function (target
, pixels
, mask
, hotx
, hoty
, w
, h
) {
659 if ((w
=== 0) || (h
=== 0)) {
660 target
.style
.cursor
= 'none';
665 for (let y
= 0; y
< h
; y
++) {
666 for (let x
= 0; x
< w
; x
++) {
667 let idx
= y
* Math
.ceil(w
/ 8) + Math
.floor(x
/ 8);
668 const alpha
= (mask
[idx
] << (x
% 8)) & 0x80 ? 255 : 0;
669 idx
= ((w
* y
) + x
) * 4;
670 cur
.push(pixels
[idx
+ 2]); // red
671 cur
.push(pixels
[idx
+ 1]); // green
672 cur
.push(pixels
[idx
]); // blue
673 cur
.push(alpha
); // alpha
677 const canvas
= document
.createElement('canvas');
678 const ctx
= canvas
.getContext('2d');
684 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR
) {
685 img
= new ImageData(new Uint8ClampedArray(cur
), w
, h
);
687 img
= ctx
.createImageData(w
, h
);
688 img
.data
.set(new Uint8ClampedArray(cur
));
690 ctx
.clearRect(0, 0, w
, h
);
691 ctx
.putImageData(img
, 0, 0);
693 const url
= canvas
.toDataURL();
694 target
.style
.cursor
= 'url(' + url
+ ')' + hotx
+ ' ' + hoty
+ ', default';