]>
Commit | Line | Data |
---|---|---|
c4164bda JM |
1 | /* |
2 | * noVNC: HTML5 VNC client | |
d58f8b51 | 3 | * Copyright (C) 2012 Joel Martin |
1d728ace | 4 | * Licensed under MPL 2.0 (see LICENSE.txt) |
c4164bda JM |
5 | * |
6 | * See README.md for usage and integration instructions. | |
7 | */ | |
c4164bda | 8 | |
1e13775b | 9 | /*jslint browser: true, white: false */ |
d3796c14 | 10 | /*global Util, Base64, changeCursor */ |
c4164bda | 11 | |
1e13775b | 12 | var Display; |
ff36b127 | 13 | |
1e13775b SR |
14 | (function () { |
15 | "use strict"; | |
e2e7c224 | 16 | |
1e13775b SR |
17 | Display = function (defaults) { |
18 | this._drawCtx = null; | |
19 | this._c_forceCanvas = false; | |
8db09746 | 20 | |
1e13775b | 21 | this._renderQ = []; // queue drawing actions for in-oder rendering |
3b20e7a9 | 22 | |
1e13775b SR |
23 | // the full frame buffer (logical canvas) size |
24 | this._fb_width = 0; | |
25 | this._fb_height = 0; | |
58b4c536 | 26 | |
1e13775b SR |
27 | // the visible "physical canvas" viewport |
28 | this._viewportLoc = { 'x': 0, 'y': 0, 'w': 0, 'h': 0 }; | |
29 | this._cleanRect = { 'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1 }; | |
58b4c536 | 30 | |
1e13775b SR |
31 | this._prevDrawStyle = ""; |
32 | this._tile = null; | |
33 | this._tile16x16 = null; | |
34 | this._tile_x = 0; | |
35 | this._tile_y = 0; | |
f272267b | 36 | |
1e13775b SR |
37 | Util.set_defaults(this, defaults, { |
38 | 'true_color': true, | |
39 | 'colourMap': [], | |
40 | 'scale': 1.0, | |
41 | 'viewport': false, | |
42 | 'render_mode': '' | |
43 | }); | |
f272267b | 44 | |
1e13775b | 45 | Util.Debug(">> Display.constructor"); |
f272267b | 46 | |
1e13775b SR |
47 | if (!this._target) { |
48 | throw new Error("Target must be set"); | |
49 | } | |
8db09746 | 50 | |
1e13775b SR |
51 | if (typeof this._target === 'string') { |
52 | throw new Error('target must be a DOM element'); | |
53 | } | |
8db09746 | 54 | |
1e13775b SR |
55 | if (!this._target.getContext) { |
56 | throw new Error("no getContext method"); | |
57 | } | |
8db09746 | 58 | |
1e13775b SR |
59 | if (!this._drawCtx) { |
60 | this._drawCtx = this._target.getContext('2d'); | |
61 | } | |
d93d3e09 | 62 | |
1e13775b SR |
63 | Util.Debug("User Agent: " + navigator.userAgent); |
64 | if (Util.Engine.gecko) { Util.Debug("Browser: gecko " + Util.Engine.gecko); } | |
65 | if (Util.Engine.webkit) { Util.Debug("Browser: webkit " + Util.Engine.webkit); } | |
66 | if (Util.Engine.trident) { Util.Debug("Browser: trident " + Util.Engine.trident); } | |
67 | if (Util.Engine.presto) { Util.Debug("Browser: presto " + Util.Engine.presto); } | |
d93d3e09 | 68 | |
1e13775b | 69 | this.clear(); |
8db09746 | 70 | |
1e13775b SR |
71 | // Check canvas features |
72 | if ('createImageData' in this._drawCtx) { | |
73 | this._render_mode = 'canvas rendering'; | |
74 | } else { | |
75 | throw new Error("Canvas does not support createImageData"); | |
76 | } | |
005d9ee9 | 77 | |
1e13775b SR |
78 | if (this._prefer_js === null) { |
79 | Util.Info("Prefering javascript operations"); | |
80 | this._prefer_js = true; | |
81 | } | |
d93d3e09 | 82 | |
1e13775b SR |
83 | // Determine browser support for setting the cursor via data URI scheme |
84 | var curDat = []; | |
85 | for (var i = 0; i < 8 * 8 * 4; i++) { | |
86 | curDat.push(255); | |
87 | } | |
88 | try { | |
89 | var curSave = this._target.style.cursor; | |
90 | Display.changeCursor(this._target, curDat, curDat, 2, 2, 8, 8); | |
91 | if (this._target.style.cursor) { | |
53762c31 | 92 | if (this._cursor_uri === null || this._cursor_uri === undefined) { |
1e13775b SR |
93 | this._cursor_uri = true; |
94 | } | |
95 | Util.Info("Data URI scheme cursor supported"); | |
b804b3e4 | 96 | this._target.style.cursor = curSave; |
1e13775b | 97 | } else { |
53762c31 | 98 | if (this._cursor_uri === null || this._cursor_uri === undefined) { |
1e13775b SR |
99 | this._cursor_uri = false; |
100 | } | |
101 | Util.Warn("Data URI scheme cursor not supported"); | |
b804b3e4 | 102 | this._target.style.cursor = "none"; |
1e13775b | 103 | } |
1e13775b SR |
104 | } catch (exc) { |
105 | Util.Error("Data URI scheme cursor test exception: " + exc); | |
106 | this._cursor_uri = false; | |
107 | } | |
cdb55d26 | 108 | |
1e13775b SR |
109 | Util.Debug("<< Display.constructor"); |
110 | }; | |
490d471c | 111 | |
1e13775b SR |
112 | Display.prototype = { |
113 | // Public methods | |
636be753 | 114 | viewportChangePos: function (deltaX, deltaY) { |
1e13775b | 115 | var vp = this._viewportLoc; |
1e13775b SR |
116 | |
117 | if (!this._viewport) { | |
1e13775b SR |
118 | deltaX = -vp.w; // clamped later of out of bounds |
119 | deltaY = -vp.h; | |
8db09746 | 120 | } |
54e7cbdf | 121 | |
1e13775b SR |
122 | var vx2 = vp.x + vp.w - 1; |
123 | var vy2 = vp.y + vp.h - 1; | |
54e7cbdf | 124 | |
1e13775b | 125 | // Position change |
54e7cbdf | 126 | |
1e13775b SR |
127 | if (deltaX < 0 && vp.x + deltaX < 0) { |
128 | deltaX = -vp.x; | |
129 | } | |
130 | if (vx2 + deltaX >= this._fb_width) { | |
131 | deltaX -= vx2 + deltaX - this._fb_width + 1; | |
132 | } | |
54e7cbdf | 133 | |
1e13775b SR |
134 | if (vp.y + deltaY < 0) { |
135 | deltaY = -vp.y; | |
136 | } | |
137 | if (vy2 + deltaY >= this._fb_height) { | |
138 | deltaY -= (vy2 + deltaY - this._fb_height + 1); | |
139 | } | |
54e7cbdf | 140 | |
1e13775b SR |
141 | if (deltaX === 0 && deltaY === 0) { |
142 | return; | |
143 | } | |
144 | Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); | |
145 | ||
146 | vp.x += deltaX; | |
147 | vx2 += deltaX; | |
148 | vp.y += deltaY; | |
149 | vy2 += deltaY; | |
150 | ||
151 | // Update the clean rectangle | |
636be753 | 152 | var cr = this._cleanRect; |
1e13775b SR |
153 | if (vp.x > cr.x1) { |
154 | cr.x1 = vp.x; | |
155 | } | |
156 | if (vx2 < cr.x2) { | |
157 | cr.x2 = vx2; | |
158 | } | |
159 | if (vp.y > cr.y1) { | |
160 | cr.y1 = vp.y; | |
161 | } | |
162 | if (vy2 < cr.y2) { | |
163 | cr.y2 = vy2; | |
164 | } | |
165 | ||
166 | var x1, w; | |
167 | if (deltaX < 0) { | |
168 | // Shift viewport left, redraw left section | |
169 | x1 = 0; | |
170 | w = -deltaX; | |
171 | } else { | |
172 | // Shift viewport right, redraw right section | |
173 | x1 = vp.w - deltaX; | |
174 | w = deltaX; | |
175 | } | |
176 | ||
177 | var y1, h; | |
178 | if (deltaY < 0) { | |
179 | // Shift viewport up, redraw top section | |
180 | y1 = 0; | |
181 | h = -deltaY; | |
182 | } else { | |
183 | // Shift viewport down, redraw bottom section | |
184 | y1 = vp.h - deltaY; | |
185 | h = deltaY; | |
186 | } | |
187 | ||
188 | // Copy the valid part of the viewport to the shifted location | |
189 | var saveStyle = this._drawCtx.fillStyle; | |
636be753 | 190 | var canvas = this._target; |
1e13775b SR |
191 | this._drawCtx.fillStyle = "rgb(255,255,255)"; |
192 | if (deltaX !== 0) { | |
193 | this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, -deltaX, 0, vp.w, vp.h); | |
194 | this._drawCtx.fillRect(x1, 0, w, vp.h); | |
195 | } | |
196 | if (deltaY !== 0) { | |
197 | this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, 0, -deltaY, vp.w, vp.h); | |
198 | this._drawCtx.fillRect(0, y1, vp.w, h); | |
199 | } | |
200 | this._drawCtx.fillStyle = saveStyle; | |
201 | }, | |
202 | ||
636be753 | 203 | viewportChangeSize: function(width, height) { |
204 | ||
205 | if (!this._viewport || | |
206 | typeof(width) === "undefined" || typeof(height) === "undefined") { | |
207 | ||
208 | Util.Debug("Setting viewport to full display region"); | |
209 | width = this._fb_width; | |
210 | height = this._fb_height; | |
211 | } | |
212 | ||
213 | var vp = this._viewportLoc; | |
214 | if (vp.w !== width || vp.h !== height) { | |
215 | ||
216 | var cr = this._cleanRect; | |
217 | ||
218 | if (width < vp.w && cr.x2 > vp.x + width - 1) { | |
219 | cr.x2 = vp.x + width - 1; | |
220 | } | |
221 | ||
222 | if (height < vp.h && cr.y2 > vp.y + height - 1) { | |
223 | cr.y2 = vp.y + height - 1; | |
224 | } | |
225 | ||
226 | if (this.fbuClip()) { | |
227 | // clipping | |
228 | vp.w = window.innerWidth; | |
229 | var cb = document.getElementById('noVNC-control-bar'); | |
230 | var controlbar_h = (cb !== null) ? cb.offsetHeight : 0; | |
231 | vp.h = window.innerHeight - controlbar_h - 5; | |
232 | } else { | |
233 | // scrollbars | |
234 | vp.w = width; | |
235 | vp.h = height; | |
236 | } | |
237 | ||
238 | var saveImg = null; | |
239 | var canvas = this._target; | |
240 | if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) { | |
241 | var img_width = canvas.width < vp.w ? canvas.width : vp.w; | |
242 | var img_height = canvas.height < vp.h ? canvas.height : vp.h; | |
243 | saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height); | |
244 | } | |
245 | ||
246 | canvas.width = vp.w; | |
247 | canvas.height = vp.h; | |
248 | ||
249 | if (saveImg) { | |
250 | this._drawCtx.putImageData(saveImg, 0, 0); | |
251 | } | |
252 | } | |
253 | }, | |
254 | ||
1e13775b SR |
255 | // Return a map of clean and dirty areas of the viewport and reset the |
256 | // tracking of clean and dirty areas | |
257 | // | |
258 | // Returns: { 'cleanBox': { 'x': x, 'y': y, 'w': w, 'h': h}, | |
259 | // 'dirtyBoxes': [{ 'x': x, 'y': y, 'w': w, 'h': h }, ...] } | |
260 | getCleanDirtyReset: function () { | |
261 | var vp = this._viewportLoc; | |
262 | var cr = this._cleanRect; | |
263 | ||
264 | var cleanBox = { 'x': cr.x1, 'y': cr.y1, | |
265 | 'w': cr.x2 - cr.x1 + 1, 'h': cr.y2 - cr.y1 + 1 }; | |
266 | ||
267 | var dirtyBoxes = []; | |
268 | if (cr.x1 >= cr.x2 || cr.y1 >= cr.y2) { | |
269 | // Whole viewport is dirty | |
270 | dirtyBoxes.push({ 'x': vp.x, 'y': vp.y, 'w': vp.w, 'h': vp.h }); | |
271 | } else { | |
272 | // Redraw dirty regions | |
273 | var vx2 = vp.x + vp.w - 1; | |
274 | var vy2 = vp.y + vp.h - 1; | |
275 | ||
276 | if (vp.x < cr.x1) { | |
277 | // left side dirty region | |
278 | dirtyBoxes.push({'x': vp.x, 'y': vp.y, | |
279 | 'w': cr.x1 - vp.x + 1, 'h': vp.h}); | |
280 | } | |
281 | if (vx2 > cr.x2) { | |
282 | // right side dirty region | |
283 | dirtyBoxes.push({'x': cr.x2 + 1, 'y': vp.y, | |
284 | 'w': vx2 - cr.x2, 'h': vp.h}); | |
285 | } | |
286 | if(vp.y < cr.y1) { | |
287 | // top/middle dirty region | |
288 | dirtyBoxes.push({'x': cr.x1, 'y': vp.y, | |
289 | 'w': cr.x2 - cr.x1 + 1, 'h': cr.y1 - vp.y}); | |
290 | } | |
291 | if (vy2 > cr.y2) { | |
292 | // bottom/middle dirty region | |
293 | dirtyBoxes.push({'x': cr.x1, 'y': cr.y2 + 1, | |
294 | 'w': cr.x2 - cr.x1 + 1, 'h': vy2 - cr.y2}); | |
295 | } | |
296 | } | |
297 | ||
298 | this._cleanRect = {'x1': vp.x, 'y1': vp.y, | |
299 | 'x2': vp.x + vp.w - 1, 'y2': vp.y + vp.h - 1}; | |
300 | ||
301 | return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes}; | |
302 | }, | |
303 | ||
304 | absX: function (x) { | |
305 | return x + this._viewportLoc.x; | |
306 | }, | |
307 | ||
308 | absY: function (y) { | |
309 | return y + this._viewportLoc.y; | |
310 | }, | |
311 | ||
312 | resize: function (width, height) { | |
313 | this._prevDrawStyle = ""; | |
314 | ||
315 | this._fb_width = width; | |
316 | this._fb_height = height; | |
317 | ||
318 | this._rescale(this._scale); | |
319 | ||
636be753 | 320 | this.viewportChangeSize(); |
1e13775b SR |
321 | }, |
322 | ||
323 | clear: function () { | |
324 | if (this._logo) { | |
325 | this.resize(this._logo.width, this._logo.height); | |
326 | this.blitStringImage(this._logo.data, 0, 0); | |
327 | } else { | |
0b0b0433 SR |
328 | if (Util.Engine.trident === 6) { |
329 | // NB(directxman12): there's a bug in IE10 where we can fail to actually | |
330 | // clear the canvas here because of the resize. | |
331 | // Clearing the current viewport first fixes the issue | |
332 | this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h); | |
333 | } | |
795fca23 | 334 | this.resize(240, 20); |
1e13775b SR |
335 | this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h); |
336 | } | |
337 | ||
338 | this._renderQ = []; | |
339 | }, | |
340 | ||
341 | fillRect: function (x, y, width, height, color) { | |
342 | this._setFillColor(color); | |
343 | this._drawCtx.fillRect(x - this._viewportLoc.x, y - this._viewportLoc.y, width, height); | |
344 | }, | |
345 | ||
346 | copyImage: function (old_x, old_y, new_x, new_y, w, h) { | |
347 | var x1 = old_x - this._viewportLoc.x; | |
348 | var y1 = old_y - this._viewportLoc.y; | |
349 | var x2 = new_x - this._viewportLoc.x; | |
350 | var y2 = new_y - this._viewportLoc.y; | |
351 | ||
352 | this._drawCtx.drawImage(this._target, x1, y1, w, h, x2, y2, w, h); | |
353 | }, | |
354 | ||
355 | // start updating a tile | |
356 | startTile: function (x, y, width, height, color) { | |
357 | this._tile_x = x; | |
358 | this._tile_y = y; | |
359 | if (width === 16 && height === 16) { | |
360 | this._tile = this._tile16x16; | |
361 | } else { | |
362 | this._tile = this._drawCtx.createImageData(width, height); | |
363 | } | |
364 | ||
365 | if (this._prefer_js) { | |
366 | var bgr; | |
367 | if (this._true_color) { | |
368 | bgr = color; | |
34d8b844 | 369 | } else { |
1e13775b | 370 | bgr = this._colourMap[color[0]]; |
34d8b844 | 371 | } |
1e13775b SR |
372 | var red = bgr[2]; |
373 | var green = bgr[1]; | |
374 | var blue = bgr[0]; | |
375 | ||
376 | var data = this._tile.data; | |
377 | for (var i = 0; i < width * height * 4; i += 4) { | |
378 | data[i] = red; | |
379 | data[i + 1] = green; | |
380 | data[i + 2] = blue; | |
381 | data[i + 3] = 255; | |
382 | } | |
383 | } else { | |
384 | this.fillRect(x, y, width, height, color); | |
385 | } | |
386 | }, | |
387 | ||
388 | // update sub-rectangle of the current tile | |
389 | subTile: function (x, y, w, h, color) { | |
390 | if (this._prefer_js) { | |
391 | var bgr; | |
392 | if (this._true_color) { | |
393 | bgr = color; | |
394 | } else { | |
395 | bgr = this._colourMap[color[0]]; | |
396 | } | |
397 | var red = bgr[2]; | |
398 | var green = bgr[1]; | |
399 | var blue = bgr[0]; | |
400 | var xend = x + w; | |
401 | var yend = y + h; | |
402 | ||
403 | var data = this._tile.data; | |
404 | var width = this._tile.width; | |
405 | for (var j = y; j < yend; j++) { | |
406 | for (var i = x; i < xend; i++) { | |
407 | var p = (i + (j * width)) * 4; | |
408 | data[p] = red; | |
409 | data[p + 1] = green; | |
410 | data[p + 2] = blue; | |
411 | data[p + 3] = 255; | |
412 | } | |
413 | } | |
414 | } else { | |
415 | this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color); | |
416 | } | |
417 | }, | |
34d8b844 | 418 | |
1e13775b SR |
419 | // draw the current tile to the screen |
420 | finishTile: function () { | |
421 | if (this._prefer_js) { | |
422 | this._drawCtx.putImageData(this._tile, this._tile_x - this._viewportLoc.x, | |
423 | this._tile_y - this._viewportLoc.y); | |
424 | } | |
425 | // else: No-op -- already done by setSubTile | |
426 | }, | |
bc28395a | 427 | |
1e13775b SR |
428 | blitImage: function (x, y, width, height, arr, offset) { |
429 | if (this._true_color) { | |
430 | this._bgrxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); | |
431 | } else { | |
432 | this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); | |
433 | } | |
434 | }, | |
2c2b492c | 435 | |
1e13775b SR |
436 | blitRgbImage: function (x, y , width, height, arr, offset) { |
437 | if (this._true_color) { | |
438 | this._rgbImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); | |
439 | } else { | |
440 | // probably wrong? | |
441 | this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); | |
442 | } | |
443 | }, | |
444 | ||
445 | blitStringImage: function (str, x, y) { | |
446 | var img = new Image(); | |
447 | img.onload = function () { | |
448 | this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y); | |
449 | }.bind(this); | |
450 | img.src = str; | |
451 | return img; // for debugging purposes | |
452 | }, | |
453 | ||
454 | // wrap ctx.drawImage but relative to viewport | |
455 | drawImage: function (img, x, y) { | |
456 | this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y); | |
457 | }, | |
458 | ||
459 | renderQ_push: function (action) { | |
460 | this._renderQ.push(action); | |
461 | if (this._renderQ.length === 1) { | |
462 | // If this can be rendered immediately it will be, otherwise | |
463 | // the scanner will start polling the queue (every | |
464 | // requestAnimationFrame interval) | |
465 | this._scan_renderQ(); | |
466 | } | |
467 | }, | |
9a23006e | 468 | |
1e13775b SR |
469 | changeCursor: function (pixels, mask, hotx, hoty, w, h) { |
470 | if (this._cursor_uri === false) { | |
471 | Util.Warn("changeCursor called but no cursor data URI support"); | |
472 | return; | |
473 | } | |
d3796c14 | 474 | |
1e13775b SR |
475 | if (this._true_color) { |
476 | Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h); | |
477 | } else { | |
478 | Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap); | |
479 | } | |
480 | }, | |
481 | ||
482 | defaultCursor: function () { | |
483 | this._target.style.cursor = "default"; | |
484 | }, | |
485 | ||
b804b3e4 | 486 | disableLocalCursor: function () { |
487 | this._target.style.cursor = "none"; | |
488 | }, | |
489 | ||
636be753 | 490 | fbuClip: function () { |
491 | var cb = document.getElementById('noVNC-control-bar'); | |
492 | var controlbar_h = (cb !== null) ? cb.offsetHeight : 0; | |
493 | return (this._viewport && | |
494 | (this._fb_width > window.innerWidth | |
495 | || this._fb_height > window.innerHeight - controlbar_h - 5)); | |
496 | }, | |
497 | ||
1e13775b SR |
498 | // Overridden getters/setters |
499 | get_context: function () { | |
500 | return this._drawCtx; | |
501 | }, | |
502 | ||
503 | set_scale: function (scale) { | |
504 | this._rescale(scale); | |
505 | }, | |
506 | ||
507 | set_width: function (w) { | |
636be753 | 508 | this._fb_width = w; |
1e13775b SR |
509 | }, |
510 | get_width: function () { | |
511 | return this._fb_width; | |
512 | }, | |
513 | ||
514 | set_height: function (h) { | |
636be753 | 515 | this._fb_height = h; |
1e13775b SR |
516 | }, |
517 | get_height: function () { | |
518 | return this._fb_height; | |
519 | }, | |
520 | ||
521 | // Private Methods | |
522 | _rescale: function (factor) { | |
523 | var canvas = this._target; | |
524 | var properties = ['transform', 'WebkitTransform', 'MozTransform']; | |
525 | var transform_prop; | |
526 | while ((transform_prop = properties.shift())) { | |
527 | if (typeof canvas.style[transform_prop] !== 'undefined') { | |
528 | break; | |
529 | } | |
530 | } | |
9a23006e | 531 | |
1e13775b SR |
532 | if (transform_prop === null) { |
533 | Util.Debug("No scaling support"); | |
534 | return; | |
535 | } | |
9a23006e | 536 | |
1e13775b SR |
537 | if (typeof(factor) === "undefined") { |
538 | factor = this._scale; | |
539 | } else if (factor > 1.0) { | |
540 | factor = 1.0; | |
541 | } else if (factor < 0.1) { | |
542 | factor = 0.1; | |
543 | } | |
9a23006e | 544 | |
1e13775b SR |
545 | if (this._scale === factor) { |
546 | return; | |
547 | } | |
548 | ||
549 | this._scale = factor; | |
550 | var x = canvas.width - (canvas.width * factor); | |
551 | var y = canvas.height - (canvas.height * factor); | |
552 | canvas.style[transform_prop] = 'scale(' + this._scale + ') translate(-' + x + 'px, -' + y + 'px)'; | |
553 | }, | |
67b4e987 | 554 | |
1e13775b SR |
555 | _setFillColor: function (color) { |
556 | var bgr; | |
557 | if (this._true_color) { | |
558 | bgr = color; | |
9a23006e | 559 | } else { |
1e13775b SR |
560 | bgr = this._colourMap[color[0]]; |
561 | } | |
562 | ||
563 | var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')'; | |
564 | if (newStyle !== this._prevDrawStyle) { | |
565 | this._drawCtx.fillStyle = newStyle; | |
566 | this._prevDrawStyle = newStyle; | |
567 | } | |
568 | }, | |
569 | ||
570 | _rgbImageData: function (x, y, vx, vy, width, height, arr, offset) { | |
571 | var img = this._drawCtx.createImageData(width, height); | |
572 | var data = img.data; | |
573 | for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) { | |
574 | data[i] = arr[j]; | |
575 | data[i + 1] = arr[j + 1]; | |
576 | data[i + 2] = arr[j + 2]; | |
577 | data[i + 3] = 255; // Alpha | |
578 | } | |
579 | this._drawCtx.putImageData(img, x - vx, y - vy); | |
580 | }, | |
581 | ||
582 | _bgrxImageData: function (x, y, vx, vy, width, height, arr, offset) { | |
583 | var img = this._drawCtx.createImageData(width, height); | |
584 | var data = img.data; | |
585 | for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) { | |
586 | data[i] = arr[j + 2]; | |
587 | data[i + 1] = arr[j + 1]; | |
588 | data[i + 2] = arr[j]; | |
589 | data[i + 3] = 255; // Alpha | |
590 | } | |
591 | this._drawCtx.putImageData(img, x - vx, y - vy); | |
592 | }, | |
593 | ||
594 | _cmapImageData: function (x, y, vx, vy, width, height, arr, offset) { | |
595 | var img = this._drawCtx.createImageData(width, height); | |
596 | var data = img.data; | |
597 | var cmap = this._colourMap; | |
598 | for (var i = 0, j = offset; i < width * height * 4; i += 4, j++) { | |
599 | var bgr = cmap[arr[j]]; | |
600 | data[i] = bgr[2]; | |
601 | data[i + 1] = bgr[1]; | |
602 | data[i + 2] = bgr[0]; | |
603 | data[i + 3] = 255; // Alpha | |
604 | } | |
605 | this._drawCtx.putImageData(img, x - vx, y - vy); | |
606 | }, | |
607 | ||
608 | _scan_renderQ: function () { | |
609 | var ready = true; | |
610 | while (ready && this._renderQ.length > 0) { | |
611 | var a = this._renderQ[0]; | |
612 | switch (a.type) { | |
613 | case 'copy': | |
614 | this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height); | |
615 | break; | |
616 | case 'fill': | |
617 | this.fillRect(a.x, a.y, a.width, a.height, a.color); | |
618 | break; | |
619 | case 'blit': | |
620 | this.blitImage(a.x, a.y, a.width, a.height, a.data, 0); | |
621 | break; | |
622 | case 'blitRgb': | |
623 | this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0); | |
624 | break; | |
625 | case 'img': | |
626 | if (a.img.complete) { | |
627 | this.drawImage(a.img, a.x, a.y); | |
628 | } else { | |
629 | // We need to wait for this image to 'load' | |
630 | // to keep things in-order | |
631 | ready = false; | |
632 | } | |
633 | break; | |
634 | } | |
635 | ||
636 | if (ready) { | |
637 | this._renderQ.shift(); | |
638 | } | |
639 | } | |
640 | ||
641 | if (this._renderQ.length > 0) { | |
642 | requestAnimFrame(this._scan_renderQ.bind(this)); | |
643 | } | |
644 | }, | |
645 | }; | |
646 | ||
647 | Util.make_properties(Display, [ | |
648 | ['target', 'wo', 'dom'], // Canvas element for rendering | |
649 | ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only) | |
650 | ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "data": data} | |
651 | ['true_color', 'rw', 'bool'], // Use true-color pixel data | |
652 | ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color) | |
653 | ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0 | |
654 | ['viewport', 'rw', 'bool'], // Use a viewport set with viewportChange() | |
655 | ['width', 'rw', 'int'], // Display area width | |
656 | ['height', 'rw', 'int'], // Display area height | |
657 | ||
658 | ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only) | |
659 | ||
660 | ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods | |
661 | ['cursor_uri', 'rw', 'raw'] // Can we render cursor using data URI | |
662 | ]); | |
663 | ||
664 | // Class Methods | |
665 | Display.changeCursor = function (target, pixels, mask, hotx, hoty, w0, h0, cmap) { | |
666 | var w = w0; | |
667 | var h = h0; | |
668 | if (h < w) { | |
669 | h = w; // increase h to make it square | |
670 | } else { | |
671 | w = h; // increase w to make it square | |
672 | } | |
673 | ||
674 | var cur = []; | |
675 | ||
676 | // Push multi-byte little-endian values | |
677 | cur.push16le = function (num) { | |
678 | this.push(num & 0xFF, (num >> 8) & 0xFF); | |
679 | }; | |
680 | cur.push32le = function (num) { | |
681 | this.push(num & 0xFF, | |
682 | (num >> 8) & 0xFF, | |
683 | (num >> 16) & 0xFF, | |
684 | (num >> 24) & 0xFF); | |
685 | }; | |
686 | ||
687 | var IHDRsz = 40; | |
688 | var RGBsz = w * h * 4; | |
689 | var XORsz = Math.ceil((w * h) / 8.0); | |
690 | var ANDsz = Math.ceil((w * h) / 8.0); | |
691 | ||
692 | cur.push16le(0); // 0: Reserved | |
693 | cur.push16le(2); // 2: .CUR type | |
694 | cur.push16le(1); // 4: Number of images, 1 for non-animated ico | |
695 | ||
696 | // Cursor #1 header (ICONDIRENTRY) | |
697 | cur.push(w); // 6: width | |
698 | cur.push(h); // 7: height | |
699 | cur.push(0); // 8: colors, 0 -> true-color | |
700 | cur.push(0); // 9: reserved | |
701 | cur.push16le(hotx); // 10: hotspot x coordinate | |
702 | cur.push16le(hoty); // 12: hotspot y coordinate | |
703 | cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz); | |
704 | // 14: cursor data byte size | |
705 | cur.push32le(22); // 18: offset of cursor data in the file | |
706 | ||
707 | // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO) | |
708 | cur.push32le(IHDRsz); // 22: InfoHeader size | |
709 | cur.push32le(w); // 26: Cursor width | |
710 | cur.push32le(h * 2); // 30: XOR+AND height | |
711 | cur.push16le(1); // 34: number of planes | |
712 | cur.push16le(32); // 36: bits per pixel | |
713 | cur.push32le(0); // 38: Type of compression | |
714 | ||
715 | cur.push32le(XORsz + ANDsz); | |
716 | // 42: Size of Image | |
717 | cur.push32le(0); // 46: reserved | |
718 | cur.push32le(0); // 50: reserved | |
719 | cur.push32le(0); // 54: reserved | |
720 | cur.push32le(0); // 58: reserved | |
721 | ||
722 | // 62: color data (RGBQUAD icColors[]) | |
723 | var y, x; | |
724 | for (y = h - 1; y >= 0; y--) { | |
725 | for (x = 0; x < w; x++) { | |
726 | if (x >= w0 || y >= h0) { | |
727 | cur.push(0); // blue | |
728 | cur.push(0); // green | |
729 | cur.push(0); // red | |
730 | cur.push(0); // alpha | |
6f4cbb3f | 731 | } else { |
1e13775b SR |
732 | var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8); |
733 | var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; | |
734 | if (cmap) { | |
735 | idx = (w0 * y) + x; | |
736 | var rgb = cmap[pixels[idx]]; | |
737 | cur.push(rgb[2]); // blue | |
738 | cur.push(rgb[1]); // green | |
739 | cur.push(rgb[0]); // red | |
740 | cur.push(alpha); // alpha | |
ec31f82e SR |
741 | } else { |
742 | idx = ((w0 * y) + x) * 4; | |
743 | cur.push(pixels[idx + 2]); // blue | |
744 | cur.push(pixels[idx + 1]); // green | |
745 | cur.push(pixels[idx]); // red | |
746 | cur.push(alpha); // alpha | |
1e13775b | 747 | } |
6f4cbb3f | 748 | } |
2c2b492c JM |
749 | } |
750 | } | |
2c2b492c | 751 | |
1e13775b SR |
752 | // XOR/bitmask data (BYTE icXOR[]) |
753 | // (ignored, just needs to be the right size) | |
754 | for (y = 0; y < h; y++) { | |
755 | for (x = 0; x < Math.ceil(w / 8); x++) { | |
756 | cur.push(0); | |
757 | } | |
9a23006e | 758 | } |
9a23006e | 759 | |
1e13775b SR |
760 | // AND/bitmask data (BYTE icAND[]) |
761 | // (ignored, just needs to be the right size) | |
762 | for (y = 0; y < h; y++) { | |
763 | for (x = 0; x < Math.ceil(w / 8); x++) { | |
764 | cur.push(0); | |
765 | } | |
2c2b492c | 766 | } |
2c2b492c | 767 | |
1e13775b SR |
768 | var url = 'data:image/x-icon;base64,' + Base64.encode(cur); |
769 | target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; | |
770 | }; | |
771 | })(); |