]>
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, 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 // Queued drawing actions for in-order rendering
25 // Predefine function variables (jslint)
26 imageDataGet
, rgbImageData
, bgrxImageData
, cmapImageData
,
27 setFillColor
, rescale
, scan_renderQ
,
29 // The full frame buffer (logical canvas) size
32 // The visible "physical canvas" viewport
33 viewport
= {'x': 0, 'y': 0, 'w' : 0, 'h' : 0 },
34 cleanRect
= {'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1},
43 // Configuration attributes
44 Util
.conf_defaults(conf
, that
, defaults
, [
45 ['target', 'wo', 'dom', null, 'Canvas element for rendering'],
46 ['context', 'ro', 'raw', null, 'Canvas 2D context for rendering (read-only)'],
47 ['logo', 'rw', 'raw', null, 'Logo to display when cleared: {"width": width, "height": height, "data": data}'],
48 ['true_color', 'rw', 'bool', true, 'Use true-color pixel data'],
49 ['colourMap', 'rw', 'arr', [], 'Colour map array (when not true-color)'],
50 ['scale', 'rw', 'float', 1.0, 'Display area scale factor 0.0 - 1.0'],
51 ['viewport', 'rw', 'bool', false, 'Use a viewport set with viewportChange()'],
52 ['width', 'rw', 'int', null, 'Display area width'],
53 ['height', 'rw', 'int', null, 'Display area height'],
55 ['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'],
57 ['prefer_js', 'rw', 'str', null, 'Prefer Javascript over canvas methods'],
58 ['cursor_uri', 'rw', 'raw', null, 'Can we render cursor using data URI']
61 // Override some specific getters/setters
62 that
.get_context = function () { return c_ctx
; };
64 that
.set_scale = function(scale
) { rescale(scale
); };
66 that
.set_width = function (val
) { that
.resize(val
, fb_height
); };
67 that
.get_width = function() { return fb_width
; };
69 that
.set_height = function (val
) { that
.resize(fb_width
, val
); };
70 that
.get_height = function() { return fb_height
; };
78 // Create the public API interface
79 function constructor() {
80 Util
.Debug(">> Display.constructor");
82 var c
, func
, i
, curDat
, curSave
,
83 has_imageData
= false, UE
= Util
.Engine
;
85 if (! conf
.target
) { throw("target must be set"); }
87 if (typeof conf
.target
=== 'string') {
88 throw("target must be a DOM element");
93 if (! c
.getContext
) { throw("no getContext method"); }
95 if (! c_ctx
) { c_ctx
= c
.getContext('2d'); }
97 Util
.Debug("User Agent: " + navigator
.userAgent
);
98 if (UE
.gecko
) { Util
.Debug("Browser: gecko " + UE
.gecko
); }
99 if (UE
.webkit
) { Util
.Debug("Browser: webkit " + UE
.webkit
); }
100 if (UE
.trident
) { Util
.Debug("Browser: trident " + UE
.trident
); }
101 if (UE
.presto
) { Util
.Debug("Browser: presto " + UE
.presto
); }
105 // Check canvas features
106 if ('createImageData' in c_ctx
) {
107 conf
.render_mode
= "canvas rendering";
109 throw("Canvas does not support createImageData");
111 if (conf
.prefer_js
=== null) {
112 Util
.Info("Prefering javascript operations");
113 conf
.prefer_js
= true;
116 // Initialize cached tile imageData
117 tile16x16
= c_ctx
.createImageData(16, 16);
120 * Determine browser support for setting the cursor via data URI
124 for (i
=0; i
< 8 * 8 * 4; i
+= 1) {
128 curSave
= c
.style
.cursor
;
129 changeCursor(conf
.target
, curDat
, curDat
, 2, 2, 8, 8);
130 if (c
.style
.cursor
) {
131 if (conf
.cursor_uri
=== null) {
132 conf
.cursor_uri
= true;
134 Util
.Info("Data URI scheme cursor supported");
136 if (conf
.cursor_uri
=== null) {
137 conf
.cursor_uri
= false;
139 Util
.Warn("Data URI scheme cursor not supported");
141 c
.style
.cursor
= curSave
;
143 Util
.Error("Data URI scheme cursor test exception: " + exc2
);
144 conf
.cursor_uri
= false;
147 Util
.Debug("<< Display.constructor");
151 rescale = function(factor
) {
153 properties
= ['transform', 'WebkitTransform', 'MozTransform', null];
155 tp
= properties
.shift();
157 if (typeof c
.style
[tp
] !== 'undefined') {
160 tp
= properties
.shift();
164 Util
.Debug("No scaling support");
169 if (typeof(factor
) === "undefined") {
171 } else if (factor
> 1.0) {
173 } else if (factor
< 0.1) {
177 if (conf
.scale
=== factor
) {
178 //Util.Debug("Display already scaled to '" + factor + "'");
183 x
= c
.width
- c
.width
* factor
;
184 y
= c
.height
- c
.height
* factor
;
185 c
.style
[tp
] = "scale(" + conf
.scale
+ ") translate(-" + x
+ "px, -" + y
+ "px)";
188 setFillColor = function(color
) {
190 if (conf
.true_color
) {
193 bgr
= conf
.colourMap
[color
[0]];
195 newStyle
= "rgb(" + bgr
[2] + "," + bgr
[1] + "," + bgr
[0] + ")";
196 if (newStyle
!== c_prevStyle
) {
197 c_ctx
.fillStyle
= newStyle
;
198 c_prevStyle
= newStyle
;
204 // Public API interface functions
207 // Shift and/or resize the visible viewport
208 that
.viewportChange = function(deltaX
, deltaY
, width
, height
) {
209 var c
= conf
.target
, v
= viewport
, cr
= cleanRect
,
210 saveImg
= null, saveStyle
, x1
, y1
, vx2
, vy2
, w
, h
;
212 if (!conf
.viewport
) {
213 Util
.Debug("Setting viewport to full display region");
214 deltaX
= -v
.w
; // Clamped later if out of bounds
215 deltaY
= -v
.h
; // Clamped later if out of bounds
220 if (typeof(deltaX
) === "undefined") { deltaX
= 0; }
221 if (typeof(deltaY
) === "undefined") { deltaY
= 0; }
222 if (typeof(width
) === "undefined") { width
= v
.w
; }
223 if (typeof(height
) === "undefined") { height
= v
.h
; }
227 if (width
> fb_width
) { width
= fb_width
; }
228 if (height
> fb_height
) { height
= fb_height
; }
230 if ((v
.w
!== width
) || (v
.h
!== height
)) {
232 if ((width
< v
.w
) && (cr
.x2
> v
.x
+ width
-1)) {
233 cr
.x2
= v
.x
+ width
- 1;
238 if ((height
< v
.h
) && (cr
.y2
> v
.y
+ height
-1)) {
239 cr
.y2
= v
.y
+ height
- 1;
244 if (v
.w
> 0 && v
.h
> 0 && c
.width
> 0 && c
.height
> 0) {
245 saveImg
= c_ctx
.getImageData(0, 0,
246 (c
.width
< v
.w
) ? c
.width
: v
.w
,
247 (c
.height
< v
.h
) ? c
.height
: v
.h
);
254 c_ctx
.putImageData(saveImg
, 0, 0);
264 if ((deltaX
< 0) && ((v
.x
+ deltaX
) < 0)) {
267 if ((vx2
+ deltaX
) >= fb_width
) {
268 deltaX
-= ((vx2
+ deltaX
) - fb_width
+ 1);
271 if ((v
.y
+ deltaY
) < 0) {
274 if ((vy2
+ deltaY
) >= fb_height
) {
275 deltaY
-= ((vy2
+ deltaY
) - fb_height
+ 1);
278 if ((deltaX
=== 0) && (deltaY
=== 0)) {
279 //Util.Debug("skipping viewport change");
282 Util
.Debug("viewportChange deltaX: " + deltaX
+ ", deltaY: " + deltaY
);
289 // Update the clean rectangle
304 // Shift viewport left, redraw left section
308 // Shift viewport right, redraw right section
313 // Shift viewport up, redraw top section
317 // Shift viewport down, redraw bottom section
322 // Copy the valid part of the viewport to the shifted location
323 saveStyle
= c_ctx
.fillStyle
;
324 c_ctx
.fillStyle
= "rgb(255,255,255)";
326 //that.copyImage(0, 0, -deltaX, 0, v.w, v.h);
327 //that.fillRect(x1, 0, w, v.h, [255,255,255]);
328 c_ctx
.drawImage(c
, 0, 0, v
.w
, v
.h
, -deltaX
, 0, v
.w
, v
.h
);
329 c_ctx
.fillRect(x1
, 0, w
, v
.h
);
332 //that.copyImage(0, 0, 0, -deltaY, v.w, v.h);
333 //that.fillRect(0, y1, v.w, h, [255,255,255]);
334 c_ctx
.drawImage(c
, 0, 0, v
.w
, v
.h
, 0, -deltaY
, v
.w
, v
.h
);
335 c_ctx
.fillRect(0, y1
, v
.w
, h
);
337 c_ctx
.fillStyle
= saveStyle
;
341 // Return a map of clean and dirty areas of the viewport and reset the
342 // tracking of clean and dirty areas.
344 // Returns: {'cleanBox': {'x': x, 'y': y, 'w': w, 'h': h},
345 // 'dirtyBoxes': [{'x': x, 'y': y, 'w': w, 'h': h}, ...]}
346 that
.getCleanDirtyReset = function() {
347 var v
= viewport
, c
= cleanRect
, cleanBox
, dirtyBoxes
= [],
348 vx2
= v
.x
+ v
.w
- 1, vy2
= v
.y
+ v
.h
- 1;
351 // Copy the cleanRect
352 cleanBox
= {'x': c
.x1
, 'y': c
.y1
,
353 'w': c
.x2
- c
.x1
+ 1, 'h': c
.y2
- c
.y1
+ 1};
355 if ((c
.x1
>= c
.x2
) || (c
.y1
>= c
.y2
)) {
356 // Whole viewport is dirty
357 dirtyBoxes
.push({'x': v
.x
, 'y': v
.y
, 'w': v
.w
, 'h': v
.h
});
359 // Redraw dirty regions
361 // left side dirty region
362 dirtyBoxes
.push({'x': v
.x
, 'y': v
.y
,
363 'w': c
.x1
- v
.x
+ 1, 'h': v
.h
});
366 // right side dirty region
367 dirtyBoxes
.push({'x': c
.x2
+ 1, 'y': v
.y
,
368 'w': vx2
- c
.x2
, 'h': v
.h
});
371 // top/middle dirty region
372 dirtyBoxes
.push({'x': c
.x1
, 'y': v
.y
,
373 'w': c
.x2
- c
.x1
+ 1, 'h': c
.y1
- v
.y
});
376 // bottom/middle dirty region
377 dirtyBoxes
.push({'x': c
.x1
, 'y': c
.y2
+ 1,
378 'w': c
.x2
- c
.x1
+ 1, 'h': vy2
- c
.y2
});
382 // Reset the cleanRect to the whole viewport
383 cleanRect
= {'x1': v
.x
, 'y1': v
.y
,
384 'x2': v
.x
+ v
.w
- 1, 'y2': v
.y
+ v
.h
- 1};
386 return {'cleanBox': cleanBox
, 'dirtyBoxes': dirtyBoxes
};
389 // Translate viewport coordinates to absolute coordinates
390 that
.absX = function(x
) {
391 return x
+ viewport
.x
;
393 that
.absY = function(y
) {
394 return y
+ viewport
.y
;
398 that
.resize = function(width
, height
) {
405 that
.viewportChange();
408 that
.clear = function() {
411 that
.resize(conf
.logo
.width
, conf
.logo
.height
);
412 that
.blitStringImage(conf
.logo
.data
, 0, 0);
414 that
.resize(640, 20);
415 c_ctx
.clearRect(0, 0, viewport
.w
, viewport
.h
);
420 // No benefit over default ("source-over") in Chrome and firefox
421 //c_ctx.globalCompositeOperation = "copy";
424 that
.fillRect = function(x
, y
, width
, height
, color
) {
426 c_ctx
.fillRect(x
- viewport
.x
, y
- viewport
.y
, width
, height
);
429 that
.copyImage = function(old_x
, old_y
, new_x
, new_y
, w
, h
) {
430 var x1
= old_x
- viewport
.x
, y1
= old_y
- viewport
.y
,
431 x2
= new_x
- viewport
.x
, y2
= new_y
- viewport
.y
;
432 c_ctx
.drawImage(conf
.target
, x1
, y1
, w
, h
, x2
, y2
, w
, h
);
436 // Start updating a tile
437 that
.startTile = function(x
, y
, width
, height
, color
) {
438 var data
, bgr
, red
, green
, blue
, i
;
441 if ((width
=== 16) && (height
=== 16)) {
444 tile
= c_ctx
.createImageData(width
, height
);
447 if (conf
.prefer_js
) {
448 if (conf
.true_color
) {
451 bgr
= conf
.colourMap
[color
[0]];
456 for (i
= 0; i
< (width
* height
* 4); i
+=4) {
463 that
.fillRect(x
, y
, width
, height
, color
);
467 // Update sub-rectangle of the current tile
468 that
.subTile = function(x
, y
, w
, h
, color
) {
469 var data
, p
, bgr
, red
, green
, blue
, width
, j
, i
, xend
, yend
;
470 if (conf
.prefer_js
) {
473 if (conf
.true_color
) {
476 bgr
= conf
.colourMap
[color
[0]];
483 for (j
= y
; j
< yend
; j
+= 1) {
484 for (i
= x
; i
< xend
; i
+= 1) {
485 p
= (i
+ (j
* width
) ) * 4;
493 that
.fillRect(tile_x
+ x
, tile_y
+ y
, w
, h
, color
);
497 // Draw the current tile to the screen
498 that
.finishTile = function() {
499 if (conf
.prefer_js
) {
500 c_ctx
.putImageData(tile
, tile_x
- viewport
.x
, tile_y
- viewport
.y
);
502 // else: No-op, if not prefer_js then already done by setSubTile
505 rgbImageData = function(x
, y
, vx
, vy
, width
, height
, arr
, offset
) {
508 if ((x - v.x >= v.w) || (y - v.y >= v.h) ||
509 (x - v.x + width < 0) || (y - v.y + height < 0)) {
510 // Skipping because outside of viewport
514 img
= c_ctx
.createImageData(width
, height
);
516 for (i
=0, j
=offset
; i
< (width
* height
* 4); i
=i
+4, j
=j
+3) {
518 data
[i
+ 1] = arr
[j
+ 1];
519 data
[i
+ 2] = arr
[j
+ 2];
520 data
[i
+ 3] = 255; // Set Alpha
522 c_ctx
.putImageData(img
, x
- vx
, y
- vy
);
525 bgrxImageData = function(x
, y
, vx
, vy
, width
, height
, arr
, offset
) {
528 if ((x - v.x >= v.w) || (y - v.y >= v.h) ||
529 (x - v.x + width < 0) || (y - v.y + height < 0)) {
530 // Skipping because outside of viewport
534 img
= c_ctx
.createImageData(width
, height
);
536 for (i
=0, j
=offset
; i
< (width
* height
* 4); i
=i
+4, j
=j
+4) {
537 data
[i
] = arr
[j
+ 2];
538 data
[i
+ 1] = arr
[j
+ 1];
539 data
[i
+ 2] = arr
[j
];
540 data
[i
+ 3] = 255; // Set Alpha
542 c_ctx
.putImageData(img
, x
- vx
, y
- vy
);
545 cmapImageData = function(x
, y
, vx
, vy
, width
, height
, arr
, offset
) {
546 var img
, i
, j
, data
, bgr
, cmap
;
547 img
= c_ctx
.createImageData(width
, height
);
549 cmap
= conf
.colourMap
;
550 for (i
=0, j
=offset
; i
< (width
* height
* 4); i
+=4, j
+=1) {
553 data
[i
+ 1] = bgr
[1];
554 data
[i
+ 2] = bgr
[0];
555 data
[i
+ 3] = 255; // Set Alpha
557 c_ctx
.putImageData(img
, x
- vx
, y
- vy
);
560 that
.blitImage = function(x
, y
, width
, height
, arr
, offset
) {
561 if (conf
.true_color
) {
562 bgrxImageData(x
, y
, viewport
.x
, viewport
.y
, width
, height
, arr
, offset
);
564 cmapImageData(x
, y
, viewport
.x
, viewport
.y
, width
, height
, arr
, offset
);
568 that
.blitRgbImage = function(x
, y
, width
, height
, arr
, offset
) {
569 if (conf
.true_color
) {
570 rgbImageData(x
, y
, viewport
.x
, viewport
.y
, width
, height
, arr
, offset
);
573 cmapImageData(x
, y
, viewport
.x
, viewport
.y
, width
, height
, arr
, offset
);
577 that
.blitStringImage = function(str
, x
, y
) {
578 var img
= new Image();
579 img
.onload = function () {
580 c_ctx
.drawImage(img
, x
- viewport
.x
, y
- viewport
.y
);
585 // Wrap ctx.drawImage but relative to viewport
586 that
.drawImage = function(img
, x
, y
) {
587 c_ctx
.drawImage(img
, x
- viewport
.x
, y
- viewport
.y
);
590 that
.renderQ_push = function(action
) {
591 renderQ
.push(action
);
592 if (renderQ
.length
=== 1) {
593 // If this can be rendered immediately it will be, otherwise
594 // the scanner will start polling the queue (every
595 // requestAnimationFrame interval)
600 scan_renderQ = function() {
602 while (ready
&& renderQ
.length
> 0) {
606 that
.copyImage(a
.old_x
, a
.old_y
, a
.x
, a
.y
, a
.width
, a
.height
);
609 that
.fillRect(a
.x
, a
.y
, a
.width
, a
.height
, a
.color
);
612 that
.blitImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0);
615 that
.blitRgbImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0);
618 if (a
.img
.complete
) {
619 that
.drawImage(a
.img
, a
.x
, a
.y
);
621 // We need to wait for this image to 'load'
622 // to keep things in-order
631 if (renderQ
.length
> 0) {
632 requestAnimFrame(scan_renderQ
);
637 that
.changeCursor = function(pixels
, mask
, hotx
, hoty
, w
, h
) {
638 if (conf
.cursor_uri
=== false) {
639 Util
.Warn("changeCursor called but no cursor data URI support");
643 if (conf
.true_color
) {
644 changeCursor(conf
.target
, pixels
, mask
, hotx
, hoty
, w
, h
);
646 changeCursor(conf
.target
, pixels
, mask
, hotx
, hoty
, w
, h
, conf
.colourMap
);
650 that
.defaultCursor = function() {
651 conf
.target
.style
.cursor
= "default";
654 return constructor(); // Return the public API interface
656 } // End of Display()
659 /* Set CSS cursor property using data URI encoded cursor file */
660 function changeCursor(target
, pixels
, mask
, hotx
, hoty
, w0
, h0
, cmap
) {
662 var cur
= [], rgb
, IHDRsz
, RGBsz
, ANDsz
, XORsz
, url
, idx
, alpha
, x
, y
;
663 //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w0: " + w0 + ", h0: " + h0);
668 h
= w
; // increase h to make it square
670 w
= h
; // increace w to make it square
672 // Push multi-byte little-endian values
673 cur
.push16le = function (num
) {
674 this.push((num
) & 0xFF,
677 cur
.push32le = function (num
) {
678 this.push((num
) & 0xFF,
681 (num
>> 24) & 0xFF );
686 XORsz
= Math
.ceil( (w
* h
) / 8.0 );
687 ANDsz
= Math
.ceil( (w
* h
) / 8.0 );
690 cur
.push16le(0); // 0: Reserved
691 cur
.push16le(2); // 2: .CUR type
692 cur
.push16le(1); // 4: Number of images, 1 for non-animated ico
694 // Cursor #1 header (ICONDIRENTRY)
695 cur
.push(w
); // 6: width
696 cur
.push(h
); // 7: height
697 cur
.push(0); // 8: colors, 0 -> true-color
698 cur
.push(0); // 9: reserved
699 cur
.push16le(hotx
); // 10: hotspot x coordinate
700 cur
.push16le(hoty
); // 12: hotspot y coordinate
701 cur
.push32le(IHDRsz
+ RGBsz
+ XORsz
+ ANDsz
);
702 // 14: cursor data byte size
703 cur
.push32le(22); // 18: offset of cursor data in the file
706 // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
707 cur
.push32le(IHDRsz
); // 22: Infoheader size
708 cur
.push32le(w
); // 26: Cursor width
709 cur
.push32le(h
*2); // 30: XOR+AND height
710 cur
.push16le(1); // 34: number of planes
711 cur
.push16le(32); // 36: bits per pixel
712 cur
.push32le(0); // 38: Type of compression
714 cur
.push32le(XORsz
+ ANDsz
); // 43: Size of Image
715 // Gimp leaves this as 0
717 cur
.push32le(0); // 46: reserved
718 cur
.push32le(0); // 50: reserved
719 cur
.push32le(0); // 54: reserved
720 cur
.push32le(0); // 58: reserved
722 // 62: color data (RGBQUAD icColors[])
723 for (y
= h
-1; y
>= 0; y
-= 1) {
724 for (x
= 0; x
< w
; x
+= 1) {
725 if (x
>= w0
|| y
>= h0
) {
727 cur
.push(0); // green
729 cur
.push(0); // alpha
731 idx
= y
* Math
.ceil(w0
/ 8) + Math
.floor(x
/8);
732 alpha
= (mask
[idx
] << (x
% 8)) & 0x80 ? 255 : 0;
735 rgb
= cmap
[pixels
[idx
]];
736 cur
.push(rgb
[2]); // blue
737 cur
.push(rgb
[1]); // green
738 cur
.push(rgb
[0]); // red
739 cur
.push(alpha
); // alpha
741 idx
= ((w0
* y
) + x
) * 4;
742 cur
.push(pixels
[idx
+ 2]); // blue
743 cur
.push(pixels
[idx
+ 1]); // green
744 cur
.push(pixels
[idx
]); // red
745 cur
.push(alpha
); // alpha
751 // XOR/bitmask data (BYTE icXOR[])
752 // (ignored, just needs to be right size)
753 for (y
= 0; y
< h
; y
+= 1) {
754 for (x
= 0; x
< Math
.ceil(w
/ 8); x
+= 1) {
759 // AND/bitmask data (BYTE icAND[])
760 // (ignored, just needs to be right size)
761 for (y
= 0; y
< h
; y
+= 1) {
762 for (x
= 0; x
< Math
.ceil(w
/ 8); x
+= 1) {
767 url
= "data:image/x-icon;base64," + Base64
.encode(cur
);
768 target
.style
.cursor
= "url(" + url
+ ") " + hotx
+ " " + hoty
+ ", default";
769 //Util.Debug("<< changeCursor, cur.length: " + cur.length);