]>
git.proxmox.com Git - mirror_novnc.git/blob - include/display.js
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2012 Joel Martin
4 * Licensed under MPL 2.0 (see LICENSE.txt)
6 * See README.md for usage and integration instructions.
9 /*jslint browser: true, white: false */
10 /*global Util, Base64, changeCursor */
17 Display = function (defaults
) {
19 this._c_forceCanvas
= false;
21 this._renderQ
= []; // queue drawing actions for in-oder rendering
23 // the full frame buffer (logical canvas) size
27 // the visible "physical canvas" viewport
28 this._viewportLoc
= { 'x': 0, 'y': 0, 'w': 0, 'h': 0 };
29 this._cleanRect
= { 'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1 };
31 this._prevDrawStyle
= "";
33 this._tile16x16
= null;
37 Util
.set_defaults(this, defaults
, {
45 Util
.Debug(">> Display.constructor");
48 throw new Error("Target must be set");
51 if (typeof this._target
=== 'string') {
52 throw new Error('target must be a DOM element');
55 if (!this._target
.getContext
) {
56 throw new Error("no getContext method");
60 this._drawCtx
= this._target
.getContext('2d');
63 Util
.Debug("User Agent: " + navigator
.userAgent
);
64 if (Util
.Engine
.gecko
) { Util
.Debug("Browser: gecko " + Util
.Engine
.gecko
); }
65 if (Util
.Engine
.webkit
) { Util
.Debug("Browser: webkit " + Util
.Engine
.webkit
); }
66 if (Util
.Engine
.trident
) { Util
.Debug("Browser: trident " + Util
.Engine
.trident
); }
67 if (Util
.Engine
.presto
) { Util
.Debug("Browser: presto " + Util
.Engine
.presto
); }
71 // Check canvas features
72 if ('createImageData' in this._drawCtx
) {
73 this._render_mode
= 'canvas rendering';
75 throw new Error("Canvas does not support createImageData");
78 if (this._prefer_js
=== null) {
79 Util
.Info("Prefering javascript operations");
80 this._prefer_js
= true;
83 // Determine browser support for setting the cursor via data URI scheme
85 for (var i
= 0; i
< 8 * 8 * 4; i
++) {
89 var curSave
= this._target
.style
.cursor
;
90 Display
.changeCursor(this._target
, curDat
, curDat
, 2, 2, 8, 8);
91 if (this._target
.style
.cursor
) {
92 if (this._cursor_uri
=== null || this._cursor_uri
=== undefined) {
93 this._cursor_uri
= true;
95 Util
.Info("Data URI scheme cursor supported");
97 if (this._cursor_uri
=== null || this._cursor_uri
=== undefined) {
98 this._cursor_uri
= false;
100 Util
.Warn("Data URI scheme cursor not supported");
102 this._target
.style
.cursor
= curSave
;
104 Util
.Error("Data URI scheme cursor test exception: " + exc
);
105 this._cursor_uri
= false;
108 Util
.Debug("<< Display.constructor");
111 Display
.prototype = {
113 viewportChange: function (deltaX
, deltaY
, width
, height
) {
114 var vp
= this._viewportLoc
;
115 var cr
= this._cleanRect
;
116 var canvas
= this._target
;
118 if (!this._viewport
) {
119 Util
.Debug("Setting viewport to full display region");
120 deltaX
= -vp
.w
; // clamped later of out of bounds
122 width
= this._fb_width
;
123 height
= this._fb_height
;
126 if (typeof(deltaX
) === "undefined") { deltaX
= 0; }
127 if (typeof(deltaY
) === "undefined") { deltaY
= 0; }
128 if (typeof(width
) === "undefined") { width
= vp
.w
; }
129 if (typeof(height
) === "undefined") { height
= vp
.h
; }
132 if (width
> this._fb_width
) { width
= this._fb_width
; }
133 if (height
> this._fb_height
) { height
= this._fb_height
; }
135 if (vp
.w
!== width
|| vp
.h
!== height
) {
137 if (width
< vp
.w
&& cr
.x2
> vp
.x
+ width
- 1) {
138 cr
.x2
= vp
.x
+ width
- 1;
143 if (height
< vp
.h
&& cr
.y2
> vp
.y
+ height
- 1) {
144 cr
.y2
= vp
.y
+ height
- 1;
149 if (vp
.w
> 0 && vp
.h
> 0 && canvas
.width
> 0 && canvas
.height
> 0) {
150 var img_width
= canvas
.width
< vp
.w
? canvas
.width
: vp
.w
;
151 var img_height
= canvas
.height
< vp
.h
? canvas
.height
: vp
.h
;
152 saveImg
= this._drawCtx
.getImageData(0, 0, img_width
, img_height
);
156 canvas
.height
= vp
.h
;
159 this._drawCtx
.putImageData(saveImg
, 0, 0);
163 var vx2
= vp
.x
+ vp
.w
- 1;
164 var vy2
= vp
.y
+ vp
.h
- 1;
168 if (deltaX
< 0 && vp
.x
+ deltaX
< 0) {
171 if (vx2
+ deltaX
>= this._fb_width
) {
172 deltaX
-= vx2
+ deltaX
- this._fb_width
+ 1;
175 if (vp
.y
+ deltaY
< 0) {
178 if (vy2
+ deltaY
>= this._fb_height
) {
179 deltaY
-= (vy2
+ deltaY
- this._fb_height
+ 1);
182 if (deltaX
=== 0 && deltaY
=== 0) {
185 Util
.Debug("viewportChange deltaX: " + deltaX
+ ", deltaY: " + deltaY
);
192 // Update the clean rectangle
208 // Shift viewport left, redraw left section
212 // Shift viewport right, redraw right section
219 // Shift viewport up, redraw top section
223 // Shift viewport down, redraw bottom section
228 // Copy the valid part of the viewport to the shifted location
229 var saveStyle
= this._drawCtx
.fillStyle
;
230 this._drawCtx
.fillStyle
= "rgb(255,255,255)";
232 this._drawCtx
.drawImage(canvas
, 0, 0, vp
.w
, vp
.h
, -deltaX
, 0, vp
.w
, vp
.h
);
233 this._drawCtx
.fillRect(x1
, 0, w
, vp
.h
);
236 this._drawCtx
.drawImage(canvas
, 0, 0, vp
.w
, vp
.h
, 0, -deltaY
, vp
.w
, vp
.h
);
237 this._drawCtx
.fillRect(0, y1
, vp
.w
, h
);
239 this._drawCtx
.fillStyle
= saveStyle
;
242 // Return a map of clean and dirty areas of the viewport and reset the
243 // tracking of clean and dirty areas
245 // Returns: { 'cleanBox': { 'x': x, 'y': y, 'w': w, 'h': h},
246 // 'dirtyBoxes': [{ 'x': x, 'y': y, 'w': w, 'h': h }, ...] }
247 getCleanDirtyReset: function () {
248 var vp
= this._viewportLoc
;
249 var cr
= this._cleanRect
;
251 var cleanBox
= { 'x': cr
.x1
, 'y': cr
.y1
,
252 'w': cr
.x2
- cr
.x1
+ 1, 'h': cr
.y2
- cr
.y1
+ 1 };
255 if (cr
.x1
>= cr
.x2
|| cr
.y1
>= cr
.y2
) {
256 // Whole viewport is dirty
257 dirtyBoxes
.push({ 'x': vp
.x
, 'y': vp
.y
, 'w': vp
.w
, 'h': vp
.h
});
259 // Redraw dirty regions
260 var vx2
= vp
.x
+ vp
.w
- 1;
261 var vy2
= vp
.y
+ vp
.h
- 1;
264 // left side dirty region
265 dirtyBoxes
.push({'x': vp
.x
, 'y': vp
.y
,
266 'w': cr
.x1
- vp
.x
+ 1, 'h': vp
.h
});
269 // right side dirty region
270 dirtyBoxes
.push({'x': cr
.x2
+ 1, 'y': vp
.y
,
271 'w': vx2
- cr
.x2
, 'h': vp
.h
});
274 // top/middle dirty region
275 dirtyBoxes
.push({'x': cr
.x1
, 'y': vp
.y
,
276 'w': cr
.x2
- cr
.x1
+ 1, 'h': cr
.y1
- vp
.y
});
279 // bottom/middle dirty region
280 dirtyBoxes
.push({'x': cr
.x1
, 'y': cr
.y2
+ 1,
281 'w': cr
.x2
- cr
.x1
+ 1, 'h': vy2
- cr
.y2
});
285 this._cleanRect
= {'x1': vp
.x
, 'y1': vp
.y
,
286 'x2': vp
.x
+ vp
.w
- 1, 'y2': vp
.y
+ vp
.h
- 1};
288 return {'cleanBox': cleanBox
, 'dirtyBoxes': dirtyBoxes
};
292 return x
+ this._viewportLoc
.x
;
296 return y
+ this._viewportLoc
.y
;
299 resize: function (width
, height
) {
300 this._prevDrawStyle
= "";
302 this._fb_width
= width
;
303 this._fb_height
= height
;
305 this._rescale(this._scale
);
307 this.viewportChange();
312 this.resize(this._logo
.width
, this._logo
.height
);
313 this.blitStringImage(this._logo
.data
, 0, 0);
315 if (Util
.Engine
.trident
=== 6) {
316 // NB(directxman12): there's a bug in IE10 where we can fail to actually
317 // clear the canvas here because of the resize.
318 // Clearing the current viewport first fixes the issue
319 this._drawCtx
.clearRect(0, 0, this._viewportLoc
.w
, this._viewportLoc
.h
);
321 this.resize(240, 20);
322 this._drawCtx
.clearRect(0, 0, this._viewportLoc
.w
, this._viewportLoc
.h
);
328 fillRect: function (x
, y
, width
, height
, color
) {
329 this._setFillColor(color
);
330 this._drawCtx
.fillRect(x
- this._viewportLoc
.x
, y
- this._viewportLoc
.y
, width
, height
);
333 copyImage: function (old_x
, old_y
, new_x
, new_y
, w
, h
) {
334 var x1
= old_x
- this._viewportLoc
.x
;
335 var y1
= old_y
- this._viewportLoc
.y
;
336 var x2
= new_x
- this._viewportLoc
.x
;
337 var y2
= new_y
- this._viewportLoc
.y
;
339 this._drawCtx
.drawImage(this._target
, x1
, y1
, w
, h
, x2
, y2
, w
, h
);
342 // start updating a tile
343 startTile: function (x
, y
, width
, height
, color
) {
346 if (width
=== 16 && height
=== 16) {
347 this._tile
= this._tile16x16
;
349 this._tile
= this._drawCtx
.createImageData(width
, height
);
352 if (this._prefer_js
) {
354 if (this._true_color
) {
357 bgr
= this._colourMap
[color
[0]];
363 var data
= this._tile
.data
;
364 for (var i
= 0; i
< width
* height
* 4; i
+= 4) {
371 this.fillRect(x
, y
, width
, height
, color
);
375 // update sub-rectangle of the current tile
376 subTile: function (x
, y
, w
, h
, color
) {
377 if (this._prefer_js
) {
379 if (this._true_color
) {
382 bgr
= this._colourMap
[color
[0]];
390 var data
= this._tile
.data
;
391 var width
= this._tile
.width
;
392 for (var j
= y
; j
< yend
; j
++) {
393 for (var i
= x
; i
< xend
; i
++) {
394 var p
= (i
+ (j
* width
)) * 4;
402 this.fillRect(this._tile_x
+ x
, this._tile_y
+ y
, w
, h
, color
);
406 // draw the current tile to the screen
407 finishTile: function () {
408 if (this._prefer_js
) {
409 this._drawCtx
.putImageData(this._tile
, this._tile_x
- this._viewportLoc
.x
,
410 this._tile_y
- this._viewportLoc
.y
);
412 // else: No-op -- already done by setSubTile
415 blitImage: function (x
, y
, width
, height
, arr
, offset
) {
416 if (this._true_color
) {
417 this._bgrxImageData(x
, y
, this._viewportLoc
.x
, this._viewportLoc
.y
, width
, height
, arr
, offset
);
419 this._cmapImageData(x
, y
, this._viewportLoc
.x
, this._viewportLoc
.y
, width
, height
, arr
, offset
);
423 blitRgbImage: function (x
, y
, width
, height
, arr
, offset
) {
424 if (this._true_color
) {
425 this._rgbImageData(x
, y
, this._viewportLoc
.x
, this._viewportLoc
.y
, width
, height
, arr
, offset
);
428 this._cmapImageData(x
, y
, this._viewportLoc
.x
, this._viewportLoc
.y
, width
, height
, arr
, offset
);
432 blitStringImage: function (str
, x
, y
) {
433 var img
= new Image();
434 img
.onload = function () {
435 this._drawCtx
.drawImage(img
, x
- this._viewportLoc
.x
, y
- this._viewportLoc
.y
);
438 return img
; // for debugging purposes
441 // wrap ctx.drawImage but relative to viewport
442 drawImage: function (img
, x
, y
) {
443 this._drawCtx
.drawImage(img
, x
- this._viewportLoc
.x
, y
- this._viewportLoc
.y
);
446 renderQ_push: function (action
) {
447 this._renderQ
.push(action
);
448 if (this._renderQ
.length
=== 1) {
449 // If this can be rendered immediately it will be, otherwise
450 // the scanner will start polling the queue (every
451 // requestAnimationFrame interval)
452 this._scan_renderQ();
456 changeCursor: function (pixels
, mask
, hotx
, hoty
, w
, h
) {
457 if (this._cursor_uri
=== false) {
458 Util
.Warn("changeCursor called but no cursor data URI support");
462 if (this._true_color
) {
463 Display
.changeCursor(this._target
, pixels
, mask
, hotx
, hoty
, w
, h
);
465 Display
.changeCursor(this._target
, pixels
, mask
, hotx
, hoty
, w
, h
, this._colourMap
);
469 defaultCursor: function () {
470 this._target
.style
.cursor
= "default";
473 // Overridden getters/setters
474 get_context: function () {
475 return this._drawCtx
;
478 set_scale: function (scale
) {
479 this._rescale(scale
);
482 set_width: function (w
) {
483 this.resize(w
, this._fb_height
);
485 get_width: function () {
486 return this._fb_width
;
489 set_height: function (h
) {
490 this.resize(this._fb_width
, h
);
492 get_height: function () {
493 return this._fb_height
;
497 _rescale: function (factor
) {
498 var canvas
= this._target
;
499 var properties
= ['transform', 'WebkitTransform', 'MozTransform'];
501 while ((transform_prop
= properties
.shift())) {
502 if (typeof canvas
.style
[transform_prop
] !== 'undefined') {
507 if (transform_prop
=== null) {
508 Util
.Debug("No scaling support");
512 if (typeof(factor
) === "undefined") {
513 factor
= this._scale
;
514 } else if (factor
> 1.0) {
516 } else if (factor
< 0.1) {
520 if (this._scale
=== factor
) {
524 this._scale
= factor
;
525 var x
= canvas
.width
- (canvas
.width
* factor
);
526 var y
= canvas
.height
- (canvas
.height
* factor
);
527 canvas
.style
[transform_prop
] = 'scale(' + this._scale
+ ') translate(-' + x
+ 'px, -' + y
+ 'px)';
530 _setFillColor: function (color
) {
532 if (this._true_color
) {
535 bgr
= this._colourMap
[color
[0]];
538 var newStyle
= 'rgb(' + bgr
[2] + ',' + bgr
[1] + ',' + bgr
[0] + ')';
539 if (newStyle
!== this._prevDrawStyle
) {
540 this._drawCtx
.fillStyle
= newStyle
;
541 this._prevDrawStyle
= newStyle
;
545 _rgbImageData: function (x
, y
, vx
, vy
, width
, height
, arr
, offset
) {
546 var img
= this._drawCtx
.createImageData(width
, height
);
548 for (var i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 3) {
550 data
[i
+ 1] = arr
[j
+ 1];
551 data
[i
+ 2] = arr
[j
+ 2];
552 data
[i
+ 3] = 255; // Alpha
554 this._drawCtx
.putImageData(img
, x
- vx
, y
- vy
);
557 _bgrxImageData: function (x
, y
, vx
, vy
, width
, height
, arr
, offset
) {
558 var img
= this._drawCtx
.createImageData(width
, height
);
560 for (var i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 4) {
561 data
[i
] = arr
[j
+ 2];
562 data
[i
+ 1] = arr
[j
+ 1];
563 data
[i
+ 2] = arr
[j
];
564 data
[i
+ 3] = 255; // Alpha
566 this._drawCtx
.putImageData(img
, x
- vx
, y
- vy
);
569 _cmapImageData: function (x
, y
, vx
, vy
, width
, height
, arr
, offset
) {
570 var img
= this._drawCtx
.createImageData(width
, height
);
572 var cmap
= this._colourMap
;
573 for (var i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
++) {
574 var bgr
= cmap
[arr
[j
]];
576 data
[i
+ 1] = bgr
[1];
577 data
[i
+ 2] = bgr
[0];
578 data
[i
+ 3] = 255; // Alpha
580 this._drawCtx
.putImageData(img
, x
- vx
, y
- vy
);
583 _scan_renderQ: function () {
585 while (ready
&& this._renderQ
.length
> 0) {
586 var a
= this._renderQ
[0];
589 this.copyImage(a
.old_x
, a
.old_y
, a
.x
, a
.y
, a
.width
, a
.height
);
592 this.fillRect(a
.x
, a
.y
, a
.width
, a
.height
, a
.color
);
595 this.blitImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0);
598 this.blitRgbImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0);
601 if (a
.img
.complete
) {
602 this.drawImage(a
.img
, a
.x
, a
.y
);
604 // We need to wait for this image to 'load'
605 // to keep things in-order
612 this._renderQ
.shift();
616 if (this._renderQ
.length
> 0) {
617 requestAnimFrame(this._scan_renderQ
.bind(this));
622 Util
.make_properties(Display
, [
623 ['target', 'wo', 'dom'], // Canvas element for rendering
624 ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only)
625 ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "data": data}
626 ['true_color', 'rw', 'bool'], // Use true-color pixel data
627 ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color)
628 ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0
629 ['viewport', 'rw', 'bool'], // Use a viewport set with viewportChange()
630 ['width', 'rw', 'int'], // Display area width
631 ['height', 'rw', 'int'], // Display area height
633 ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only)
635 ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods
636 ['cursor_uri', 'rw', 'raw'] // Can we render cursor using data URI
640 Display
.changeCursor = function (target
, pixels
, mask
, hotx
, hoty
, w0
, h0
, cmap
) {
644 h
= w
; // increase h to make it square
646 w
= h
; // increase w to make it square
651 // Push multi-byte little-endian values
652 cur
.push16le = function (num
) {
653 this.push(num
& 0xFF, (num
>> 8) & 0xFF);
655 cur
.push32le = function (num
) {
656 this.push(num
& 0xFF,
663 var RGBsz
= w
* h
* 4;
664 var XORsz
= Math
.ceil((w
* h
) / 8.0);
665 var ANDsz
= Math
.ceil((w
* h
) / 8.0);
667 cur
.push16le(0); // 0: Reserved
668 cur
.push16le(2); // 2: .CUR type
669 cur
.push16le(1); // 4: Number of images, 1 for non-animated ico
671 // Cursor #1 header (ICONDIRENTRY)
672 cur
.push(w
); // 6: width
673 cur
.push(h
); // 7: height
674 cur
.push(0); // 8: colors, 0 -> true-color
675 cur
.push(0); // 9: reserved
676 cur
.push16le(hotx
); // 10: hotspot x coordinate
677 cur
.push16le(hoty
); // 12: hotspot y coordinate
678 cur
.push32le(IHDRsz
+ RGBsz
+ XORsz
+ ANDsz
);
679 // 14: cursor data byte size
680 cur
.push32le(22); // 18: offset of cursor data in the file
682 // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
683 cur
.push32le(IHDRsz
); // 22: InfoHeader size
684 cur
.push32le(w
); // 26: Cursor width
685 cur
.push32le(h
* 2); // 30: XOR+AND height
686 cur
.push16le(1); // 34: number of planes
687 cur
.push16le(32); // 36: bits per pixel
688 cur
.push32le(0); // 38: Type of compression
690 cur
.push32le(XORsz
+ ANDsz
);
692 cur
.push32le(0); // 46: reserved
693 cur
.push32le(0); // 50: reserved
694 cur
.push32le(0); // 54: reserved
695 cur
.push32le(0); // 58: reserved
697 // 62: color data (RGBQUAD icColors[])
699 for (y
= h
- 1; y
>= 0; y
--) {
700 for (x
= 0; x
< w
; x
++) {
701 if (x
>= w0
|| y
>= h0
) {
703 cur
.push(0); // green
705 cur
.push(0); // alpha
707 var idx
= y
* Math
.ceil(w0
/ 8) + Math
.floor(x
/ 8);
708 var alpha
= (mask
[idx
] << (x
% 8)) & 0x80 ? 255 : 0;
711 var rgb
= cmap
[pixels
[idx
]];
712 cur
.push(rgb
[2]); // blue
713 cur
.push(rgb
[1]); // green
714 cur
.push(rgb
[0]); // red
715 cur
.push(alpha
); // alpha
717 idx
= ((w0
* y
) + x
) * 4;
718 cur
.push(pixels
[idx
+ 2]); // blue
719 cur
.push(pixels
[idx
+ 1]); // green
720 cur
.push(pixels
[idx
]); // red
721 cur
.push(alpha
); // alpha
727 // XOR/bitmask data (BYTE icXOR[])
728 // (ignored, just needs to be the right size)
729 for (y
= 0; y
< h
; y
++) {
730 for (x
= 0; x
< Math
.ceil(w
/ 8); x
++) {
735 // AND/bitmask data (BYTE icAND[])
736 // (ignored, just needs to be the right size)
737 for (y
= 0; y
< h
; y
++) {
738 for (x
= 0; x
< Math
.ceil(w
/ 8); x
++) {
743 var url
= 'data:image/x-icon;base64,' + Base64
.encode(cur
);
744 target
.style
.cursor
= 'url(' + url
+ ')' + hotx
+ ' ' + hoty
+ ', default';