screen.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. "use strict";
  2. if(typeof window !== "undefined" && !window.requestAnimationFrame)
  3. {
  4. window.requestAnimationFrame =
  5. window.mozRequestAnimationFrame ||
  6. window.webkitRequestAnimationFrame;
  7. }
  8. /**
  9. * Adapter to use visual screen in browsers (in constrast to node)
  10. * @constructor
  11. *
  12. * @param {BusConnector} bus
  13. */
  14. function ScreenAdapter(screen_container, bus)
  15. {
  16. console.assert(screen_container, "1st argument must be a DOM container");
  17. var
  18. graphic_screen = screen_container.getElementsByTagName("canvas")[0],
  19. graphic_context = graphic_screen.getContext("2d"),
  20. text_screen = screen_container.getElementsByTagName("div")[0],
  21. cursor_element = document.createElement("div");
  22. var
  23. graphic_image_data,
  24. graphic_buffer,
  25. graphic_buffer32,
  26. /** @type {number} */
  27. cursor_row,
  28. /** @type {number} */
  29. cursor_col,
  30. /** @type {number} */
  31. scale_x = 1,
  32. /** @type {number} */
  33. scale_y = 1,
  34. graphical_mode_width,
  35. graphical_mode_height,
  36. modified_pixel_min = 0,
  37. modified_pixel_max = 0,
  38. changed_rows,
  39. // are we in graphical mode now?
  40. is_graphical = false,
  41. // Index 0: ASCII code
  42. // Index 1: Background color
  43. // Index 2: Foreground color
  44. text_mode_data,
  45. // number of columns
  46. text_mode_width,
  47. // number of rows
  48. text_mode_height;
  49. var layers = [];
  50. var screen = this;
  51. // 0x12345 -> "#012345"
  52. function number_as_color(n)
  53. {
  54. n = n.toString(16);
  55. return "#" + Array(7 - n.length).join("0") + n;
  56. }
  57. /**
  58. * Charmaps that containt unicode sequences for the default dospage
  59. * @const
  60. */
  61. var charmap_high = new Uint16Array([
  62. 0xC7, 0xFC, 0xE9, 0xE2, 0xE4, 0xE0, 0xE5, 0xE7,
  63. 0xEA, 0xEB, 0xE8, 0xEF, 0xEE, 0xEC, 0xC4, 0xC5,
  64. 0xC9, 0xE6, 0xC6, 0xF4, 0xF6, 0xF2, 0xFB, 0xF9,
  65. 0xFF, 0xD6, 0xDC, 0xA2, 0xA3, 0xA5, 0x20A7, 0x192,
  66. 0xE1, 0xED, 0xF3, 0xFA, 0xF1, 0xD1, 0xAA, 0xBA,
  67. 0xBF, 0x2310, 0xAC, 0xBD, 0xBC, 0xA1, 0xAB, 0xBB,
  68. 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556,
  69. 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510,
  70. 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
  71. 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567,
  72. 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B,
  73. 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580,
  74. 0x3B1, 0xDF, 0x393, 0x3C0, 0x3A3, 0x3C3, 0xB5, 0x3C4,
  75. 0x3A6, 0x398, 0x3A9, 0x3B4, 0x221E, 0x3C6, 0x3B5, 0x2229,
  76. 0x2261, 0xB1, 0x2265, 0x2264, 0x2320, 0x2321, 0xF7,
  77. 0x2248, 0xB0, 0x2219, 0xB7, 0x221A, 0x207F, 0xB2, 0x25A0, 0xA0
  78. ]);
  79. /** @const */
  80. var charmap_low = new Uint16Array([
  81. 0x20, 0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x2022,
  82. 0x25D8, 0x25CB, 0x25D9, 0x2642, 0x2640, 0x266A, 0x266B, 0x263C,
  83. 0x25BA, 0x25C4, 0x2195, 0x203C, 0xB6, 0xA7, 0x25AC, 0x21A8,
  84. 0x2191, 0x2193, 0x2192, 0x2190, 0x221F, 0x2194, 0x25B2, 0x25BC
  85. ]);
  86. var charmap = [],
  87. chr;
  88. for(var i = 0; i < 256; i++)
  89. {
  90. if(i > 127)
  91. {
  92. chr = charmap_high[i - 0x80];
  93. }
  94. else if(i < 32)
  95. {
  96. chr = charmap_low[i];
  97. }
  98. else
  99. {
  100. chr = i;
  101. }
  102. charmap[i] = String.fromCharCode(chr);
  103. }
  104. graphic_context["imageSmoothingEnabled"] = false;
  105. cursor_element.style.position = "absolute";
  106. cursor_element.style.backgroundColor = "#ccc";
  107. cursor_element.style.width = "7px";
  108. cursor_element.style.display = "inline-block";
  109. text_screen.style.display = "block";
  110. graphic_screen.style.display = "none";
  111. this.bus = bus;
  112. bus.register("screen-set-mode", function(data)
  113. {
  114. this.set_mode(data);
  115. }, this);
  116. bus.register("screen-fill-buffer-end", function(data)
  117. {
  118. var min = data[0];
  119. var max = data[1];
  120. this.update_buffer(min, max);
  121. }, this);
  122. bus.register("screen-put-char", function(data)
  123. {
  124. //console.log(data);
  125. this.put_char(data[0], data[1], data[2], data[3], data[4]);
  126. }, this);
  127. bus.register("screen-update-cursor", function(data)
  128. {
  129. this.update_cursor(data[0], data[1]);
  130. }, this);
  131. bus.register("screen-update-cursor-scanline", function(data)
  132. {
  133. this.update_cursor_scanline(data[0], data[1]);
  134. }, this);
  135. bus.register("screen-update-layers", function(data)
  136. {
  137. this.layers = data;
  138. }, this);
  139. bus.register("screen-set-size-text", function(data)
  140. {
  141. this.set_size_text(data[0], data[1]);
  142. }, this);
  143. bus.register("screen-set-size-graphical", function(data)
  144. {
  145. if(DEBUG_SCREEN_LAYERS)
  146. {
  147. data[0] = data[3];
  148. data[1] = data[4];
  149. }
  150. if(!data[0]) data[0] = 1;
  151. if(!data[1]) data[1] = 1;
  152. if(!data[3]) data[3] = data[0];
  153. if(!data[4]) data[4] = data[1];
  154. this.set_size_graphical(data[0], data[1], data[3], data[4]);
  155. }, this);
  156. this.init = function()
  157. {
  158. // not necessary, because this gets initialized by the bios early,
  159. // but nicer to look at
  160. this.set_size_text(80, 25);
  161. this.timer();
  162. };
  163. this.make_screenshot = function()
  164. {
  165. try {
  166. window.open(graphic_screen.toDataURL());
  167. }
  168. catch(e) {}
  169. };
  170. this.put_char = function(row, col, chr, bg_color, fg_color)
  171. {
  172. if(row < text_mode_height && col < text_mode_width)
  173. {
  174. var p = 3 * (row * text_mode_width + col);
  175. text_mode_data[p] = chr;
  176. text_mode_data[p + 1] = bg_color;
  177. text_mode_data[p + 2] = fg_color;
  178. changed_rows[row] = 1;
  179. }
  180. };
  181. this.timer = function()
  182. {
  183. requestAnimationFrame(is_graphical ? update_graphical : update_text);
  184. };
  185. var update_text = function()
  186. {
  187. for(var i = 0; i < text_mode_height; i++)
  188. {
  189. if(changed_rows[i])
  190. {
  191. screen.text_update_row(i);
  192. changed_rows[i] = 0;
  193. }
  194. }
  195. this.timer();
  196. }.bind(this);
  197. var update_graphical = function()
  198. {
  199. this.bus.send("screen-fill-buffer");
  200. this.timer();
  201. }.bind(this);
  202. this.destroy = function()
  203. {
  204. };
  205. this.set_mode = function(graphical)
  206. {
  207. is_graphical = graphical;
  208. if(graphical)
  209. {
  210. text_screen.style.display = "none";
  211. graphic_screen.style.display = "block";
  212. }
  213. else
  214. {
  215. text_screen.style.display = "block";
  216. graphic_screen.style.display = "none";
  217. }
  218. };
  219. this.clear_screen = function()
  220. {
  221. graphic_context.fillStyle = "#000";
  222. graphic_context.fillRect(0, 0, graphic_screen.width, graphic_screen.height);
  223. };
  224. /**
  225. * @param {number} cols
  226. * @param {number} rows
  227. */
  228. this.set_size_text = function(cols, rows)
  229. {
  230. if(cols === text_mode_width && rows === text_mode_height)
  231. {
  232. return;
  233. }
  234. changed_rows = new Int8Array(rows);
  235. text_mode_data = new Int32Array(cols * rows * 3);
  236. text_mode_width = cols;
  237. text_mode_height = rows;
  238. while(text_screen.childNodes.length > rows)
  239. {
  240. text_screen.removeChild(text_screen.firstChild);
  241. }
  242. while(text_screen.childNodes.length < rows)
  243. {
  244. text_screen.appendChild(document.createElement("div"));
  245. }
  246. for(var i = 0; i < rows; i++)
  247. {
  248. this.text_update_row(i);
  249. }
  250. update_scale_text();
  251. };
  252. this.set_size_graphical = function(width, height, buffer_width, buffer_height)
  253. {
  254. graphic_screen.style.display = "block";
  255. graphic_screen.width = width;
  256. graphic_screen.height = height;
  257. //graphic_screen.style.width = width * scale_x + "px";
  258. //graphic_screen.style.height = height * scale_y + "px";
  259. // Make sure to call this here, because pixels are transparent otherwise
  260. //screen.clear_screen();
  261. graphic_image_data = graphic_context.createImageData(buffer_width, buffer_height);
  262. graphic_buffer = new Uint8Array(graphic_image_data.data.buffer);
  263. graphic_buffer32 = new Int32Array(graphic_image_data.data.buffer);
  264. graphical_mode_width = width;
  265. graphical_mode_height = height;
  266. this.layers =
  267. [{
  268. screen_x: 0, screen_y: 0,
  269. buffer_x: 0, buffer_y: 0,
  270. buffer_width: width, buffer_height: height
  271. }];
  272. this.bus.send("screen-tell-buffer", [graphic_buffer32], [graphic_buffer32.buffer]);
  273. update_scale_graphic();
  274. };
  275. this.set_scale = function(s_x, s_y)
  276. {
  277. scale_x = s_x;
  278. scale_y = s_y;
  279. update_scale_text();
  280. update_scale_graphic();
  281. };
  282. this.set_scale(scale_x, scale_y);
  283. function update_scale_text()
  284. {
  285. elem_set_scale(text_screen, scale_x, scale_y, true);
  286. }
  287. function update_scale_graphic()
  288. {
  289. elem_set_scale(graphic_screen, scale_x, scale_y, false);
  290. }
  291. function elem_set_scale(elem, scale_x, scale_y, use_scale)
  292. {
  293. elem.style.width = "";
  294. elem.style.height = "";
  295. if(use_scale)
  296. {
  297. elem.style.transform = elem.style.webkitTransform = elem.style.MozTransform = "";
  298. }
  299. var rectangle = elem.getBoundingClientRect();
  300. if(use_scale)
  301. {
  302. var scale_str = "";
  303. scale_str += scale_x === 1 ? "" : " scaleX(" + scale_x + ")";
  304. scale_str += scale_y === 1 ? "" : " scaleY(" + scale_y + ")";
  305. elem.style.transform = elem.style.webkitTransform = elem.style.MozTransform = scale_str;
  306. }
  307. else
  308. {
  309. // unblur non-fractional scales
  310. if(scale_x % 1 === 0 && scale_y % 1 === 0)
  311. {
  312. graphic_screen.style.imageRendering = "-moz-crisp-edges";
  313. graphic_screen.style.imageRendering = "moz-crisp-edges";
  314. graphic_screen.style.imageRendering = "webkit-optimize-contrast";
  315. graphic_screen.style.imageRendering = "o-crisp-edges";
  316. graphic_screen.style.imageRendering = "pixelated";
  317. graphic_screen.style["-ms-interpolation-mode"] = "nearest-neighbor";
  318. }
  319. else
  320. {
  321. graphic_screen.style.imageRendering = "";
  322. graphic_screen.style["-ms-interpolation-mode"] = "";
  323. }
  324. // undo fractional css-to-device pixel ratios
  325. var device_pixel_ratio = window.devicePixelRatio || 1;
  326. if(device_pixel_ratio % 1 !== 0)
  327. {
  328. scale_x /= device_pixel_ratio;
  329. scale_y /= device_pixel_ratio;
  330. }
  331. }
  332. if(scale_x !== 1)
  333. {
  334. elem.style.width = rectangle.width * scale_x + "px";
  335. }
  336. if(scale_y !== 1)
  337. {
  338. elem.style.height = rectangle.height * scale_y + "px";
  339. }
  340. }
  341. this.update_cursor_scanline = function(start, end)
  342. {
  343. if(start & 0x20)
  344. {
  345. cursor_element.style.display = "none";
  346. }
  347. else
  348. {
  349. cursor_element.style.display = "inline";
  350. cursor_element.style.height = Math.min(15, end - start) + "px";
  351. cursor_element.style.marginTop = Math.min(15, start) + "px";
  352. }
  353. };
  354. this.update_cursor = function(row, col)
  355. {
  356. if(row !== cursor_row || col !== cursor_col)
  357. {
  358. changed_rows[row] = 1;
  359. changed_rows[cursor_row] = 1;
  360. cursor_row = row;
  361. cursor_col = col;
  362. }
  363. };
  364. this.text_update_row = function(row)
  365. {
  366. var offset = 3 * row * text_mode_width,
  367. row_element,
  368. color_element,
  369. fragment;
  370. var bg_color,
  371. fg_color,
  372. text;
  373. row_element = text_screen.childNodes[row];
  374. fragment = document.createElement("div");
  375. for(var i = 0; i < text_mode_width; )
  376. {
  377. color_element = document.createElement("span");
  378. bg_color = text_mode_data[offset + 1];
  379. fg_color = text_mode_data[offset + 2];
  380. color_element.style.backgroundColor = number_as_color(bg_color);
  381. color_element.style.color = number_as_color(fg_color);
  382. text = "";
  383. // put characters of the same color in one element
  384. while(i < text_mode_width &&
  385. text_mode_data[offset + 1] === bg_color &&
  386. text_mode_data[offset + 2] === fg_color)
  387. {
  388. var ascii = text_mode_data[offset];
  389. text += charmap[ascii];
  390. i++;
  391. offset += 3;
  392. if(row === cursor_row)
  393. {
  394. if(i === cursor_col)
  395. {
  396. // next row will be cursor
  397. // create new element
  398. break;
  399. }
  400. else if(i === cursor_col + 1)
  401. {
  402. // found the cursor
  403. fragment.appendChild(cursor_element);
  404. break;
  405. }
  406. }
  407. }
  408. color_element.textContent = text;
  409. fragment.appendChild(color_element);
  410. }
  411. row_element.parentNode.replaceChild(fragment, row_element);
  412. };
  413. this.update_buffer = function(min, max)
  414. {
  415. /*if(max < min)
  416. {
  417. return;
  418. }
  419. var min_y = min / graphical_mode_width | 0;
  420. var max_y = max / graphical_mode_width | 0;
  421. graphic_context.putImageData(
  422. graphic_image_data,
  423. 0, 0,
  424. 0, min_y,
  425. graphical_mode_width, max_y - min_y + 1
  426. );
  427. */
  428. this.clear_screen();
  429. if(DEBUG_SCREEN_LAYERS)
  430. {
  431. graphic_context.putImageData(
  432. graphic_image_data,
  433. 0, 0
  434. );
  435. graphic_context.strokeStyle = "#0F0";
  436. graphic_context.lineWidth = 4;
  437. this.layers.forEach((layer) =>
  438. {
  439. graphic_context.strokeRect(
  440. layer.buffer_x,
  441. layer.buffer_y,
  442. layer.buffer_width,
  443. layer.buffer_height
  444. );
  445. });
  446. graphic_context.lineWidth = 1;
  447. return;
  448. }
  449. this.layers.forEach((layer) =>
  450. {
  451. graphic_context.putImageData(
  452. graphic_image_data,
  453. layer.screen_x - layer.buffer_x,
  454. layer.screen_y - layer.buffer_y,
  455. layer.buffer_x,
  456. layer.buffer_y,
  457. layer.buffer_width,
  458. layer.buffer_height
  459. );
  460. });
  461. };
  462. this.init();
  463. }