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