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