]>
git.proxmox.com Git - mirror_novnc.git/blob - include/display.js
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2011 Joel Martin
4 * Licensed under LGPL-3 (see LICENSE.txt)
6 * See README.md for usage and integration instructions.
9 /*jslint browser: true, white: false, bitwise: false */
10 /*global Util, Base64, changeCursor */
12 function Display(defaults
) {
15 var that
= {}, // Public API methods
16 conf
= {}, // Configuration attributes
18 // Private Display namespace variables
20 c_forceCanvas
= false,
22 c_imageData
, c_rgbxImage
, c_cmapImage
,
24 // Predefine function variables (jslint)
25 imageDataCreate
, imageDataGet
, rgbxImageData
, cmapImageData
,
26 rgbxImageFill
, cmapImageFill
, setFillColor
, rescale
, flush
,
28 // The full frame buffer (logical canvas) size
31 // The visible "physical canvas" viewport
32 viewport
= {'x': 0, 'y': 0, 'w' : 0, 'h' : 0 },
33 cleanRect
= {'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1},
40 // Configuration attributes
41 Util
.conf_defaults(conf
, that
, defaults
, [
42 ['target', 'wo', 'dom', null, 'Canvas element for rendering'],
43 ['context', 'ro', 'raw', null, 'Canvas 2D context for rendering (read-only)'],
44 ['logo', 'rw', 'raw', null, 'Logo to display when cleared: {"width": width, "height": height, "data": data}'],
45 ['true_color', 'rw', 'bool', true, 'Use true-color pixel data'],
46 ['colourMap', 'rw', 'arr', [], 'Colour map array (when not true-color)'],
47 ['scale', 'rw', 'float', 1.0, 'Display area scale factor 0.0 - 1.0'],
48 ['width', 'rw', 'int', null, 'Display area width'],
49 ['height', 'rw', 'int', null, 'Display area height'],
51 ['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'],
53 ['prefer_js', 'rw', 'str', null, 'Prefer Javascript over canvas methods'],
54 ['cursor_uri', 'rw', 'raw', null, 'Can we render cursor using data URI']
57 // Override some specific getters/setters
58 that
.get_context = function () { return c_ctx
; };
60 that
.set_scale = function(scale
) { rescale(scale
); };
62 that
.set_width = function (val
) { that
.resize(val
, fb_height
); };
63 that
.get_width = function() { return fb_width
; };
65 that
.set_height = function (val
) { that
.resize(fb_width
, val
); };
66 that
.get_height = function() { return fb_height
; };
68 that
.set_prefer_js = function(val
) {
69 if (val
&& c_forceCanvas
) {
70 Util
.Warn("Preferring Javascript to Canvas ops is not supported");
83 // Create the public API interface
84 function constructor() {
85 Util
.Debug(">> Display.constructor");
87 var c
, func
, imgTest
, tval
, i
, curDat
, curSave
,
88 has_imageData
= false, UE
= Util
.Engine
;
90 if (! conf
.target
) { throw("target must be set"); }
92 if (typeof conf
.target
=== 'string') {
93 throw("target must be a DOM element");
98 if (! c
.getContext
) { throw("no getContext method"); }
100 if (! c_ctx
) { c_ctx
= c
.getContext('2d'); }
102 Util
.Debug("User Agent: " + navigator
.userAgent
);
103 if (UE
.gecko
) { Util
.Debug("Browser: gecko " + UE
.gecko
); }
104 if (UE
.webkit
) { Util
.Debug("Browser: webkit " + UE
.webkit
); }
105 if (UE
.trident
) { Util
.Debug("Browser: trident " + UE
.trident
); }
106 if (UE
.presto
) { Util
.Debug("Browser: presto " + UE
.presto
); }
111 * Determine browser Canvas feature support
112 * and select fastest rendering methods
116 imgTest
= c_ctx
.getImageData(0, 0, 1,1);
117 imgTest
.data
[0] = 123;
118 imgTest
.data
[3] = 255;
119 c_ctx
.putImageData(imgTest
, 0, 0);
120 tval
= c_ctx
.getImageData(0, 0, 1, 1).data
[0];
122 has_imageData
= true;
127 Util
.Info("Canvas supports imageData");
128 c_forceCanvas
= false;
129 if (c_ctx
.createImageData
) {
130 // If it's there, it's faster
131 Util
.Info("Using Canvas createImageData");
132 conf
.render_mode
= "createImageData rendering";
133 c_imageData
= imageDataCreate
;
134 } else if (c_ctx
.getImageData
) {
135 // I think this is mostly just Opera
136 Util
.Info("Using Canvas getImageData");
137 conf
.render_mode
= "getImageData rendering";
138 c_imageData
= imageDataGet
;
140 Util
.Info("Prefering javascript operations");
141 if (conf
.prefer_js
=== null) {
142 conf
.prefer_js
= true;
144 c_rgbxImage
= rgbxImageData
;
145 c_cmapImage
= cmapImageData
;
147 Util
.Warn("Canvas lacks imageData, using fillRect (slow)");
148 conf
.render_mode
= "fillRect rendering (slow)";
149 c_forceCanvas
= true;
150 conf
.prefer_js
= false;
151 c_rgbxImage
= rgbxImageFill
;
152 c_cmapImage
= cmapImageFill
;
155 if (UE
.webkit
&& UE
.webkit
>= 534.7 && UE
.webkit
<= 534.9) {
156 // Workaround WebKit canvas rendering bug #46319
157 conf
.render_mode
+= ", webkit bug workaround";
158 Util
.Debug("Working around WebKit bug #46319");
160 for (func
in {"fillRect":1, "copyImage":1, "rgbxImage":1,
161 "cmapImage":1, "blitStringImage":1}) {
162 that
[func
] = (function() {
163 var myfunc
= that
[func
]; // Save original function
164 //Util.Debug("Wrapping " + func);
166 myfunc
.apply(this, arguments
);
167 if (!c_flush_timer
) {
168 c_flush_timer
= setTimeout(flush
, 100);
176 * Determine browser support for setting the cursor via data URI
180 for (i
=0; i
< 8 * 8 * 4; i
+= 1) {
184 curSave
= c
.style
.cursor
;
185 changeCursor(conf
.target
, curDat
, curDat
, 2, 2, 8, 8);
186 if (c
.style
.cursor
) {
187 if (conf
.cursor_uri
=== null) {
188 conf
.cursor_uri
= true;
190 Util
.Info("Data URI scheme cursor supported");
192 if (conf
.cursor_uri
=== null) {
193 conf
.cursor_uri
= false;
195 Util
.Warn("Data URI scheme cursor not supported");
197 c
.style
.cursor
= curSave
;
199 Util
.Error("Data URI scheme cursor test exception: " + exc2
);
200 conf
.cursor_uri
= false;
203 Util
.Debug("<< Display.constructor");
207 rescale = function(factor
) {
209 properties
= ['transform', 'WebkitTransform', 'MozTransform', null];
211 tp
= properties
.shift();
213 if (typeof c
.style
[tp
] !== 'undefined') {
216 tp
= properties
.shift();
220 Util
.Debug("No scaling support");
225 if (typeof(factor
) === "undefined") {
227 } else if (factor
> 1.0) {
229 } else if (factor
< 0.1) {
233 if (conf
.scale
=== factor
) {
234 //Util.Debug("Display already scaled to '" + factor + "'");
239 x
= c
.width
- c
.width
* factor
;
240 y
= c
.height
- c
.height
* factor
;
241 c
.style
[tp
] = "scale(" + conf
.scale
+ ") translate(-" + x
+ "px, -" + y
+ "px)";
244 that
.viewportChange = function(deltaX
, deltaY
, width
, height
) {
245 var c
= conf
.target
, v
= viewport
, cr
= cleanRect
,
246 saveImg
= null, saveStyle
, x1
, y1
, vx2
, vy2
, w
, h
;
248 if (typeof(deltaX
) === "undefined") { deltaX
= 0; }
249 if (typeof(deltaY
) === "undefined") { deltaY
= 0; }
250 if (typeof(width
) === "undefined") { width
= v
.w
; }
251 if (typeof(height
) === "undefined") { height
= v
.h
; }
255 if (width
> fb_width
) { width
= fb_width
; }
256 if (height
> fb_height
) { height
= fb_height
; }
258 if ((v
.w
!== width
) || (v
.h
!== height
)) {
260 if ((width
< v
.w
) && (cr
.x2
> v
.x
+ width
-1)) {
261 cr
.x2
= v
.x
+ width
- 1;
266 if ((height
< v
.h
) && (cr
.y2
> v
.y
+ height
-1)) {
267 cr
.y2
= v
.y
+ height
- 1;
272 if (v
.w
> 0 && v
.h
> 0) {
273 saveImg
= c_ctx
.getImageData(0, 0,
274 (c
.width
< v
.w
) ? c
.width
: v
.w
,
275 (c
.height
< v
.h
) ? c
.height
: v
.h
);
282 c_ctx
.putImageData(saveImg
, 0, 0);
292 if ((deltaX
< 0) && ((v
.x
+ deltaX
) < 0)) {
295 if ((vx2
+ deltaX
) >= fb_width
) {
296 deltaX
-= ((vx2
+ deltaX
) - fb_width
+ 1);
299 if ((v
.y
+ deltaY
) < 0) {
302 if ((vy2
+ deltaY
) >= fb_height
) {
303 deltaY
-= ((vy2
+ deltaY
) - fb_height
+ 1);
306 if ((deltaX
=== 0) && (deltaY
=== 0)) {
307 //message("skipping");
310 message("deltaX: " + deltaX
+ ", deltaY: " + deltaY
);
317 // Update the clean rectangle
332 // Shift viewport left, redraw left section
336 // Shift viewport right, redraw right section
341 // Shift viewport up, redraw top section
345 // Shift viewport down, redraw bottom section
350 // Copy the valid part of the viewport to the shifted location
351 saveStyle
= c_ctx
.fillStyle
;
352 c_ctx
.fillStyle
= "rgb(255,255,255)";
354 //that.copyImage(0, 0, -deltaX, 0, v.w, v.h);
355 //that.fillRect(x1, 0, w, v.h, [255,255,255]);
356 c_ctx
.drawImage(c
, 0, 0, v
.w
, v
.h
, -deltaX
, 0, v
.w
, v
.h
);
357 c_ctx
.fillRect(x1
, 0, w
, v
.h
);
360 //that.copyImage(0, 0, 0, -deltaY, v.w, v.h);
361 //that.fillRect(0, y1, v.w, h, [255,255,255]);
362 c_ctx
.drawImage(c
, 0, 0, v
.w
, v
.h
, 0, -deltaY
, v
.w
, v
.h
);
363 c_ctx
.fillRect(0, y1
, v
.w
, h
);
365 c_ctx
.fillStyle
= saveStyle
;
368 that
.getCleanDirtyReset = function() {
369 var v
= viewport
, c
= cleanRect
, cleanBox
, dirtyBoxes
= [],
370 vx2
= v
.x
+ v
.w
- 1, vy2
= v
.y
+ v
.h
- 1;
373 // Copy the cleanRect
374 cleanBox
= {'x': c
.x1
, 'y': c
.y1
,
375 'w': c
.x2
- c
.x1
+ 1, 'h': c
.y2
- c
.y1
+ 1};
377 if ((c
.x1
>= c
.x2
) || (c
.y1
>= c
.y2
)) {
378 // Whole viewport is dirty
379 dirtyBoxes
.push({'x': v
.x
, 'y': v
.y
, 'w': v
.w
, 'h': v
.h
});
381 // Redraw dirty regions
383 // left side dirty region
384 dirtyBoxes
.push({'x': v
.x
, 'y': v
.y
,
385 'w': c
.x1
- v
.x
+ 1, 'h': v
.h
});
388 // right side dirty region
389 dirtyBoxes
.push({'x': c
.x2
+ 1, 'y': v
.y
,
390 'w': vx2
- c
.x2
, 'h': v
.h
});
393 // top/middle dirty region
394 dirtyBoxes
.push({'x': c
.x1
, 'y': v
.y
,
395 'w': c
.x2
- c
.x1
+ 1, 'h': c
.y1
- v
.y
});
398 // bottom/middle dirty region
399 dirtyBoxes
.push({'x': c
.x1
, 'y': c
.y2
+ 1,
400 'w': c
.x2
- c
.x1
+ 1, 'h': vy2
- c
.y2
});
404 // Reset the cleanRect to the whole viewport
405 cleanRect
= {'x1': v
.x
, 'y1': v
.y
,
406 'x2': v
.x
+ v
.w
- 1, 'y2': v
.y
+ v
.h
- 1};
408 return {'cleanBox': cleanBox
, 'dirtyBoxes': dirtyBoxes
};
412 // Force canvas redraw (for webkit bug #46319 workaround)
415 //Util.Debug(">> flush");
416 old_val
= conf
.target
.style
.marginRight
;
417 conf
.target
.style
.marginRight
= "1px";
418 c_flush_timer
= null;
419 setTimeout(function () {
420 conf
.target
.style
.marginRight
= old_val
;
424 setFillColor = function(color
) {
426 if (conf
.true_color
) {
429 rgb
= conf
.colourMap
[color
[0]];
431 newStyle
= "rgb(" + rgb
[0] + "," + rgb
[1] + "," + rgb
[2] + ")";
432 if (newStyle
!== c_prevStyle
) {
433 c_ctx
.fillStyle
= newStyle
;
434 c_prevStyle
= newStyle
;
440 // Public API interface functions
443 that
.resize = function(width
, height
) {
450 that
.viewportChange();
453 that
.clear = function() {
456 that
.resize(conf
.logo
.width
, conf
.logo
.height
);
457 that
.viewportChange(0, 0, conf
.logo
.width
, conf
.logo
.height
);
458 that
.blitStringImage(conf
.logo
.data
, 0, 0);
460 that
.resize(640, 20);
461 that
.viewportChange(0, 0, 640, 20);
462 c_ctx
.clearRect(0, 0, viewport
.w
, viewport
.h
);
465 // No benefit over default ("source-over") in Chrome and firefox
466 //c_ctx.globalCompositeOperation = "copy";
469 that
.fillRect = function(x
, y
, width
, height
, color
) {
471 c_ctx
.fillRect(x
- viewport
.x
, y
- viewport
.y
, width
, height
);
474 that
.copyImage = function(old_x
, old_y
, new_x
, new_y
, w
, h
) {
475 var x1
= old_x
- viewport
.x
, y1
= old_y
- viewport
.y
,
476 x2
= new_x
- viewport
.x
, y2
= new_y
- viewport
.y
;
477 c_ctx
.drawImage(conf
.target
, x1
, y1
, w
, h
, x2
, y2
, w
, h
);
481 * Tile rendering functions optimized for rendering engines.
483 * - In Chrome/webkit, Javascript image data array manipulations are
484 * faster than direct Canvas fillStyle, fillRect rendering. In
485 * gecko, Javascript array handling is much slower.
487 that
.getTile = function(x
, y
, width
, height
, color
) {
488 var img
, data
= [], rgb
, red
, green
, blue
, i
;
489 img
= {'x': x
, 'y': y
, 'width': width
, 'height': height
,
491 if (conf
.prefer_js
) {
492 if (conf
.true_color
) {
495 rgb
= conf
.colourMap
[color
[0]];
500 for (i
= 0; i
< (width
* height
* 4); i
+=4) {
506 that
.fillRect(x
, y
, width
, height
, color
);
511 that
.setSubTile = function(img
, x
, y
, w
, h
, color
) {
512 var data
, p
, rgb
, red
, green
, blue
, width
, j
, i
, xend
, yend
;
513 if (conf
.prefer_js
) {
516 if (conf
.true_color
) {
519 rgb
= conf
.colourMap
[color
[0]];
526 for (j
= y
; j
< yend
; j
+= 1) {
527 for (i
= x
; i
< xend
; i
+= 1) {
528 p
= (i
+ (j
* width
) ) * 4;
535 that
.fillRect(img
.x
+ x
, img
.y
+ y
, w
, h
, color
);
539 that
.putTile = function(img
) {
540 if (conf
.prefer_js
) {
541 c_rgbxImage(img
.x
, img
.y
, img
.width
, img
.height
, img
.data
, 0);
543 // else: No-op, under gecko already done by setSubTile
546 imageDataGet = function(width
, height
) {
547 return c_ctx
.getImageData(0, 0, width
, height
);
549 imageDataCreate = function(width
, height
) {
550 return c_ctx
.createImageData(width
, height
);
553 rgbxImageData = function(x
, y
, width
, height
, arr
, offset
) {
555 img
= c_imageData(width
, height
);
557 for (i
=0, j
=offset
; i
< (width
* height
* 4); i
=i
+4, j
=j
+4) {
559 data
[i
+ 1] = arr
[j
+ 1];
560 data
[i
+ 2] = arr
[j
+ 2];
561 data
[i
+ 3] = 255; // Set Alpha
563 c_ctx
.putImageData(img
, x
- viewport
.x
, y
- viewport
.y
);
566 // really slow fallback if we don't have imageData
567 rgbxImageFill = function(x
, y
, width
, height
, arr
, offset
) {
568 var i
, j
, sx
= 0, sy
= 0;
569 for (i
=0, j
=offset
; i
< (width
* height
); i
+=1, j
+=4) {
570 that
.fillRect(x
+sx
, y
+sy
, 1, 1, [arr
[j
], arr
[j
+1], arr
[j
+2]]);
572 if ((sx
% width
) === 0) {
579 cmapImageData = function(x
, y
, width
, height
, arr
, offset
) {
580 var img
, i
, j
, data
, rgb
, cmap
;
581 img
= c_imageData(width
, height
);
583 cmap
= conf
.colourMap
;
584 for (i
=0, j
=offset
; i
< (width
* height
* 4); i
+=4, j
+=1) {
587 data
[i
+ 1] = rgb
[1];
588 data
[i
+ 2] = rgb
[2];
589 data
[i
+ 3] = 255; // Set Alpha
591 c_ctx
.putImageData(img
, x
- viewport
.x
, y
- viewport
.y
);
594 cmapImageFill = function(x
, y
, width
, height
, arr
, offset
) {
595 var i
, j
, sx
= 0, sy
= 0, cmap
;
596 cmap
= conf
.colourMap
;
597 for (i
=0, j
=offset
; i
< (width
* height
); i
+=1, j
+=1) {
598 that
.fillRect(x
+sx
, y
+sy
, 1, 1, [arr
[j
]]);
600 if ((sx
% width
) === 0) {
608 that
.blitImage = function(x
, y
, width
, height
, arr
, offset
) {
609 if (conf
.true_color
) {
610 c_rgbxImage(x
, y
, width
, height
, arr
, offset
);
612 c_cmapImage(x
, y
, width
, height
, arr
, offset
);
616 that
.blitStringImage = function(str
, x
, y
) {
617 var img
= new Image();
618 img
.onload = function () {
619 c_ctx
.drawImage(img
, x
- viewport
.x
, y
- viewport
.y
);
624 that
.changeCursor = function(pixels
, mask
, hotx
, hoty
, w
, h
) {
625 if (conf
.cursor_uri
=== false) {
626 Util
.Warn("changeCursor called but no cursor data URI support");
630 if (conf
.true_color
) {
631 changeCursor(conf
.target
, pixels
, mask
, hotx
, hoty
, w
, h
);
633 changeCursor(conf
.target
, pixels
, mask
, hotx
, hoty
, w
, h
, conf
.colourMap
);
637 that
.defaultCursor = function() {
638 conf
.target
.style
.cursor
= "default";
641 return constructor(); // Return the public API interface
643 } // End of Display()
646 /* Set CSS cursor property using data URI encoded cursor file */
647 function changeCursor(target
, pixels
, mask
, hotx
, hoty
, w
, h
, cmap
) {
649 var cur
= [], rgb
, IHDRsz
, RGBsz
, ANDsz
, XORsz
, url
, idx
, alpha
, x
, y
;
650 //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h);
652 // Push multi-byte little-endian values
653 cur
.push16le = function (num
) {
654 this.push((num
) & 0xFF,
657 cur
.push32le = function (num
) {
658 this.push((num
) & 0xFF,
661 (num
>> 24) & 0xFF );
666 XORsz
= Math
.ceil( (w
* h
) / 8.0 );
667 ANDsz
= Math
.ceil( (w
* h
) / 8.0 );
670 cur
.push16le(0); // 0: Reserved
671 cur
.push16le(2); // 2: .CUR type
672 cur
.push16le(1); // 4: Number of images, 1 for non-animated ico
674 // Cursor #1 header (ICONDIRENTRY)
675 cur
.push(w
); // 6: width
676 cur
.push(h
); // 7: height
677 cur
.push(0); // 8: colors, 0 -> true-color
678 cur
.push(0); // 9: reserved
679 cur
.push16le(hotx
); // 10: hotspot x coordinate
680 cur
.push16le(hoty
); // 12: hotspot y coordinate
681 cur
.push32le(IHDRsz
+ RGBsz
+ XORsz
+ ANDsz
);
682 // 14: cursor data byte size
683 cur
.push32le(22); // 18: offset of cursor data in the file
686 // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
687 cur
.push32le(IHDRsz
); // 22: Infoheader size
688 cur
.push32le(w
); // 26: Cursor width
689 cur
.push32le(h
*2); // 30: XOR+AND height
690 cur
.push16le(1); // 34: number of planes
691 cur
.push16le(32); // 36: bits per pixel
692 cur
.push32le(0); // 38: Type of compression
694 cur
.push32le(XORsz
+ ANDsz
); // 43: Size of Image
695 // Gimp leaves this as 0
697 cur
.push32le(0); // 46: reserved
698 cur
.push32le(0); // 50: reserved
699 cur
.push32le(0); // 54: reserved
700 cur
.push32le(0); // 58: reserved
702 // 62: color data (RGBQUAD icColors[])
703 for (y
= h
-1; y
>= 0; y
-= 1) {
704 for (x
= 0; x
< w
; x
+= 1) {
705 idx
= y
* Math
.ceil(w
/ 8) + Math
.floor(x
/8);
706 alpha
= (mask
[idx
] << (x
% 8)) & 0x80 ? 255 : 0;
710 rgb
= cmap
[pixels
[idx
]];
711 cur
.push(rgb
[2]); // blue
712 cur
.push(rgb
[1]); // green
713 cur
.push(rgb
[0]); // red
714 cur
.push(alpha
); // alpha
716 idx
= ((w
* y
) + x
) * 4;
717 cur
.push(pixels
[idx
+ 2]); // blue
718 cur
.push(pixels
[idx
+ 1]); // green
719 cur
.push(pixels
[idx
]); // red
720 cur
.push(alpha
); // alpha
725 // XOR/bitmask data (BYTE icXOR[])
726 // (ignored, just needs to be right size)
727 for (y
= 0; y
< h
; y
+= 1) {
728 for (x
= 0; x
< Math
.ceil(w
/ 8); x
+= 1) {
733 // AND/bitmask data (BYTE icAND[])
734 // (ignored, just needs to be right size)
735 for (y
= 0; y
< h
; y
+= 1) {
736 for (x
= 0; x
< Math
.ceil(w
/ 8); x
+= 1) {
741 url
= "data:image/x-icon;base64," + Base64
.encode(cur
);
742 target
.style
.cursor
= "url(" + url
+ ") " + hotx
+ " " + hoty
+ ", default";
743 //Util.Debug("<< changeCursor, cur.length: " + cur.length);