]> git.proxmox.com Git - mirror_novnc.git/blob - core/display.js
Refactor ES6 module structure/split up Util
[mirror_novnc.git] / core / display.js
1 /*
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)
6 *
7 * See README.md for usage and integration instructions.
8 */
9
10 /*jslint browser: true, white: false */
11 /*global Util, Base64, changeCursor */
12
13 import { Engine, browserSupportsCursorURIs as cursorURIsSupported } from './util/browsers.js';
14 import { set_defaults, make_properties } from './util/properties.js';
15 import * as Log from './util/logging.js';
16 import Base64 from "./base64.js";
17
18 export default function Display(defaults) {
19 this._drawCtx = null;
20 this._c_forceCanvas = false;
21
22 this._renderQ = []; // queue drawing actions for in-oder rendering
23 this._flushing = false;
24
25 // the full frame buffer (logical canvas) size
26 this._fb_width = 0;
27 this._fb_height = 0;
28
29 this._prevDrawStyle = "";
30 this._tile = null;
31 this._tile16x16 = null;
32 this._tile_x = 0;
33 this._tile_y = 0;
34
35 set_defaults(this, defaults, {
36 'true_color': true,
37 'colourMap': [],
38 'scale': 1.0,
39 'viewport': false,
40 'render_mode': '',
41 "onFlush": function () {},
42 });
43
44 Log.Debug(">> Display.constructor");
45
46 // The visible canvas
47 if (!this._target) {
48 throw new Error("Target must be set");
49 }
50
51 if (typeof this._target === 'string') {
52 throw new Error('target must be a DOM element');
53 }
54
55 if (!this._target.getContext) {
56 throw new Error("no getContext method");
57 }
58
59 this._targetCtx = this._target.getContext('2d');
60
61 // the visible canvas viewport (i.e. what actually gets seen)
62 this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };
63
64 // The hidden canvas, where we do the actual rendering
65 this._backbuffer = document.createElement('canvas');
66 this._drawCtx = this._backbuffer.getContext('2d');
67
68 this._damageBounds = { left:0, top:0,
69 right: this._backbuffer.width,
70 bottom: this._backbuffer.height };
71
72 Log.Debug("User Agent: " + navigator.userAgent);
73 if (Engine.gecko) { Log.Debug("Browser: gecko " + Engine.gecko); }
74 if (Engine.webkit) { Log.Debug("Browser: webkit " + Engine.webkit); }
75 if (Engine.trident) { Log.Debug("Browser: trident " + Engine.trident); }
76 if (Engine.presto) { Log.Debug("Browser: presto " + Engine.presto); }
77
78 this.clear();
79
80 // Check canvas features
81 if ('createImageData' in this._drawCtx) {
82 this._render_mode = 'canvas rendering';
83 } else {
84 throw new Error("Canvas does not support createImageData");
85 }
86
87 if (this._prefer_js === null) {
88 Log.Info("Prefering javascript operations");
89 this._prefer_js = true;
90 }
91
92 // Determine browser support for setting the cursor via data URI scheme
93 if (this._cursor_uri || this._cursor_uri === null ||
94 this._cursor_uri === undefined) {
95 this._cursor_uri = cursorURIsSupported();
96 }
97
98 Log.Debug("<< Display.constructor");
99 };
100
101 var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false;
102 try {
103 new ImageData(new Uint8ClampedArray(4), 1, 1);
104 SUPPORTS_IMAGEDATA_CONSTRUCTOR = true;
105 } catch (ex) {
106 // ignore failure
107 }
108
109 Display.prototype = {
110 // Public methods
111 viewportChangePos: function (deltaX, deltaY) {
112 var vp = this._viewportLoc;
113 deltaX = Math.floor(deltaX);
114 deltaY = Math.floor(deltaY);
115
116 if (!this._viewport) {
117 deltaX = -vp.w; // clamped later of out of bounds
118 deltaY = -vp.h;
119 }
120
121 var vx2 = vp.x + vp.w - 1;
122 var vy2 = vp.y + vp.h - 1;
123
124 // Position change
125
126 if (deltaX < 0 && vp.x + deltaX < 0) {
127 deltaX = -vp.x;
128 }
129 if (vx2 + deltaX >= this._fb_width) {
130 deltaX -= vx2 + deltaX - this._fb_width + 1;
131 }
132
133 if (vp.y + deltaY < 0) {
134 deltaY = -vp.y;
135 }
136 if (vy2 + deltaY >= this._fb_height) {
137 deltaY -= (vy2 + deltaY - this._fb_height + 1);
138 }
139
140 if (deltaX === 0 && deltaY === 0) {
141 return;
142 }
143 Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
144
145 vp.x += deltaX;
146 vp.y += deltaY;
147
148 this._damage(vp.x, vp.y, vp.w, vp.h);
149
150 this.flip();
151 },
152
153 viewportChangeSize: function(width, height) {
154
155 if (!this._viewport ||
156 typeof(width) === "undefined" ||
157 typeof(height) === "undefined") {
158
159 Log.Debug("Setting viewport to full display region");
160 width = this._fb_width;
161 height = this._fb_height;
162 }
163
164 if (width > this._fb_width) {
165 width = this._fb_width;
166 }
167 if (height > this._fb_height) {
168 height = this._fb_height;
169 }
170
171 var vp = this._viewportLoc;
172 if (vp.w !== width || vp.h !== height) {
173 vp.w = width;
174 vp.h = height;
175
176 var canvas = this._target;
177 canvas.width = width;
178 canvas.height = height;
179
180 // The position might need to be updated if we've grown
181 this.viewportChangePos(0, 0);
182
183 this._damage(vp.x, vp.y, vp.w, vp.h);
184 this.flip();
185
186 // Update the visible size of the target canvas
187 this._rescale(this._scale);
188 }
189 },
190
191 absX: function (x) {
192 return x / this._scale + this._viewportLoc.x;
193 },
194
195 absY: function (y) {
196 return y / this._scale + this._viewportLoc.y;
197 },
198
199 resize: function (width, height) {
200 this._prevDrawStyle = "";
201
202 this._fb_width = width;
203 this._fb_height = height;
204
205 var canvas = this._backbuffer;
206 if (canvas.width !== width || canvas.height !== height) {
207
208 // We have to save the canvas data since changing the size will clear it
209 var saveImg = null;
210 if (canvas.width > 0 && canvas.height > 0) {
211 saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);
212 }
213
214 if (canvas.width !== width) {
215 canvas.width = width;
216 }
217 if (canvas.height !== height) {
218 canvas.height = height;
219 }
220
221 if (saveImg) {
222 this._drawCtx.putImageData(saveImg, 0, 0);
223 }
224 }
225
226 // Readjust the viewport as it may be incorrectly sized
227 // and positioned
228 var vp = this._viewportLoc;
229 this.viewportChangeSize(vp.w, vp.h);
230 this.viewportChangePos(0, 0);
231 },
232
233 // Track what parts of the visible canvas that need updating
234 _damage: function(x, y, w, h) {
235 if (x < this._damageBounds.left) {
236 this._damageBounds.left = x;
237 }
238 if (y < this._damageBounds.top) {
239 this._damageBounds.top = y;
240 }
241 if ((x + w) > this._damageBounds.right) {
242 this._damageBounds.right = x + w;
243 }
244 if ((y + h) > this._damageBounds.bottom) {
245 this._damageBounds.bottom = y + h;
246 }
247 },
248
249 // Update the visible canvas with the contents of the
250 // rendering canvas
251 flip: function(from_queue) {
252 if (this._renderQ.length !== 0 && !from_queue) {
253 this._renderQ_push({
254 'type': 'flip'
255 });
256 } else {
257 var x, y, vx, vy, w, h;
258
259 x = this._damageBounds.left;
260 y = this._damageBounds.top;
261 w = this._damageBounds.right - x;
262 h = this._damageBounds.bottom - y;
263
264 vx = x - this._viewportLoc.x;
265 vy = y - this._viewportLoc.y;
266
267 if (vx < 0) {
268 w += vx;
269 x -= vx;
270 vx = 0;
271 }
272 if (vy < 0) {
273 h += vy;
274 y -= vy;
275 vy = 0;
276 }
277
278 if ((vx + w) > this._viewportLoc.w) {
279 w = this._viewportLoc.w - vx;
280 }
281 if ((vy + h) > this._viewportLoc.h) {
282 h = this._viewportLoc.h - vy;
283 }
284
285 if ((w > 0) && (h > 0)) {
286 // FIXME: We may need to disable image smoothing here
287 // as well (see copyImage()), but we haven't
288 // noticed any problem yet.
289 this._targetCtx.drawImage(this._backbuffer,
290 x, y, w, h,
291 vx, vy, w, h);
292 }
293
294 this._damageBounds.left = this._damageBounds.top = 65535;
295 this._damageBounds.right = this._damageBounds.bottom = 0;
296 }
297 },
298
299 clear: function () {
300 if (this._logo) {
301 this.resize(this._logo.width, this._logo.height);
302 this.imageRect(0, 0, this._logo.type, this._logo.data);
303 } else {
304 this.resize(240, 20);
305 this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height);
306 }
307 this.flip();
308 },
309
310 pending: function() {
311 return this._renderQ.length > 0;
312 },
313
314 flush: function() {
315 if (this._renderQ.length === 0) {
316 this._onFlush();
317 } else {
318 this._flushing = true;
319 }
320 },
321
322 fillRect: function (x, y, width, height, color, from_queue) {
323 if (this._renderQ.length !== 0 && !from_queue) {
324 this._renderQ_push({
325 'type': 'fill',
326 'x': x,
327 'y': y,
328 'width': width,
329 'height': height,
330 'color': color
331 });
332 } else {
333 this._setFillColor(color);
334 this._drawCtx.fillRect(x, y, width, height);
335 this._damage(x, y, width, height);
336 }
337 },
338
339 copyImage: function (old_x, old_y, new_x, new_y, w, h, from_queue) {
340 if (this._renderQ.length !== 0 && !from_queue) {
341 this._renderQ_push({
342 'type': 'copy',
343 'old_x': old_x,
344 'old_y': old_y,
345 'x': new_x,
346 'y': new_y,
347 'width': w,
348 'height': h,
349 });
350 } else {
351 // Due to this bug among others [1] we need to disable the image-smoothing to
352 // avoid getting a blur effect when copying data.
353 //
354 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
355 //
356 // We need to set these every time since all properties are reset
357 // when the the size is changed
358 this._drawCtx.mozImageSmoothingEnabled = false;
359 this._drawCtx.webkitImageSmoothingEnabled = false;
360 this._drawCtx.msImageSmoothingEnabled = false;
361 this._drawCtx.imageSmoothingEnabled = false;
362
363 this._drawCtx.drawImage(this._backbuffer,
364 old_x, old_y, w, h,
365 new_x, new_y, w, h);
366 this._damage(new_x, new_y, w, h);
367 }
368 },
369
370 imageRect: function(x, y, mime, arr) {
371 var img = new Image();
372 img.src = "data: " + mime + ";base64," + Base64.encode(arr);
373 this._renderQ_push({
374 'type': 'img',
375 'img': img,
376 'x': x,
377 'y': y
378 });
379 },
380
381 // start updating a tile
382 startTile: function (x, y, width, height, color) {
383 this._tile_x = x;
384 this._tile_y = y;
385 if (width === 16 && height === 16) {
386 this._tile = this._tile16x16;
387 } else {
388 this._tile = this._drawCtx.createImageData(width, height);
389 }
390
391 if (this._prefer_js) {
392 var bgr;
393 if (this._true_color) {
394 bgr = color;
395 } else {
396 bgr = this._colourMap[color[0]];
397 }
398 var red = bgr[2];
399 var green = bgr[1];
400 var blue = bgr[0];
401
402 var data = this._tile.data;
403 for (var i = 0; i < width * height * 4; i += 4) {
404 data[i] = red;
405 data[i + 1] = green;
406 data[i + 2] = blue;
407 data[i + 3] = 255;
408 }
409 } else {
410 this.fillRect(x, y, width, height, color, true);
411 }
412 },
413
414 // update sub-rectangle of the current tile
415 subTile: function (x, y, w, h, color) {
416 if (this._prefer_js) {
417 var bgr;
418 if (this._true_color) {
419 bgr = color;
420 } else {
421 bgr = this._colourMap[color[0]];
422 }
423 var red = bgr[2];
424 var green = bgr[1];
425 var blue = bgr[0];
426 var xend = x + w;
427 var yend = y + h;
428
429 var data = this._tile.data;
430 var width = this._tile.width;
431 for (var j = y; j < yend; j++) {
432 for (var i = x; i < xend; i++) {
433 var p = (i + (j * width)) * 4;
434 data[p] = red;
435 data[p + 1] = green;
436 data[p + 2] = blue;
437 data[p + 3] = 255;
438 }
439 }
440 } else {
441 this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color, true);
442 }
443 },
444
445 // draw the current tile to the screen
446 finishTile: function () {
447 if (this._prefer_js) {
448 this._drawCtx.putImageData(this._tile, this._tile_x, this._tile_y);
449 this._damage(this._tile_x, this._tile_y,
450 this._tile.width, this._tile.height);
451 }
452 // else: No-op -- already done by setSubTile
453 },
454
455 blitImage: function (x, y, width, height, arr, offset, from_queue) {
456 if (this._renderQ.length !== 0 && !from_queue) {
457 // NB(directxman12): it's technically more performant here to use preallocated arrays,
458 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
459 // this probably isn't getting called *nearly* as much
460 var new_arr = new Uint8Array(width * height * 4);
461 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
462 this._renderQ_push({
463 'type': 'blit',
464 'data': new_arr,
465 'x': x,
466 'y': y,
467 'width': width,
468 'height': height,
469 });
470 } else if (this._true_color) {
471 this._bgrxImageData(x, y, width, height, arr, offset);
472 } else {
473 this._cmapImageData(x, y, width, height, arr, offset);
474 }
475 },
476
477 blitRgbImage: function (x, y , width, height, arr, offset, from_queue) {
478 if (this._renderQ.length !== 0 && !from_queue) {
479 // NB(directxman12): it's technically more performant here to use preallocated arrays,
480 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
481 // this probably isn't getting called *nearly* as much
482 var new_arr = new Uint8Array(width * height * 3);
483 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
484 this._renderQ_push({
485 'type': 'blitRgb',
486 'data': new_arr,
487 'x': x,
488 'y': y,
489 'width': width,
490 'height': height,
491 });
492 } else if (this._true_color) {
493 this._rgbImageData(x, y, width, height, arr, offset);
494 } else {
495 // probably wrong?
496 this._cmapImageData(x, y, width, height, arr, offset);
497 }
498 },
499
500 blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) {
501 if (this._renderQ.length !== 0 && !from_queue) {
502 // NB(directxman12): it's technically more performant here to use preallocated arrays,
503 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
504 // this probably isn't getting called *nearly* as much
505 var new_arr = new Uint8Array(width * height * 4);
506 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
507 this._renderQ_push({
508 'type': 'blitRgbx',
509 'data': new_arr,
510 'x': x,
511 'y': y,
512 'width': width,
513 'height': height,
514 });
515 } else {
516 this._rgbxImageData(x, y, width, height, arr, offset);
517 }
518 },
519
520 drawImage: function (img, x, y) {
521 this._drawCtx.drawImage(img, x, y);
522 this._damage(x, y, img.width, img.height);
523 },
524
525 changeCursor: function (pixels, mask, hotx, hoty, w, h) {
526 if (this._cursor_uri === false) {
527 Log.Warn("changeCursor called but no cursor data URI support");
528 return;
529 }
530
531 if (this._true_color) {
532 Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h);
533 } else {
534 Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap);
535 }
536 },
537
538 defaultCursor: function () {
539 this._target.style.cursor = "default";
540 },
541
542 disableLocalCursor: function () {
543 this._target.style.cursor = "none";
544 },
545
546 clippingDisplay: function () {
547 var vp = this._viewportLoc;
548 return this._fb_width > vp.w || this._fb_height > vp.h;
549 },
550
551 // Overridden getters/setters
552 set_scale: function (scale) {
553 this._rescale(scale);
554 },
555
556 set_viewport: function (viewport) {
557 this._viewport = viewport;
558 // May need to readjust the viewport dimensions
559 var vp = this._viewportLoc;
560 this.viewportChangeSize(vp.w, vp.h);
561 this.viewportChangePos(0, 0);
562 },
563
564 get_width: function () {
565 return this._fb_width;
566 },
567 get_height: function () {
568 return this._fb_height;
569 },
570
571 autoscale: function (containerWidth, containerHeight, downscaleOnly) {
572 var vp = this._viewportLoc;
573 var targetAspectRatio = containerWidth / containerHeight;
574 var fbAspectRatio = vp.w / vp.h;
575
576 var scaleRatio;
577 if (fbAspectRatio >= targetAspectRatio) {
578 scaleRatio = containerWidth / vp.w;
579 } else {
580 scaleRatio = containerHeight / vp.h;
581 }
582
583 if (scaleRatio > 1.0 && downscaleOnly) {
584 scaleRatio = 1.0;
585 }
586
587 this._rescale(scaleRatio);
588 },
589
590 // Private Methods
591 _rescale: function (factor) {
592 this._scale = factor;
593 var vp = this._viewportLoc;
594
595 // NB(directxman12): If you set the width directly, or set the
596 // style width to a number, the canvas is cleared.
597 // However, if you set the style width to a string
598 // ('NNNpx'), the canvas is scaled without clearing.
599 var width = Math.round(factor * vp.w) + 'px';
600 var height = Math.round(factor * vp.h) + 'px';
601
602 if ((this._target.style.width !== width) ||
603 (this._target.style.height !== height)) {
604 this._target.style.width = width;
605 this._target.style.height = height;
606 }
607 },
608
609 _setFillColor: function (color) {
610 var bgr;
611 if (this._true_color) {
612 bgr = color;
613 } else {
614 bgr = this._colourMap[color];
615 }
616
617 var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')';
618 if (newStyle !== this._prevDrawStyle) {
619 this._drawCtx.fillStyle = newStyle;
620 this._prevDrawStyle = newStyle;
621 }
622 },
623
624 _rgbImageData: function (x, y, width, height, arr, offset) {
625 var img = this._drawCtx.createImageData(width, height);
626 var data = img.data;
627 for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) {
628 data[i] = arr[j];
629 data[i + 1] = arr[j + 1];
630 data[i + 2] = arr[j + 2];
631 data[i + 3] = 255; // Alpha
632 }
633 this._drawCtx.putImageData(img, x, y);
634 this._damage(x, y, img.width, img.height);
635 },
636
637 _bgrxImageData: function (x, y, width, height, arr, offset) {
638 var img = this._drawCtx.createImageData(width, height);
639 var data = img.data;
640 for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) {
641 data[i] = arr[j + 2];
642 data[i + 1] = arr[j + 1];
643 data[i + 2] = arr[j];
644 data[i + 3] = 255; // Alpha
645 }
646 this._drawCtx.putImageData(img, x, y);
647 this._damage(x, y, img.width, img.height);
648 },
649
650 _rgbxImageData: function (x, y, width, height, arr, offset) {
651 // NB(directxman12): arr must be an Type Array view
652 var img;
653 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) {
654 img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height);
655 } else {
656 img = this._drawCtx.createImageData(width, height);
657 img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4));
658 }
659 this._drawCtx.putImageData(img, x, y);
660 this._damage(x, y, img.width, img.height);
661 },
662
663 _cmapImageData: function (x, y, width, height, arr, offset) {
664 var img = this._drawCtx.createImageData(width, height);
665 var data = img.data;
666 var cmap = this._colourMap;
667 for (var i = 0, j = offset; i < width * height * 4; i += 4, j++) {
668 var bgr = cmap[arr[j]];
669 data[i] = bgr[2];
670 data[i + 1] = bgr[1];
671 data[i + 2] = bgr[0];
672 data[i + 3] = 255; // Alpha
673 }
674 this._drawCtx.putImageData(img, x, y);
675 this._damage(x, y, img.width, img.height);
676 },
677
678 _renderQ_push: function (action) {
679 this._renderQ.push(action);
680 if (this._renderQ.length === 1) {
681 // If this can be rendered immediately it will be, otherwise
682 // the scanner will wait for the relevant event
683 this._scan_renderQ();
684 }
685 },
686
687 _resume_renderQ: function() {
688 // "this" is the object that is ready, not the
689 // display object
690 this.removeEventListener('load', this._noVNC_display._resume_renderQ);
691 this._noVNC_display._scan_renderQ();
692 },
693
694 _scan_renderQ: function () {
695 var ready = true;
696 while (ready && this._renderQ.length > 0) {
697 var a = this._renderQ[0];
698 switch (a.type) {
699 case 'flip':
700 this.flip(true);
701 break;
702 case 'copy':
703 this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true);
704 break;
705 case 'fill':
706 this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
707 break;
708 case 'blit':
709 this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
710 break;
711 case 'blitRgb':
712 this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true);
713 break;
714 case 'blitRgbx':
715 this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true);
716 break;
717 case 'img':
718 if (a.img.complete) {
719 this.drawImage(a.img, a.x, a.y);
720 } else {
721 a.img._noVNC_display = this;
722 a.img.addEventListener('load', this._resume_renderQ);
723 // We need to wait for this image to 'load'
724 // to keep things in-order
725 ready = false;
726 }
727 break;
728 }
729
730 if (ready) {
731 this._renderQ.shift();
732 }
733 }
734
735 if (this._renderQ.length === 0 && this._flushing) {
736 this._flushing = false;
737 this._onFlush();
738 }
739 },
740 };
741
742 make_properties(Display, [
743 ['target', 'wo', 'dom'], // Canvas element for rendering
744 ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only)
745 ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "type": mime-type, "data": data}
746 ['true_color', 'rw', 'bool'], // Use true-color pixel data
747 ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color)
748 ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0
749 ['viewport', 'rw', 'bool'], // Use viewport clipping
750 ['width', 'ro', 'int'], // Display area width
751 ['height', 'ro', 'int'], // Display area height
752
753 ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only)
754
755 ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods
756 ['cursor_uri', 'rw', 'raw'], // Can we render cursor using data URI
757
758 ['onFlush', 'rw', 'func'], // onFlush(): A flush request has finished
759 ]);
760
761 // Class Methods
762 Display.changeCursor = function (target, pixels, mask, hotx, hoty, w0, h0, cmap) {
763 var w = w0;
764 var h = h0;
765 if (h < w) {
766 h = w; // increase h to make it square
767 } else {
768 w = h; // increase w to make it square
769 }
770
771 var cur = [];
772
773 // Push multi-byte little-endian values
774 cur.push16le = function (num) {
775 this.push(num & 0xFF, (num >> 8) & 0xFF);
776 };
777 cur.push32le = function (num) {
778 this.push(num & 0xFF,
779 (num >> 8) & 0xFF,
780 (num >> 16) & 0xFF,
781 (num >> 24) & 0xFF);
782 };
783
784 var IHDRsz = 40;
785 var RGBsz = w * h * 4;
786 var XORsz = Math.ceil((w * h) / 8.0);
787 var ANDsz = Math.ceil((w * h) / 8.0);
788
789 cur.push16le(0); // 0: Reserved
790 cur.push16le(2); // 2: .CUR type
791 cur.push16le(1); // 4: Number of images, 1 for non-animated ico
792
793 // Cursor #1 header (ICONDIRENTRY)
794 cur.push(w); // 6: width
795 cur.push(h); // 7: height
796 cur.push(0); // 8: colors, 0 -> true-color
797 cur.push(0); // 9: reserved
798 cur.push16le(hotx); // 10: hotspot x coordinate
799 cur.push16le(hoty); // 12: hotspot y coordinate
800 cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
801 // 14: cursor data byte size
802 cur.push32le(22); // 18: offset of cursor data in the file
803
804 // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
805 cur.push32le(IHDRsz); // 22: InfoHeader size
806 cur.push32le(w); // 26: Cursor width
807 cur.push32le(h * 2); // 30: XOR+AND height
808 cur.push16le(1); // 34: number of planes
809 cur.push16le(32); // 36: bits per pixel
810 cur.push32le(0); // 38: Type of compression
811
812 cur.push32le(XORsz + ANDsz);
813 // 42: Size of Image
814 cur.push32le(0); // 46: reserved
815 cur.push32le(0); // 50: reserved
816 cur.push32le(0); // 54: reserved
817 cur.push32le(0); // 58: reserved
818
819 // 62: color data (RGBQUAD icColors[])
820 var y, x;
821 for (y = h - 1; y >= 0; y--) {
822 for (x = 0; x < w; x++) {
823 if (x >= w0 || y >= h0) {
824 cur.push(0); // blue
825 cur.push(0); // green
826 cur.push(0); // red
827 cur.push(0); // alpha
828 } else {
829 var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8);
830 var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
831 if (cmap) {
832 idx = (w0 * y) + x;
833 var rgb = cmap[pixels[idx]];
834 cur.push(rgb[2]); // blue
835 cur.push(rgb[1]); // green
836 cur.push(rgb[0]); // red
837 cur.push(alpha); // alpha
838 } else {
839 idx = ((w0 * y) + x) * 4;
840 cur.push(pixels[idx]); // blue
841 cur.push(pixels[idx + 1]); // green
842 cur.push(pixels[idx + 2]); // red
843 cur.push(alpha); // alpha
844 }
845 }
846 }
847 }
848
849 // XOR/bitmask data (BYTE icXOR[])
850 // (ignored, just needs to be the right size)
851 for (y = 0; y < h; y++) {
852 for (x = 0; x < Math.ceil(w / 8); x++) {
853 cur.push(0);
854 }
855 }
856
857 // AND/bitmask data (BYTE icAND[])
858 // (ignored, just needs to be the right size)
859 for (y = 0; y < h; y++) {
860 for (x = 0; x < Math.ceil(w / 8); x++) {
861 cur.push(0);
862 }
863 }
864
865 var url = 'data:image/x-icon;base64,' + Base64.encode(cur);
866 target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
867 };