]> git.proxmox.com Git - mirror_novnc.git/blob - include/display.js
Viewport handling in include/display.js
[mirror_novnc.git] / include / display.js
1 /*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2011 Joel Martin
4 * Licensed under LGPL-3 (see LICENSE.txt)
5 *
6 * See README.md for usage and integration instructions.
7 */
8
9 /*jslint browser: true, white: false, bitwise: false */
10 /*global Util, Base64, changeCursor */
11
12 function Display(defaults) {
13 "use strict";
14
15 var that = {}, // Public API methods
16 conf = {}, // Configuration attributes
17
18 // Private Display namespace variables
19 c_ctx = null,
20 c_forceCanvas = false,
21
22 c_imageData, c_rgbxImage, c_cmapImage,
23
24 // Predefine function variables (jslint)
25 imageDataCreate, imageDataGet, rgbxImageData, cmapImageData,
26 rgbxImageFill, cmapImageFill, setFillColor, rescale, flush,
27
28 // The full frame buffer (logical canvas) size
29 fb_width = 0,
30 fb_height = 0,
31 // The visible "physical canvas" viewport
32 viewport = {'x': 0, 'y': 0, 'w' : 0, 'h' : 0 },
33 cleanRect = {'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1},
34
35 c_prevStyle = "",
36
37 c_webkit_bug = false,
38 c_flush_timer = null;
39
40 // Configuration attributes
41 Util.conf_defaults(conf, that, defaults, [
42 ['target', 'wo', 'dom', null, 'Canvas element for rendering'],
43 ['context', 'ro', 'raw', null, 'Canvas 2D context for rendering (read-only)'],
44 ['logo', 'rw', 'raw', null, 'Logo to display when cleared: {"width": width, "height": height, "data": data}'],
45 ['true_color', 'rw', 'bool', true, 'Use true-color pixel data'],
46 ['colourMap', 'rw', 'arr', [], 'Colour map array (when not true-color)'],
47 ['scale', 'rw', 'float', 1.0, 'Display area scale factor 0.0 - 1.0'],
48 ['width', 'rw', 'int', null, 'Display area width'],
49 ['height', 'rw', 'int', null, 'Display area height'],
50
51 ['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'],
52
53 ['prefer_js', 'rw', 'str', null, 'Prefer Javascript over canvas methods'],
54 ['cursor_uri', 'rw', 'raw', null, 'Can we render cursor using data URI']
55 ]);
56
57 // Override some specific getters/setters
58 that.get_context = function () { return c_ctx; };
59
60 that.set_scale = function(scale) { rescale(scale); };
61
62 that.set_width = function (val) { that.resize(val, fb_height); };
63 that.get_width = function() { return fb_width; };
64
65 that.set_height = function (val) { that.resize(fb_width, val); };
66 that.get_height = function() { return fb_height; };
67
68 that.set_prefer_js = function(val) {
69 if (val && c_forceCanvas) {
70 Util.Warn("Preferring Javascript to Canvas ops is not supported");
71 return false;
72 }
73 conf.prefer_js = val;
74 return true;
75 };
76
77
78
79 //
80 // Private functions
81 //
82
83 // Create the public API interface
84 function constructor() {
85 Util.Debug(">> Display.constructor");
86
87 var c, func, imgTest, tval, i, curDat, curSave,
88 has_imageData = false, UE = Util.Engine;
89
90 if (! conf.target) { throw("target must be set"); }
91
92 if (typeof conf.target === 'string') {
93 throw("target must be a DOM element");
94 }
95
96 c = conf.target;
97
98 if (! c.getContext) { throw("no getContext method"); }
99
100 if (! c_ctx) { c_ctx = c.getContext('2d'); }
101
102 Util.Debug("User Agent: " + navigator.userAgent);
103 if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); }
104 if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); }
105 if (UE.trident) { Util.Debug("Browser: trident " + UE.trident); }
106 if (UE.presto) { Util.Debug("Browser: presto " + UE.presto); }
107
108 that.clear();
109
110 /*
111 * Determine browser Canvas feature support
112 * and select fastest rendering methods
113 */
114 tval = 0;
115 try {
116 imgTest = c_ctx.getImageData(0, 0, 1,1);
117 imgTest.data[0] = 123;
118 imgTest.data[3] = 255;
119 c_ctx.putImageData(imgTest, 0, 0);
120 tval = c_ctx.getImageData(0, 0, 1, 1).data[0];
121 if (tval === 123) {
122 has_imageData = true;
123 }
124 } catch (exc1) {}
125
126 if (has_imageData) {
127 Util.Info("Canvas supports imageData");
128 c_forceCanvas = false;
129 if (c_ctx.createImageData) {
130 // If it's there, it's faster
131 Util.Info("Using Canvas createImageData");
132 conf.render_mode = "createImageData rendering";
133 c_imageData = imageDataCreate;
134 } else if (c_ctx.getImageData) {
135 // I think this is mostly just Opera
136 Util.Info("Using Canvas getImageData");
137 conf.render_mode = "getImageData rendering";
138 c_imageData = imageDataGet;
139 }
140 Util.Info("Prefering javascript operations");
141 if (conf.prefer_js === null) {
142 conf.prefer_js = true;
143 }
144 c_rgbxImage = rgbxImageData;
145 c_cmapImage = cmapImageData;
146 } else {
147 Util.Warn("Canvas lacks imageData, using fillRect (slow)");
148 conf.render_mode = "fillRect rendering (slow)";
149 c_forceCanvas = true;
150 conf.prefer_js = false;
151 c_rgbxImage = rgbxImageFill;
152 c_cmapImage = cmapImageFill;
153 }
154
155 if (UE.webkit && UE.webkit >= 534.7 && UE.webkit <= 534.9) {
156 // Workaround WebKit canvas rendering bug #46319
157 conf.render_mode += ", webkit bug workaround";
158 Util.Debug("Working around WebKit bug #46319");
159 c_webkit_bug = true;
160 for (func in {"fillRect":1, "copyImage":1, "rgbxImage":1,
161 "cmapImage":1, "blitStringImage":1}) {
162 that[func] = (function() {
163 var myfunc = that[func]; // Save original function
164 //Util.Debug("Wrapping " + func);
165 return function() {
166 myfunc.apply(this, arguments);
167 if (!c_flush_timer) {
168 c_flush_timer = setTimeout(flush, 100);
169 }
170 };
171 }());
172 }
173 }
174
175 /*
176 * Determine browser support for setting the cursor via data URI
177 * scheme
178 */
179 curDat = [];
180 for (i=0; i < 8 * 8 * 4; i += 1) {
181 curDat.push(255);
182 }
183 try {
184 curSave = c.style.cursor;
185 changeCursor(conf.target, curDat, curDat, 2, 2, 8, 8);
186 if (c.style.cursor) {
187 if (conf.cursor_uri === null) {
188 conf.cursor_uri = true;
189 }
190 Util.Info("Data URI scheme cursor supported");
191 } else {
192 if (conf.cursor_uri === null) {
193 conf.cursor_uri = false;
194 }
195 Util.Warn("Data URI scheme cursor not supported");
196 }
197 c.style.cursor = curSave;
198 } catch (exc2) {
199 Util.Error("Data URI scheme cursor test exception: " + exc2);
200 conf.cursor_uri = false;
201 }
202
203 Util.Debug("<< Display.constructor");
204 return that ;
205 }
206
207 rescale = function(factor) {
208 var c, tp, x, y,
209 properties = ['transform', 'WebkitTransform', 'MozTransform', null];
210 c = conf.target;
211 tp = properties.shift();
212 while (tp) {
213 if (typeof c.style[tp] !== 'undefined') {
214 break;
215 }
216 tp = properties.shift();
217 }
218
219 if (tp === null) {
220 Util.Debug("No scaling support");
221 return;
222 }
223
224
225 if (typeof(factor) === "undefined") {
226 factor = conf.scale;
227 } else if (factor > 1.0) {
228 factor = 1.0;
229 } else if (factor < 0.1) {
230 factor = 0.1;
231 }
232
233 if (conf.scale === factor) {
234 //Util.Debug("Display already scaled to '" + factor + "'");
235 return;
236 }
237
238 conf.scale = factor;
239 x = c.width - c.width * factor;
240 y = c.height - c.height * factor;
241 c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)";
242 };
243
244 that.viewportChange = function(deltaX, deltaY, width, height) {
245 var c = conf.target, v = viewport, cr = cleanRect,
246 saveImg = null, saveStyle, x1, y1, vx2, vy2, w, h;
247
248 if (typeof(deltaX) === "undefined") { deltaX = 0; }
249 if (typeof(deltaY) === "undefined") { deltaY = 0; }
250 if (typeof(width) === "undefined") { width = v.w; }
251 if (typeof(height) === "undefined") { height = v.h; }
252
253 // Size change
254
255 if (width > fb_width) { width = fb_width; }
256 if (height > fb_height) { height = fb_height; }
257
258 if ((v.w !== width) || (v.h !== height)) {
259 // Change width
260 if ((width < v.w) && (cr.x2 > v.x + width -1)) {
261 cr.x2 = v.x + width - 1;
262 }
263 v.w = width;
264
265 // Change height
266 if ((height < v.h) && (cr.y2 > v.y + height -1)) {
267 cr.y2 = v.y + height - 1;
268 }
269 v.h = height;
270
271
272 if (v.w > 0 && v.h > 0) {
273 saveImg = c_ctx.getImageData(0, 0,
274 (c.width < v.w) ? c.width : v.w,
275 (c.height < v.h) ? c.height : v.h);
276 }
277
278 c.width = v.w;
279 c.height = v.h;
280
281 if (saveImg) {
282 c_ctx.putImageData(saveImg, 0, 0);
283 }
284 }
285
286 vx2 = v.x + v.w - 1;
287 vy2 = v.y + v.h - 1;
288
289
290 // Position change
291
292 if ((deltaX < 0) && ((v.x + deltaX) < 0)) {
293 deltaX = - v.x;
294 }
295 if ((vx2 + deltaX) >= fb_width) {
296 deltaX -= ((vx2 + deltaX) - fb_width + 1);
297 }
298
299 if ((v.y + deltaY) < 0) {
300 deltaY = - v.y;
301 }
302 if ((vy2 + deltaY) >= fb_height) {
303 deltaY -= ((vy2 + deltaY) - fb_height + 1);
304 }
305
306 if ((deltaX === 0) && (deltaY === 0)) {
307 //message("skipping");
308 return;
309 }
310 message("deltaX: " + deltaX + ", deltaY: " + deltaY);
311
312 v.x += deltaX;
313 vx2 += deltaX;
314 v.y += deltaY;
315 vy2 += deltaY;
316
317 // Update the clean rectangle
318 if (v.x > cr.x1) {
319 cr.x1 = v.x;
320 }
321 if (vx2 < cr.x2) {
322 cr.x2 = vx2;
323 }
324 if (v.y > cr.y1) {
325 cr.y1 = v.y;
326 }
327 if (vy2 < cr.y2) {
328 cr.y2 = vy2;
329 }
330
331 if (deltaX < 0) {
332 // Shift viewport left, redraw left section
333 x1 = 0;
334 w = - deltaX;
335 } else {
336 // Shift viewport right, redraw right section
337 x1 = v.w - deltaX;
338 w = deltaX;
339 }
340 if (deltaY < 0) {
341 // Shift viewport up, redraw top section
342 y1 = 0;
343 h = - deltaY;
344 } else {
345 // Shift viewport down, redraw bottom section
346 y1 = v.h - deltaY;
347 h = deltaY;
348 }
349
350 // Copy the valid part of the viewport to the shifted location
351 saveStyle = c_ctx.fillStyle;
352 c_ctx.fillStyle = "rgb(255,255,255)";
353 if (deltaX !== 0) {
354 //that.copyImage(0, 0, -deltaX, 0, v.w, v.h);
355 //that.fillRect(x1, 0, w, v.h, [255,255,255]);
356 c_ctx.drawImage(c, 0, 0, v.w, v.h, -deltaX, 0, v.w, v.h);
357 c_ctx.fillRect(x1, 0, w, v.h);
358 }
359 if (deltaY !== 0) {
360 //that.copyImage(0, 0, 0, -deltaY, v.w, v.h);
361 //that.fillRect(0, y1, v.w, h, [255,255,255]);
362 c_ctx.drawImage(c, 0, 0, v.w, v.h, 0, -deltaY, v.w, v.h);
363 c_ctx.fillRect(0, y1, v.w, h);
364 }
365 c_ctx.fillStyle = saveStyle;
366 }
367
368 that.getCleanDirtyReset = function() {
369 var v = viewport, c = cleanRect, cleanBox, dirtyBoxes = [],
370 vx2 = v.x + v.w - 1, vy2 = v.y + v.h - 1;
371
372
373 // Copy the cleanRect
374 cleanBox = {'x': c.x1, 'y': c.y1,
375 'w': c.x2 - c.x1 + 1, 'h': c.y2 - c.y1 + 1};
376
377 if ((c.x1 >= c.x2) || (c.y1 >= c.y2)) {
378 // Whole viewport is dirty
379 dirtyBoxes.push({'x': v.x, 'y': v.y, 'w': v.w, 'h': v.h});
380 } else {
381 // Redraw dirty regions
382 if (v.x < c.x1) {
383 // left side dirty region
384 dirtyBoxes.push({'x': v.x, 'y': v.y,
385 'w': c.x1 - v.x + 1, 'h': v.h});
386 }
387 if (vx2 > c.x2) {
388 // right side dirty region
389 dirtyBoxes.push({'x': c.x2 + 1, 'y': v.y,
390 'w': vx2 - c.x2, 'h': v.h});
391 }
392 if (v.y < c.y1) {
393 // top/middle dirty region
394 dirtyBoxes.push({'x': c.x1, 'y': v.y,
395 'w': c.x2 - c.x1 + 1, 'h': c.y1 - v.y});
396 }
397 if (vy2 > c.y2) {
398 // bottom/middle dirty region
399 dirtyBoxes.push({'x': c.x1, 'y': c.y2 + 1,
400 'w': c.x2 - c.x1 + 1, 'h': vy2 - c.y2});
401 }
402 }
403
404 // Reset the cleanRect to the whole viewport
405 cleanRect = {'x1': v.x, 'y1': v.y,
406 'x2': v.x + v.w - 1, 'y2': v.y + v.h - 1};
407
408 return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes};
409 }
410
411
412 // Force canvas redraw (for webkit bug #46319 workaround)
413 flush = function() {
414 var old_val;
415 //Util.Debug(">> flush");
416 old_val = conf.target.style.marginRight;
417 conf.target.style.marginRight = "1px";
418 c_flush_timer = null;
419 setTimeout(function () {
420 conf.target.style.marginRight = old_val;
421 }, 1);
422 };
423
424 setFillColor = function(color) {
425 var rgb, newStyle;
426 if (conf.true_color) {
427 rgb = color;
428 } else {
429 rgb = conf.colourMap[color[0]];
430 }
431 newStyle = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
432 if (newStyle !== c_prevStyle) {
433 c_ctx.fillStyle = newStyle;
434 c_prevStyle = newStyle;
435 }
436 };
437
438
439 //
440 // Public API interface functions
441 //
442
443 that.resize = function(width, height) {
444 c_prevStyle = "";
445
446 fb_width = width;
447 fb_height = height;
448
449 rescale(conf.scale);
450 that.viewportChange();
451 };
452
453 that.clear = function() {
454
455 if (conf.logo) {
456 that.resize(conf.logo.width, conf.logo.height);
457 that.viewportChange(0, 0, conf.logo.width, conf.logo.height);
458 that.blitStringImage(conf.logo.data, 0, 0);
459 } else {
460 that.resize(640, 20);
461 that.viewportChange(0, 0, 640, 20);
462 c_ctx.clearRect(0, 0, viewport.w, viewport.h);
463 }
464
465 // No benefit over default ("source-over") in Chrome and firefox
466 //c_ctx.globalCompositeOperation = "copy";
467 };
468
469 that.fillRect = function(x, y, width, height, color) {
470 setFillColor(color);
471 c_ctx.fillRect(x - viewport.x, y - viewport.y, width, height);
472 };
473
474 that.copyImage = function(old_x, old_y, new_x, new_y, w, h) {
475 var x1 = old_x - viewport.x, y1 = old_y - viewport.y,
476 x2 = new_x - viewport.x, y2 = new_y - viewport.y;
477 c_ctx.drawImage(conf.target, x1, y1, w, h, x2, y2, w, h);
478 };
479
480 /*
481 * Tile rendering functions optimized for rendering engines.
482 *
483 * - In Chrome/webkit, Javascript image data array manipulations are
484 * faster than direct Canvas fillStyle, fillRect rendering. In
485 * gecko, Javascript array handling is much slower.
486 */
487 that.getTile = function(x, y, width, height, color) {
488 var img, data = [], rgb, red, green, blue, i;
489 img = {'x': x, 'y': y, 'width': width, 'height': height,
490 'data': data};
491 if (conf.prefer_js) {
492 if (conf.true_color) {
493 rgb = color;
494 } else {
495 rgb = conf.colourMap[color[0]];
496 }
497 red = rgb[0];
498 green = rgb[1];
499 blue = rgb[2];
500 for (i = 0; i < (width * height * 4); i+=4) {
501 data[i ] = red;
502 data[i + 1] = green;
503 data[i + 2] = blue;
504 }
505 } else {
506 that.fillRect(x, y, width, height, color);
507 }
508 return img;
509 };
510
511 that.setSubTile = function(img, x, y, w, h, color) {
512 var data, p, rgb, red, green, blue, width, j, i, xend, yend;
513 if (conf.prefer_js) {
514 data = img.data;
515 width = img.width;
516 if (conf.true_color) {
517 rgb = color;
518 } else {
519 rgb = conf.colourMap[color[0]];
520 }
521 red = rgb[0];
522 green = rgb[1];
523 blue = rgb[2];
524 xend = x + w;
525 yend = y + h;
526 for (j = y; j < yend; j += 1) {
527 for (i = x; i < xend; i += 1) {
528 p = (i + (j * width) ) * 4;
529 data[p ] = red;
530 data[p + 1] = green;
531 data[p + 2] = blue;
532 }
533 }
534 } else {
535 that.fillRect(img.x + x, img.y + y, w, h, color);
536 }
537 };
538
539 that.putTile = function(img) {
540 if (conf.prefer_js) {
541 c_rgbxImage(img.x, img.y, img.width, img.height, img.data, 0);
542 }
543 // else: No-op, under gecko already done by setSubTile
544 };
545
546 imageDataGet = function(width, height) {
547 return c_ctx.getImageData(0, 0, width, height);
548 };
549 imageDataCreate = function(width, height) {
550 return c_ctx.createImageData(width, height);
551 };
552
553 rgbxImageData = function(x, y, width, height, arr, offset) {
554 var img, i, j, data;
555 img = c_imageData(width, height);
556 data = img.data;
557 for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) {
558 data[i ] = arr[j ];
559 data[i + 1] = arr[j + 1];
560 data[i + 2] = arr[j + 2];
561 data[i + 3] = 255; // Set Alpha
562 }
563 c_ctx.putImageData(img, x - viewport.x, y - viewport.y);
564 };
565
566 // really slow fallback if we don't have imageData
567 rgbxImageFill = function(x, y, width, height, arr, offset) {
568 var i, j, sx = 0, sy = 0;
569 for (i=0, j=offset; i < (width * height); i+=1, j+=4) {
570 that.fillRect(x+sx, y+sy, 1, 1, [arr[j], arr[j+1], arr[j+2]]);
571 sx += 1;
572 if ((sx % width) === 0) {
573 sx = 0;
574 sy += 1;
575 }
576 }
577 };
578
579 cmapImageData = function(x, y, width, height, arr, offset) {
580 var img, i, j, data, rgb, cmap;
581 img = c_imageData(width, height);
582 data = img.data;
583 cmap = conf.colourMap;
584 for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) {
585 rgb = cmap[arr[j]];
586 data[i ] = rgb[0];
587 data[i + 1] = rgb[1];
588 data[i + 2] = rgb[2];
589 data[i + 3] = 255; // Set Alpha
590 }
591 c_ctx.putImageData(img, x - viewport.x, y - viewport.y);
592 };
593
594 cmapImageFill = function(x, y, width, height, arr, offset) {
595 var i, j, sx = 0, sy = 0, cmap;
596 cmap = conf.colourMap;
597 for (i=0, j=offset; i < (width * height); i+=1, j+=1) {
598 that.fillRect(x+sx, y+sy, 1, 1, [arr[j]]);
599 sx += 1;
600 if ((sx % width) === 0) {
601 sx = 0;
602 sy += 1;
603 }
604 }
605 };
606
607
608 that.blitImage = function(x, y, width, height, arr, offset) {
609 if (conf.true_color) {
610 c_rgbxImage(x, y, width, height, arr, offset);
611 } else {
612 c_cmapImage(x, y, width, height, arr, offset);
613 }
614 };
615
616 that.blitStringImage = function(str, x, y) {
617 var img = new Image();
618 img.onload = function () {
619 c_ctx.drawImage(img, x - viewport.x, y - viewport.y);
620 };
621 img.src = str;
622 };
623
624 that.changeCursor = function(pixels, mask, hotx, hoty, w, h) {
625 if (conf.cursor_uri === false) {
626 Util.Warn("changeCursor called but no cursor data URI support");
627 return;
628 }
629
630 if (conf.true_color) {
631 changeCursor(conf.target, pixels, mask, hotx, hoty, w, h);
632 } else {
633 changeCursor(conf.target, pixels, mask, hotx, hoty, w, h, conf.colourMap);
634 }
635 };
636
637 that.defaultCursor = function() {
638 conf.target.style.cursor = "default";
639 };
640
641 return constructor(); // Return the public API interface
642
643 } // End of Display()
644
645
646 /* Set CSS cursor property using data URI encoded cursor file */
647 function changeCursor(target, pixels, mask, hotx, hoty, w, h, cmap) {
648 "use strict";
649 var cur = [], rgb, IHDRsz, RGBsz, ANDsz, XORsz, url, idx, alpha, x, y;
650 //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h);
651
652 // Push multi-byte little-endian values
653 cur.push16le = function (num) {
654 this.push((num ) & 0xFF,
655 (num >> 8) & 0xFF );
656 };
657 cur.push32le = function (num) {
658 this.push((num ) & 0xFF,
659 (num >> 8) & 0xFF,
660 (num >> 16) & 0xFF,
661 (num >> 24) & 0xFF );
662 };
663
664 IHDRsz = 40;
665 RGBsz = w * h * 4;
666 XORsz = Math.ceil( (w * h) / 8.0 );
667 ANDsz = Math.ceil( (w * h) / 8.0 );
668
669 // Main header
670 cur.push16le(0); // 0: Reserved
671 cur.push16le(2); // 2: .CUR type
672 cur.push16le(1); // 4: Number of images, 1 for non-animated ico
673
674 // Cursor #1 header (ICONDIRENTRY)
675 cur.push(w); // 6: width
676 cur.push(h); // 7: height
677 cur.push(0); // 8: colors, 0 -> true-color
678 cur.push(0); // 9: reserved
679 cur.push16le(hotx); // 10: hotspot x coordinate
680 cur.push16le(hoty); // 12: hotspot y coordinate
681 cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
682 // 14: cursor data byte size
683 cur.push32le(22); // 18: offset of cursor data in the file
684
685
686 // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
687 cur.push32le(IHDRsz); // 22: Infoheader size
688 cur.push32le(w); // 26: Cursor width
689 cur.push32le(h*2); // 30: XOR+AND height
690 cur.push16le(1); // 34: number of planes
691 cur.push16le(32); // 36: bits per pixel
692 cur.push32le(0); // 38: Type of compression
693
694 cur.push32le(XORsz + ANDsz); // 43: Size of Image
695 // Gimp leaves this as 0
696
697 cur.push32le(0); // 46: reserved
698 cur.push32le(0); // 50: reserved
699 cur.push32le(0); // 54: reserved
700 cur.push32le(0); // 58: reserved
701
702 // 62: color data (RGBQUAD icColors[])
703 for (y = h-1; y >= 0; y -= 1) {
704 for (x = 0; x < w; x += 1) {
705 idx = y * Math.ceil(w / 8) + Math.floor(x/8);
706 alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
707
708 if (cmap) {
709 idx = (w * y) + x;
710 rgb = cmap[pixels[idx]];
711 cur.push(rgb[2]); // blue
712 cur.push(rgb[1]); // green
713 cur.push(rgb[0]); // red
714 cur.push(alpha); // alpha
715 } else {
716 idx = ((w * y) + x) * 4;
717 cur.push(pixels[idx + 2]); // blue
718 cur.push(pixels[idx + 1]); // green
719 cur.push(pixels[idx ]); // red
720 cur.push(alpha); // alpha
721 }
722 }
723 }
724
725 // XOR/bitmask data (BYTE icXOR[])
726 // (ignored, just needs to be right size)
727 for (y = 0; y < h; y += 1) {
728 for (x = 0; x < Math.ceil(w / 8); x += 1) {
729 cur.push(0x00);
730 }
731 }
732
733 // AND/bitmask data (BYTE icAND[])
734 // (ignored, just needs to be right size)
735 for (y = 0; y < h; y += 1) {
736 for (x = 0; x < Math.ceil(w / 8); x += 1) {
737 cur.push(0x00);
738 }
739 }
740
741 url = "data:image/x-icon;base64," + Base64.encode(cur);
742 target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default";
743 //Util.Debug("<< changeCursor, cur.length: " + cur.length);
744 }