]>
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 | |
ae510306 SR |
13 | /* [module] |
14 | * import Util from "./util"; | |
15 | * import Base64 from "./base64"; | |
16 | */ | |
17 | ||
18 | /* [module] export default */ function Display(defaults) { | |
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 | ||
35 | Util.set_defaults(this, defaults, { | |
36 | 'true_color': true, | |
37 | 'colourMap': [], | |
38 | 'scale': 1.0, | |
39 | 'viewport': false, | |
d9ca5e5b PO |
40 | 'render_mode': '', |
41 | "onFlush": function () {}, | |
ae510306 SR |
42 | }); |
43 | ||
44 | Util.Debug(">> Display.constructor"); | |
45 | ||
2ba767a7 | 46 | // The visible canvas |
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 | ||
ae510306 SR |
72 | Util.Debug("User Agent: " + navigator.userAgent); |
73 | if (Util.Engine.gecko) { Util.Debug("Browser: gecko " + Util.Engine.gecko); } | |
74 | if (Util.Engine.webkit) { Util.Debug("Browser: webkit " + Util.Engine.webkit); } | |
75 | if (Util.Engine.trident) { Util.Debug("Browser: trident " + Util.Engine.trident); } | |
76 | if (Util.Engine.presto) { Util.Debug("Browser: presto " + Util.Engine.presto); } | |
77 | ||
78 | this.clear(); | |
79 | ||
80 | // Check canvas features | |
81 | if ('createImageData' in this._drawCtx) { | |
82 | this._render_mode = 'canvas rendering'; | |
83 | } else { | |
84 | throw new Error("Canvas does not support createImageData"); | |
85 | } | |
86 | ||
87 | if (this._prefer_js === null) { | |
88 | Util.Info("Prefering javascript operations"); | |
89 | this._prefer_js = true; | |
90 | } | |
91 | ||
92 | // Determine browser support for setting the cursor via data URI scheme | |
93 | if (this._cursor_uri || this._cursor_uri === null || | |
94 | this._cursor_uri === undefined) { | |
95 | this._cursor_uri = Util.browserSupportsCursorURIs(); | |
96 | } | |
97 | ||
98 | Util.Debug("<< Display.constructor"); | |
99 | }; | |
ff36b127 | 100 | |
1e13775b SR |
101 | (function () { |
102 | "use strict"; | |
e2e7c224 | 103 | |
d1800d09 SR |
104 | var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false; |
105 | try { | |
6521c6ac | 106 | new ImageData(new Uint8ClampedArray(4), 1, 1); |
d1800d09 SR |
107 | SUPPORTS_IMAGEDATA_CONSTRUCTOR = true; |
108 | } catch (ex) { | |
109 | // ignore failure | |
110 | } | |
111 | ||
490d471c | 112 | |
1e13775b SR |
113 | Display.prototype = { |
114 | // Public methods | |
636be753 | 115 | viewportChangePos: function (deltaX, deltaY) { |
1e13775b | 116 | var vp = this._viewportLoc; |
a8255821 | 117 | deltaX = Math.floor(deltaX); |
118 | deltaY = Math.floor(deltaY); | |
1e13775b SR |
119 | |
120 | if (!this._viewport) { | |
1e13775b SR |
121 | deltaX = -vp.w; // clamped later of out of bounds |
122 | deltaY = -vp.h; | |
8db09746 | 123 | } |
54e7cbdf | 124 | |
1e13775b SR |
125 | var vx2 = vp.x + vp.w - 1; |
126 | var vy2 = vp.y + vp.h - 1; | |
54e7cbdf | 127 | |
1e13775b | 128 | // Position change |
54e7cbdf | 129 | |
1e13775b SR |
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 | } | |
54e7cbdf | 136 | |
1e13775b SR |
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 | } | |
54e7cbdf | 143 | |
1e13775b SR |
144 | if (deltaX === 0 && deltaY === 0) { |
145 | return; | |
146 | } | |
147 | Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); | |
148 | ||
149 | vp.x += deltaX; | |
1e13775b | 150 | vp.y += deltaY; |
a8255821 | 151 | |
84cd0e71 PO |
152 | this._damage(vp.x, vp.y, vp.w, vp.h); |
153 | ||
2ba767a7 | 154 | this.flip(); |
1e13775b SR |
155 | }, |
156 | ||
636be753 | 157 | viewportChangeSize: function(width, height) { |
158 | ||
adf345fd PO |
159 | if (!this._viewport || |
160 | typeof(width) === "undefined" || | |
161 | typeof(height) === "undefined") { | |
636be753 | 162 | |
163 | Util.Debug("Setting viewport to full display region"); | |
164 | width = this._fb_width; | |
165 | height = this._fb_height; | |
166 | } | |
167 | ||
adf345fd PO |
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 | ||
636be753 | 175 | var vp = this._viewportLoc; |
176 | if (vp.w !== width || vp.h !== height) { | |
fdedbafb | 177 | vp.w = width; |
178 | vp.h = height; | |
636be753 | 179 | |
636be753 | 180 | var canvas = this._target; |
adf345fd PO |
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); | |
636be753 | 192 | } |
193 | }, | |
194 | ||
1e13775b | 195 | absX: function (x) { |
280676c7 | 196 | return x / this._scale + this._viewportLoc.x; |
1e13775b SR |
197 | }, |
198 | ||
199 | absY: function (y) { | |
280676c7 | 200 | return y / this._scale + this._viewportLoc.y; |
1e13775b SR |
201 | }, |
202 | ||
203 | resize: function (width, height) { | |
204 | this._prevDrawStyle = ""; | |
205 | ||
206 | this._fb_width = width; | |
207 | this._fb_height = height; | |
208 | ||
2ba767a7 PO |
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 | ||
adf345fd PO |
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); | |
1e13775b SR |
235 | }, |
236 | ||
84cd0e71 PO |
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 | ||
2ba767a7 PO |
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 { | |
84cd0e71 PO |
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; | |
2ba767a7 PO |
300 | } |
301 | }, | |
302 | ||
1e13775b SR |
303 | clear: function () { |
304 | if (this._logo) { | |
305 | this.resize(this._logo.width, this._logo.height); | |
74e39051 | 306 | this.imageRect(0, 0, this._logo.type, this._logo.data); |
1e13775b | 307 | } else { |
795fca23 | 308 | this.resize(240, 20); |
2ba767a7 | 309 | this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height); |
1e13775b | 310 | } |
2ba767a7 | 311 | this.flip(); |
1e13775b SR |
312 | }, |
313 | ||
d9ca5e5b PO |
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 | ||
f00193e0 SR |
326 | fillRect: function (x, y, width, height, color, from_queue) { |
327 | if (this._renderQ.length !== 0 && !from_queue) { | |
1578fa68 | 328 | this._renderQ_push({ |
f00193e0 SR |
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); | |
2ba767a7 | 338 | this._drawCtx.fillRect(x, y, width, height); |
84cd0e71 | 339 | this._damage(x, y, width, height); |
f00193e0 SR |
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) { | |
1578fa68 | 345 | this._renderQ_push({ |
f00193e0 SR |
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 { | |
2ba767a7 PO |
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; | |
1e13775b | 366 | |
2ba767a7 PO |
367 | this._drawCtx.drawImage(this._backbuffer, |
368 | old_x, old_y, w, h, | |
369 | new_x, new_y, w, h); | |
84cd0e71 | 370 | this._damage(new_x, new_y, w, h); |
f00193e0 | 371 | } |
1e13775b SR |
372 | }, |
373 | ||
1578fa68 PO |
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 | ||
1e13775b SR |
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 | if (this._prefer_js) { | |
396 | var bgr; | |
397 | if (this._true_color) { | |
398 | bgr = color; | |
34d8b844 | 399 | } else { |
1e13775b | 400 | bgr = this._colourMap[color[0]]; |
34d8b844 | 401 | } |
1e13775b SR |
402 | var red = bgr[2]; |
403 | var green = bgr[1]; | |
404 | var blue = bgr[0]; | |
405 | ||
406 | var data = this._tile.data; | |
407 | for (var i = 0; i < width * height * 4; i += 4) { | |
408 | data[i] = red; | |
409 | data[i + 1] = green; | |
410 | data[i + 2] = blue; | |
411 | data[i + 3] = 255; | |
412 | } | |
413 | } else { | |
f00193e0 | 414 | this.fillRect(x, y, width, height, color, true); |
1e13775b SR |
415 | } |
416 | }, | |
417 | ||
418 | // update sub-rectangle of the current tile | |
419 | subTile: function (x, y, w, h, color) { | |
420 | if (this._prefer_js) { | |
421 | var bgr; | |
422 | if (this._true_color) { | |
423 | bgr = color; | |
424 | } else { | |
425 | bgr = this._colourMap[color[0]]; | |
426 | } | |
427 | var red = bgr[2]; | |
428 | var green = bgr[1]; | |
429 | var blue = bgr[0]; | |
430 | var xend = x + w; | |
431 | var yend = y + h; | |
432 | ||
433 | var data = this._tile.data; | |
434 | var width = this._tile.width; | |
435 | for (var j = y; j < yend; j++) { | |
436 | for (var i = x; i < xend; i++) { | |
437 | var p = (i + (j * width)) * 4; | |
438 | data[p] = red; | |
439 | data[p + 1] = green; | |
440 | data[p + 2] = blue; | |
441 | data[p + 3] = 255; | |
442 | } | |
443 | } | |
444 | } else { | |
f00193e0 | 445 | this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color, true); |
1e13775b SR |
446 | } |
447 | }, | |
34d8b844 | 448 | |
1e13775b SR |
449 | // draw the current tile to the screen |
450 | finishTile: function () { | |
451 | if (this._prefer_js) { | |
2ba767a7 | 452 | this._drawCtx.putImageData(this._tile, this._tile_x, this._tile_y); |
84cd0e71 PO |
453 | this._damage(this._tile_x, this._tile_y, |
454 | this._tile.width, this._tile.height); | |
1e13775b SR |
455 | } |
456 | // else: No-op -- already done by setSubTile | |
457 | }, | |
bc28395a | 458 | |
f00193e0 SR |
459 | blitImage: function (x, y, width, height, arr, offset, from_queue) { |
460 | if (this._renderQ.length !== 0 && !from_queue) { | |
7bc383e8 SR |
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 * 4); | |
465 | new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); | |
1578fa68 | 466 | this._renderQ_push({ |
f00193e0 | 467 | 'type': 'blit', |
7bc383e8 | 468 | 'data': new_arr, |
f00193e0 SR |
469 | 'x': x, |
470 | 'y': y, | |
471 | 'width': width, | |
472 | 'height': height, | |
473 | }); | |
474 | } else if (this._true_color) { | |
2ba767a7 | 475 | this._bgrxImageData(x, y, width, height, arr, offset); |
1e13775b | 476 | } else { |
2ba767a7 | 477 | this._cmapImageData(x, y, width, height, arr, offset); |
1e13775b SR |
478 | } |
479 | }, | |
2c2b492c | 480 | |
f00193e0 SR |
481 | blitRgbImage: function (x, y , width, height, arr, offset, from_queue) { |
482 | if (this._renderQ.length !== 0 && !from_queue) { | |
7bc383e8 SR |
483 | // NB(directxman12): it's technically more performant here to use preallocated arrays, |
484 | // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, | |
485 | // this probably isn't getting called *nearly* as much | |
6024677f | 486 | var new_arr = new Uint8Array(width * height * 3); |
7bc383e8 | 487 | new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); |
1578fa68 | 488 | this._renderQ_push({ |
f00193e0 | 489 | 'type': 'blitRgb', |
7bc383e8 | 490 | 'data': new_arr, |
f00193e0 SR |
491 | 'x': x, |
492 | 'y': y, | |
493 | 'width': width, | |
494 | 'height': height, | |
495 | }); | |
496 | } else if (this._true_color) { | |
2ba767a7 | 497 | this._rgbImageData(x, y, width, height, arr, offset); |
1e13775b SR |
498 | } else { |
499 | // probably wrong? | |
2ba767a7 | 500 | this._cmapImageData(x, y, width, height, arr, offset); |
1e13775b SR |
501 | } |
502 | }, | |
503 | ||
f00193e0 SR |
504 | blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) { |
505 | if (this._renderQ.length !== 0 && !from_queue) { | |
7bc383e8 | 506 | // NB(directxman12): it's technically more performant here to use preallocated arrays, |
f00193e0 SR |
507 | // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, |
508 | // this probably isn't getting called *nearly* as much | |
509 | var new_arr = new Uint8Array(width * height * 4); | |
510 | new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); | |
1578fa68 | 511 | this._renderQ_push({ |
f00193e0 SR |
512 | 'type': 'blitRgbx', |
513 | 'data': new_arr, | |
514 | 'x': x, | |
515 | 'y': y, | |
516 | 'width': width, | |
517 | 'height': height, | |
518 | }); | |
519 | } else { | |
2ba767a7 | 520 | this._rgbxImageData(x, y, width, height, arr, offset); |
f00193e0 | 521 | } |
d1800d09 SR |
522 | }, |
523 | ||
1e13775b | 524 | drawImage: function (img, x, y) { |
2ba767a7 | 525 | this._drawCtx.drawImage(img, x, y); |
84cd0e71 | 526 | this._damage(x, y, img.width, img.height); |
1e13775b SR |
527 | }, |
528 | ||
1e13775b SR |
529 | changeCursor: function (pixels, mask, hotx, hoty, w, h) { |
530 | if (this._cursor_uri === false) { | |
531 | Util.Warn("changeCursor called but no cursor data URI support"); | |
532 | return; | |
533 | } | |
d3796c14 | 534 | |
1e13775b SR |
535 | if (this._true_color) { |
536 | Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h); | |
537 | } else { | |
538 | Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap); | |
539 | } | |
540 | }, | |
541 | ||
542 | defaultCursor: function () { | |
543 | this._target.style.cursor = "default"; | |
544 | }, | |
545 | ||
b804b3e4 | 546 | disableLocalCursor: function () { |
547 | this._target.style.cursor = "none"; | |
548 | }, | |
549 | ||
fdedbafb | 550 | clippingDisplay: function () { |
551 | var vp = this._viewportLoc; | |
3f781f2a | 552 | return this._fb_width > vp.w || this._fb_height > vp.h; |
636be753 | 553 | }, |
554 | ||
1e13775b | 555 | // Overridden getters/setters |
1e13775b SR |
556 | set_scale: function (scale) { |
557 | this._rescale(scale); | |
558 | }, | |
559 | ||
adf345fd PO |
560 | set_viewport: function (viewport) { |
561 | this._viewport = viewport; | |
562 | // May need to readjust the viewport dimensions | |
563 | var vp = this._viewportLoc; | |
564 | this.viewportChangeSize(vp.w, vp.h); | |
565 | this.viewportChangePos(0, 0); | |
566 | }, | |
567 | ||
1e13775b SR |
568 | get_width: function () { |
569 | return this._fb_width; | |
570 | }, | |
1e13775b SR |
571 | get_height: function () { |
572 | return this._fb_height; | |
573 | }, | |
574 | ||
72747869 | 575 | autoscale: function (containerWidth, containerHeight, downscaleOnly) { |
a6e52f9a | 576 | var vp = this._viewportLoc; |
72747869 | 577 | var targetAspectRatio = containerWidth / containerHeight; |
a6e52f9a | 578 | var fbAspectRatio = vp.w / vp.h; |
9a23006e | 579 | |
72747869 SR |
580 | var scaleRatio; |
581 | if (fbAspectRatio >= targetAspectRatio) { | |
a6e52f9a | 582 | scaleRatio = containerWidth / vp.w; |
72747869 | 583 | } else { |
a6e52f9a | 584 | scaleRatio = containerHeight / vp.h; |
1e13775b | 585 | } |
9a23006e | 586 | |
72747869 | 587 | if (scaleRatio > 1.0 && downscaleOnly) { |
72747869 | 588 | scaleRatio = 1.0; |
1e13775b | 589 | } |
9a23006e | 590 | |
a6e52f9a | 591 | this._rescale(scaleRatio); |
72747869 | 592 | }, |
1e13775b | 593 | |
72747869 SR |
594 | // Private Methods |
595 | _rescale: function (factor) { | |
1e13775b | 596 | this._scale = factor; |
3f781f2a | 597 | var vp = this._viewportLoc; |
a6e52f9a PO |
598 | |
599 | // NB(directxman12): If you set the width directly, or set the | |
600 | // style width to a number, the canvas is cleared. | |
601 | // However, if you set the style width to a string | |
602 | // ('NNNpx'), the canvas is scaled without clearing. | |
2e6a58fb PO |
603 | var width = Math.round(factor * vp.w) + 'px'; |
604 | var height = Math.round(factor * vp.h) + 'px'; | |
605 | ||
606 | if ((this._target.style.width !== width) || | |
607 | (this._target.style.height !== height)) { | |
608 | this._target.style.width = width; | |
609 | this._target.style.height = height; | |
610 | } | |
1e13775b | 611 | }, |
67b4e987 | 612 | |
1e13775b SR |
613 | _setFillColor: function (color) { |
614 | var bgr; | |
615 | if (this._true_color) { | |
616 | bgr = color; | |
9a23006e | 617 | } else { |
a369a80c | 618 | bgr = this._colourMap[color]; |
1e13775b SR |
619 | } |
620 | ||
621 | var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')'; | |
622 | if (newStyle !== this._prevDrawStyle) { | |
623 | this._drawCtx.fillStyle = newStyle; | |
624 | this._prevDrawStyle = newStyle; | |
625 | } | |
626 | }, | |
627 | ||
2ba767a7 | 628 | _rgbImageData: function (x, y, width, height, arr, offset) { |
1e13775b SR |
629 | var img = this._drawCtx.createImageData(width, height); |
630 | var data = img.data; | |
631 | for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) { | |
632 | data[i] = arr[j]; | |
633 | data[i + 1] = arr[j + 1]; | |
634 | data[i + 2] = arr[j + 2]; | |
635 | data[i + 3] = 255; // Alpha | |
636 | } | |
2ba767a7 | 637 | this._drawCtx.putImageData(img, x, y); |
84cd0e71 | 638 | this._damage(x, y, img.width, img.height); |
1e13775b SR |
639 | }, |
640 | ||
2ba767a7 | 641 | _bgrxImageData: function (x, y, width, height, arr, offset) { |
1e13775b SR |
642 | var img = this._drawCtx.createImageData(width, height); |
643 | var data = img.data; | |
644 | for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) { | |
645 | data[i] = arr[j + 2]; | |
646 | data[i + 1] = arr[j + 1]; | |
647 | data[i + 2] = arr[j]; | |
648 | data[i + 3] = 255; // Alpha | |
649 | } | |
2ba767a7 | 650 | this._drawCtx.putImageData(img, x, y); |
84cd0e71 | 651 | this._damage(x, y, img.width, img.height); |
1e13775b SR |
652 | }, |
653 | ||
2ba767a7 | 654 | _rgbxImageData: function (x, y, width, height, arr, offset) { |
d1800d09 | 655 | // NB(directxman12): arr must be an Type Array view |
d1800d09 SR |
656 | var img; |
657 | if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) { | |
f00193e0 | 658 | img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height); |
d1800d09 SR |
659 | } else { |
660 | img = this._drawCtx.createImageData(width, height); | |
f00193e0 | 661 | img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4)); |
d1800d09 | 662 | } |
2ba767a7 | 663 | this._drawCtx.putImageData(img, x, y); |
84cd0e71 | 664 | this._damage(x, y, img.width, img.height); |
d1800d09 SR |
665 | }, |
666 | ||
2ba767a7 | 667 | _cmapImageData: function (x, y, width, height, arr, offset) { |
1e13775b SR |
668 | var img = this._drawCtx.createImageData(width, height); |
669 | var data = img.data; | |
670 | var cmap = this._colourMap; | |
671 | for (var i = 0, j = offset; i < width * height * 4; i += 4, j++) { | |
672 | var bgr = cmap[arr[j]]; | |
673 | data[i] = bgr[2]; | |
674 | data[i + 1] = bgr[1]; | |
675 | data[i + 2] = bgr[0]; | |
676 | data[i + 3] = 255; // Alpha | |
677 | } | |
2ba767a7 | 678 | this._drawCtx.putImageData(img, x, y); |
84cd0e71 | 679 | this._damage(x, y, img.width, img.height); |
1e13775b SR |
680 | }, |
681 | ||
1578fa68 PO |
682 | _renderQ_push: function (action) { |
683 | this._renderQ.push(action); | |
684 | if (this._renderQ.length === 1) { | |
685 | // If this can be rendered immediately it will be, otherwise | |
bb6965f2 | 686 | // the scanner will wait for the relevant event |
1578fa68 PO |
687 | this._scan_renderQ(); |
688 | } | |
689 | }, | |
690 | ||
bb6965f2 PO |
691 | _resume_renderQ: function() { |
692 | // "this" is the object that is ready, not the | |
693 | // display object | |
694 | this.removeEventListener('load', this._noVNC_display._resume_renderQ); | |
695 | this._noVNC_display._scan_renderQ(); | |
696 | }, | |
697 | ||
1e13775b SR |
698 | _scan_renderQ: function () { |
699 | var ready = true; | |
700 | while (ready && this._renderQ.length > 0) { | |
701 | var a = this._renderQ[0]; | |
702 | switch (a.type) { | |
2ba767a7 PO |
703 | case 'flip': |
704 | this.flip(true); | |
705 | break; | |
1e13775b | 706 | case 'copy': |
f00193e0 | 707 | this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true); |
1e13775b SR |
708 | break; |
709 | case 'fill': | |
f00193e0 | 710 | this.fillRect(a.x, a.y, a.width, a.height, a.color, true); |
1e13775b SR |
711 | break; |
712 | case 'blit': | |
f00193e0 | 713 | this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true); |
1e13775b SR |
714 | break; |
715 | case 'blitRgb': | |
f00193e0 | 716 | this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true); |
1e13775b | 717 | break; |
d1800d09 | 718 | case 'blitRgbx': |
f00193e0 | 719 | this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true); |
1e13775b SR |
720 | break; |
721 | case 'img': | |
722 | if (a.img.complete) { | |
723 | this.drawImage(a.img, a.x, a.y); | |
724 | } else { | |
bb6965f2 PO |
725 | a.img._noVNC_display = this; |
726 | a.img.addEventListener('load', this._resume_renderQ); | |
1e13775b SR |
727 | // We need to wait for this image to 'load' |
728 | // to keep things in-order | |
729 | ready = false; | |
730 | } | |
731 | break; | |
732 | } | |
733 | ||
734 | if (ready) { | |
735 | this._renderQ.shift(); | |
736 | } | |
737 | } | |
d9ca5e5b PO |
738 | |
739 | if (this._renderQ.length === 0 && this._flushing) { | |
740 | this._flushing = false; | |
741 | this._onFlush(); | |
742 | } | |
1e13775b SR |
743 | }, |
744 | }; | |
745 | ||
746 | Util.make_properties(Display, [ | |
747 | ['target', 'wo', 'dom'], // Canvas element for rendering | |
748 | ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only) | |
74e39051 | 749 | ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "type": mime-type, "data": data} |
1e13775b SR |
750 | ['true_color', 'rw', 'bool'], // Use true-color pixel data |
751 | ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color) | |
752 | ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0 | |
fdedbafb | 753 | ['viewport', 'rw', 'bool'], // Use viewport clipping |
e549ae07 PO |
754 | ['width', 'ro', 'int'], // Display area width |
755 | ['height', 'ro', 'int'], // Display area height | |
1e13775b SR |
756 | |
757 | ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only) | |
758 | ||
759 | ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods | |
d9ca5e5b PO |
760 | ['cursor_uri', 'rw', 'raw'], // Can we render cursor using data URI |
761 | ||
762 | ['onFlush', 'rw', 'func'], // onFlush(): A flush request has finished | |
1e13775b SR |
763 | ]); |
764 | ||
765 | // Class Methods | |
766 | Display.changeCursor = function (target, pixels, mask, hotx, hoty, w0, h0, cmap) { | |
767 | var w = w0; | |
768 | var h = h0; | |
769 | if (h < w) { | |
770 | h = w; // increase h to make it square | |
771 | } else { | |
772 | w = h; // increase w to make it square | |
773 | } | |
774 | ||
775 | var cur = []; | |
776 | ||
777 | // Push multi-byte little-endian values | |
778 | cur.push16le = function (num) { | |
779 | this.push(num & 0xFF, (num >> 8) & 0xFF); | |
780 | }; | |
781 | cur.push32le = function (num) { | |
782 | this.push(num & 0xFF, | |
783 | (num >> 8) & 0xFF, | |
784 | (num >> 16) & 0xFF, | |
785 | (num >> 24) & 0xFF); | |
786 | }; | |
787 | ||
788 | var IHDRsz = 40; | |
789 | var RGBsz = w * h * 4; | |
790 | var XORsz = Math.ceil((w * h) / 8.0); | |
791 | var ANDsz = Math.ceil((w * h) / 8.0); | |
792 | ||
793 | cur.push16le(0); // 0: Reserved | |
794 | cur.push16le(2); // 2: .CUR type | |
795 | cur.push16le(1); // 4: Number of images, 1 for non-animated ico | |
796 | ||
797 | // Cursor #1 header (ICONDIRENTRY) | |
798 | cur.push(w); // 6: width | |
799 | cur.push(h); // 7: height | |
800 | cur.push(0); // 8: colors, 0 -> true-color | |
801 | cur.push(0); // 9: reserved | |
802 | cur.push16le(hotx); // 10: hotspot x coordinate | |
803 | cur.push16le(hoty); // 12: hotspot y coordinate | |
804 | cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz); | |
805 | // 14: cursor data byte size | |
806 | cur.push32le(22); // 18: offset of cursor data in the file | |
807 | ||
808 | // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO) | |
809 | cur.push32le(IHDRsz); // 22: InfoHeader size | |
810 | cur.push32le(w); // 26: Cursor width | |
811 | cur.push32le(h * 2); // 30: XOR+AND height | |
812 | cur.push16le(1); // 34: number of planes | |
813 | cur.push16le(32); // 36: bits per pixel | |
814 | cur.push32le(0); // 38: Type of compression | |
815 | ||
816 | cur.push32le(XORsz + ANDsz); | |
817 | // 42: Size of Image | |
818 | cur.push32le(0); // 46: reserved | |
819 | cur.push32le(0); // 50: reserved | |
820 | cur.push32le(0); // 54: reserved | |
821 | cur.push32le(0); // 58: reserved | |
822 | ||
823 | // 62: color data (RGBQUAD icColors[]) | |
824 | var y, x; | |
825 | for (y = h - 1; y >= 0; y--) { | |
826 | for (x = 0; x < w; x++) { | |
827 | if (x >= w0 || y >= h0) { | |
828 | cur.push(0); // blue | |
829 | cur.push(0); // green | |
830 | cur.push(0); // red | |
831 | cur.push(0); // alpha | |
6f4cbb3f | 832 | } else { |
1e13775b SR |
833 | var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8); |
834 | var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; | |
835 | if (cmap) { | |
836 | idx = (w0 * y) + x; | |
837 | var rgb = cmap[pixels[idx]]; | |
838 | cur.push(rgb[2]); // blue | |
839 | cur.push(rgb[1]); // green | |
840 | cur.push(rgb[0]); // red | |
841 | cur.push(alpha); // alpha | |
ec31f82e SR |
842 | } else { |
843 | idx = ((w0 * y) + x) * 4; | |
a7ca8e5c | 844 | cur.push(pixels[idx]); // blue |
ec31f82e | 845 | cur.push(pixels[idx + 1]); // green |
a7ca8e5c | 846 | cur.push(pixels[idx + 2]); // red |
ec31f82e | 847 | cur.push(alpha); // alpha |
1e13775b | 848 | } |
6f4cbb3f | 849 | } |
2c2b492c JM |
850 | } |
851 | } | |
2c2b492c | 852 | |
1e13775b SR |
853 | // XOR/bitmask data (BYTE icXOR[]) |
854 | // (ignored, just needs to be the right size) | |
855 | for (y = 0; y < h; y++) { | |
856 | for (x = 0; x < Math.ceil(w / 8); x++) { | |
857 | cur.push(0); | |
858 | } | |
9a23006e | 859 | } |
9a23006e | 860 | |
1e13775b SR |
861 | // AND/bitmask data (BYTE icAND[]) |
862 | // (ignored, just needs to be the right size) | |
863 | for (y = 0; y < h; y++) { | |
864 | for (x = 0; x < Math.ceil(w / 8); x++) { | |
865 | cur.push(0); | |
866 | } | |
2c2b492c | 867 | } |
2c2b492c | 868 | |
1e13775b SR |
869 | var url = 'data:image/x-icon;base64,' + Base64.encode(cur); |
870 | target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; | |
871 | }; | |
872 | })(); |