]>
git.proxmox.com Git - mirror_novnc.git/blob - include/canvas.js
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2010 Joel Martin
4 * Licensed under LGPL-3 (see LICENSE.txt)
6 * See README.md for usage and integration instructions.
10 /*jslint browser: true, white: false, bitwise: false */
11 /*global window, Util, Base64 */
13 function Canvas(conf
) {
15 conf
= conf
|| {}; // Configuration
16 var that
= {}, // Public API interface
18 // Pre-declare functions used before definitions (jslint)jslint
19 setFillColor
, fillRect
,
21 // Private Canvas namespace variables
22 c_forceCanvas
= false,
34 // Capability settings, default can be overridden
35 Util
.conf_default(conf
, that
, 'prefer_js', null);
36 Util
.conf_default(conf
, that
, 'cursor_uri', null);
38 // Configuration settings
39 Util
.conf_default(conf
, that
, 'target', null);
40 // Area that traps keyboard input
41 Util
.conf_default(conf
, that
, 'focusContainer', document
);
42 Util
.conf_default(conf
, that
, 'true_color', true);
43 Util
.conf_default(conf
, that
, 'focused', true);
44 Util
.conf_default(conf
, that
, 'colourMap', []);
45 Util
.conf_default(conf
, that
, 'scale', 1);
47 // Override some specific getters/setters
48 that
.set_prefer_js = function(val
) {
49 if (val
&& c_forceCanvas
) {
50 Util
.Warn("Preferring Javascript to Canvas ops is not supported");
57 that
.get_colourMap = function(idx
) {
58 if (typeof idx
=== 'undefined') {
59 return conf
.colourMap
;
61 return conf
.colourMap
[idx
];
65 that
.set_colourMap = function(val
, idx
) {
66 if (typeof idx
=== 'undefined') {
69 conf
.colourMap
[idx
] = val
;
73 // Add some other getters/setters
74 that
.get_width = function() {
77 that
.get_height = function() {
87 // Create the public API interface
88 function constructor() {
89 Util
.Debug(">> Canvas.init");
91 var c
, ctx
, imgTest
, tval
, i
, curDat
, curSave
,
92 has_imageData
= false, UE
= Util
.Engine
;
94 if (! conf
.target
) { throw("target must be set"); }
96 if (typeof conf
.target
=== 'string') {
97 conf
.target
= window
.$(conf
.target
);
102 if (! c
.getContext
) { throw("no getContext method"); }
104 if (! conf
.ctx
) { conf
.ctx
= c
.getContext('2d'); }
107 if (UE
.gecko
) { Util
.Debug("Browser: gecko " + UE
.gecko
); }
108 if (UE
.webkit
) { Util
.Debug("Browser: webkit " + UE
.webkit
); }
109 if (UE
.trident
) { Util
.Debug("Browser: webkit " + UE
.trident
); }
110 if (UE
.presto
) { Util
.Debug("Browser: webkit " + UE
.presto
); }
115 * Determine browser Canvas feature support
116 * and select fastest rendering methods
120 imgTest
= ctx
.getImageData(0, 0, 1,1);
121 imgTest
.data
[0] = 123;
122 imgTest
.data
[3] = 255;
123 ctx
.putImageData(imgTest
, 0, 0);
124 tval
= ctx
.getImageData(0, 0, 1, 1).data
[0];
126 has_imageData
= true;
131 Util
.Info("Canvas supports imageData");
132 c_forceCanvas
= false;
133 if (ctx
.createImageData
) {
134 // If it's there, it's faster
135 Util
.Info("Using Canvas createImageData");
136 that
.imageData
= that
.imageDataCreate
;
137 } else if (ctx
.getImageData
) {
138 Util
.Info("Using Canvas getImageData");
139 that
.imageData
= that
.imageDataGet
;
141 Util
.Info("Prefering javascript operations");
142 if (conf
.prefer_js
=== null) {
143 conf
.prefer_js
= true;
145 that
.rgbxImage
= that
.rgbxImageData
;
146 that
.cmapImage
= that
.cmapImageData
;
148 Util
.Warn("Canvas lacks imageData, using fillRect (slow)");
149 c_forceCanvas
= true;
150 conf
.prefer_js
= false;
151 that
.rgbxImage
= that
.rgbxImageFill
;
152 that
.cmapImage
= that
.cmapImageFill
;
156 * Determine browser support for setting the cursor via data URI
160 for (i
=0; i
< 8 * 8 * 4; i
+= 1) {
164 curSave
= c
.style
.cursor
;
165 that
.changeCursor(curDat
, curDat
, 2, 2, 8, 8);
166 if (c
.style
.cursor
) {
167 if (conf
.cursor_uri
=== null) {
168 conf
.cursor_uri
= true;
170 Util
.Info("Data URI scheme cursor supported");
172 if (conf
.cursor_uri
=== null) {
173 conf
.cursor_uri
= false;
175 Util
.Warn("Data URI scheme cursor not supported");
177 c
.style
.cursor
= curSave
;
179 Util
.Error("Data URI scheme cursor test exception: " + exc2
);
180 conf
.cursor_uri
= false;
185 Util
.Debug("<< Canvas.init");
189 /* Translate DOM key event to keysym value */
190 function getKeysym(e
) {
192 evt
= (e
? e
: window
.event
);
194 /* Remap modifier and special keys */
195 switch ( evt
.keyCode
) {
196 case 8 : keysym
= 0xFF08; break; // BACKSPACE
197 case 9 : keysym
= 0xFF09; break; // TAB
198 case 13 : keysym
= 0xFF0D; break; // ENTER
199 case 27 : keysym
= 0xFF1B; break; // ESCAPE
200 case 45 : keysym
= 0xFF63; break; // INSERT
201 case 46 : keysym
= 0xFFFF; break; // DELETE
202 case 36 : keysym
= 0xFF50; break; // HOME
203 case 35 : keysym
= 0xFF57; break; // END
204 case 33 : keysym
= 0xFF55; break; // PAGE_UP
205 case 34 : keysym
= 0xFF56; break; // PAGE_DOWN
206 case 37 : keysym
= 0xFF51; break; // LEFT
207 case 38 : keysym
= 0xFF52; break; // UP
208 case 39 : keysym
= 0xFF53; break; // RIGHT
209 case 40 : keysym
= 0xFF54; break; // DOWN
210 case 112 : keysym
= 0xFFBE; break; // F1
211 case 113 : keysym
= 0xFFBF; break; // F2
212 case 114 : keysym
= 0xFFC0; break; // F3
213 case 115 : keysym
= 0xFFC1; break; // F4
214 case 116 : keysym
= 0xFFC2; break; // F5
215 case 117 : keysym
= 0xFFC3; break; // F6
216 case 118 : keysym
= 0xFFC4; break; // F7
217 case 119 : keysym
= 0xFFC5; break; // F8
218 case 120 : keysym
= 0xFFC6; break; // F9
219 case 121 : keysym
= 0xFFC7; break; // F10
220 case 122 : keysym
= 0xFFC8; break; // F11
221 case 123 : keysym
= 0xFFC9; break; // F12
222 case 16 : keysym
= 0xFFE1; break; // SHIFT
223 case 17 : keysym
= 0xFFE3; break; // CONTROL
224 //case 18 : keysym = 0xFFE7; break; // Left Meta (Mac Option)
225 case 18 : keysym
= 0xFFE9; break; // Left ALT (Mac Command)
226 default : keysym
= evt
.keyCode
; break;
231 case 186 : keysym
= 59; break; // ; (IE)
232 case 187 : keysym
= 61; break; // = (IE)
233 case 188 : keysym
= 44; break; // , (Mozilla, IE)
234 case 109 : // - (Mozilla)
235 if (Util
.Engine
.gecko
) {
238 case 189 : keysym
= 45; break; // - (IE)
239 case 190 : keysym
= 46; break; // . (Mozilla, IE)
240 case 191 : keysym
= 47; break; // / (Mozilla, IE)
241 case 192 : keysym
= 96; break; // ` (Mozilla, IE)
242 case 219 : keysym
= 91; break; // [ (Mozilla, IE)
243 case 220 : keysym
= 92; break; // \ (Mozilla, IE)
244 case 221 : keysym
= 93; break; // ] (Mozilla, IE)
245 case 222 : keysym
= 39; break; // ' (Mozilla, IE)
248 /* Remap shifted and unshifted keys */
249 if (!!evt
.shiftKey
) {
251 case 48 : keysym
= 41 ; break; // ) (shifted 0)
252 case 49 : keysym
= 33 ; break; // ! (shifted 1)
253 case 50 : keysym
= 64 ; break; // @ (shifted 2)
254 case 51 : keysym
= 35 ; break; // # (shifted 3)
255 case 52 : keysym
= 36 ; break; // $ (shifted 4)
256 case 53 : keysym
= 37 ; break; // % (shifted 5)
257 case 54 : keysym
= 94 ; break; // ^ (shifted 6)
258 case 55 : keysym
= 38 ; break; // & (shifted 7)
259 case 56 : keysym
= 42 ; break; // * (shifted 8)
260 case 57 : keysym
= 40 ; break; // ( (shifted 9)
262 case 59 : keysym
= 58 ; break; // : (shifted `)
263 case 61 : keysym
= 43 ; break; // + (shifted ;)
264 case 44 : keysym
= 60 ; break; // < (shifted ,)
265 case 45 : keysym
= 95 ; break; // _ (shifted -)
266 case 46 : keysym
= 62 ; break; // > (shifted .)
267 case 47 : keysym
= 63 ; break; // ? (shifted /)
268 case 96 : keysym
= 126; break; // ~ (shifted `)
269 case 91 : keysym
= 123; break; // { (shifted [)
270 case 92 : keysym
= 124; break; // | (shifted \)
271 case 93 : keysym
= 125; break; // } (shifted ])
272 case 39 : keysym
= 34 ; break; // " (shifted ')
274 } else if ((keysym
>= 65) && (keysym
<=90)) {
275 /* Remap unshifted A-Z */
282 function onMouseButton(e
, down
) {
284 if (! conf
.focused
) {
287 evt
= (e
? e
: window
.event
);
288 pos
= Util
.getEventPosition(e
, conf
.target
, conf
.scale
);
289 bmask
= 1 << evt
.button
;
290 //Util.Debug('mouse ' + pos.x + "," + pos.y + " down: " + down + " bmask: " + bmask);
292 c_mouseButton(pos
.x
, pos
.y
, down
, bmask
);
298 function onMouseDown(e
) {
302 function onMouseUp(e
) {
306 function onMouseWheel(e
) {
307 var evt
, pos
, bmask
, wheelData
;
308 evt
= (e
? e
: window
.event
);
309 pos
= Util
.getEventPosition(e
, conf
.target
, conf
.scale
);
310 wheelData
= evt
.detail
? evt
.detail
* -1 : evt
.wheelDelta
/ 40;
316 //Util.Debug('mouse scroll by ' + wheelData + ':' + pos.x + "," + pos.y);
318 c_mouseButton(pos
.x
, pos
.y
, 1, bmask
);
319 c_mouseButton(pos
.x
, pos
.y
, 0, bmask
);
325 function onMouseMove(e
) {
327 evt
= (e
? e
: window
.event
);
328 pos
= Util
.getEventPosition(e
, conf
.target
, conf
.scale
);
329 //Util.Debug('mouse ' + evt.which + '/' + evt.button + ' up:' + pos.x + "," + pos.y);
331 c_mouseMove(pos
.x
, pos
.y
);
335 function onKeyDown(e
) {
336 //Util.Debug("keydown: " + getKeysym(e));
337 if (! conf
.focused
) {
341 c_keyPress(getKeysym(e
), 1);
347 function onKeyUp(e
) {
348 //Util.Debug("keyup: " + getKeysym(e));
349 if (! conf
.focused
) {
353 c_keyPress(getKeysym(e
), 0);
359 function onMouseDisable(e
) {
361 if (! conf
.focused
) {
364 evt
= (e
? e
: window
.event
);
365 pos
= Util
.getEventPosition(e
, conf
.target
, conf
.scale
);
366 /* Stop propagation if inside canvas area */
367 if ((pos
.x
>= 0) && (pos
.y
>= 0) &&
368 (pos
.x
< c_width
) && (pos
.y
< c_height
)) {
369 //Util.Debug("mouse event disabled");
373 //Util.Debug("mouse event not disabled");
378 // Public API interface functions
381 that
.getContext = function () {
385 that
.start = function(keyPressFunc
, mouseButtonFunc
, mouseMoveFunc
) {
387 Util
.Debug(">> Canvas.start");
390 c_keyPress
= keyPressFunc
|| null;
391 c_mouseButton
= mouseButtonFunc
|| null;
392 c_mouseMove
= mouseMoveFunc
|| null;
394 Util
.addEvent(conf
.focusContainer
, 'keydown', onKeyDown
);
395 Util
.addEvent(conf
.focusContainer
, 'keyup', onKeyUp
);
396 Util
.addEvent(c
, 'mousedown', onMouseDown
);
397 Util
.addEvent(c
, 'mouseup', onMouseUp
);
398 Util
.addEvent(c
, 'mousemove', onMouseMove
);
399 Util
.addEvent(c
, (Util
.Engine
.gecko
) ? 'DOMMouseScroll' : 'mousewheel',
402 /* Work around right and middle click browser behaviors */
403 Util
.addEvent(conf
.focusContainer
, 'click', onMouseDisable
);
404 Util
.addEvent(conf
.focusContainer
.body
, 'contextmenu', onMouseDisable
);
406 Util
.Debug("<< Canvas.start");
409 that
.rescale = function(factor
) {
411 properties
= ['transform', 'WebkitTransform', 'MozTransform', null];
413 tp
= properties
.shift();
415 if (typeof c
.style
[tp
] !== 'undefined') {
418 tp
= properties
.shift();
422 Util
.Debug("No scaling support");
426 if (conf
.scale
=== factor
) {
427 //Util.Debug("Canvas already scaled to '" + factor + "'");
432 x
= c
.width
- c
.width
* factor
;
433 y
= c
.height
- c
.height
* factor
;
434 c
.style
[tp
] = "scale(" + conf
.scale
+ ") translate(-" + x
+ "px, -" + y
+ "px)";
437 that
.resize = function(width
, height
, true_color
) {
440 if (typeof true_color
!== "undefined") {
441 conf
.true_color
= true_color
;
447 c_width
= c
.offsetWidth
;
448 c_height
= c
.offsetHeight
;
450 that
.rescale(conf
.scale
);
453 that
.clear = function() {
454 that
.resize(640, 20);
455 conf
.ctx
.clearRect(0, 0, c_width
, c_height
);
458 that
.stop = function() {
460 Util
.removeEvent(conf
.focusContainer
, 'keydown', onKeyDown
);
461 Util
.removeEvent(conf
.focusContainer
, 'keyup', onKeyUp
);
462 Util
.removeEvent(c
, 'mousedown', onMouseDown
);
463 Util
.removeEvent(c
, 'mouseup', onMouseUp
);
464 Util
.removeEvent(c
, 'mousemove', onMouseMove
);
465 Util
.removeEvent(c
, (Util
.Engine
.gecko
) ? 'DOMMouseScroll' : 'mousewheel',
468 /* Work around right and middle click browser behaviors */
469 Util
.removeEvent(conf
.focusContainer
, 'click', onMouseDisable
);
470 Util
.removeEvent(conf
.focusContainer
.body
, 'contextmenu', onMouseDisable
);
472 // Turn off cursor rendering
473 if (conf
.cursor_uri
) {
474 c
.style
.cursor
= "default";
478 setFillColor = function(color
) {
480 if (conf
.true_color
) {
483 rgb
= conf
.colourMap
[color
[0]];
485 if (newStyle
!== c_prevStyle
) {
486 newStyle
= "rgb(" + rgb
[0] + "," + rgb
[1] + "," + rgb
[2] + ")";
487 conf
.ctx
.fillStyle
= newStyle
;
488 c_prevStyle
= newStyle
;
491 that
.setFillColor
= setFillColor
;
493 fillRect = function(x
, y
, width
, height
, color
) {
495 conf
.ctx
.fillRect(x
, y
, width
, height
);
497 that
.fillRect
= fillRect
;
499 that
.copyImage = function(old_x
, old_y
, new_x
, new_y
, width
, height
) {
500 conf
.ctx
.drawImage(conf
.target
, old_x
, old_y
, width
, height
,
501 new_x
, new_y
, width
, height
);
505 * Tile rendering functions optimized for rendering engines.
507 * - In Chrome/webkit, Javascript image data array manipulations are
508 * faster than direct Canvas fillStyle, fillRect rendering. In
509 * gecko, Javascript array handling is much slower.
511 that
.getTile = function(x
, y
, width
, height
, color
) {
512 var img
, data
, p
, rgb
, red
, green
, blue
, j
, i
;
513 img
= {'x': x
, 'y': y
, 'width': width
, 'height': height
,
515 if (conf
.prefer_js
) {
517 if (conf
.true_color
) {
520 rgb
= conf
.colourMap
[color
[0]];
525 for (j
= 0; j
< height
; j
+= 1) {
526 for (i
= 0; i
< width
; i
+= 1) {
527 p
= (i
+ (j
* width
) ) * 4;
531 //data[p + 3] = 255; // Set Alpha
535 fillRect(x
, y
, width
, height
, color
);
540 that
.setSubTile = function(img
, x
, y
, w
, h
, color
) {
541 var data
, p
, rgb
, red
, green
, blue
, width
, j
, i
;
542 if (conf
.prefer_js
) {
545 if (conf
.true_color
) {
548 rgb
= conf
.colourMap
[color
[0]];
553 for (j
= 0; j
< h
; j
+= 1) {
554 for (i
= 0; i
< w
; i
+= 1) {
555 p
= (x
+ i
+ ((y
+ j
) * width
) ) * 4;
559 //img.data[p + 3] = 255; // Set Alpha
563 fillRect(img
.x
+ x
, img
.y
+ y
, w
, h
, color
);
567 that
.putTile = function(img
) {
568 if (conf
.prefer_js
) {
569 that
.rgbxImage(img
.x
, img
.y
, img
.width
, img
.height
, img
.data
, 0);
571 // No-op, under gecko already done by setSubTile
575 that
.imageDataGet = function(width
, height
) {
576 return conf
.ctx
.getImageData(0, 0, width
, height
);
578 that
.imageDataCreate = function(width
, height
) {
579 return conf
.ctx
.createImageData(width
, height
);
582 that
.rgbxImageData = function(x
, y
, width
, height
, arr
, offset
) {
584 img
= that
.imageData(width
, height
);
586 for (i
=0, j
=offset
; i
< (width
* height
* 4); i
=i
+4, j
=j
+4) {
587 data
[i
+ 0] = arr
[j
+ 0];
588 data
[i
+ 1] = arr
[j
+ 1];
589 data
[i
+ 2] = arr
[j
+ 2];
590 data
[i
+ 3] = 255; // Set Alpha
592 conf
.ctx
.putImageData(img
, x
, y
);
595 // really slow fallback if we don't have imageData
596 that
.rgbxImageFill = function(x
, y
, width
, height
, arr
, offset
) {
597 var i
, j
, sx
= 0, sy
= 0;
598 for (i
=0, j
=offset
; i
< (width
* height
); i
+=1, j
+=4) {
599 fillRect(x
+sx
, y
+sy
, 1, 1, [arr
[j
+0], arr
[j
+1], arr
[j
+2]]);
601 if ((sx
% width
) === 0) {
608 that
.cmapImageData = function(x
, y
, width
, height
, arr
, offset
) {
609 var img
, i
, j
, data
, rgb
, cmap
;
610 img
= that
.imageData(width
, height
);
612 cmap
= conf
.colourMap
;
613 for (i
=0, j
=offset
; i
< (width
* height
* 4); i
+=4, j
+=1) {
615 data
[i
+ 0] = rgb
[0];
616 data
[i
+ 1] = rgb
[1];
617 data
[i
+ 2] = rgb
[2];
618 data
[i
+ 3] = 255; // Set Alpha
620 conf
.ctx
.putImageData(img
, x
, y
);
623 that
.cmapImageFill = function(x
, y
, width
, height
, arr
, offset
) {
624 var i
, j
, sx
= 0, sy
= 0, cmap
;
625 cmap
= conf
.colourMap
;
626 for (i
=0, j
=offset
; i
< (width
* height
); i
+=1, j
+=1) {
627 fillRect(x
+sx
, y
+sy
, 1, 1, [arr
[j
]]);
629 if ((sx
% width
) === 0) {
637 that
.blitImage = function(x
, y
, width
, height
, arr
, offset
) {
638 if (conf
.true_color
) {
639 that
.rgbxImage(x
, y
, width
, height
, arr
, offset
);
641 that
.cmapImage(x
, y
, width
, height
, arr
, offset
);
645 that
.blitStringImage = function(str
, x
, y
) {
646 var img
= new Image();
647 img
.onload = function () { conf
.ctx
.drawImage(img
, x
, y
); };
651 that
.changeCursor = function(pixels
, mask
, hotx
, hoty
, w
, h
) {
652 var cur
= [], cmap
, rgb
, IHDRsz
, ANDsz
, XORsz
, url
, idx
, alpha
, x
, y
;
653 //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h);
655 if (conf
.cursor_uri
=== false) {
656 Util
.Warn("changeCursor called but no cursor data URI support");
660 // Push multi-byte little-endian values
661 cur
.push16le = function (num
) {
662 this.push((num
) & 0xFF,
665 cur
.push32le = function (num
) {
666 this.push((num
) & 0xFF,
669 (num
>> 24) & 0xFF );
672 cmap
= conf
.colourMap
;
675 XORsz
= Math
.ceil( (w
* h
) / 8.0 );
678 cur
.push16le(0); // Reserved
679 cur
.push16le(2); // .CUR type
680 cur
.push16le(1); // Number of images, 1 for non-animated ico
683 cur
.push(w
); // width
684 cur
.push(h
); // height
685 cur
.push(0); // colors, 0 -> true-color
686 cur
.push(0); // reserved
687 cur
.push16le(hotx
); // hotspot x coordinate
688 cur
.push16le(hoty
); // hotspot y coordinate
689 cur
.push32le(IHDRsz
+ XORsz
+ ANDsz
); // cursor data byte size
690 cur
.push32le(22); // offset of cursor data in the file
692 // Cursor #1 InfoHeader
693 cur
.push32le(IHDRsz
); // Infoheader size
694 cur
.push32le(w
); // Cursor width
695 cur
.push32le(h
*2); // XOR+AND height
696 cur
.push16le(1); // number of planes
697 cur
.push16le(32); // bits per pixel
698 cur
.push32le(0); // Type of compression
699 cur
.push32le(XORsz
+ ANDsz
); // Size of Image
706 for (y
= h
-1; y
>= 0; y
-= 1) {
707 for (x
= 0; x
< w
; x
+= 1) {
708 idx
= y
* Math
.ceil(w
/ 8) + Math
.floor(x
/8);
709 alpha
= (mask
[idx
] << (x
% 8)) & 0x80 ? 255 : 0;
711 if (conf
.true_color
) {
712 idx
= ((w
* y
) + x
) * 4;
713 cur
.push(pixels
[idx
+ 2]); // blue
714 cur
.push(pixels
[idx
+ 1]); // green
715 cur
.push(pixels
[idx
+ 0]); // red
716 cur
.push(alpha
); // red
719 rgb
= cmap
[pixels
[idx
]];
720 cur
.push(rgb
[2]); // blue
721 cur
.push(rgb
[1]); // green
722 cur
.push(rgb
[0]); // red
723 cur
.push(alpha
); // alpha
728 // AND/bitmask data (ignored, just needs to be right size)
729 for (y
= 0; y
< h
; y
+= 1) {
730 for (x
= 0; x
< Math
.ceil(w
/ 8); x
+= 1) {
735 url
= "data:image/x-icon;base64," + Base64
.encode(cur
);
736 conf
.target
.style
.cursor
= "url(" + url
+ ") " + hotx
+ " " + hoty
+ ", default";
737 //Util.Debug("<< changeCursor, cur.length: " + cur.length);
742 return constructor(); // Return the public API interface