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