2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2012 Joel Martin
4 * Copyright (C) 2015 Samuel Mannehed for Cendio AB
5 * Licensed under MPL 2.0 (see LICENSE.txt)
7 * See README.md for usage and integration instructions.
10 /*jslint browser: true, white: false */
11 /*global Util, Base64, changeCursor */
14 * import Util from "./util";
15 * import Base64 from "./base64";
18 /* [module] export default */ function Display(defaults
) {
20 this._c_forceCanvas
= false;
22 this._renderQ
= []; // queue drawing actions for in-oder rendering
23 this._flushing
= false;
25 // the full frame buffer (logical canvas) size
29 // the size limit of the viewport (start disabled)
33 // the visible "physical canvas" viewport
34 this._viewportLoc
= { 'x': 0, 'y': 0, 'w': 0, 'h': 0 };
35 this._cleanRect
= { 'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1 };
37 this._prevDrawStyle
= "";
39 this._tile16x16
= null;
43 Util
.set_defaults(this, defaults
, {
49 "onFlush": function () {},
52 Util
.Debug(">> Display.constructor");
55 throw new Error("Target must be set");
58 if (typeof this._target
=== 'string') {
59 throw new Error('target must be a DOM element');
62 if (!this._target
.getContext
) {
63 throw new Error("no getContext method");
67 this._drawCtx
= this._target
.getContext('2d');
70 Util
.Debug("User Agent: " + navigator
.userAgent
);
71 if (Util
.Engine
.gecko
) { Util
.Debug("Browser: gecko " + Util
.Engine
.gecko
); }
72 if (Util
.Engine
.webkit
) { Util
.Debug("Browser: webkit " + Util
.Engine
.webkit
); }
73 if (Util
.Engine
.trident
) { Util
.Debug("Browser: trident " + Util
.Engine
.trident
); }
74 if (Util
.Engine
.presto
) { Util
.Debug("Browser: presto " + Util
.Engine
.presto
); }
78 // Check canvas features
79 if ('createImageData' in this._drawCtx
) {
80 this._render_mode
= 'canvas rendering';
82 throw new Error("Canvas does not support createImageData");
85 if (this._prefer_js
=== null) {
86 Util
.Info("Prefering javascript operations");
87 this._prefer_js
= true;
90 // Determine browser support for setting the cursor via data URI scheme
91 if (this._cursor_uri
|| this._cursor_uri
=== null ||
92 this._cursor_uri
=== undefined) {
93 this._cursor_uri
= Util
.browserSupportsCursorURIs();
96 Util
.Debug("<< Display.constructor");
102 var SUPPORTS_IMAGEDATA_CONSTRUCTOR
= false;
104 new ImageData(new Uint8ClampedArray(4), 1, 1);
105 SUPPORTS_IMAGEDATA_CONSTRUCTOR
= true;
111 Display
.prototype = {
113 viewportChangePos: function (deltaX
, deltaY
) {
114 var vp
= this._viewportLoc
;
115 deltaX
= Math
.floor(deltaX
);
116 deltaY
= Math
.floor(deltaY
);
118 if (!this._viewport
) {
119 deltaX
= -vp
.w
; // clamped later of out of bounds
123 var vx2
= vp
.x
+ vp
.w
- 1;
124 var vy2
= vp
.y
+ vp
.h
- 1;
128 if (deltaX
< 0 && vp
.x
+ deltaX
< 0) {
131 if (vx2
+ deltaX
>= this._fb_width
) {
132 deltaX
-= vx2
+ deltaX
- this._fb_width
+ 1;
135 if (vp
.y
+ deltaY
< 0) {
138 if (vy2
+ deltaY
>= this._fb_height
) {
139 deltaY
-= (vy2
+ deltaY
- this._fb_height
+ 1);
142 if (deltaX
=== 0 && deltaY
=== 0) {
145 Util
.Debug("viewportChange deltaX: " + deltaX
+ ", deltaY: " + deltaY
);
152 // Update the clean rectangle
153 var cr
= this._cleanRect
;
169 // Shift viewport left, redraw left section
173 // Shift viewport right, redraw right section
180 // Shift viewport up, redraw top section
184 // Shift viewport down, redraw bottom section
189 var saveStyle
= this._drawCtx
.fillStyle
;
190 var canvas
= this._target
;
191 this._drawCtx
.fillStyle
= "rgb(255,255,255)";
193 // Due to this bug among others [1] we need to disable the image-smoothing to
194 // avoid getting a blur effect when panning.
196 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
198 // We need to set these every time since all properties are reset
199 // when the the size is changed
200 if (this._drawCtx
.mozImageSmoothingEnabled
) {
201 this._drawCtx
.mozImageSmoothingEnabled
= false;
202 } else if (this._drawCtx
.webkitImageSmoothingEnabled
) {
203 this._drawCtx
.webkitImageSmoothingEnabled
= false;
204 } else if (this._drawCtx
.msImageSmoothingEnabled
) {
205 this._drawCtx
.msImageSmoothingEnabled
= false;
206 } else if (this._drawCtx
.imageSmoothingEnabled
) {
207 this._drawCtx
.imageSmoothingEnabled
= false;
210 // Copy the valid part of the viewport to the shifted location
211 this._drawCtx
.drawImage(canvas
, 0, 0, vp
.w
, vp
.h
, -deltaX
, -deltaY
, vp
.w
, vp
.h
);
214 this._drawCtx
.fillRect(x1
, 0, w
, vp
.h
);
217 this._drawCtx
.fillRect(0, y1
, vp
.w
, h
);
219 this._drawCtx
.fillStyle
= saveStyle
;
222 viewportChangeSize: function(width
, height
) {
224 if (typeof(width
) === "undefined" || typeof(height
) === "undefined") {
226 Util
.Debug("Setting viewport to full display region");
227 width
= this._fb_width
;
228 height
= this._fb_height
;
231 var vp
= this._viewportLoc
;
232 if (vp
.w
!== width
|| vp
.h
!== height
) {
234 if (this._viewport
) {
235 if (this._maxWidth
!== 0 && width
> this._maxWidth
) {
236 width
= this._maxWidth
;
238 if (this._maxHeight
!== 0 && height
> this._maxHeight
) {
239 height
= this._maxHeight
;
243 var cr
= this._cleanRect
;
245 if (width
< vp
.w
&& cr
.x2
> vp
.x
+ width
- 1) {
246 cr
.x2
= vp
.x
+ width
- 1;
248 if (height
< vp
.h
&& cr
.y2
> vp
.y
+ height
- 1) {
249 cr
.y2
= vp
.y
+ height
- 1;
255 var canvas
= this._target
;
256 if (canvas
.width
!== width
|| canvas
.height
!== height
) {
258 // We have to save the canvas data since changing the size will clear it
260 if (vp
.w
> 0 && vp
.h
> 0 && canvas
.width
> 0 && canvas
.height
> 0) {
261 var img_width
= canvas
.width
< vp
.w
? canvas
.width
: vp
.w
;
262 var img_height
= canvas
.height
< vp
.h
? canvas
.height
: vp
.h
;
263 saveImg
= this._drawCtx
.getImageData(0, 0, img_width
, img_height
);
266 if (canvas
.width
!== width
) {
267 canvas
.width
= width
;
268 canvas
.style
.width
= width
+ 'px';
270 if (canvas
.height
!== height
) {
271 canvas
.height
= height
;
272 canvas
.style
.height
= height
+ 'px';
276 this._drawCtx
.putImageData(saveImg
, 0, 0);
282 // Return a map of clean and dirty areas of the viewport and reset the
283 // tracking of clean and dirty areas
285 // Returns: { 'cleanBox': { 'x': x, 'y': y, 'w': w, 'h': h},
286 // 'dirtyBoxes': [{ 'x': x, 'y': y, 'w': w, 'h': h }, ...] }
287 getCleanDirtyReset: function () {
288 var vp
= this._viewportLoc
;
289 var cr
= this._cleanRect
;
291 var cleanBox
= { 'x': cr
.x1
, 'y': cr
.y1
,
292 'w': cr
.x2
- cr
.x1
+ 1, 'h': cr
.y2
- cr
.y1
+ 1 };
295 if (cr
.x1
>= cr
.x2
|| cr
.y1
>= cr
.y2
) {
296 // Whole viewport is dirty
297 dirtyBoxes
.push({ 'x': vp
.x
, 'y': vp
.y
, 'w': vp
.w
, 'h': vp
.h
});
299 // Redraw dirty regions
300 var vx2
= vp
.x
+ vp
.w
- 1;
301 var vy2
= vp
.y
+ vp
.h
- 1;
304 // left side dirty region
305 dirtyBoxes
.push({'x': vp
.x
, 'y': vp
.y
,
306 'w': cr
.x1
- vp
.x
+ 1, 'h': vp
.h
});
309 // right side dirty region
310 dirtyBoxes
.push({'x': cr
.x2
+ 1, 'y': vp
.y
,
311 'w': vx2
- cr
.x2
, 'h': vp
.h
});
314 // top/middle dirty region
315 dirtyBoxes
.push({'x': cr
.x1
, 'y': vp
.y
,
316 'w': cr
.x2
- cr
.x1
+ 1, 'h': cr
.y1
- vp
.y
});
319 // bottom/middle dirty region
320 dirtyBoxes
.push({'x': cr
.x1
, 'y': cr
.y2
+ 1,
321 'w': cr
.x2
- cr
.x1
+ 1, 'h': vy2
- cr
.y2
});
325 this._cleanRect
= {'x1': vp
.x
, 'y1': vp
.y
,
326 'x2': vp
.x
+ vp
.w
- 1, 'y2': vp
.y
+ vp
.h
- 1};
328 return {'cleanBox': cleanBox
, 'dirtyBoxes': dirtyBoxes
};
332 return x
+ this._viewportLoc
.x
;
336 return y
+ this._viewportLoc
.y
;
339 resize: function (width
, height
) {
340 this._prevDrawStyle
= "";
342 this._fb_width
= width
;
343 this._fb_height
= height
;
345 this._rescale(this._scale
);
347 this.viewportChangeSize();
352 this.resize(this._logo
.width
, this._logo
.height
);
353 this.blitStringImage(this._logo
.data
, 0, 0);
355 if (Util
.Engine
.trident
=== 6) {
356 // NB(directxman12): there's a bug in IE10 where we can fail to actually
357 // clear the canvas here because of the resize.
358 // Clearing the current viewport first fixes the issue
359 this._drawCtx
.clearRect(0, 0, this._viewportLoc
.w
, this._viewportLoc
.h
);
361 this.resize(240, 20);
362 this._drawCtx
.clearRect(0, 0, this._viewportLoc
.w
, this._viewportLoc
.h
);
368 pending: function() {
369 return this._renderQ
.length
> 0;
373 if (this._renderQ
.length
=== 0) {
376 this._flushing
= true;
380 fillRect: function (x
, y
, width
, height
, color
, from_queue
) {
381 if (this._renderQ
.length
!== 0 && !from_queue
) {
391 this._setFillColor(color
);
392 this._drawCtx
.fillRect(x
- this._viewportLoc
.x
, y
- this._viewportLoc
.y
, width
, height
);
396 copyImage: function (old_x
, old_y
, new_x
, new_y
, w
, h
, from_queue
) {
397 if (this._renderQ
.length
!== 0 && !from_queue
) {
408 var x1
= old_x
- this._viewportLoc
.x
;
409 var y1
= old_y
- this._viewportLoc
.y
;
410 var x2
= new_x
- this._viewportLoc
.x
;
411 var y2
= new_y
- this._viewportLoc
.y
;
413 this._drawCtx
.drawImage(this._target
, x1
, y1
, w
, h
, x2
, y2
, w
, h
);
417 imageRect: function(x
, y
, mime
, arr
) {
418 var img
= new Image();
419 img
.src
= "data: " + mime
+ ";base64," + Base64
.encode(arr
);
428 // start updating a tile
429 startTile: function (x
, y
, width
, height
, color
) {
432 if (width
=== 16 && height
=== 16) {
433 this._tile
= this._tile16x16
;
435 this._tile
= this._drawCtx
.createImageData(width
, height
);
438 if (this._prefer_js
) {
440 if (this._true_color
) {
443 bgr
= this._colourMap
[color
[0]];
449 var data
= this._tile
.data
;
450 for (var i
= 0; i
< width
* height
* 4; i
+= 4) {
457 this.fillRect(x
, y
, width
, height
, color
, true);
461 // update sub-rectangle of the current tile
462 subTile: function (x
, y
, w
, h
, color
) {
463 if (this._prefer_js
) {
465 if (this._true_color
) {
468 bgr
= this._colourMap
[color
[0]];
476 var data
= this._tile
.data
;
477 var width
= this._tile
.width
;
478 for (var j
= y
; j
< yend
; j
++) {
479 for (var i
= x
; i
< xend
; i
++) {
480 var p
= (i
+ (j
* width
)) * 4;
488 this.fillRect(this._tile_x
+ x
, this._tile_y
+ y
, w
, h
, color
, true);
492 // draw the current tile to the screen
493 finishTile: function () {
494 if (this._prefer_js
) {
495 this._drawCtx
.putImageData(this._tile
, this._tile_x
- this._viewportLoc
.x
,
496 this._tile_y
- this._viewportLoc
.y
);
498 // else: No-op -- already done by setSubTile
501 blitImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
502 if (this._renderQ
.length
!== 0 && !from_queue
) {
503 // NB(directxman12): it's technically more performant here to use preallocated arrays,
504 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
505 // this probably isn't getting called *nearly* as much
506 var new_arr
= new Uint8Array(width
* height
* 4);
507 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
516 } else if (this._true_color
) {
517 this._bgrxImageData(x
, y
, this._viewportLoc
.x
, this._viewportLoc
.y
, width
, height
, arr
, offset
);
519 this._cmapImageData(x
, y
, this._viewportLoc
.x
, this._viewportLoc
.y
, width
, height
, arr
, offset
);
523 blitRgbImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
524 if (this._renderQ
.length
!== 0 && !from_queue
) {
525 // NB(directxman12): it's technically more performant here to use preallocated arrays,
526 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
527 // this probably isn't getting called *nearly* as much
528 var new_arr
= new Uint8Array(width
* height
* 3);
529 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
538 } else if (this._true_color
) {
539 this._rgbImageData(x
, y
, this._viewportLoc
.x
, this._viewportLoc
.y
, width
, height
, arr
, offset
);
542 this._cmapImageData(x
, y
, this._viewportLoc
.x
, this._viewportLoc
.y
, width
, height
, arr
, offset
);
546 blitRgbxImage: function (x
, y
, width
, height
, arr
, offset
, from_queue
) {
547 if (this._renderQ
.length
!== 0 && !from_queue
) {
548 // NB(directxman12): it's technically more performant here to use preallocated arrays,
549 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
550 // this probably isn't getting called *nearly* as much
551 var new_arr
= new Uint8Array(width
* height
* 4);
552 new_arr
.set(new Uint8Array(arr
.buffer
, 0, new_arr
.length
));
562 this._rgbxImageData(x
, y
, this._viewportLoc
.x
, this._viewportLoc
.y
, width
, height
, arr
, offset
);
566 blitStringImage: function (str
, x
, y
) {
567 var img
= new Image();
568 img
.onload = function () {
569 this._drawCtx
.drawImage(img
, x
- this._viewportLoc
.x
, y
- this._viewportLoc
.y
);
572 return img
; // for debugging purposes
575 // wrap ctx.drawImage but relative to viewport
576 drawImage: function (img
, x
, y
) {
577 this._drawCtx
.drawImage(img
, x
- this._viewportLoc
.x
, y
- this._viewportLoc
.y
);
580 changeCursor: function (pixels
, mask
, hotx
, hoty
, w
, h
) {
581 if (this._cursor_uri
=== false) {
582 Util
.Warn("changeCursor called but no cursor data URI support");
586 if (this._true_color
) {
587 Display
.changeCursor(this._target
, pixels
, mask
, hotx
, hoty
, w
, h
);
589 Display
.changeCursor(this._target
, pixels
, mask
, hotx
, hoty
, w
, h
, this._colourMap
);
593 defaultCursor: function () {
594 this._target
.style
.cursor
= "default";
597 disableLocalCursor: function () {
598 this._target
.style
.cursor
= "none";
601 clippingDisplay: function () {
602 var vp
= this._viewportLoc
;
604 var fbClip
= this._fb_width
> vp
.w
|| this._fb_height
> vp
.h
;
605 var limitedVp
= this._maxWidth
!== 0 && this._maxHeight
!== 0;
606 var clipping
= false;
609 clipping
= vp
.w
> this._maxWidth
|| vp
.h
> this._maxHeight
;
612 return fbClip
|| (limitedVp
&& clipping
);
615 // Overridden getters/setters
616 get_context: function () {
617 return this._drawCtx
;
620 set_scale: function (scale
) {
621 this._rescale(scale
);
624 set_width: function (w
) {
627 get_width: function () {
628 return this._fb_width
;
631 set_height: function (h
) {
634 get_height: function () {
635 return this._fb_height
;
638 autoscale: function (containerWidth
, containerHeight
, downscaleOnly
) {
639 var targetAspectRatio
= containerWidth
/ containerHeight
;
640 var fbAspectRatio
= this._fb_width
/ this._fb_height
;
643 if (fbAspectRatio
>= targetAspectRatio
) {
644 scaleRatio
= containerWidth
/ this._fb_width
;
646 scaleRatio
= containerHeight
/ this._fb_height
;
649 var targetW
, targetH
;
650 if (scaleRatio
> 1.0 && downscaleOnly
) {
651 targetW
= this._fb_width
;
652 targetH
= this._fb_height
;
654 } else if (fbAspectRatio
>= targetAspectRatio
) {
655 targetW
= containerWidth
;
656 targetH
= Math
.round(containerWidth
/ fbAspectRatio
);
658 targetW
= Math
.round(containerHeight
* fbAspectRatio
);
659 targetH
= containerHeight
;
662 // NB(directxman12): If you set the width directly, or set the
663 // style width to a number, the canvas is cleared.
664 // However, if you set the style width to a string
665 // ('NNNpx'), the canvas is scaled without clearing.
666 this._target
.style
.width
= targetW
+ 'px';
667 this._target
.style
.height
= targetH
+ 'px';
669 this._scale
= scaleRatio
;
671 return scaleRatio
; // so that the mouse, etc scale can be set
675 _rescale: function (factor
) {
676 this._scale
= factor
;
681 if (this._viewport
&&
682 this._maxWidth
!== 0 && this._maxHeight
!== 0) {
683 w
= Math
.min(this._fb_width
, this._maxWidth
);
684 h
= Math
.min(this._fb_height
, this._maxHeight
);
690 this._target
.style
.width
= Math
.round(factor
* w
) + 'px';
691 this._target
.style
.height
= Math
.round(factor
* h
) + 'px';
694 _setFillColor: function (color
) {
696 if (this._true_color
) {
699 bgr
= this._colourMap
[color
];
702 var newStyle
= 'rgb(' + bgr
[2] + ',' + bgr
[1] + ',' + bgr
[0] + ')';
703 if (newStyle
!== this._prevDrawStyle
) {
704 this._drawCtx
.fillStyle
= newStyle
;
705 this._prevDrawStyle
= newStyle
;
709 _rgbImageData: function (x
, y
, vx
, vy
, width
, height
, arr
, offset
) {
710 var img
= this._drawCtx
.createImageData(width
, height
);
712 for (var i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 3) {
714 data
[i
+ 1] = arr
[j
+ 1];
715 data
[i
+ 2] = arr
[j
+ 2];
716 data
[i
+ 3] = 255; // Alpha
718 this._drawCtx
.putImageData(img
, x
- vx
, y
- vy
);
721 _bgrxImageData: function (x
, y
, vx
, vy
, width
, height
, arr
, offset
) {
722 var img
= this._drawCtx
.createImageData(width
, height
);
724 for (var i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
+= 4) {
725 data
[i
] = arr
[j
+ 2];
726 data
[i
+ 1] = arr
[j
+ 1];
727 data
[i
+ 2] = arr
[j
];
728 data
[i
+ 3] = 255; // Alpha
730 this._drawCtx
.putImageData(img
, x
- vx
, y
- vy
);
733 _rgbxImageData: function (x
, y
, vx
, vy
, width
, height
, arr
, offset
) {
734 // NB(directxman12): arr must be an Type Array view
736 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR
) {
737 img
= new ImageData(new Uint8ClampedArray(arr
.buffer
, arr
.byteOffset
, width
* height
* 4), width
, height
);
739 img
= this._drawCtx
.createImageData(width
, height
);
740 img
.data
.set(new Uint8ClampedArray(arr
.buffer
, arr
.byteOffset
, width
* height
* 4));
742 this._drawCtx
.putImageData(img
, x
- vx
, y
- vy
);
745 _cmapImageData: function (x
, y
, vx
, vy
, width
, height
, arr
, offset
) {
746 var img
= this._drawCtx
.createImageData(width
, height
);
748 var cmap
= this._colourMap
;
749 for (var i
= 0, j
= offset
; i
< width
* height
* 4; i
+= 4, j
++) {
750 var bgr
= cmap
[arr
[j
]];
752 data
[i
+ 1] = bgr
[1];
753 data
[i
+ 2] = bgr
[0];
754 data
[i
+ 3] = 255; // Alpha
756 this._drawCtx
.putImageData(img
, x
- vx
, y
- vy
);
759 _renderQ_push: function (action
) {
760 this._renderQ
.push(action
);
761 if (this._renderQ
.length
=== 1) {
762 // If this can be rendered immediately it will be, otherwise
763 // the scanner will wait for the relevant event
764 this._scan_renderQ();
768 _resume_renderQ: function() {
769 // "this" is the object that is ready, not the
771 this.removeEventListener('load', this._noVNC_display
._resume_renderQ
);
772 this._noVNC_display
._scan_renderQ();
775 _scan_renderQ: function () {
777 while (ready
&& this._renderQ
.length
> 0) {
778 var a
= this._renderQ
[0];
781 this.copyImage(a
.old_x
, a
.old_y
, a
.x
, a
.y
, a
.width
, a
.height
, true);
784 this.fillRect(a
.x
, a
.y
, a
.width
, a
.height
, a
.color
, true);
787 this.blitImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
790 this.blitRgbImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
793 this.blitRgbxImage(a
.x
, a
.y
, a
.width
, a
.height
, a
.data
, 0, true);
796 if (a
.img
.complete
) {
797 this.drawImage(a
.img
, a
.x
, a
.y
);
799 a
.img
._noVNC_display
= this;
800 a
.img
.addEventListener('load', this._resume_renderQ
);
801 // We need to wait for this image to 'load'
802 // to keep things in-order
809 this._renderQ
.shift();
813 if (this._renderQ
.length
=== 0 && this._flushing
) {
814 this._flushing
= false;
820 Util
.make_properties(Display
, [
821 ['target', 'wo', 'dom'], // Canvas element for rendering
822 ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only)
823 ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "data": data}
824 ['true_color', 'rw', 'bool'], // Use true-color pixel data
825 ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color)
826 ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0
827 ['viewport', 'rw', 'bool'], // Use viewport clipping
828 ['width', 'rw', 'int'], // Display area width
829 ['height', 'rw', 'int'], // Display area height
830 ['maxWidth', 'rw', 'int'], // Viewport max width (0 if disabled)
831 ['maxHeight', 'rw', 'int'], // Viewport max height (0 if disabled)
833 ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only)
835 ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods
836 ['cursor_uri', 'rw', 'raw'], // Can we render cursor using data URI
838 ['onFlush', 'rw', 'func'], // onFlush(): A flush request has finished
842 Display
.changeCursor = function (target
, pixels
, mask
, hotx
, hoty
, w0
, h0
, cmap
) {
846 h
= w
; // increase h to make it square
848 w
= h
; // increase w to make it square
853 // Push multi-byte little-endian values
854 cur
.push16le = function (num
) {
855 this.push(num
& 0xFF, (num
>> 8) & 0xFF);
857 cur
.push32le = function (num
) {
858 this.push(num
& 0xFF,
865 var RGBsz
= w
* h
* 4;
866 var XORsz
= Math
.ceil((w
* h
) / 8.0);
867 var ANDsz
= Math
.ceil((w
* h
) / 8.0);
869 cur
.push16le(0); // 0: Reserved
870 cur
.push16le(2); // 2: .CUR type
871 cur
.push16le(1); // 4: Number of images, 1 for non-animated ico
873 // Cursor #1 header (ICONDIRENTRY)
874 cur
.push(w
); // 6: width
875 cur
.push(h
); // 7: height
876 cur
.push(0); // 8: colors, 0 -> true-color
877 cur
.push(0); // 9: reserved
878 cur
.push16le(hotx
); // 10: hotspot x coordinate
879 cur
.push16le(hoty
); // 12: hotspot y coordinate
880 cur
.push32le(IHDRsz
+ RGBsz
+ XORsz
+ ANDsz
);
881 // 14: cursor data byte size
882 cur
.push32le(22); // 18: offset of cursor data in the file
884 // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
885 cur
.push32le(IHDRsz
); // 22: InfoHeader size
886 cur
.push32le(w
); // 26: Cursor width
887 cur
.push32le(h
* 2); // 30: XOR+AND height
888 cur
.push16le(1); // 34: number of planes
889 cur
.push16le(32); // 36: bits per pixel
890 cur
.push32le(0); // 38: Type of compression
892 cur
.push32le(XORsz
+ ANDsz
);
894 cur
.push32le(0); // 46: reserved
895 cur
.push32le(0); // 50: reserved
896 cur
.push32le(0); // 54: reserved
897 cur
.push32le(0); // 58: reserved
899 // 62: color data (RGBQUAD icColors[])
901 for (y
= h
- 1; y
>= 0; y
--) {
902 for (x
= 0; x
< w
; x
++) {
903 if (x
>= w0
|| y
>= h0
) {
905 cur
.push(0); // green
907 cur
.push(0); // alpha
909 var idx
= y
* Math
.ceil(w0
/ 8) + Math
.floor(x
/ 8);
910 var alpha
= (mask
[idx
] << (x
% 8)) & 0x80 ? 255 : 0;
913 var rgb
= cmap
[pixels
[idx
]];
914 cur
.push(rgb
[2]); // blue
915 cur
.push(rgb
[1]); // green
916 cur
.push(rgb
[0]); // red
917 cur
.push(alpha
); // alpha
919 idx
= ((w0
* y
) + x
) * 4;
920 cur
.push(pixels
[idx
+ 2]); // blue
921 cur
.push(pixels
[idx
+ 1]); // green
922 cur
.push(pixels
[idx
]); // red
923 cur
.push(alpha
); // alpha
929 // XOR/bitmask data (BYTE icXOR[])
930 // (ignored, just needs to be the right size)
931 for (y
= 0; y
< h
; y
++) {
932 for (x
= 0; x
< Math
.ceil(w
/ 8); x
++) {
937 // AND/bitmask data (BYTE icAND[])
938 // (ignored, just needs to be the right size)
939 for (y
= 0; y
< h
; y
++) {
940 for (x
= 0; x
< Math
.ceil(w
/ 8); x
++) {
945 var url
= 'data:image/x-icon;base64,' + Base64
.encode(cur
);
946 target
.style
.cursor
= 'url(' + url
+ ')' + hotx
+ ' ' + hoty
+ ', default';