]> git.proxmox.com Git - mirror_novnc.git/blob - core/display.js
Merge pull request #718 from ossman/po
[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 /* [module]
14 * import Util from "./util";
15 * import Base64 from "./base64";
16 */
17
18 /* [module] 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 // the size limit of the viewport (start disabled)
30 this._maxWidth = 0;
31 this._maxHeight = 0;
32
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 };
36
37 this._prevDrawStyle = "";
38 this._tile = null;
39 this._tile16x16 = null;
40 this._tile_x = 0;
41 this._tile_y = 0;
42
43 Util.set_defaults(this, defaults, {
44 'true_color': true,
45 'colourMap': [],
46 'scale': 1.0,
47 'viewport': false,
48 'render_mode': '',
49 "onFlush": function () {},
50 });
51
52 Util.Debug(">> Display.constructor");
53
54 if (!this._target) {
55 throw new Error("Target must be set");
56 }
57
58 if (typeof this._target === 'string') {
59 throw new Error('target must be a DOM element');
60 }
61
62 if (!this._target.getContext) {
63 throw new Error("no getContext method");
64 }
65
66 if (!this._drawCtx) {
67 this._drawCtx = this._target.getContext('2d');
68 }
69
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); }
75
76 this.clear();
77
78 // Check canvas features
79 if ('createImageData' in this._drawCtx) {
80 this._render_mode = 'canvas rendering';
81 } else {
82 throw new Error("Canvas does not support createImageData");
83 }
84
85 if (this._prefer_js === null) {
86 Util.Info("Prefering javascript operations");
87 this._prefer_js = true;
88 }
89
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();
94 }
95
96 Util.Debug("<< Display.constructor");
97 };
98
99 (function () {
100 "use strict";
101
102 var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false;
103 try {
104 new ImageData(new Uint8ClampedArray(4), 1, 1);
105 SUPPORTS_IMAGEDATA_CONSTRUCTOR = true;
106 } catch (ex) {
107 // ignore failure
108 }
109
110
111 Display.prototype = {
112 // Public methods
113 viewportChangePos: function (deltaX, deltaY) {
114 var vp = this._viewportLoc;
115 deltaX = Math.floor(deltaX);
116 deltaY = Math.floor(deltaY);
117
118 if (!this._viewport) {
119 deltaX = -vp.w; // clamped later of out of bounds
120 deltaY = -vp.h;
121 }
122
123 var vx2 = vp.x + vp.w - 1;
124 var vy2 = vp.y + vp.h - 1;
125
126 // Position change
127
128 if (deltaX < 0 && vp.x + deltaX < 0) {
129 deltaX = -vp.x;
130 }
131 if (vx2 + deltaX >= this._fb_width) {
132 deltaX -= vx2 + deltaX - this._fb_width + 1;
133 }
134
135 if (vp.y + deltaY < 0) {
136 deltaY = -vp.y;
137 }
138 if (vy2 + deltaY >= this._fb_height) {
139 deltaY -= (vy2 + deltaY - this._fb_height + 1);
140 }
141
142 if (deltaX === 0 && deltaY === 0) {
143 return;
144 }
145 Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
146
147 vp.x += deltaX;
148 vx2 += deltaX;
149 vp.y += deltaY;
150 vy2 += deltaY;
151
152 // Update the clean rectangle
153 var cr = this._cleanRect;
154 if (vp.x > cr.x1) {
155 cr.x1 = vp.x;
156 }
157 if (vx2 < cr.x2) {
158 cr.x2 = vx2;
159 }
160 if (vp.y > cr.y1) {
161 cr.y1 = vp.y;
162 }
163 if (vy2 < cr.y2) {
164 cr.y2 = vy2;
165 }
166
167 var x1, w;
168 if (deltaX < 0) {
169 // Shift viewport left, redraw left section
170 x1 = 0;
171 w = -deltaX;
172 } else {
173 // Shift viewport right, redraw right section
174 x1 = vp.w - deltaX;
175 w = deltaX;
176 }
177
178 var y1, h;
179 if (deltaY < 0) {
180 // Shift viewport up, redraw top section
181 y1 = 0;
182 h = -deltaY;
183 } else {
184 // Shift viewport down, redraw bottom section
185 y1 = vp.h - deltaY;
186 h = deltaY;
187 }
188
189 var saveStyle = this._drawCtx.fillStyle;
190 var canvas = this._target;
191 this._drawCtx.fillStyle = "rgb(255,255,255)";
192
193 // Due to this bug among others [1] we need to disable the image-smoothing to
194 // avoid getting a blur effect when panning.
195 //
196 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
197 //
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;
208 }
209
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);
212
213 if (deltaX !== 0) {
214 this._drawCtx.fillRect(x1, 0, w, vp.h);
215 }
216 if (deltaY !== 0) {
217 this._drawCtx.fillRect(0, y1, vp.w, h);
218 }
219 this._drawCtx.fillStyle = saveStyle;
220 },
221
222 viewportChangeSize: function(width, height) {
223
224 if (typeof(width) === "undefined" || typeof(height) === "undefined") {
225
226 Util.Debug("Setting viewport to full display region");
227 width = this._fb_width;
228 height = this._fb_height;
229 }
230
231 var vp = this._viewportLoc;
232 if (vp.w !== width || vp.h !== height) {
233
234 if (this._viewport) {
235 if (this._maxWidth !== 0 && width > this._maxWidth) {
236 width = this._maxWidth;
237 }
238 if (this._maxHeight !== 0 && height > this._maxHeight) {
239 height = this._maxHeight;
240 }
241 }
242
243 var cr = this._cleanRect;
244
245 if (width < vp.w && cr.x2 > vp.x + width - 1) {
246 cr.x2 = vp.x + width - 1;
247 }
248 if (height < vp.h && cr.y2 > vp.y + height - 1) {
249 cr.y2 = vp.y + height - 1;
250 }
251
252 vp.w = width;
253 vp.h = height;
254
255 var canvas = this._target;
256 if (canvas.width !== width || canvas.height !== height) {
257
258 // We have to save the canvas data since changing the size will clear it
259 var saveImg = null;
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);
264 }
265
266 if (canvas.width !== width) {
267 canvas.width = width;
268 canvas.style.width = width + 'px';
269 }
270 if (canvas.height !== height) {
271 canvas.height = height;
272 canvas.style.height = height + 'px';
273 }
274
275 if (saveImg) {
276 this._drawCtx.putImageData(saveImg, 0, 0);
277 }
278 }
279 }
280 },
281
282 // Return a map of clean and dirty areas of the viewport and reset the
283 // tracking of clean and dirty areas
284 //
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;
290
291 var cleanBox = { 'x': cr.x1, 'y': cr.y1,
292 'w': cr.x2 - cr.x1 + 1, 'h': cr.y2 - cr.y1 + 1 };
293
294 var dirtyBoxes = [];
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 });
298 } else {
299 // Redraw dirty regions
300 var vx2 = vp.x + vp.w - 1;
301 var vy2 = vp.y + vp.h - 1;
302
303 if (vp.x < cr.x1) {
304 // left side dirty region
305 dirtyBoxes.push({'x': vp.x, 'y': vp.y,
306 'w': cr.x1 - vp.x + 1, 'h': vp.h});
307 }
308 if (vx2 > cr.x2) {
309 // right side dirty region
310 dirtyBoxes.push({'x': cr.x2 + 1, 'y': vp.y,
311 'w': vx2 - cr.x2, 'h': vp.h});
312 }
313 if(vp.y < cr.y1) {
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});
317 }
318 if (vy2 > cr.y2) {
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});
322 }
323 }
324
325 this._cleanRect = {'x1': vp.x, 'y1': vp.y,
326 'x2': vp.x + vp.w - 1, 'y2': vp.y + vp.h - 1};
327
328 return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes};
329 },
330
331 absX: function (x) {
332 return x + this._viewportLoc.x;
333 },
334
335 absY: function (y) {
336 return y + this._viewportLoc.y;
337 },
338
339 resize: function (width, height) {
340 this._prevDrawStyle = "";
341
342 this._fb_width = width;
343 this._fb_height = height;
344
345 this._rescale(this._scale);
346
347 this.viewportChangeSize();
348 },
349
350 clear: function () {
351 if (this._logo) {
352 this.resize(this._logo.width, this._logo.height);
353 this.blitStringImage(this._logo.data, 0, 0);
354 } else {
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);
360 }
361 this.resize(240, 20);
362 this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h);
363 }
364
365 this._renderQ = [];
366 },
367
368 pending: function() {
369 return this._renderQ.length > 0;
370 },
371
372 flush: function() {
373 if (this._renderQ.length === 0) {
374 this._onFlush();
375 } else {
376 this._flushing = true;
377 }
378 },
379
380 fillRect: function (x, y, width, height, color, from_queue) {
381 if (this._renderQ.length !== 0 && !from_queue) {
382 this._renderQ_push({
383 'type': 'fill',
384 'x': x,
385 'y': y,
386 'width': width,
387 'height': height,
388 'color': color
389 });
390 } else {
391 this._setFillColor(color);
392 this._drawCtx.fillRect(x - this._viewportLoc.x, y - this._viewportLoc.y, width, height);
393 }
394 },
395
396 copyImage: function (old_x, old_y, new_x, new_y, w, h, from_queue) {
397 if (this._renderQ.length !== 0 && !from_queue) {
398 this._renderQ_push({
399 'type': 'copy',
400 'old_x': old_x,
401 'old_y': old_y,
402 'x': new_x,
403 'y': new_y,
404 'width': w,
405 'height': h,
406 });
407 } else {
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;
412
413 this._drawCtx.drawImage(this._target, x1, y1, w, h, x2, y2, w, h);
414 }
415 },
416
417 imageRect: function(x, y, mime, arr) {
418 var img = new Image();
419 img.src = "data: " + mime + ";base64," + Base64.encode(arr);
420 this._renderQ_push({
421 'type': 'img',
422 'img': img,
423 'x': x,
424 'y': y
425 });
426 },
427
428 // start updating a tile
429 startTile: function (x, y, width, height, color) {
430 this._tile_x = x;
431 this._tile_y = y;
432 if (width === 16 && height === 16) {
433 this._tile = this._tile16x16;
434 } else {
435 this._tile = this._drawCtx.createImageData(width, height);
436 }
437
438 if (this._prefer_js) {
439 var bgr;
440 if (this._true_color) {
441 bgr = color;
442 } else {
443 bgr = this._colourMap[color[0]];
444 }
445 var red = bgr[2];
446 var green = bgr[1];
447 var blue = bgr[0];
448
449 var data = this._tile.data;
450 for (var i = 0; i < width * height * 4; i += 4) {
451 data[i] = red;
452 data[i + 1] = green;
453 data[i + 2] = blue;
454 data[i + 3] = 255;
455 }
456 } else {
457 this.fillRect(x, y, width, height, color, true);
458 }
459 },
460
461 // update sub-rectangle of the current tile
462 subTile: function (x, y, w, h, color) {
463 if (this._prefer_js) {
464 var bgr;
465 if (this._true_color) {
466 bgr = color;
467 } else {
468 bgr = this._colourMap[color[0]];
469 }
470 var red = bgr[2];
471 var green = bgr[1];
472 var blue = bgr[0];
473 var xend = x + w;
474 var yend = y + h;
475
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;
481 data[p] = red;
482 data[p + 1] = green;
483 data[p + 2] = blue;
484 data[p + 3] = 255;
485 }
486 }
487 } else {
488 this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color, true);
489 }
490 },
491
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);
497 }
498 // else: No-op -- already done by setSubTile
499 },
500
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));
508 this._renderQ_push({
509 'type': 'blit',
510 'data': new_arr,
511 'x': x,
512 'y': y,
513 'width': width,
514 'height': height,
515 });
516 } else if (this._true_color) {
517 this._bgrxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
518 } else {
519 this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
520 }
521 },
522
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));
530 this._renderQ_push({
531 'type': 'blitRgb',
532 'data': new_arr,
533 'x': x,
534 'y': y,
535 'width': width,
536 'height': height,
537 });
538 } else if (this._true_color) {
539 this._rgbImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
540 } else {
541 // probably wrong?
542 this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
543 }
544 },
545
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));
553 this._renderQ_push({
554 'type': 'blitRgbx',
555 'data': new_arr,
556 'x': x,
557 'y': y,
558 'width': width,
559 'height': height,
560 });
561 } else {
562 this._rgbxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
563 }
564 },
565
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);
570 }.bind(this);
571 img.src = str;
572 return img; // for debugging purposes
573 },
574
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);
578 },
579
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");
583 return;
584 }
585
586 if (this._true_color) {
587 Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h);
588 } else {
589 Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap);
590 }
591 },
592
593 defaultCursor: function () {
594 this._target.style.cursor = "default";
595 },
596
597 disableLocalCursor: function () {
598 this._target.style.cursor = "none";
599 },
600
601 clippingDisplay: function () {
602 var vp = this._viewportLoc;
603
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;
607
608 if (limitedVp) {
609 clipping = vp.w > this._maxWidth || vp.h > this._maxHeight;
610 }
611
612 return fbClip || (limitedVp && clipping);
613 },
614
615 // Overridden getters/setters
616 get_context: function () {
617 return this._drawCtx;
618 },
619
620 set_scale: function (scale) {
621 this._rescale(scale);
622 },
623
624 set_width: function (w) {
625 this._fb_width = w;
626 },
627 get_width: function () {
628 return this._fb_width;
629 },
630
631 set_height: function (h) {
632 this._fb_height = h;
633 },
634 get_height: function () {
635 return this._fb_height;
636 },
637
638 autoscale: function (containerWidth, containerHeight, downscaleOnly) {
639 var targetAspectRatio = containerWidth / containerHeight;
640 var fbAspectRatio = this._fb_width / this._fb_height;
641
642 var scaleRatio;
643 if (fbAspectRatio >= targetAspectRatio) {
644 scaleRatio = containerWidth / this._fb_width;
645 } else {
646 scaleRatio = containerHeight / this._fb_height;
647 }
648
649 var targetW, targetH;
650 if (scaleRatio > 1.0 && downscaleOnly) {
651 targetW = this._fb_width;
652 targetH = this._fb_height;
653 scaleRatio = 1.0;
654 } else if (fbAspectRatio >= targetAspectRatio) {
655 targetW = containerWidth;
656 targetH = Math.round(containerWidth / fbAspectRatio);
657 } else {
658 targetW = Math.round(containerHeight * fbAspectRatio);
659 targetH = containerHeight;
660 }
661
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';
668
669 this._scale = scaleRatio;
670
671 return scaleRatio; // so that the mouse, etc scale can be set
672 },
673
674 // Private Methods
675 _rescale: function (factor) {
676 this._scale = factor;
677
678 var w;
679 var h;
680
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);
685 } else {
686 w = this._fb_width;
687 h = this._fb_height;
688 }
689
690 this._target.style.width = Math.round(factor * w) + 'px';
691 this._target.style.height = Math.round(factor * h) + 'px';
692 },
693
694 _setFillColor: function (color) {
695 var bgr;
696 if (this._true_color) {
697 bgr = color;
698 } else {
699 bgr = this._colourMap[color];
700 }
701
702 var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')';
703 if (newStyle !== this._prevDrawStyle) {
704 this._drawCtx.fillStyle = newStyle;
705 this._prevDrawStyle = newStyle;
706 }
707 },
708
709 _rgbImageData: function (x, y, vx, vy, width, height, arr, offset) {
710 var img = this._drawCtx.createImageData(width, height);
711 var data = img.data;
712 for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) {
713 data[i] = arr[j];
714 data[i + 1] = arr[j + 1];
715 data[i + 2] = arr[j + 2];
716 data[i + 3] = 255; // Alpha
717 }
718 this._drawCtx.putImageData(img, x - vx, y - vy);
719 },
720
721 _bgrxImageData: function (x, y, vx, vy, width, height, arr, offset) {
722 var img = this._drawCtx.createImageData(width, height);
723 var data = img.data;
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
729 }
730 this._drawCtx.putImageData(img, x - vx, y - vy);
731 },
732
733 _rgbxImageData: function (x, y, vx, vy, width, height, arr, offset) {
734 // NB(directxman12): arr must be an Type Array view
735 var img;
736 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) {
737 img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height);
738 } else {
739 img = this._drawCtx.createImageData(width, height);
740 img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4));
741 }
742 this._drawCtx.putImageData(img, x - vx, y - vy);
743 },
744
745 _cmapImageData: function (x, y, vx, vy, width, height, arr, offset) {
746 var img = this._drawCtx.createImageData(width, height);
747 var data = img.data;
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]];
751 data[i] = bgr[2];
752 data[i + 1] = bgr[1];
753 data[i + 2] = bgr[0];
754 data[i + 3] = 255; // Alpha
755 }
756 this._drawCtx.putImageData(img, x - vx, y - vy);
757 },
758
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();
765 }
766 },
767
768 _resume_renderQ: function() {
769 // "this" is the object that is ready, not the
770 // display object
771 this.removeEventListener('load', this._noVNC_display._resume_renderQ);
772 this._noVNC_display._scan_renderQ();
773 },
774
775 _scan_renderQ: function () {
776 var ready = true;
777 while (ready && this._renderQ.length > 0) {
778 var a = this._renderQ[0];
779 switch (a.type) {
780 case 'copy':
781 this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true);
782 break;
783 case 'fill':
784 this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
785 break;
786 case 'blit':
787 this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
788 break;
789 case 'blitRgb':
790 this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true);
791 break;
792 case 'blitRgbx':
793 this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true);
794 break;
795 case 'img':
796 if (a.img.complete) {
797 this.drawImage(a.img, a.x, a.y);
798 } else {
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
803 ready = false;
804 }
805 break;
806 }
807
808 if (ready) {
809 this._renderQ.shift();
810 }
811 }
812
813 if (this._renderQ.length === 0 && this._flushing) {
814 this._flushing = false;
815 this._onFlush();
816 }
817 },
818 };
819
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)
832
833 ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only)
834
835 ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods
836 ['cursor_uri', 'rw', 'raw'], // Can we render cursor using data URI
837
838 ['onFlush', 'rw', 'func'], // onFlush(): A flush request has finished
839 ]);
840
841 // Class Methods
842 Display.changeCursor = function (target, pixels, mask, hotx, hoty, w0, h0, cmap) {
843 var w = w0;
844 var h = h0;
845 if (h < w) {
846 h = w; // increase h to make it square
847 } else {
848 w = h; // increase w to make it square
849 }
850
851 var cur = [];
852
853 // Push multi-byte little-endian values
854 cur.push16le = function (num) {
855 this.push(num & 0xFF, (num >> 8) & 0xFF);
856 };
857 cur.push32le = function (num) {
858 this.push(num & 0xFF,
859 (num >> 8) & 0xFF,
860 (num >> 16) & 0xFF,
861 (num >> 24) & 0xFF);
862 };
863
864 var IHDRsz = 40;
865 var RGBsz = w * h * 4;
866 var XORsz = Math.ceil((w * h) / 8.0);
867 var ANDsz = Math.ceil((w * h) / 8.0);
868
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
872
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
883
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
891
892 cur.push32le(XORsz + ANDsz);
893 // 42: Size of Image
894 cur.push32le(0); // 46: reserved
895 cur.push32le(0); // 50: reserved
896 cur.push32le(0); // 54: reserved
897 cur.push32le(0); // 58: reserved
898
899 // 62: color data (RGBQUAD icColors[])
900 var y, x;
901 for (y = h - 1; y >= 0; y--) {
902 for (x = 0; x < w; x++) {
903 if (x >= w0 || y >= h0) {
904 cur.push(0); // blue
905 cur.push(0); // green
906 cur.push(0); // red
907 cur.push(0); // alpha
908 } else {
909 var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8);
910 var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
911 if (cmap) {
912 idx = (w0 * y) + x;
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
918 } else {
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
924 }
925 }
926 }
927 }
928
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++) {
933 cur.push(0);
934 }
935 }
936
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++) {
941 cur.push(0);
942 }
943 }
944
945 var url = 'data:image/x-icon;base64,' + Base64.encode(cur);
946 target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
947 };
948 })();