]> git.proxmox.com Git - mirror_novnc.git/blame - core/display.js
Merge branch 'feature/support-existing-rtcdatachannel-or-websocket-squashed' of https...
[mirror_novnc.git] / core / display.js
CommitLineData
c4164bda
JM
1/*
2 * noVNC: HTML5 VNC client
412d9306 3 * Copyright (C) 2019 The noVNC Authors
1d728ace 4 * Licensed under MPL 2.0 (see LICENSE.txt)
c4164bda
JM
5 *
6 * See README.md for usage and integration instructions.
7 */
c4164bda 8
6d6f0db0 9import * as Log from './util/logging.js';
3ae0bb09 10import Base64 from "./base64.js";
97b86abc 11import { toSigned32bit } from './util/int.js';
ae510306 12
0e4808bf
JD
13export default class Display {
14 constructor(target) {
15 this._drawCtx = null;
ae510306 16
0e4808bf
JD
17 this._renderQ = []; // queue drawing actions for in-oder rendering
18 this._flushing = false;
ae510306 19
0e4808bf 20 // the full frame buffer (logical canvas) size
5d570207
SM
21 this._fbWidth = 0;
22 this._fbHeight = 0;
ae510306 23
0e4808bf 24 this._prevDrawStyle = "";
ae510306 25
0e4808bf 26 Log.Debug(">> Display.constructor");
3d7bb020 27
0e4808bf
JD
28 // The visible canvas
29 this._target = target;
ae510306 30
0e4808bf
JD
31 if (!this._target) {
32 throw new Error("Target must be set");
33 }
ae510306 34
0e4808bf
JD
35 if (typeof this._target === 'string') {
36 throw new Error('target must be a DOM element');
37 }
ae510306 38
0e4808bf
JD
39 if (!this._target.getContext) {
40 throw new Error("no getContext method");
41 }
2ba767a7 42
0e4808bf 43 this._targetCtx = this._target.getContext('2d');
adf345fd 44
0e4808bf
JD
45 // the visible canvas viewport (i.e. what actually gets seen)
46 this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };
ae510306 47
0e4808bf
JD
48 // The hidden canvas, where we do the actual rendering
49 this._backbuffer = document.createElement('canvas');
50 this._drawCtx = this._backbuffer.getContext('2d');
84cd0e71 51
942a3127 52 this._damageBounds = { left: 0, top: 0,
0e4808bf
JD
53 right: this._backbuffer.width,
54 bottom: this._backbuffer.height };
ae510306 55
0e4808bf 56 Log.Debug("User Agent: " + navigator.userAgent);
ae510306 57
0e4808bf
JD
58 Log.Debug("<< Display.constructor");
59
60 // ===== PROPERTIES =====
61
62 this._scale = 1.0;
63 this._clipViewport = false;
0e4808bf
JD
64
65 // ===== EVENT HANDLERS =====
66
67 this.onflush = () => {}; // A flush request has finished
68 }
6d6f0db0 69
747b4623
PO
70 // ===== PROPERTIES =====
71
0e4808bf 72 get scale() { return this._scale; }
747b4623
PO
73 set scale(scale) {
74 this._rescale(scale);
0e4808bf 75 }
747b4623 76
0e4808bf 77 get clipViewport() { return this._clipViewport; }
0460e5fd
PO
78 set clipViewport(viewport) {
79 this._clipViewport = viewport;
747b4623 80 // May need to readjust the viewport dimensions
2b5f94fa 81 const vp = this._viewportLoc;
747b4623
PO
82 this.viewportChangeSize(vp.w, vp.h);
83 this.viewportChangePos(0, 0);
0e4808bf 84 }
747b4623
PO
85
86 get width() {
5d570207 87 return this._fbWidth;
0e4808bf
JD
88 }
89
747b4623 90 get height() {
5d570207 91 return this._fbHeight;
0e4808bf 92 }
747b4623
PO
93
94 // ===== PUBLIC METHODS =====
95
0e4808bf 96 viewportChangePos(deltaX, deltaY) {
2b5f94fa 97 const vp = this._viewportLoc;
6d6f0db0
SR
98 deltaX = Math.floor(deltaX);
99 deltaY = Math.floor(deltaY);
100
0460e5fd 101 if (!this._clipViewport) {
6d6f0db0
SR
102 deltaX = -vp.w; // clamped later of out of bounds
103 deltaY = -vp.h;
104 }
d1800d09 105
2b5f94fa
JD
106 const vx2 = vp.x + vp.w - 1;
107 const vy2 = vp.y + vp.h - 1;
490d471c 108
6d6f0db0 109 // Position change
1e13775b 110
6d6f0db0
SR
111 if (deltaX < 0 && vp.x + deltaX < 0) {
112 deltaX = -vp.x;
113 }
5d570207
SM
114 if (vx2 + deltaX >= this._fbWidth) {
115 deltaX -= vx2 + deltaX - this._fbWidth + 1;
6d6f0db0 116 }
54e7cbdf 117
6d6f0db0
SR
118 if (vp.y + deltaY < 0) {
119 deltaY = -vp.y;
120 }
5d570207
SM
121 if (vy2 + deltaY >= this._fbHeight) {
122 deltaY -= (vy2 + deltaY - this._fbHeight + 1);
6d6f0db0 123 }
54e7cbdf 124
6d6f0db0
SR
125 if (deltaX === 0 && deltaY === 0) {
126 return;
127 }
128 Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
54e7cbdf 129
6d6f0db0
SR
130 vp.x += deltaX;
131 vp.y += deltaY;
54e7cbdf 132
6d6f0db0 133 this._damage(vp.x, vp.y, vp.w, vp.h);
54e7cbdf 134
6d6f0db0 135 this.flip();
0e4808bf 136 }
1e13775b 137
0e4808bf 138 viewportChangeSize(width, height) {
a8255821 139
0460e5fd 140 if (!this._clipViewport ||
6d6f0db0
SR
141 typeof(width) === "undefined" ||
142 typeof(height) === "undefined") {
84cd0e71 143
6d6f0db0 144 Log.Debug("Setting viewport to full display region");
5d570207
SM
145 width = this._fbWidth;
146 height = this._fbHeight;
6d6f0db0 147 }
1e13775b 148
ab1ace38
PO
149 width = Math.floor(width);
150 height = Math.floor(height);
151
5d570207
SM
152 if (width > this._fbWidth) {
153 width = this._fbWidth;
6d6f0db0 154 }
5d570207
SM
155 if (height > this._fbHeight) {
156 height = this._fbHeight;
6d6f0db0 157 }
636be753 158
2b5f94fa 159 const vp = this._viewportLoc;
6d6f0db0
SR
160 if (vp.w !== width || vp.h !== height) {
161 vp.w = width;
162 vp.h = height;
636be753 163
2b5f94fa 164 const canvas = this._target;
6d6f0db0
SR
165 canvas.width = width;
166 canvas.height = height;
636be753 167
6d6f0db0
SR
168 // The position might need to be updated if we've grown
169 this.viewportChangePos(0, 0);
adf345fd 170
6d6f0db0
SR
171 this._damage(vp.x, vp.y, vp.w, vp.h);
172 this.flip();
636be753 173
6d6f0db0
SR
174 // Update the visible size of the target canvas
175 this._rescale(this._scale);
176 }
0e4808bf 177 }
adf345fd 178
0e4808bf 179 absX(x) {
a136b4b0
SM
180 if (this._scale === 0) {
181 return 0;
182 }
97b86abc 183 return toSigned32bit(x / this._scale + this._viewportLoc.x);
0e4808bf 184 }
adf345fd 185
0e4808bf 186 absY(y) {
a136b4b0
SM
187 if (this._scale === 0) {
188 return 0;
189 }
97b86abc 190 return toSigned32bit(y / this._scale + this._viewportLoc.y);
0e4808bf 191 }
adf345fd 192
0e4808bf 193 resize(width, height) {
6d6f0db0 194 this._prevDrawStyle = "";
636be753 195
5d570207
SM
196 this._fbWidth = width;
197 this._fbHeight = height;
1e13775b 198
2b5f94fa 199 const canvas = this._backbuffer;
6d6f0db0 200 if (canvas.width !== width || canvas.height !== height) {
1e13775b 201
6d6f0db0 202 // We have to save the canvas data since changing the size will clear it
2b5f94fa 203 let saveImg = null;
6d6f0db0
SR
204 if (canvas.width > 0 && canvas.height > 0) {
205 saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);
206 }
1e13775b 207
6d6f0db0
SR
208 if (canvas.width !== width) {
209 canvas.width = width;
210 }
211 if (canvas.height !== height) {
212 canvas.height = height;
213 }
1e13775b 214
6d6f0db0
SR
215 if (saveImg) {
216 this._drawCtx.putImageData(saveImg, 0, 0);
217 }
218 }
2ba767a7 219
6d6f0db0
SR
220 // Readjust the viewport as it may be incorrectly sized
221 // and positioned
2b5f94fa 222 const vp = this._viewportLoc;
6d6f0db0
SR
223 this.viewportChangeSize(vp.w, vp.h);
224 this.viewportChangePos(0, 0);
0e4808bf 225 }
6d6f0db0
SR
226
227 // Track what parts of the visible canvas that need updating
0e4808bf 228 _damage(x, y, w, h) {
6d6f0db0
SR
229 if (x < this._damageBounds.left) {
230 this._damageBounds.left = x;
231 }
232 if (y < this._damageBounds.top) {
233 this._damageBounds.top = y;
234 }
235 if ((x + w) > this._damageBounds.right) {
236 this._damageBounds.right = x + w;
237 }
238 if ((y + h) > this._damageBounds.bottom) {
239 this._damageBounds.bottom = y + h;
240 }
0e4808bf 241 }
2ba767a7 242
6d6f0db0
SR
243 // Update the visible canvas with the contents of the
244 // rendering canvas
5d570207
SM
245 flip(fromQueue) {
246 if (this._renderQ.length !== 0 && !fromQueue) {
247 this._renderQPush({
6d6f0db0
SR
248 'type': 'flip'
249 });
250 } else {
2b5f94fa
JD
251 let x = this._damageBounds.left;
252 let y = this._damageBounds.top;
253 let w = this._damageBounds.right - x;
254 let h = this._damageBounds.bottom - y;
2ba767a7 255
2b5f94fa
JD
256 let vx = x - this._viewportLoc.x;
257 let vy = y - this._viewportLoc.y;
1e13775b 258
6d6f0db0
SR
259 if (vx < 0) {
260 w += vx;
261 x -= vx;
262 vx = 0;
84cd0e71 263 }
6d6f0db0
SR
264 if (vy < 0) {
265 h += vy;
266 y -= vy;
267 vy = 0;
84cd0e71 268 }
6d6f0db0
SR
269
270 if ((vx + w) > this._viewportLoc.w) {
271 w = this._viewportLoc.w - vx;
84cd0e71 272 }
6d6f0db0
SR
273 if ((vy + h) > this._viewportLoc.h) {
274 h = this._viewportLoc.h - vy;
84cd0e71 275 }
84cd0e71 276
6d6f0db0
SR
277 if ((w > 0) && (h > 0)) {
278 // FIXME: We may need to disable image smoothing here
279 // as well (see copyImage()), but we haven't
280 // noticed any problem yet.
281 this._targetCtx.drawImage(this._backbuffer,
282 x, y, w, h,
283 vx, vy, w, h);
284 }
84cd0e71 285
6d6f0db0
SR
286 this._damageBounds.left = this._damageBounds.top = 65535;
287 this._damageBounds.right = this._damageBounds.bottom = 0;
288 }
0e4808bf 289 }
84cd0e71 290
0e4808bf 291 pending() {
6d6f0db0 292 return this._renderQ.length > 0;
0e4808bf 293 }
84cd0e71 294
0e4808bf 295 flush() {
6d6f0db0 296 if (this._renderQ.length === 0) {
747b4623 297 this.onflush();
6d6f0db0
SR
298 } else {
299 this._flushing = true;
300 }
0e4808bf 301 }
2ba767a7 302
5d570207
SM
303 fillRect(x, y, width, height, color, fromQueue) {
304 if (this._renderQ.length !== 0 && !fromQueue) {
305 this._renderQPush({
6d6f0db0
SR
306 'type': 'fill',
307 'x': x,
308 'y': y,
309 'width': width,
310 'height': height,
311 'color': color
312 });
313 } else {
314 this._setFillColor(color);
315 this._drawCtx.fillRect(x, y, width, height);
316 this._damage(x, y, width, height);
317 }
0e4808bf 318 }
1e13775b 319
5d570207
SM
320 copyImage(oldX, oldY, newX, newY, w, h, fromQueue) {
321 if (this._renderQ.length !== 0 && !fromQueue) {
322 this._renderQPush({
6d6f0db0 323 'type': 'copy',
5d570207
SM
324 'oldX': oldX,
325 'oldY': oldY,
326 'x': newX,
327 'y': newY,
6d6f0db0
SR
328 'width': w,
329 'height': h,
330 });
331 } else {
332 // Due to this bug among others [1] we need to disable the image-smoothing to
333 // avoid getting a blur effect when copying data.
334 //
335 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
336 //
337 // We need to set these every time since all properties are reset
338 // when the the size is changed
339 this._drawCtx.mozImageSmoothingEnabled = false;
340 this._drawCtx.webkitImageSmoothingEnabled = false;
341 this._drawCtx.msImageSmoothingEnabled = false;
342 this._drawCtx.imageSmoothingEnabled = false;
343
344 this._drawCtx.drawImage(this._backbuffer,
5d570207
SM
345 oldX, oldY, w, h,
346 newX, newY, w, h);
347 this._damage(newX, newY, w, h);
6d6f0db0 348 }
0e4808bf 349 }
6d6f0db0 350
4babdf33 351 imageRect(x, y, width, height, mime, arr) {
c4eb4ddc
PO
352 /* The internal logic cannot handle empty images, so bail early */
353 if ((width === 0) || (height === 0)) {
354 return;
355 }
356
2b5f94fa 357 const img = new Image();
6d6f0db0 358 img.src = "data: " + mime + ";base64," + Base64.encode(arr);
c4eb4ddc 359
5d570207 360 this._renderQPush({
6d6f0db0
SR
361 'type': 'img',
362 'img': img,
363 'x': x,
4babdf33
PO
364 'y': y,
365 'width': width,
366 'height': height
6d6f0db0 367 });
0e4808bf 368 }
6d6f0db0 369
5d570207
SM
370 blitImage(x, y, width, height, arr, offset, fromQueue) {
371 if (this._renderQ.length !== 0 && !fromQueue) {
6d6f0db0
SR
372 // NB(directxman12): it's technically more performant here to use preallocated arrays,
373 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
374 // this probably isn't getting called *nearly* as much
5d570207
SM
375 const newArr = new Uint8Array(width * height * 4);
376 newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
377 this._renderQPush({
6d6f0db0 378 'type': 'blit',
5d570207 379 'data': newArr,
1578fa68 380 'x': x,
6d6f0db0
SR
381 'y': y,
382 'width': width,
383 'height': height,
1578fa68 384 });
6d6f0db0 385 } else {
6a19390b
PO
386 // NB(directxman12): arr must be an Type Array view
387 let data = new Uint8ClampedArray(arr.buffer,
388 arr.byteOffset + offset,
389 width * height * 4);
27496941 390 let img = new ImageData(data, width, height);
6a19390b
PO
391 this._drawCtx.putImageData(img, x, y);
392 this._damage(x, y, width, height);
6d6f0db0 393 }
0e4808bf 394 }
1e13775b 395
0e4808bf 396 drawImage(img, x, y) {
6d6f0db0
SR
397 this._drawCtx.drawImage(img, x, y);
398 this._damage(x, y, img.width, img.height);
0e4808bf 399 }
d1800d09 400
0e4808bf 401 autoscale(containerWidth, containerHeight) {
a136b4b0 402 let scaleRatio;
6e7e6f9c 403
a136b4b0
SM
404 if (containerWidth === 0 || containerHeight === 0) {
405 scaleRatio = 0;
6d6f0db0 406
6d6f0db0 407 } else {
a136b4b0
SM
408
409 const vp = this._viewportLoc;
410 const targetAspectRatio = containerWidth / containerHeight;
411 const fbAspectRatio = vp.w / vp.h;
412
413 if (fbAspectRatio >= targetAspectRatio) {
414 scaleRatio = containerWidth / vp.w;
415 } else {
416 scaleRatio = containerHeight / vp.h;
417 }
6d6f0db0 418 }
d3796c14 419
6d6f0db0 420 this._rescale(scaleRatio);
0e4808bf 421 }
6d6f0db0 422
747b4623
PO
423 // ===== PRIVATE METHODS =====
424
0e4808bf 425 _rescale(factor) {
6d6f0db0 426 this._scale = factor;
2b5f94fa 427 const vp = this._viewportLoc;
6d6f0db0
SR
428
429 // NB(directxman12): If you set the width directly, or set the
430 // style width to a number, the canvas is cleared.
431 // However, if you set the style width to a string
432 // ('NNNpx'), the canvas is scaled without clearing.
ab1ace38
PO
433 const width = factor * vp.w + 'px';
434 const height = factor * vp.h + 'px';
6d6f0db0
SR
435
436 if ((this._target.style.width !== width) ||
437 (this._target.style.height !== height)) {
438 this._target.style.width = width;
439 this._target.style.height = height;
440 }
0e4808bf 441 }
9a23006e 442
0e4808bf 443 _setFillColor(color) {
6a19390b 444 const newStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';
6d6f0db0
SR
445 if (newStyle !== this._prevDrawStyle) {
446 this._drawCtx.fillStyle = newStyle;
447 this._prevDrawStyle = newStyle;
448 }
0e4808bf 449 }
6d6f0db0 450
5d570207 451 _renderQPush(action) {
6d6f0db0
SR
452 this._renderQ.push(action);
453 if (this._renderQ.length === 1) {
454 // If this can be rendered immediately it will be, otherwise
455 // the scanner will wait for the relevant event
5d570207 456 this._scanRenderQ();
6d6f0db0 457 }
0e4808bf 458 }
6d6f0db0 459
5d570207 460 _resumeRenderQ() {
6d6f0db0
SR
461 // "this" is the object that is ready, not the
462 // display object
5d570207
SM
463 this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ);
464 this._noVNCDisplay._scanRenderQ();
0e4808bf 465 }
6d6f0db0 466
5d570207 467 _scanRenderQ() {
2b5f94fa 468 let ready = true;
6d6f0db0 469 while (ready && this._renderQ.length > 0) {
2b5f94fa 470 const a = this._renderQ[0];
6d6f0db0
SR
471 switch (a.type) {
472 case 'flip':
473 this.flip(true);
474 break;
475 case 'copy':
5d570207 476 this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true);
6d6f0db0
SR
477 break;
478 case 'fill':
479 this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
480 break;
481 case 'blit':
482 this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
483 break;
6d6f0db0 484 case 'img':
5b5b7474 485 if (a.img.complete) {
4babdf33
PO
486 if (a.img.width !== a.width || a.img.height !== a.height) {
487 Log.Error("Decoded image has incorrect dimensions. Got " +
488 a.img.width + "x" + a.img.height + ". Expected " +
489 a.width + "x" + a.height + ".");
490 return;
491 }
6d6f0db0
SR
492 this.drawImage(a.img, a.x, a.y);
493 } else {
5d570207
SM
494 a.img._noVNCDisplay = this;
495 a.img.addEventListener('load', this._resumeRenderQ);
6d6f0db0
SR
496 // We need to wait for this image to 'load'
497 // to keep things in-order
498 ready = false;
499 }
500 break;
1e13775b
SR
501 }
502
6d6f0db0
SR
503 if (ready) {
504 this._renderQ.shift();
1578fa68 505 }
6d6f0db0 506 }
1e13775b 507
6d6f0db0
SR
508 if (this._renderQ.length === 0 && this._flushing) {
509 this._flushing = false;
747b4623 510 this.onflush();
6d6f0db0 511 }
0e4808bf
JD
512 }
513}