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