"use strict"; /** * Adapter to use visual screen in browsers (in contrast to node) * @constructor * * @param {BusConnector} bus */ function ScreenAdapter(screen_container, bus) { console.assert(screen_container, "1st argument must be a DOM container"); var graphic_screen = screen_container.getElementsByTagName("canvas")[0], graphic_context = graphic_screen.getContext("2d", { alpha: false }), text_screen = screen_container.getElementsByTagName("div")[0], cursor_element = document.createElement("div"); var graphic_image_data, graphic_buffer, graphic_buffer32, /** @type {number} */ cursor_row, /** @type {number} */ cursor_col, /** @type {number} */ scale_x = 1, /** @type {number} */ scale_y = 1, base_scale = 1, graphical_mode_width, graphical_mode_height, modified_pixel_min = 0, modified_pixel_max = 0, changed_rows, // are we in graphical mode now? is_graphical = false, // Index 0: ASCII code // Index 1: Background color // Index 2: Foreground color text_mode_data, // number of columns text_mode_width, // number of rows text_mode_height; var stopped = false; var screen = this; // 0x12345 -> "#012345" function number_as_color(n) { n = n.toString(16); return "#" + Array(7 - n.length).join("0") + n; } /** * Charmaps that constraint unicode sequences for the default dospage * @const */ var charmap_high = new Uint16Array([ 0xC7, 0xFC, 0xE9, 0xE2, 0xE4, 0xE0, 0xE5, 0xE7, 0xEA, 0xEB, 0xE8, 0xEF, 0xEE, 0xEC, 0xC4, 0xC5, 0xC9, 0xE6, 0xC6, 0xF4, 0xF6, 0xF2, 0xFB, 0xF9, 0xFF, 0xD6, 0xDC, 0xA2, 0xA3, 0xA5, 0x20A7, 0x192, 0xE1, 0xED, 0xF3, 0xFA, 0xF1, 0xD1, 0xAA, 0xBA, 0xBF, 0x2310, 0xAC, 0xBD, 0xBC, 0xA1, 0xAB, 0xBB, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, 0x3B1, 0xDF, 0x393, 0x3C0, 0x3A3, 0x3C3, 0xB5, 0x3C4, 0x3A6, 0x398, 0x3A9, 0x3B4, 0x221E, 0x3C6, 0x3B5, 0x2229, 0x2261, 0xB1, 0x2265, 0x2264, 0x2320, 0x2321, 0xF7, 0x2248, 0xB0, 0x2219, 0xB7, 0x221A, 0x207F, 0xB2, 0x25A0, 0xA0 ]); /** @const */ var charmap_low = new Uint16Array([ 0x20, 0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x2022, 0x25D8, 0x25CB, 0x25D9, 0x2642, 0x2640, 0x266A, 0x266B, 0x263C, 0x25BA, 0x25C4, 0x2195, 0x203C, 0xB6, 0xA7, 0x25AC, 0x21A8, 0x2191, 0x2193, 0x2192, 0x2190, 0x221F, 0x2194, 0x25B2, 0x25BC ]); var charmap = [], chr; for(var i = 0; i < 256; i++) { if(i > 127) { chr = charmap_high[i - 0x80]; } else if(i < 32) { chr = charmap_low[i]; } else { chr = i; } charmap[i] = String.fromCharCode(chr); } graphic_context["imageSmoothingEnabled"] = false; cursor_element.style.position = "absolute"; cursor_element.style.backgroundColor = "#ccc"; cursor_element.style.width = "7px"; cursor_element.style.display = "inline-block"; text_screen.style.display = "block"; graphic_screen.style.display = "none"; this.bus = bus; bus.register("screen-set-mode", function(data) { this.set_mode(data); }, this); bus.register("screen-fill-buffer-end", function(data) { this.update_buffer(data); }, this); bus.register("screen-put-char", function(data) { //console.log(data); this.put_char(data[0], data[1], data[2], data[3], data[4]); }, this); bus.register("screen-update-cursor", function(data) { this.update_cursor(data[0], data[1]); }, this); bus.register("screen-update-cursor-scanline", function(data) { this.update_cursor_scanline(data[0], data[1]); }, this); bus.register("screen-clear", function() { this.clear_screen(); }, this); bus.register("screen-set-size-text", function(data) { this.set_size_text(data[0], data[1]); }, this); bus.register("screen-set-size-graphical", function(data) { this.set_size_graphical(data[0], data[1], data[2], data[3]); }, this); this.init = function() { // not necessary, because this gets initialized by the bios early, // but nicer to look at this.set_size_text(80, 25); this.timer(); }; this.make_screenshot = function() { try { const image = new Image(); image.src = graphic_screen.toDataURL("image/png"); const w = window.open(""); w.document.write(image.outerHTML); } catch(e) {} }; this.put_char = function(row, col, chr, bg_color, fg_color) { if(row < text_mode_height && col < text_mode_width) { var p = 3 * (row * text_mode_width + col); text_mode_data[p] = chr; text_mode_data[p + 1] = bg_color; text_mode_data[p + 2] = fg_color; changed_rows[row] = 1; } }; this.timer = function() { if(!stopped) { requestAnimationFrame(is_graphical ? update_graphical : update_text); } }; var update_text = function() { for(var i = 0; i < text_mode_height; i++) { if(changed_rows[i]) { screen.text_update_row(i); changed_rows[i] = 0; } } this.timer(); }.bind(this); var update_graphical = function() { this.bus.send("screen-fill-buffer"); this.timer(); }.bind(this); this.destroy = function() { stopped = true; }; this.set_mode = function(graphical) { is_graphical = graphical; if(graphical) { text_screen.style.display = "none"; graphic_screen.style.display = "block"; } else { text_screen.style.display = "block"; graphic_screen.style.display = "none"; } }; this.clear_screen = function() { graphic_context.fillStyle = "#000"; graphic_context.fillRect(0, 0, graphic_screen.width, graphic_screen.height); }; /** * @param {number} cols * @param {number} rows */ this.set_size_text = function(cols, rows) { if(cols === text_mode_width && rows === text_mode_height) { return; } changed_rows = new Int8Array(rows); text_mode_data = new Int32Array(cols * rows * 3); text_mode_width = cols; text_mode_height = rows; while(text_screen.childNodes.length > rows) { text_screen.removeChild(text_screen.firstChild); } while(text_screen.childNodes.length < rows) { text_screen.appendChild(document.createElement("div")); } for(var i = 0; i < rows; i++) { this.text_update_row(i); } update_scale_text(); }; this.set_size_graphical = function(width, height, buffer_width, buffer_height) { if(DEBUG_SCREEN_LAYERS) { // Draw the entire buffer. Useful for debugging // panning / page flipping / screen splitting code for both // v86 developers and os developers width = buffer_width; height = buffer_height; } graphic_screen.style.display = "block"; graphic_screen.width = width; graphic_screen.height = height; //graphic_screen.style.width = width * scale_x + "px"; //graphic_screen.style.height = height * scale_y + "px"; // Make sure to call this here, because pixels are transparent otherwise //screen.clear_screen(); graphic_image_data = graphic_context.createImageData(buffer_width, buffer_height); graphic_buffer = new Uint8Array(graphic_image_data.data.buffer); graphic_buffer32 = new Int32Array(graphic_image_data.data.buffer); graphical_mode_width = width; graphical_mode_height = height; // add some scaling to tiny resolutions if(graphical_mode_width <= 640) { base_scale = 2; } else { base_scale = 1; } this.bus.send("screen-tell-buffer", [graphic_buffer32], [graphic_buffer32.buffer]); update_scale_graphic(); }; this.set_scale = function(s_x, s_y) { scale_x = s_x; scale_y = s_y; update_scale_text(); update_scale_graphic(); }; this.set_scale(scale_x, scale_y); function update_scale_text() { elem_set_scale(text_screen, scale_x, scale_y, true); } function update_scale_graphic() { elem_set_scale(graphic_screen, scale_x * base_scale, scale_y * base_scale, false); } function elem_set_scale(elem, scale_x, scale_y, use_scale) { elem.style.width = ""; elem.style.height = ""; if(use_scale) { elem.style.transform = ""; } var rectangle = elem.getBoundingClientRect(); if(use_scale) { var scale_str = ""; scale_str += scale_x === 1 ? "" : " scaleX(" + scale_x + ")"; scale_str += scale_y === 1 ? "" : " scaleY(" + scale_y + ")"; elem.style.transform = scale_str; } else { // unblur non-fractional scales if(scale_x % 1 === 0 && scale_y % 1 === 0) { graphic_screen.style["imageRendering"] = "crisp-edges"; // firefox graphic_screen.style["imageRendering"] = "pixelated"; graphic_screen.style["-ms-interpolation-mode"] = "nearest-neighbor"; } else { graphic_screen.style.imageRendering = ""; graphic_screen.style["-ms-interpolation-mode"] = ""; } // undo fractional css-to-device pixel ratios var device_pixel_ratio = window.devicePixelRatio || 1; if(device_pixel_ratio % 1 !== 0) { scale_x /= device_pixel_ratio; scale_y /= device_pixel_ratio; } } if(scale_x !== 1) { elem.style.width = rectangle.width * scale_x + "px"; } if(scale_y !== 1) { elem.style.height = rectangle.height * scale_y + "px"; } } this.update_cursor_scanline = function(start, end) { if(start & 0x20) { cursor_element.style.display = "none"; } else { cursor_element.style.display = "inline"; cursor_element.style.height = Math.min(15, end - start) + "px"; cursor_element.style.marginTop = Math.min(15, start) + "px"; } }; this.update_cursor = function(row, col) { if(row !== cursor_row || col !== cursor_col) { changed_rows[row] = 1; changed_rows[cursor_row] = 1; cursor_row = row; cursor_col = col; } }; this.text_update_row = function(row) { var offset = 3 * row * text_mode_width, row_element, color_element, fragment; var bg_color, fg_color, text; row_element = text_screen.childNodes[row]; fragment = document.createElement("div"); for(var i = 0; i < text_mode_width; ) { color_element = document.createElement("span"); bg_color = text_mode_data[offset + 1]; fg_color = text_mode_data[offset + 2]; color_element.style.backgroundColor = number_as_color(bg_color); color_element.style.color = number_as_color(fg_color); text = ""; // put characters of the same color in one element while(i < text_mode_width && text_mode_data[offset + 1] === bg_color && text_mode_data[offset + 2] === fg_color) { var ascii = text_mode_data[offset]; text += charmap[ascii]; i++; offset += 3; if(row === cursor_row) { if(i === cursor_col) { // next row will be cursor // create new element break; } else if(i === cursor_col + 1) { // found the cursor fragment.appendChild(cursor_element); break; } } } color_element.textContent = text; fragment.appendChild(color_element); } row_element.parentNode.replaceChild(fragment, row_element); }; this.update_buffer = function(layers) { if(DEBUG_SCREEN_LAYERS) { // Draw the entire buffer. Useful for debugging // panning / page flipping / screen splitting code for both // v86 developers and os developers graphic_context.putImageData( graphic_image_data, 0, 0 ); // For each visible layer that would've been drawn, draw a // rectangle to visualise the layer instead. graphic_context.strokeStyle = "#0F0"; graphic_context.lineWidth = 4; layers.forEach((layer) => { graphic_context.strokeRect( layer.buffer_x, layer.buffer_y, layer.buffer_width, layer.buffer_height ); }); graphic_context.lineWidth = 1; return; } layers.forEach((layer) => { graphic_context.putImageData( graphic_image_data, layer.screen_x - layer.buffer_x, layer.screen_y - layer.buffer_y, layer.buffer_x, layer.buffer_y, layer.buffer_width, layer.buffer_height ); }); }; this.init(); }