]> git.proxmox.com Git - mirror_novnc.git/blob - core/display.js
Add eslint and fix reported issues
[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 import * as Log from './util/logging.js';
11 import Base64 from "./base64.js";
12
13 export default function Display(target) {
14 this._drawCtx = null;
15 this._c_forceCanvas = false;
16
17 this._renderQ = []; // queue drawing actions for in-oder rendering
18 this._flushing = false;
19
20 // the full frame buffer (logical canvas) size
21 this._fb_width = 0;
22 this._fb_height = 0;
23
24 this._prevDrawStyle = "";
25 this._tile = null;
26 this._tile16x16 = null;
27 this._tile_x = 0;
28 this._tile_y = 0;
29
30 Log.Debug(">> Display.constructor");
31
32 // The visible canvas
33 this._target = target;
34
35 if (!this._target) {
36 throw new Error("Target must be set");
37 }
38
39 if (typeof this._target === 'string') {
40 throw new Error('target must be a DOM element');
41 }
42
43 if (!this._target.getContext) {
44 throw new Error("no getContext method");
45 }
46
47 this._targetCtx = this._target.getContext('2d');
48
49 // the visible canvas viewport (i.e. what actually gets seen)
50 this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };
51
52 // The hidden canvas, where we do the actual rendering
53 this._backbuffer = document.createElement('canvas');
54 this._drawCtx = this._backbuffer.getContext('2d');
55
56 this._damageBounds = { left:0, top:0,
57 right: this._backbuffer.width,
58 bottom: this._backbuffer.height };
59
60 Log.Debug("User Agent: " + navigator.userAgent);
61
62 this.clear();
63
64 // Check canvas features
65 if (!('createImageData' in this._drawCtx)) {
66 throw new Error("Canvas does not support createImageData");
67 }
68
69 this._tile16x16 = this._drawCtx.createImageData(16, 16);
70 Log.Debug("<< Display.constructor");
71 }
72
73 var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false;
74 try {
75 new ImageData(new Uint8ClampedArray(4), 1, 1);
76 SUPPORTS_IMAGEDATA_CONSTRUCTOR = true;
77 } catch (ex) {
78 // ignore failure
79 }
80
81 Display.prototype = {
82 // ===== PROPERTIES =====
83
84 _scale: 1.0,
85 get scale() { return this._scale; },
86 set scale(scale) {
87 this._rescale(scale);
88 },
89
90 _clipViewport: false,
91 get clipViewport() { return this._clipViewport; },
92 set clipViewport(viewport) {
93 this._clipViewport = viewport;
94 // May need to readjust the viewport dimensions
95 var vp = this._viewportLoc;
96 this.viewportChangeSize(vp.w, vp.h);
97 this.viewportChangePos(0, 0);
98 },
99
100 get width() {
101 return this._fb_width;
102 },
103 get height() {
104 return this._fb_height;
105 },
106
107 logo: null,
108
109 // ===== EVENT HANDLERS =====
110
111 onflush: function () {}, // A flush request has finished
112
113 // ===== PUBLIC METHODS =====
114
115 viewportChangePos: function (deltaX, deltaY) {
116 var vp = this._viewportLoc;
117 deltaX = Math.floor(deltaX);
118 deltaY = Math.floor(deltaY);
119
120 if (!this._clipViewport) {
121 deltaX = -vp.w; // clamped later of out of bounds
122 deltaY = -vp.h;
123 }
124
125 var vx2 = vp.x + vp.w - 1;
126 var vy2 = vp.y + vp.h - 1;
127
128 // Position change
129
130 if (deltaX < 0 && vp.x + deltaX < 0) {
131 deltaX = -vp.x;
132 }
133 if (vx2 + deltaX >= this._fb_width) {
134 deltaX -= vx2 + deltaX - this._fb_width + 1;
135 }
136
137 if (vp.y + deltaY < 0) {
138 deltaY = -vp.y;
139 }
140 if (vy2 + deltaY >= this._fb_height) {
141 deltaY -= (vy2 + deltaY - this._fb_height + 1);
142 }
143
144 if (deltaX === 0 && deltaY === 0) {
145 return;
146 }
147 Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
148
149 vp.x += deltaX;
150 vp.y += deltaY;
151
152 this._damage(vp.x, vp.y, vp.w, vp.h);
153
154 this.flip();
155 },
156
157 viewportChangeSize: function(width, height) {
158
159 if (!this._clipViewport ||
160 typeof(width) === "undefined" ||
161 typeof(height) === "undefined") {
162
163 Log.Debug("Setting viewport to full display region");
164 width = this._fb_width;
165 height = this._fb_height;
166 }
167
168 if (width > this._fb_width) {
169 width = this._fb_width;
170 }
171 if (height > this._fb_height) {
172 height = this._fb_height;
173 }
174
175 var vp = this._viewportLoc;
176 if (vp.w !== width || vp.h !== height) {
177 vp.w = width;
178 vp.h = height;
179
180 var canvas = this._target;
181 canvas.width = width;
182 canvas.height = height;
183
184 // The position might need to be updated if we've grown
185 this.viewportChangePos(0, 0);
186
187 this._damage(vp.x, vp.y, vp.w, vp.h);
188 this.flip();
189
190 // Update the visible size of the target canvas
191 this._rescale(this._scale);
192 }
193 },
194
195 absX: function (x) {
196 return x / this._scale + this._viewportLoc.x;
197 },
198
199 absY: function (y) {
200 return y / this._scale + this._viewportLoc.y;
201 },
202
203 resize: function (width, height) {
204 this._prevDrawStyle = "";
205
206 this._fb_width = width;
207 this._fb_height = height;
208
209 var canvas = this._backbuffer;
210 if (canvas.width !== width || canvas.height !== height) {
211
212 // We have to save the canvas data since changing the size will clear it
213 var saveImg = null;
214 if (canvas.width > 0 && canvas.height > 0) {
215 saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);
216 }
217
218 if (canvas.width !== width) {
219 canvas.width = width;
220 }
221 if (canvas.height !== height) {
222 canvas.height = height;
223 }
224
225 if (saveImg) {
226 this._drawCtx.putImageData(saveImg, 0, 0);
227 }
228 }
229
230 // Readjust the viewport as it may be incorrectly sized
231 // and positioned
232 var vp = this._viewportLoc;
233 this.viewportChangeSize(vp.w, vp.h);
234 this.viewportChangePos(0, 0);
235 },
236
237 // Track what parts of the visible canvas that need updating
238 _damage: function(x, y, w, h) {
239 if (x < this._damageBounds.left) {
240 this._damageBounds.left = x;
241 }
242 if (y < this._damageBounds.top) {
243 this._damageBounds.top = y;
244 }
245 if ((x + w) > this._damageBounds.right) {
246 this._damageBounds.right = x + w;
247 }
248 if ((y + h) > this._damageBounds.bottom) {
249 this._damageBounds.bottom = y + h;
250 }
251 },
252
253 // Update the visible canvas with the contents of the
254 // rendering canvas
255 flip: function(from_queue) {
256 if (this._renderQ.length !== 0 && !from_queue) {
257 this._renderQ_push({
258 'type': 'flip'
259 });
260 } else {
261 var x, y, vx, vy, w, h;
262
263 x = this._damageBounds.left;
264 y = this._damageBounds.top;
265 w = this._damageBounds.right - x;
266 h = this._damageBounds.bottom - y;
267
268 vx = x - this._viewportLoc.x;
269 vy = y - this._viewportLoc.y;
270
271 if (vx < 0) {
272 w += vx;
273 x -= vx;
274 vx = 0;
275 }
276 if (vy < 0) {
277 h += vy;
278 y -= vy;
279 vy = 0;
280 }
281
282 if ((vx + w) > this._viewportLoc.w) {
283 w = this._viewportLoc.w - vx;
284 }
285 if ((vy + h) > this._viewportLoc.h) {
286 h = this._viewportLoc.h - vy;
287 }
288
289 if ((w > 0) && (h > 0)) {
290 // FIXME: We may need to disable image smoothing here
291 // as well (see copyImage()), but we haven't
292 // noticed any problem yet.
293 this._targetCtx.drawImage(this._backbuffer,
294 x, y, w, h,
295 vx, vy, w, h);
296 }
297
298 this._damageBounds.left = this._damageBounds.top = 65535;
299 this._damageBounds.right = this._damageBounds.bottom = 0;
300 }
301 },
302
303 clear: function () {
304 if (this._logo) {
305 this.resize(this._logo.width, this._logo.height);
306 this.imageRect(0, 0, this._logo.type, this._logo.data);
307 } else {
308 this.resize(240, 20);
309 this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height);
310 }
311 this.flip();
312 },
313
314 pending: function() {
315 return this._renderQ.length > 0;
316 },
317
318 flush: function() {
319 if (this._renderQ.length === 0) {
320 this.onflush();
321 } else {
322 this._flushing = true;
323 }
324 },
325
326 fillRect: function (x, y, width, height, color, from_queue) {
327 if (this._renderQ.length !== 0 && !from_queue) {
328 this._renderQ_push({
329 'type': 'fill',
330 'x': x,
331 'y': y,
332 'width': width,
333 'height': height,
334 'color': color
335 });
336 } else {
337 this._setFillColor(color);
338 this._drawCtx.fillRect(x, y, width, height);
339 this._damage(x, y, width, height);
340 }
341 },
342
343 copyImage: function (old_x, old_y, new_x, new_y, w, h, from_queue) {
344 if (this._renderQ.length !== 0 && !from_queue) {
345 this._renderQ_push({
346 'type': 'copy',
347 'old_x': old_x,
348 'old_y': old_y,
349 'x': new_x,
350 'y': new_y,
351 'width': w,
352 'height': h,
353 });
354 } else {
355 // Due to this bug among others [1] we need to disable the image-smoothing to
356 // avoid getting a blur effect when copying data.
357 //
358 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
359 //
360 // We need to set these every time since all properties are reset
361 // when the the size is changed
362 this._drawCtx.mozImageSmoothingEnabled = false;
363 this._drawCtx.webkitImageSmoothingEnabled = false;
364 this._drawCtx.msImageSmoothingEnabled = false;
365 this._drawCtx.imageSmoothingEnabled = false;
366
367 this._drawCtx.drawImage(this._backbuffer,
368 old_x, old_y, w, h,
369 new_x, new_y, w, h);
370 this._damage(new_x, new_y, w, h);
371 }
372 },
373
374 imageRect: function(x, y, mime, arr) {
375 var img = new Image();
376 img.src = "data: " + mime + ";base64," + Base64.encode(arr);
377 this._renderQ_push({
378 'type': 'img',
379 'img': img,
380 'x': x,
381 'y': y
382 });
383 },
384
385 // start updating a tile
386 startTile: function (x, y, width, height, color) {
387 this._tile_x = x;
388 this._tile_y = y;
389 if (width === 16 && height === 16) {
390 this._tile = this._tile16x16;
391 } else {
392 this._tile = this._drawCtx.createImageData(width, height);
393 }
394
395 var red = color[2];
396 var green = color[1];
397 var blue = color[0];
398
399 var data = this._tile.data;
400 for (var i = 0; i < width * height * 4; i += 4) {
401 data[i] = red;
402 data[i + 1] = green;
403 data[i + 2] = blue;
404 data[i + 3] = 255;
405 }
406 },
407
408 // update sub-rectangle of the current tile
409 subTile: function (x, y, w, h, color) {
410 var red = color[2];
411 var green = color[1];
412 var blue = color[0];
413 var xend = x + w;
414 var yend = y + h;
415
416 var data = this._tile.data;
417 var width = this._tile.width;
418 for (var j = y; j < yend; j++) {
419 for (var i = x; i < xend; i++) {
420 var p = (i + (j * width)) * 4;
421 data[p] = red;
422 data[p + 1] = green;
423 data[p + 2] = blue;
424 data[p + 3] = 255;
425 }
426 }
427 },
428
429 // draw the current tile to the screen
430 finishTile: function () {
431 this._drawCtx.putImageData(this._tile, this._tile_x, this._tile_y);
432 this._damage(this._tile_x, this._tile_y,
433 this._tile.width, this._tile.height);
434 },
435
436 blitImage: function (x, y, width, height, arr, offset, from_queue) {
437 if (this._renderQ.length !== 0 && !from_queue) {
438 // NB(directxman12): it's technically more performant here to use preallocated arrays,
439 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
440 // this probably isn't getting called *nearly* as much
441 var new_arr = new Uint8Array(width * height * 4);
442 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
443 this._renderQ_push({
444 'type': 'blit',
445 'data': new_arr,
446 'x': x,
447 'y': y,
448 'width': width,
449 'height': height,
450 });
451 } else {
452 this._bgrxImageData(x, y, width, height, arr, offset);
453 }
454 },
455
456 blitRgbImage: function (x, y , width, height, arr, offset, from_queue) {
457 if (this._renderQ.length !== 0 && !from_queue) {
458 // NB(directxman12): it's technically more performant here to use preallocated arrays,
459 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
460 // this probably isn't getting called *nearly* as much
461 var new_arr = new Uint8Array(width * height * 3);
462 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
463 this._renderQ_push({
464 'type': 'blitRgb',
465 'data': new_arr,
466 'x': x,
467 'y': y,
468 'width': width,
469 'height': height,
470 });
471 } else {
472 this._rgbImageData(x, y, width, height, arr, offset);
473 }
474 },
475
476 blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) {
477 if (this._renderQ.length !== 0 && !from_queue) {
478 // NB(directxman12): it's technically more performant here to use preallocated arrays,
479 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
480 // this probably isn't getting called *nearly* as much
481 var new_arr = new Uint8Array(width * height * 4);
482 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
483 this._renderQ_push({
484 'type': 'blitRgbx',
485 'data': new_arr,
486 'x': x,
487 'y': y,
488 'width': width,
489 'height': height,
490 });
491 } else {
492 this._rgbxImageData(x, y, width, height, arr, offset);
493 }
494 },
495
496 drawImage: function (img, x, y) {
497 this._drawCtx.drawImage(img, x, y);
498 this._damage(x, y, img.width, img.height);
499 },
500
501 changeCursor: function (pixels, mask, hotx, hoty, w, h) {
502 Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h);
503 },
504
505 defaultCursor: function () {
506 this._target.style.cursor = "default";
507 },
508
509 disableLocalCursor: function () {
510 this._target.style.cursor = "none";
511 },
512
513 autoscale: function (containerWidth, containerHeight) {
514 var vp = this._viewportLoc;
515 var targetAspectRatio = containerWidth / containerHeight;
516 var fbAspectRatio = vp.w / vp.h;
517
518 var scaleRatio;
519 if (fbAspectRatio >= targetAspectRatio) {
520 scaleRatio = containerWidth / vp.w;
521 } else {
522 scaleRatio = containerHeight / vp.h;
523 }
524
525 this._rescale(scaleRatio);
526 },
527
528 // ===== PRIVATE METHODS =====
529
530 _rescale: function (factor) {
531 this._scale = factor;
532 var vp = this._viewportLoc;
533
534 // NB(directxman12): If you set the width directly, or set the
535 // style width to a number, the canvas is cleared.
536 // However, if you set the style width to a string
537 // ('NNNpx'), the canvas is scaled without clearing.
538 var width = Math.round(factor * vp.w) + 'px';
539 var height = Math.round(factor * vp.h) + 'px';
540
541 if ((this._target.style.width !== width) ||
542 (this._target.style.height !== height)) {
543 this._target.style.width = width;
544 this._target.style.height = height;
545 }
546 },
547
548 _setFillColor: function (color) {
549 var newStyle = 'rgb(' + color[2] + ',' + color[1] + ',' + color[0] + ')';
550 if (newStyle !== this._prevDrawStyle) {
551 this._drawCtx.fillStyle = newStyle;
552 this._prevDrawStyle = newStyle;
553 }
554 },
555
556 _rgbImageData: function (x, y, width, height, arr, offset) {
557 var img = this._drawCtx.createImageData(width, height);
558 var data = img.data;
559 for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) {
560 data[i] = arr[j];
561 data[i + 1] = arr[j + 1];
562 data[i + 2] = arr[j + 2];
563 data[i + 3] = 255; // Alpha
564 }
565 this._drawCtx.putImageData(img, x, y);
566 this._damage(x, y, img.width, img.height);
567 },
568
569 _bgrxImageData: function (x, y, width, height, arr, offset) {
570 var img = this._drawCtx.createImageData(width, height);
571 var data = img.data;
572 for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) {
573 data[i] = arr[j + 2];
574 data[i + 1] = arr[j + 1];
575 data[i + 2] = arr[j];
576 data[i + 3] = 255; // Alpha
577 }
578 this._drawCtx.putImageData(img, x, y);
579 this._damage(x, y, img.width, img.height);
580 },
581
582 _rgbxImageData: function (x, y, width, height, arr, offset) {
583 // NB(directxman12): arr must be an Type Array view
584 var img;
585 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) {
586 img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height);
587 } else {
588 img = this._drawCtx.createImageData(width, height);
589 img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4));
590 }
591 this._drawCtx.putImageData(img, x, y);
592 this._damage(x, y, img.width, img.height);
593 },
594
595 _renderQ_push: function (action) {
596 this._renderQ.push(action);
597 if (this._renderQ.length === 1) {
598 // If this can be rendered immediately it will be, otherwise
599 // the scanner will wait for the relevant event
600 this._scan_renderQ();
601 }
602 },
603
604 _resume_renderQ: function() {
605 // "this" is the object that is ready, not the
606 // display object
607 this.removeEventListener('load', this._noVNC_display._resume_renderQ);
608 this._noVNC_display._scan_renderQ();
609 },
610
611 _scan_renderQ: function () {
612 var ready = true;
613 while (ready && this._renderQ.length > 0) {
614 var a = this._renderQ[0];
615 switch (a.type) {
616 case 'flip':
617 this.flip(true);
618 break;
619 case 'copy':
620 this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true);
621 break;
622 case 'fill':
623 this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
624 break;
625 case 'blit':
626 this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
627 break;
628 case 'blitRgb':
629 this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true);
630 break;
631 case 'blitRgbx':
632 this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true);
633 break;
634 case 'img':
635 if (a.img.complete) {
636 this.drawImage(a.img, a.x, a.y);
637 } else {
638 a.img._noVNC_display = this;
639 a.img.addEventListener('load', this._resume_renderQ);
640 // We need to wait for this image to 'load'
641 // to keep things in-order
642 ready = false;
643 }
644 break;
645 }
646
647 if (ready) {
648 this._renderQ.shift();
649 }
650 }
651
652 if (this._renderQ.length === 0 && this._flushing) {
653 this._flushing = false;
654 this.onflush();
655 }
656 },
657 };
658
659 // Class Methods
660 Display.changeCursor = function (target, pixels, mask, hotx, hoty, w, h) {
661 if ((w === 0) || (h === 0)) {
662 target.style.cursor = 'none';
663 return;
664 }
665
666 var cur = []
667 var y, x;
668 for (y = 0; y < h; y++) {
669 for (x = 0; x < w; x++) {
670 var idx = y * Math.ceil(w / 8) + Math.floor(x / 8);
671 var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
672 idx = ((w * y) + x) * 4;
673 cur.push(pixels[idx + 2]); // red
674 cur.push(pixels[idx + 1]); // green
675 cur.push(pixels[idx]); // blue
676 cur.push(alpha); // alpha
677 }
678 }
679
680 var canvas = document.createElement('canvas');
681 var ctx = canvas.getContext('2d');
682
683 canvas.width = w;
684 canvas.height = h;
685
686 var img;
687 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) {
688 img = new ImageData(new Uint8ClampedArray(cur), w, h);
689 } else {
690 img = ctx.createImageData(w, h);
691 img.data.set(new Uint8ClampedArray(cur));
692 }
693 ctx.clearRect(0, 0, w, h);
694 ctx.putImageData(img, 0, 0);
695
696 var url = canvas.toDataURL();
697 target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
698 };