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