This is an old revision of the document!
Table of Contents
JavaScript Terminal
This section will outline the basic JavaScript terminal code. This code was not used much when I taught JavaScript to Roger and Neo, but it serves the same basic purpose as the PyGame Terminal.
The level of this code will be towards the end of JavaScript Season 1, or at the beginning of JavaScript Season 2. I think, that we will use console for Season 1, and start with this in Season 2 – maybe. It's advanced because it has several classes and data structures, so it feels more like a Season 2 item. Anyways,
index.html
You can of course also use an index.php.
<!doctype html> <html> <head> <meta charset="utf-8"> <title>Basic Javascript Terminal</title> <style> body, html { margin: 0; overflow: hidden; } canvas { display: block; } </style> </head> <body> <!-- Add your site or application content here --> <canvas style="border: none"></canvas> <script src="js/Color.js"></script> <script src="js/Character.js"></script> <script src="js/Terminal.js"></script> <script src="js/main.js"></script> </body> </html>
There is not much to see here. In the original we used normalizer, modernizer and a few other things, but I have stripped those off to show the basic idea.
There are four main parts: Color.js, which is a color map, Character.js which discusses what needs to be known to represent a character, Terminal, which holds the information about characters on the screen and how to draw them, and main.js which sets up everything and starts the game loop.
Actually, you could probably refactor some things in main into a class (such as a Game.js class) and then just instantiate and run the class, like we do in Python for PyGame. Perhaps this will be done in a part two sequel article to this one. For now, it's left as an exercise to the reader.
Color.js
<code:JavaScript> class Color {
static black = '#000000'; static blue = '#0000AA'; static green = '#00AA00'; static cyan = '#00AAAA'; static red = '#AA0000'; static magenta = '#AA00AA'; static brown = '#AA5500'; static lightgray = '#AAAAAA'; static darkgray = '#555555'; static lightblue = '#5555FF'; static lightgreen = '#55FF55'; static lightcyan = '#55FFFF'; static lightred = '#FF5555'; static lightmagenta = '#FF55FF'; static yellow = '#FFFF55'; static white = '#FFFFFF'; static colorMap = [ this.black, this.blue, this.green, this.cyan, this.red, this.magenta, this.brown, this.lightgray, this.darkgray, this.lightblue, this.lightgreen, this.lightcyan, this.lightred, this.lightmagenta, this.yellow, this.white ]
static getColor(index) { if (index >= 0 && index <= 15) { return this.colorMap[index]; } else { return null; // Return null for invalid index } }
static getColorByName(colorName) { const lowerCaseColorName = colorName.toLowerCase(); const index = Object.keys(Color).findIndex(key => Color[key].toLowerCase() === lowerCaseColorName);
if (index !== -1) { return Color.getColor(index); } else { return null; // Return null for unsupported colors } }
} </Code>
Info
This code contains four important parts; the static references, the colormap, and two getters (by color number and by name).
Static References
These can be accessed by Color.lightgray, Color.black, etc. once the file is loaded. This is also how we define the colormap.
The ColorMap
These are an enumerated list of colors, which allow us to reference the color by their original color number. Although VGA had access to 256 colors, we are most concerned with the first sixteen here; they are listed in order from 0 to 15. This will allow us to simulate the look and feel of “color ASCII nethack” on an IBM PC.
getColor() and getColorByName()
You can do Color.getColor(number), and 0 will return black.
You can also do Color.getColorByName('black') and this will return Color.black. This is probably useful if you need to input a color by the user, or you don't wish to remember the digits or use the static references. The digits are probably the most space-efficient way of storing the color data – although bit packing would be more efficient – the digits (0 to 15) are a decent trade-off.
Character.js
class Character { constructor(ch = ' ', color = 'LightGray', background = 'black') { this.ch = ch; this.color = color; this.background = background; } } Simple! We will store the character (or it's ASCII code for printing), it's color, and it's background. This is all the data we need to represent a character on the screen. == Terminal.js This is the workhorse. <Code:JavaScript> class Terminal { constructor(cols, rows) { this.charWidth = 16; this.charHeight = 32; this.font_baseline = 32; this.cols = cols; this.rows = rows; this.cx = 0; this.cy = 0; this.interval = 535; // 535ms this.cc = false; this.repeatTimer(); this.buf = new Array(rows); for (let y = 0; y < rows; y++) { this.buf[y] = new Array(cols); for (let x = 0; x < cols; x++) { this.buf[y][x] = new Character(); } } } setch(x, y, ch, color = 'gray', background = 'black') { this.buf[y][x].ch = ch; this.buf[y][x].color = color; this.buf[y][x].background = background; } putch(ch, color) { this.buf[this.cy][this.cx].ch = ch; this.buf[this.cy][this.cx].color = color; this.cx = this.cx + 1; if (this.cx >= this.cols - 1) { this.cx = 0; this.cy = this.cy + 1; } if (this.cy >= this.rows - 1) { console.log("scroll screen"); } } delch() { this.cx = this.cx - 1; if (this.cx < 0) { this.cx = 0; } this.buf[this.cy][this.cx].ch = ' '; this.buf[this.cy][this.cx].color = 'gray'; } // Function to schedule the timer to repeat repeatTimer() { this.timerId = setInterval(() => { this.cc = !this.cc; //console.log(`Cursor state: ${this.cc ? 'On' : 'Off'}`); }, this.interval); } drawCharacter(ctx, x, y, ch, color = 'light gray', background = 'black') { // Assuming font is a monospaced font loaded in your HTML ctx.font = '32px myvga'; // Calculate the actual position on the canvas based on character width and height const xPos = x * this.charWidth; const yPos = y * this.charHeight; // Set the background ctx.fillStyle = background; ctx.fillRect(xPos, yPos, this.charWidth, this.charHeight); // Draw the character on the canvas ctx.fillStyle = color; ctx.fillText(ch, xPos, yPos); } draw(ctx) { for (let y = 0; y < this.rows; y++) { for (let x = 0; x < this.cols; x++) { // Get the character and color from the buf array const ch = this.buf[y][x].ch; const fg = this.buf[y][x].color; const bg = this.buf[y][x].background; this.drawCharacter(ctx, x, y, ch, fg, bg); } } } copyContentTo(newTerminal) { // Determine the number of rows and columns to copy const rowsToCopy = Math.min(this.rows, newTerminal.rows); const colsToCopy = Math.min(this.cols, newTerminal.cols); // Copy content from the old terminal to the new terminal for (let y = 0; y < rowsToCopy; y++) { for (let x = 0; x < colsToCopy; x++) { const ch = this.buf[y][x].ch; const color = this.buf[y][x].color; const background = this.buf[y][x].background; newTerminal.setch(x, y, ch, color, background); } } } } == Analysis === Constructor The constructor sets up various metrics. First, we have hardcoded the font metrics we will be using. We could discover these, but, for now we will design it around a particular font as much as we can. Next are rows and columns, then the cursor information. cx and cy are the cursor x and y. cc is the state; true means on and false means off. I have included a basic timer (which works) but I have neglected to include code to actually print the cursor here. We will leave it as an exercise to the reader; mainly as it is only useful for a full terminal simulator, which this is not -- this is only 'the ability to print characters on a screen', for example, for use in a game similar to one of the ones we did coming out of [[PyGame Terminal]].