screen.js 15 KB

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