]> git.proxmox.com Git - mirror_novnc.git/blob - core/display.js
Prefer const/let over var
[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 let 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 const 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 const 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 const vx2 = vp.x + vp.w - 1;
126 const 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 const vp = this._viewportLoc;
176 if (vp.w !== width || vp.h !== height) {
177 vp.w = width;
178 vp.h = height;
179
180 const 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 const 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 let 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 const 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 let x = this._damageBounds.left;
262 let y = this._damageBounds.top;
263 let w = this._damageBounds.right - x;
264 let h = this._damageBounds.bottom - y;
265
266 let vx = x - this._viewportLoc.x;
267 let vy = y - this._viewportLoc.y;
268
269 if (vx < 0) {
270 w += vx;
271 x -= vx;
272 vx = 0;
273 }
274 if (vy < 0) {
275 h += vy;
276 y -= vy;
277 vy = 0;
278 }
279
280 if ((vx + w) > this._viewportLoc.w) {
281 w = this._viewportLoc.w - vx;
282 }
283 if ((vy + h) > this._viewportLoc.h) {
284 h = this._viewportLoc.h - vy;
285 }
286
287 if ((w > 0) && (h > 0)) {
288 // FIXME: We may need to disable image smoothing here
289 // as well (see copyImage()), but we haven't
290 // noticed any problem yet.
291 this._targetCtx.drawImage(this._backbuffer,
292 x, y, w, h,
293 vx, vy, w, h);
294 }
295
296 this._damageBounds.left = this._damageBounds.top = 65535;
297 this._damageBounds.right = this._damageBounds.bottom = 0;
298 }
299 },
300
301 clear: function () {
302 if (this._logo) {
303 this.resize(this._logo.width, this._logo.height);
304 this.imageRect(0, 0, this._logo.type, this._logo.data);
305 } else {
306 this.resize(240, 20);
307 this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height);
308 }
309 this.flip();
310 },
311
312 pending: function() {
313 return this._renderQ.length > 0;
314 },
315
316 flush: function() {
317 if (this._renderQ.length === 0) {
318 this.onflush();
319 } else {
320 this._flushing = true;
321 }
322 },
323
324 fillRect: function (x, y, width, height, color, from_queue) {
325 if (this._renderQ.length !== 0 && !from_queue) {
326 this._renderQ_push({
327 'type': 'fill',
328 'x': x,
329 'y': y,
330 'width': width,
331 'height': height,
332 'color': color
333 });
334 } else {
335 this._setFillColor(color);
336 this._drawCtx.fillRect(x, y, width, height);
337 this._damage(x, y, width, height);
338 }
339 },
340
341 copyImage: function (old_x, old_y, new_x, new_y, w, h, from_queue) {
342 if (this._renderQ.length !== 0 && !from_queue) {
343 this._renderQ_push({
344 'type': 'copy',
345 'old_x': old_x,
346 'old_y': old_y,
347 'x': new_x,
348 'y': new_y,
349 'width': w,
350 'height': h,
351 });
352 } else {
353 // Due to this bug among others [1] we need to disable the image-smoothing to
354 // avoid getting a blur effect when copying data.
355 //
356 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
357 //
358 // We need to set these every time since all properties are reset
359 // when the the size is changed
360 this._drawCtx.mozImageSmoothingEnabled = false;
361 this._drawCtx.webkitImageSmoothingEnabled = false;
362 this._drawCtx.msImageSmoothingEnabled = false;
363 this._drawCtx.imageSmoothingEnabled = false;
364
365 this._drawCtx.drawImage(this._backbuffer,
366 old_x, old_y, w, h,
367 new_x, new_y, w, h);
368 this._damage(new_x, new_y, w, h);
369 }
370 },
371
372 imageRect: function(x, y, mime, arr) {
373 const img = new Image();
374 img.src = "data: " + mime + ";base64," + Base64.encode(arr);
375 this._renderQ_push({
376 'type': 'img',
377 'img': img,
378 'x': x,
379 'y': y
380 });
381 },
382
383 // start updating a tile
384 startTile: function (x, y, width, height, color) {
385 this._tile_x = x;
386 this._tile_y = y;
387 if (width === 16 && height === 16) {
388 this._tile = this._tile16x16;
389 } else {
390 this._tile = this._drawCtx.createImageData(width, height);
391 }
392
393 const red = color[2];
394 const green = color[1];
395 const blue = color[0];
396
397 const data = this._tile.data;
398 for (let i = 0; i < width * height * 4; i += 4) {
399 data[i] = red;
400 data[i + 1] = green;
401 data[i + 2] = blue;
402 data[i + 3] = 255;
403 }
404 },
405
406 // update sub-rectangle of the current tile
407 subTile: function (x, y, w, h, color) {
408 const red = color[2];
409 const green = color[1];
410 const blue = color[0];
411 const xend = x + w;
412 const yend = y + h;
413
414 const data = this._tile.data;
415 const width = this._tile.width;
416 for (let j = y; j < yend; j++) {
417 for (let i = x; i < xend; i++) {
418 const p = (i + (j * width)) * 4;
419 data[p] = red;
420 data[p + 1] = green;
421 data[p + 2] = blue;
422 data[p + 3] = 255;
423 }
424 }
425 },
426
427 // draw the current tile to the screen
428 finishTile: function () {
429 this._drawCtx.putImageData(this._tile, this._tile_x, this._tile_y);
430 this._damage(this._tile_x, this._tile_y,
431 this._tile.width, this._tile.height);
432 },
433
434 blitImage: function (x, y, width, height, arr, offset, from_queue) {
435 if (this._renderQ.length !== 0 && !from_queue) {
436 // NB(directxman12): it's technically more performant here to use preallocated arrays,
437 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
438 // this probably isn't getting called *nearly* as much
439 const new_arr = new Uint8Array(width * height * 4);
440 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
441 this._renderQ_push({
442 'type': 'blit',
443 'data': new_arr,
444 'x': x,
445 'y': y,
446 'width': width,
447 'height': height,
448 });
449 } else {
450 this._bgrxImageData(x, y, width, height, arr, offset);
451 }
452 },
453
454 blitRgbImage: function (x, y , width, height, arr, offset, from_queue) {
455 if (this._renderQ.length !== 0 && !from_queue) {
456 // NB(directxman12): it's technically more performant here to use preallocated arrays,
457 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
458 // this probably isn't getting called *nearly* as much
459 const new_arr = new Uint8Array(width * height * 3);
460 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
461 this._renderQ_push({
462 'type': 'blitRgb',
463 'data': new_arr,
464 'x': x,
465 'y': y,
466 'width': width,
467 'height': height,
468 });
469 } else {
470 this._rgbImageData(x, y, width, height, arr, offset);
471 }
472 },
473
474 blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) {
475 if (this._renderQ.length !== 0 && !from_queue) {
476 // NB(directxman12): it's technically more performant here to use preallocated arrays,
477 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
478 // this probably isn't getting called *nearly* as much
479 const new_arr = new Uint8Array(width * height * 4);
480 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
481 this._renderQ_push({
482 'type': 'blitRgbx',
483 'data': new_arr,
484 'x': x,
485 'y': y,
486 'width': width,
487 'height': height,
488 });
489 } else {
490 this._rgbxImageData(x, y, width, height, arr, offset);
491 }
492 },
493
494 drawImage: function (img, x, y) {
495 this._drawCtx.drawImage(img, x, y);
496 this._damage(x, y, img.width, img.height);
497 },
498
499 changeCursor: function (pixels, mask, hotx, hoty, w, h) {
500 Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h);
501 },
502
503 defaultCursor: function () {
504 this._target.style.cursor = "default";
505 },
506
507 disableLocalCursor: function () {
508 this._target.style.cursor = "none";
509 },
510
511 autoscale: function (containerWidth, containerHeight) {
512 const vp = this._viewportLoc;
513 const targetAspectRatio = containerWidth / containerHeight;
514 const fbAspectRatio = vp.w / vp.h;
515
516 let scaleRatio;
517 if (fbAspectRatio >= targetAspectRatio) {
518 scaleRatio = containerWidth / vp.w;
519 } else {
520 scaleRatio = containerHeight / vp.h;
521 }
522
523 this._rescale(scaleRatio);
524 },
525
526 // ===== PRIVATE METHODS =====
527
528 _rescale: function (factor) {
529 this._scale = factor;
530 const vp = this._viewportLoc;
531
532 // NB(directxman12): If you set the width directly, or set the
533 // style width to a number, the canvas is cleared.
534 // However, if you set the style width to a string
535 // ('NNNpx'), the canvas is scaled without clearing.
536 const width = Math.round(factor * vp.w) + 'px';
537 const height = Math.round(factor * vp.h) + 'px';
538
539 if ((this._target.style.width !== width) ||
540 (this._target.style.height !== height)) {
541 this._target.style.width = width;
542 this._target.style.height = height;
543 }
544 },
545
546 _setFillColor: function (color) {
547 const newStyle = 'rgb(' + color[2] + ',' + color[1] + ',' + color[0] + ')';
548 if (newStyle !== this._prevDrawStyle) {
549 this._drawCtx.fillStyle = newStyle;
550 this._prevDrawStyle = newStyle;
551 }
552 },
553
554 _rgbImageData: function (x, y, width, height, arr, offset) {
555 const img = this._drawCtx.createImageData(width, height);
556 const data = img.data;
557 for (let i = 0, j = offset; i < width * height * 4; i += 4, j += 3) {
558 data[i] = arr[j];
559 data[i + 1] = arr[j + 1];
560 data[i + 2] = arr[j + 2];
561 data[i + 3] = 255; // Alpha
562 }
563 this._drawCtx.putImageData(img, x, y);
564 this._damage(x, y, img.width, img.height);
565 },
566
567 _bgrxImageData: function (x, y, width, height, arr, offset) {
568 const img = this._drawCtx.createImageData(width, height);
569 const data = img.data;
570 for (let i = 0, j = offset; i < width * height * 4; i += 4, j += 4) {
571 data[i] = arr[j + 2];
572 data[i + 1] = arr[j + 1];
573 data[i + 2] = arr[j];
574 data[i + 3] = 255; // Alpha
575 }
576 this._drawCtx.putImageData(img, x, y);
577 this._damage(x, y, img.width, img.height);
578 },
579
580 _rgbxImageData: function (x, y, width, height, arr, offset) {
581 // NB(directxman12): arr must be an Type Array view
582 let img;
583 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) {
584 img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height);
585 } else {
586 img = this._drawCtx.createImageData(width, height);
587 img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4));
588 }
589 this._drawCtx.putImageData(img, x, y);
590 this._damage(x, y, img.width, img.height);
591 },
592
593 _renderQ_push: function (action) {
594 this._renderQ.push(action);
595 if (this._renderQ.length === 1) {
596 // If this can be rendered immediately it will be, otherwise
597 // the scanner will wait for the relevant event
598 this._scan_renderQ();
599 }
600 },
601
602 _resume_renderQ: function() {
603 // "this" is the object that is ready, not the
604 // display object
605 this.removeEventListener('load', this._noVNC_display._resume_renderQ);
606 this._noVNC_display._scan_renderQ();
607 },
608
609 _scan_renderQ: function () {
610 let ready = true;
611 while (ready && this._renderQ.length > 0) {
612 const a = this._renderQ[0];
613 switch (a.type) {
614 case 'flip':
615 this.flip(true);
616 break;
617 case 'copy':
618 this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true);
619 break;
620 case 'fill':
621 this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
622 break;
623 case 'blit':
624 this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
625 break;
626 case 'blitRgb':
627 this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true);
628 break;
629 case 'blitRgbx':
630 this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true);
631 break;
632 case 'img':
633 if (a.img.complete) {
634 this.drawImage(a.img, a.x, a.y);
635 } else {
636 a.img._noVNC_display = this;
637 a.img.addEventListener('load', this._resume_renderQ);
638 // We need to wait for this image to 'load'
639 // to keep things in-order
640 ready = false;
641 }
642 break;
643 }
644
645 if (ready) {
646 this._renderQ.shift();
647 }
648 }
649
650 if (this._renderQ.length === 0 && this._flushing) {
651 this._flushing = false;
652 this.onflush();
653 }
654 },
655 };
656
657 // Class Methods
658 Display.changeCursor = function (target, pixels, mask, hotx, hoty, w, h) {
659 if ((w === 0) || (h === 0)) {
660 target.style.cursor = 'none';
661 return;
662 }
663
664 const cur = []
665 for (let y = 0; y < h; y++) {
666 for (let x = 0; x < w; x++) {
667 let idx = y * Math.ceil(w / 8) + Math.floor(x / 8);
668 const alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
669 idx = ((w * y) + x) * 4;
670 cur.push(pixels[idx + 2]); // red
671 cur.push(pixels[idx + 1]); // green
672 cur.push(pixels[idx]); // blue
673 cur.push(alpha); // alpha
674 }
675 }
676
677 const canvas = document.createElement('canvas');
678 const ctx = canvas.getContext('2d');
679
680 canvas.width = w;
681 canvas.height = h;
682
683 let img;
684 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) {
685 img = new ImageData(new Uint8ClampedArray(cur), w, h);
686 } else {
687 img = ctx.createImageData(w, h);
688 img.data.set(new Uint8ClampedArray(cur));
689 }
690 ctx.clearRect(0, 0, w, h);
691 ctx.putImageData(img, 0, 0);
692
693 const url = canvas.toDataURL();
694 target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
695 };