]>
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.LGPL-3)
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 Util
.conf_default(conf
, that
, 'true_color', true);
41 Util
.conf_default(conf
, that
, 'focused', true);
42 Util
.conf_default(conf
, that
, 'colourMap', []);
43 Util
.conf_default(conf
, that
, 'scale', 1);
45 // Override some specific getters/setters
46 that
.set_prefer_js = function(val
) {
47 if (val
&& c_forceCanvas
) {
48 Util
.Warn("Preferring Javascript to Canvas ops is not supported");
55 that
.get_colourMap = function(idx
) {
56 if (typeof idx
=== 'undefined') {
57 return conf
.colourMap
;
59 return conf
.colourMap
[idx
];
63 that
.set_colourMap = function(val
, idx
) {
64 if (typeof idx
=== 'undefined') {
67 conf
.colourMap
[idx
] = val
;
71 // Add some other getters/setters
72 that
.get_width = function() {
75 that
.get_height = function() {
85 // Create the public API interface
86 function constructor() {
87 Util
.Debug(">> Canvas.init");
89 var c
, ctx
, imgTest
, tval
, i
, curDat
, curSave
,
90 has_imageData
= false;
92 if (! conf
.target
) { throw("target must be set"); }
94 if (typeof conf
.target
=== 'string') {
95 conf
.target
= window
.$(conf
.target
);
100 if (! c
.getContext
) { throw("no getContext method"); }
102 if (! conf
.ctx
) { conf
.ctx
= c
.getContext('2d'); }
108 * Determine browser Canvas feature support
109 * and select fastest rendering methods
113 imgTest
= ctx
.getImageData(0, 0, 1,1);
114 imgTest
.data
[0] = 123;
115 imgTest
.data
[3] = 255;
116 ctx
.putImageData(imgTest
, 0, 0);
117 tval
= ctx
.getImageData(0, 0, 1, 1).data
[0];
119 has_imageData
= true;
124 Util
.Info("Canvas supports imageData");
125 c_forceCanvas
= false;
126 if (ctx
.createImageData
) {
127 // If it's there, it's faster
128 Util
.Info("Using Canvas createImageData");
129 that
.imageData
= that
.imageDataCreate
;
130 } else if (ctx
.getImageData
) {
131 Util
.Info("Using Canvas getImageData");
132 that
.imageData
= that
.imageDataGet
;
134 Util
.Info("Prefering javascript operations");
135 if (conf
.prefer_js
=== null) {
136 conf
.prefer_js
= true;
138 that
.rgbxImage
= that
.rgbxImageData
;
139 that
.cmapImage
= that
.cmapImageData
;
141 Util
.Warn("Canvas lacks imageData, using fillRect (slow)");
142 c_forceCanvas
= true;
143 conf
.prefer_js
= false;
144 that
.rgbxImage
= that
.rgbxImageFill
;
145 that
.cmapImage
= that
.cmapImageFill
;
149 * Determine browser support for setting the cursor via data URI
153 for (i
=0; i
< 8 * 8 * 4; i
+= 1) {
157 curSave
= c
.style
.cursor
;
158 that
.changeCursor(curDat
, curDat
, 2, 2, 8, 8);
159 if (c
.style
.cursor
) {
160 if (conf
.cursor_uri
=== null) {
161 conf
.cursor_uri
= true;
163 Util
.Info("Data URI scheme cursor supported");
165 if (conf
.cursor_uri
=== null) {
166 conf
.cursor_uri
= false;
168 Util
.Warn("Data URI scheme cursor not supported");
170 c
.style
.cursor
= curSave
;
172 Util
.Error("Data URI scheme cursor test exception: " + exc2
);
173 conf
.cursor_uri
= false;
178 Util
.Debug("<< Canvas.init");
182 /* Translate DOM key event to keysym value */
183 function getKeysym(e
) {
185 evt
= (e
? e
: window
.event
);
187 /* Remap modifier and special keys */
188 switch ( evt
.keyCode
) {
189 case 8 : keysym
= 0xFF08; break; // BACKSPACE
190 case 9 : keysym
= 0xFF09; break; // TAB
191 case 13 : keysym
= 0xFF0D; break; // ENTER
192 case 27 : keysym
= 0xFF1B; break; // ESCAPE
193 case 45 : keysym
= 0xFF63; break; // INSERT
194 case 46 : keysym
= 0xFFFF; break; // DELETE
195 case 36 : keysym
= 0xFF50; break; // HOME
196 case 35 : keysym
= 0xFF57; break; // END
197 case 33 : keysym
= 0xFF55; break; // PAGE_UP
198 case 34 : keysym
= 0xFF56; break; // PAGE_DOWN
199 case 37 : keysym
= 0xFF51; break; // LEFT
200 case 38 : keysym
= 0xFF52; break; // UP
201 case 39 : keysym
= 0xFF53; break; // RIGHT
202 case 40 : keysym
= 0xFF54; break; // DOWN
203 case 112 : keysym
= 0xFFBE; break; // F1
204 case 113 : keysym
= 0xFFBF; break; // F2
205 case 114 : keysym
= 0xFFC0; break; // F3
206 case 115 : keysym
= 0xFFC1; break; // F4
207 case 116 : keysym
= 0xFFC2; break; // F5
208 case 117 : keysym
= 0xFFC3; break; // F6
209 case 118 : keysym
= 0xFFC4; break; // F7
210 case 119 : keysym
= 0xFFC5; break; // F8
211 case 120 : keysym
= 0xFFC6; break; // F9
212 case 121 : keysym
= 0xFFC7; break; // F10
213 case 122 : keysym
= 0xFFC8; break; // F11
214 case 123 : keysym
= 0xFFC9; break; // F12
215 case 16 : keysym
= 0xFFE1; break; // SHIFT
216 case 17 : keysym
= 0xFFE3; break; // CONTROL
217 //case 18 : keysym = 0xFFE7; break; // Left Meta (Mac Option)
218 case 18 : keysym
= 0xFFE9; break; // Left ALT (Mac Command)
219 default : keysym
= evt
.keyCode
; break;
224 case 186 : keysym
= 59; break; // ; (IE)
225 case 187 : keysym
= 61; break; // = (IE)
226 case 188 : keysym
= 44; break; // , (Mozilla, IE)
227 case 109 : // - (Mozilla)
228 if (Util
.Engine
.gecko
) {
231 case 189 : keysym
= 45; break; // - (IE)
232 case 190 : keysym
= 46; break; // . (Mozilla, IE)
233 case 191 : keysym
= 47; break; // / (Mozilla, IE)
234 case 192 : keysym
= 96; break; // ` (Mozilla, IE)
235 case 219 : keysym
= 91; break; // [ (Mozilla, IE)
236 case 220 : keysym
= 92; break; // \ (Mozilla, IE)
237 case 221 : keysym
= 93; break; // ] (Mozilla, IE)
238 case 222 : keysym
= 39; break; // ' (Mozilla, IE)
241 /* Remap shifted and unshifted keys */
242 if (!!evt
.shiftKey
) {
244 case 48 : keysym
= 41 ; break; // ) (shifted 0)
245 case 49 : keysym
= 33 ; break; // ! (shifted 1)
246 case 50 : keysym
= 64 ; break; // @ (shifted 2)
247 case 51 : keysym
= 35 ; break; // # (shifted 3)
248 case 52 : keysym
= 36 ; break; // $ (shifted 4)
249 case 53 : keysym
= 37 ; break; // % (shifted 5)
250 case 54 : keysym
= 94 ; break; // ^ (shifted 6)
251 case 55 : keysym
= 38 ; break; // & (shifted 7)
252 case 56 : keysym
= 42 ; break; // * (shifted 8)
253 case 57 : keysym
= 40 ; break; // ( (shifted 9)
255 case 59 : keysym
= 58 ; break; // : (shifted `)
256 case 61 : keysym
= 43 ; break; // + (shifted ;)
257 case 44 : keysym
= 60 ; break; // < (shifted ,)
258 case 45 : keysym
= 95 ; break; // _ (shifted -)
259 case 46 : keysym
= 62 ; break; // > (shifted .)
260 case 47 : keysym
= 63 ; break; // ? (shifted /)
261 case 96 : keysym
= 126; break; // ~ (shifted `)
262 case 91 : keysym
= 123; break; // { (shifted [)
263 case 92 : keysym
= 124; break; // | (shifted \)
264 case 93 : keysym
= 125; break; // } (shifted ])
265 case 39 : keysym
= 34 ; break; // " (shifted ')
267 } else if ((keysym
>= 65) && (keysym
<=90)) {
268 /* Remap unshifted A-Z */
275 function onMouseButton(e
, down
) {
277 if (! conf
.focused
) {
280 evt
= (e
? e
: window
.event
);
281 pos
= Util
.getEventPosition(e
, conf
.target
, conf
.scale
);
282 bmask
= 1 << evt
.button
;
283 //Util.Debug('mouse ' + pos.x + "," + pos.y + " down: " + down + " bmask: " + bmask);
285 c_mouseButton(pos
.x
, pos
.y
, down
, bmask
);
291 function onMouseDown(e
) {
295 function onMouseUp(e
) {
299 function onMouseWheel(e
) {
300 var evt
, pos
, bmask
, wheelData
;
301 evt
= (e
? e
: window
.event
);
302 pos
= Util
.getEventPosition(e
, conf
.target
, conf
.scale
);
303 wheelData
= evt
.detail
? evt
.detail
* -1 : evt
.wheelDelta
/ 40;
309 //Util.Debug('mouse scroll by ' + wheelData + ':' + pos.x + "," + pos.y);
311 c_mouseButton(pos
.x
, pos
.y
, 1, bmask
);
312 c_mouseButton(pos
.x
, pos
.y
, 0, bmask
);
318 function onMouseMove(e
) {
320 evt
= (e
? e
: window
.event
);
321 pos
= Util
.getEventPosition(e
, conf
.target
, conf
.scale
);
322 //Util.Debug('mouse ' + evt.which + '/' + evt.button + ' up:' + pos.x + "," + pos.y);
324 c_mouseMove(pos
.x
, pos
.y
);
328 function onKeyDown(e
) {
329 //Util.Debug("keydown: " + getKeysym(e));
330 if (! conf
.focused
) {
334 c_keyPress(getKeysym(e
), 1);
340 function onKeyUp(e
) {
341 //Util.Debug("keyup: " + getKeysym(e));
342 if (! conf
.focused
) {
346 c_keyPress(getKeysym(e
), 0);
352 function onMouseDisable(e
) {
354 if (! conf
.focused
) {
357 evt
= (e
? e
: window
.event
);
358 pos
= Util
.getPosition(conf
.target
);
359 /* Stop propagation if inside canvas area */
360 if ((evt
.clientX
>= pos
.x
) &&
361 (evt
.clientY
>= pos
.y
) &&
362 (evt
.clientX
< (pos
.x
+ c_width
)) &&
363 (evt
.clientY
< (pos
.y
+ c_height
))) {
364 //Util.Debug("mouse event disabled");
368 //Util.Debug("mouse event not disabled");
373 // Public API interface functions
376 that
.getContext = function () {
380 that
.start = function(keyPressFunc
, mouseButtonFunc
, mouseMoveFunc
) {
382 Util
.Debug(">> Canvas.start");
385 c_keyPress
= keyPressFunc
|| null;
386 c_mouseButton
= mouseButtonFunc
|| null;
387 c_mouseMove
= mouseMoveFunc
|| null;
389 Util
.addEvent(document
, 'keydown', onKeyDown
);
390 Util
.addEvent(document
, 'keyup', onKeyUp
);
391 Util
.addEvent(c
, 'mousedown', onMouseDown
);
392 Util
.addEvent(c
, 'mouseup', onMouseUp
);
393 Util
.addEvent(c
, 'mousemove', onMouseMove
);
394 Util
.addEvent(c
, (Util
.Engine
.gecko
) ? 'DOMMouseScroll' : 'mousewheel',
397 /* Work around right and middle click browser behaviors */
398 Util
.addEvent(document
, 'click', onMouseDisable
);
399 Util
.addEvent(document
.body
, 'contextmenu', onMouseDisable
);
401 Util
.Debug("<< Canvas.start");
404 that
.rescale = function(factor
) {
406 properties
= ['transform', 'WebkitTransform', 'MozTransform', null];
408 tp
= properties
.shift();
410 if (typeof c
.style
[tp
] !== 'undefined') {
413 tp
= properties
.shift();
417 Util
.Debug("No scaling support");
421 if (conf
.scale
=== factor
) {
422 //Util.Debug("Canvas already scaled to '" + factor + "'");
427 x
= c
.width
- c
.width
* factor
;
428 y
= c
.height
- c
.height
* factor
;
429 c
.style
[tp
] = "scale(" + conf
.scale
+ ") translate(-" + x
+ "px, -" + y
+ "px)";
432 that
.resize = function(width
, height
, true_color
) {
435 if (typeof true_color
!== "undefined") {
436 conf
.true_color
= true_color
;
442 c_width
= c
.offsetWidth
;
443 c_height
= c
.offsetHeight
;
445 that
.rescale(conf
.scale
);
448 that
.clear = function() {
449 that
.resize(640, 20);
450 conf
.ctx
.clearRect(0, 0, c_width
, c_height
);
453 that
.stop = function() {
455 Util
.removeEvent(document
, 'keydown', onKeyDown
);
456 Util
.removeEvent(document
, 'keyup', onKeyUp
);
457 Util
.removeEvent(c
, 'mousedown', onMouseDown
);
458 Util
.removeEvent(c
, 'mouseup', onMouseUp
);
459 Util
.removeEvent(c
, 'mousemove', onMouseMove
);
460 Util
.removeEvent(c
, (Util
.Engine
.gecko
) ? 'DOMMouseScroll' : 'mousewheel',
463 /* Work around right and middle click browser behaviors */
464 Util
.removeEvent(document
, 'click', onMouseDisable
);
465 Util
.removeEvent(document
.body
, 'contextmenu', onMouseDisable
);
467 // Turn off cursor rendering
468 if (conf
.cursor_uri
) {
469 c
.style
.cursor
= "default";
473 setFillColor = function(color
) {
475 if (conf
.true_color
) {
478 rgb
= conf
.colourMap
[color
[0]];
480 if (newStyle
!== c_prevStyle
) {
481 newStyle
= "rgb(" + rgb
[0] + "," + rgb
[1] + "," + rgb
[2] + ")";
482 conf
.ctx
.fillStyle
= newStyle
;
483 c_prevStyle
= newStyle
;
486 that
.setFillColor
= setFillColor
;
488 fillRect = function(x
, y
, width
, height
, color
) {
490 conf
.ctx
.fillRect(x
, y
, width
, height
);
492 that
.fillRect
= fillRect
;
494 that
.copyImage = function(old_x
, old_y
, new_x
, new_y
, width
, height
) {
495 conf
.ctx
.drawImage(conf
.target
, old_x
, old_y
, width
, height
,
496 new_x
, new_y
, width
, height
);
500 * Tile rendering functions optimized for rendering engines.
502 * - In Chrome/webkit, Javascript image data array manipulations are
503 * faster than direct Canvas fillStyle, fillRect rendering. In
504 * gecko, Javascript array handling is much slower.
506 that
.getTile = function(x
, y
, width
, height
, color
) {
507 var img
, data
, p
, rgb
, red
, green
, blue
, j
, i
;
508 img
= {'x': x
, 'y': y
, 'width': width
, 'height': height
,
510 if (conf
.prefer_js
) {
512 if (conf
.true_color
) {
515 rgb
= conf
.colourMap
[color
[0]];
520 for (j
= 0; j
< height
; j
+= 1) {
521 for (i
= 0; i
< width
; i
+= 1) {
522 p
= (i
+ (j
* width
) ) * 4;
526 //data[p + 3] = 255; // Set Alpha
530 fillRect(x
, y
, width
, height
, color
);
535 that
.setSubTile = function(img
, x
, y
, w
, h
, color
) {
536 var data
, p
, rgb
, red
, green
, blue
, width
, j
, i
;
537 if (conf
.prefer_js
) {
540 if (conf
.true_color
) {
543 rgb
= conf
.colourMap
[color
[0]];
548 for (j
= 0; j
< h
; j
+= 1) {
549 for (i
= 0; i
< w
; i
+= 1) {
550 p
= (x
+ i
+ ((y
+ j
) * width
) ) * 4;
554 //img.data[p + 3] = 255; // Set Alpha
558 fillRect(img
.x
+ x
, img
.y
+ y
, w
, h
, color
);
562 that
.putTile = function(img
) {
563 if (conf
.prefer_js
) {
564 that
.rgbxImage(img
.x
, img
.y
, img
.width
, img
.height
, img
.data
, 0);
566 // No-op, under gecko already done by setSubTile
570 that
.imageDataGet = function(width
, height
) {
571 return conf
.ctx
.getImageData(0, 0, width
, height
);
573 that
.imageDataCreate = function(width
, height
) {
574 return conf
.ctx
.createImageData(width
, height
);
577 that
.rgbxImageData = function(x
, y
, width
, height
, arr
, offset
) {
579 img
= that
.imageData(width
, height
);
581 for (i
=0, j
=offset
; i
< (width
* height
* 4); i
=i
+4, j
=j
+4) {
582 data
[i
+ 0] = arr
[j
+ 0];
583 data
[i
+ 1] = arr
[j
+ 1];
584 data
[i
+ 2] = arr
[j
+ 2];
585 data
[i
+ 3] = 255; // Set Alpha
587 conf
.ctx
.putImageData(img
, x
, y
);
590 // really slow fallback if we don't have imageData
591 that
.rgbxImageFill = function(x
, y
, width
, height
, arr
, offset
) {
592 var i
, j
, sx
= 0, sy
= 0;
593 for (i
=0, j
=offset
; i
< (width
* height
); i
+=1, j
+=4) {
594 fillRect(x
+sx
, y
+sy
, 1, 1, [arr
[j
+0], arr
[j
+1], arr
[j
+2]]);
596 if ((sx
% width
) === 0) {
603 that
.cmapImageData = function(x
, y
, width
, height
, arr
, offset
) {
604 var img
, i
, j
, data
, rgb
, cmap
;
605 img
= that
.imageData(width
, height
);
607 cmap
= conf
.colourMap
;
608 for (i
=0, j
=offset
; i
< (width
* height
* 4); i
+=4, j
+=1) {
610 data
[i
+ 0] = rgb
[0];
611 data
[i
+ 1] = rgb
[1];
612 data
[i
+ 2] = rgb
[2];
613 data
[i
+ 3] = 255; // Set Alpha
615 conf
.ctx
.putImageData(img
, x
, y
);
618 that
.cmapImageFill = function(x
, y
, width
, height
, arr
, offset
) {
619 var i
, j
, sx
= 0, sy
= 0, cmap
;
620 cmap
= conf
.colourMap
;
621 for (i
=0, j
=offset
; i
< (width
* height
); i
+=1, j
+=1) {
622 fillRect(x
+sx
, y
+sy
, 1, 1, [arr
[j
]]);
624 if ((sx
% width
) === 0) {
632 that
.blitImage = function(x
, y
, width
, height
, arr
, offset
) {
633 if (conf
.true_color
) {
634 that
.rgbxImage(x
, y
, width
, height
, arr
, offset
);
636 that
.cmapImage(x
, y
, width
, height
, arr
, offset
);
640 that
.blitStringImage = function(str
, x
, y
) {
641 var img
= new Image();
642 img
.onload = function () { conf
.ctx
.drawImage(img
, x
, y
); };
646 that
.changeCursor = function(pixels
, mask
, hotx
, hoty
, w
, h
) {
647 var cur
= [], cmap
, rgb
, IHDRsz
, ANDsz
, XORsz
, url
, idx
, alpha
, x
, y
;
648 //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h);
650 if (conf
.cursor_uri
=== false) {
651 Util
.Warn("changeCursor called but no cursor data URI support");
655 cmap
= conf
.colourMap
;
658 XORsz
= Math
.ceil( (w
* h
) / 8.0 );
661 cur
.push16le(0); // Reserved
662 cur
.push16le(2); // .CUR type
663 cur
.push16le(1); // Number of images, 1 for non-animated ico
666 cur
.push(w
); // width
667 cur
.push(h
); // height
668 cur
.push(0); // colors, 0 -> true-color
669 cur
.push(0); // reserved
670 cur
.push16le(hotx
); // hotspot x coordinate
671 cur
.push16le(hoty
); // hotspot y coordinate
672 cur
.push32le(IHDRsz
+ XORsz
+ ANDsz
); // cursor data byte size
673 cur
.push32le(22); // offset of cursor data in the file
675 // Cursor #1 InfoHeader
676 cur
.push32le(IHDRsz
); // Infoheader size
677 cur
.push32le(w
); // Cursor width
678 cur
.push32le(h
*2); // XOR+AND height
679 cur
.push16le(1); // number of planes
680 cur
.push16le(32); // bits per pixel
681 cur
.push32le(0); // Type of compression
682 cur
.push32le(XORsz
+ ANDsz
); // Size of Image
689 for (y
= h
-1; y
>= 0; y
-= 1) {
690 for (x
= 0; x
< w
; x
+= 1) {
691 idx
= y
* Math
.ceil(w
/ 8) + Math
.floor(x
/8);
692 alpha
= (mask
[idx
] << (x
% 8)) & 0x80 ? 255 : 0;
694 if (conf
.true_color
) {
695 idx
= ((w
* y
) + x
) * 4;
696 cur
.push(pixels
[idx
+ 2]); // blue
697 cur
.push(pixels
[idx
+ 1]); // green
698 cur
.push(pixels
[idx
+ 0]); // red
699 cur
.push(alpha
); // red
702 rgb
= cmap
[pixels
[idx
]];
703 cur
.push(rgb
[2]); // blue
704 cur
.push(rgb
[1]); // green
705 cur
.push(rgb
[0]); // red
706 cur
.push(alpha
); // alpha
711 // AND/bitmask data (ignored, just needs to be right size)
712 for (y
= 0; y
< h
; y
+= 1) {
713 for (x
= 0; x
< Math
.ceil(w
/ 8); x
+= 1) {
718 url
= "data:image/x-icon;base64," + Base64
.encode(cur
);
719 conf
.target
.style
.cursor
= "url(" + url
+ ") " + hotx
+ " " + hoty
+ ", default";
720 //Util.Debug("<< changeCursor, cur.length: " + cur.length);
725 return constructor(); // Return the public API interface