javascript_terminal
Differences
This shows you the differences between two versions of the page.
Next revision | Previous revision | ||
javascript_terminal [2023/11/20 11:30] – created appledog | javascript_terminal [2023/11/24 01:34] (current) – appledog | ||
---|---|---|---|
Line 1: | Line 1: | ||
= JavaScript Terminal | = JavaScript Terminal | ||
+ | * Also see: [[JavaScript Terminal v2|Version 2]] which is a more advanced version of this codebase. | ||
+ | |||
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]]. | 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 | + | The level of this code is the beginning of Season 2, for many reasons. If we took out the resize code, it might be late season |
+ | |||
+ | You can view a technical demo of this program at: [[https:// | ||
+ | |||
+ | == The Font | ||
+ | The font used in this technical demo is PxPlus_IBM_VGA_8x16.ttf which is available from the [[https:// | ||
== index.html | == index.html | ||
Line 8: | Line 15: | ||
< | < | ||
- | < | ||
< | < | ||
< | < | ||
- | | + | |
- | < | + | < |
- | | + | |
- | body, html { | + | body, html { |
- | margin: 0; | + | margin: 0; |
- | overflow: hidden; | + | overflow: hidden; |
- | } | + | } |
- | canvas { | + | |
- | display: block; | + | |
- | } | + | display: block; |
- | + | | |
- | </ | + | |
+ | |||
+ | </ | ||
</ | </ | ||
- | | + | |
< | < | ||
- | <!-- Add your site or application content here --> | + | < |
- | | + | <script src=" |
- | <script src=" | + | <script src=" |
- | <script src=" | + | <script src=" |
- | <script src=" | + | <script src=" |
- | <script src=" | + | |
</ | </ | ||
Line 38: | Line 45: | ||
</ | </ | ||
- | 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 is not much to see here. The most interesting line is probably |
- | 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. | + | Otherwise our main goal is to load and run the JavaScript files. |
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. | 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 | == Color.js | ||
- | <code: | + | <Code: |
class Color { | class Color { | ||
static black = '# | static black = '# | ||
Line 63: | Line 70: | ||
static yellow = '# | static yellow = '# | ||
static white = '# | static white = '# | ||
- | | + | |
- | static colorMap = [ this.black, this.blue, this.green, this.cyan, | + | static colorMap = [this.black, |
this.red, this.magenta, | this.red, this.magenta, | ||
this.darkgray, | this.darkgray, | ||
- | this.lightred, | + | this.lightred, |
static getColor(index) { | static getColor(index) { | ||
Line 108: | Line 116: | ||
< | < | ||
class Character { | class Character { | ||
- | | + | |
- | this.ch = ch; | + | this.ch = ch; |
- | this.color = color; | + | this.color = color; |
- | this.background = background; | + | 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. | 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 | == Terminal.js | ||
- | This is the workhorse. | ||
- | |||
< | < | ||
class Terminal { | class Terminal { | ||
Line 125: | Line 132: | ||
this.charWidth = 16; | this.charWidth = 16; | ||
this.charHeight = 32; | this.charHeight = 32; | ||
- | this.font_baseline = 32; | ||
- | | ||
this.cols = cols; | this.cols = cols; | ||
this.rows = rows; | this.rows = rows; | ||
- | | ||
- | this.cx = 0; | ||
- | this.cy = 0; | ||
- | this.interval = 535; // 535ms | ||
- | this.cc = false; | ||
- | this.repeatTimer(); | ||
this.buf = new Array(rows); | this.buf = new Array(rows); | ||
Line 143: | Line 142: | ||
} | } | ||
} | } | ||
+ | |||
+ | this.calcFontMetrics() | ||
} | } | ||
+ | calcFontMetrics() { | ||
+ | // Measure the width of a single character (assuming monospaced font) | ||
+ | const measureTextResult = ctx.measureText(' | ||
+ | // | ||
+ | |||
+ | // Calculate the baseline offset | ||
+ | // If there is a problem, adjust by hand. | ||
+ | // PxPlus_IBM_VGA_9x16 seems to work well at 24... | ||
+ | this.font_yadj = Math.round(32 - measureTextResult.fontBoundingBoxAscent) + 1; | ||
+ | this.font_yadj = 24 | ||
+ | // | ||
+ | } | ||
+ | | ||
setch(x, y, ch, color = ' | setch(x, y, ch, color = ' | ||
this.buf[y][x].ch = ch; | this.buf[y][x].ch = ch; | ||
this.buf[y][x].color = color; | this.buf[y][x].color = color; | ||
this.buf[y][x].background = background; | 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(" | ||
- | } | ||
- | } | ||
- | |||
- | 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 = ' | ||
- | } | ||
- | |||
- | // Function to schedule the timer to repeat | ||
- | repeatTimer() { | ||
- | this.timerId = setInterval(() => { | ||
- | this.cc = !this.cc; | ||
- | // | ||
- | }, this.interval); | ||
} | } | ||
drawCharacter(ctx, | drawCharacter(ctx, | ||
- | // 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 | // Calculate the actual position on the canvas based on character width and height | ||
const xPos = x * this.charWidth; | const xPos = x * this.charWidth; | ||
- | const yPos = y * this.charHeight; | + | const yPos = (y * this.charHeight); |
// Set the background | // Set the background | ||
Line 197: | Line 176: | ||
// Draw the character on the canvas | // Draw the character on the canvas | ||
ctx.fillStyle = color; | ctx.fillStyle = color; | ||
- | ctx.fillText(ch, | + | ctx.fillText(ch, |
} | } | ||
Line 226: | Line 206: | ||
newTerminal.setch(x, | newTerminal.setch(x, | ||
} | } | ||
+ | } | ||
+ | | ||
+ | if (this.timerId) { | ||
+ | clearInterval(this.timerId) | ||
} | } | ||
} | } | ||
} | } | ||
+ | </ | ||
== Analysis | == Analysis | ||
+ | This is the workhorse and it's a bit large, so review it carefully. | ||
+ | |||
=== Constructor | === 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. | 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, | + | 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 | + | === calculateFontMetrics() |
+ | This function | ||
+ | === setch() | ||
+ | setch() is a simple function that sets the character data of an x,y location. It is the standard way such data should be set. | ||
+ | |||
+ | === drawCharacter() | ||
+ | This is an important one. We first blank out the space by drawing the background color. Next, we draw the character. Note the y_adj. | ||
+ | |||
+ | === draw() | ||
+ | This just draws every character. It could theoretically be improved by only drawing characters which have changed, but this would require a change to the game loop which removed the clear-screen. It can also lead to subtle changes if some characters are drawn outside their box. Such a feature might be included in a part 2 (or part 3) in this series. | ||
+ | |||
+ | === copyTo | ||
+ | A simple function which copies important data to a new terminal, used mainly in resizing (see main.js). | ||
+ | |||
+ | == main.js | ||
+ | < | ||
+ | // Set up Canvas | ||
+ | let canvas = document.getElementById(' | ||
+ | let ctx = canvas.getContext(' | ||
+ | |||
+ | // Set up Terminal | ||
+ | maxtrows = 10 | ||
+ | maxtcols = 10 | ||
+ | terminal = new Terminal(10, | ||
+ | |||
+ | // load font | ||
+ | const font = new FontFace(' | ||
+ | |||
+ | font.load().then(() => { | ||
+ | document.fonts.add(font); | ||
+ | ctx.font = '32px myvga'; | ||
+ | }); | ||
+ | |||
+ | function getRandomInt(min, | ||
+ | return Math.floor(Math.random() * (max - min + 1)) + min; | ||
+ | } | ||
+ | |||
+ | function getRandomLetter() { | ||
+ | const s = " | ||
+ | const i = Math.floor(Math.random() * s.length); | ||
+ | return s.charAt(i); | ||
+ | } | ||
+ | |||
+ | // Function to resize and recreate the canvas | ||
+ | function resizeCanvas() { | ||
+ | canvas.width = window.innerWidth; | ||
+ | canvas.height = window.innerHeight; | ||
+ | |||
+ | maxTcols = Math.floor(canvas.width / terminal.charWidth) | ||
+ | maxTrows = Math.floor(canvas.height / terminal.charHeight); | ||
+ | |||
+ | t = new Terminal(maxTcols, | ||
+ | terminal.copyContentTo(t); | ||
+ | terminal = t; | ||
+ | |||
+ | ctx.font = '32px myvga'; | ||
+ | } | ||
+ | |||
+ | // Call the resizeCanvas function when the window is resized | ||
+ | window.addEventListener(' | ||
+ | |||
+ | // Animation logic, timed events, etc. | ||
+ | // I don't plan for this to be used much in a roguelike. | ||
+ | // but it is useful for (ex.) a static demo of the display engine. | ||
+ | function update() { | ||
+ | n = getRandomInt(1, | ||
+ | if (n === 1) { | ||
+ | x = getRandomInt(0, | ||
+ | y = getRandomInt(0, | ||
+ | ch = getRandomLetter() | ||
+ | color = Color.getColor(getRandomInt(0, | ||
+ | background = Color.black | ||
+ | terminal.setch(x, | ||
+ | // | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // All we really need to do is draw the terminal. | ||
+ | // What is on the terminal is done using (ex.) Terminal.setch() | ||
+ | function render() { | ||
+ | // clear screen | ||
+ | ctx.fillStyle = ' | ||
+ | ctx.fillRect(0, | ||
+ | |||
+ | // draw characters on terminal | ||
+ | terminal.draw(ctx); | ||
+ | } | ||
+ | |||
+ | function gameLoop() { | ||
+ | // Update game logic here | ||
+ | update() | ||
+ | |||
+ | // Render the game state | ||
+ | render(); | ||
+ | |||
+ | // Request the next frame | ||
+ | requestAnimationFrame(gameLoop); | ||
+ | } | ||
+ | |||
+ | // Initial canvas setup | ||
+ | resizeCanvas(); | ||
+ | |||
+ | // Quick test -- see update() for the rest of the ' | ||
+ | terminal.setch(3, | ||
+ | terminal.setch(5, | ||
+ | terminal.setch(6, | ||
+ | terminal.setch(7, | ||
+ | terminal.setch(8, | ||
+ | |||
+ | // Start the game loop | ||
+ | gameLoop(); | ||
</ | </ | ||
+ | |||
+ | == Analysis | ||
+ | Most of this should already be familiar since all of it would have been covered in JavaScript Season 1. | ||
+ | |||
+ | However, one interesting quirk which arose is that the ctx.font has to be set upon terminal resize. I am not sure where the best place is to set it, but it seems to work great setting it on a resize. It would also work if you set it every time you drew a character, but if we assume that we are only using one font and are essentially the main application (a one-canvas app) then setting it on every character draw is probably unnecessary. | ||
+ | |||
+ | == Also See | ||
+ | * Also see: [[JavaScript Terminal v2|Version 2]] which is a more advanced version of this codebase. | ||
+ | |||
+ | |||
+ |
javascript_terminal.1700479833.txt.gz · Last modified: 2023/11/20 11:30 by appledog