display.js 24 KB

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