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