v6502/vm/computer/textDisplayCanvas.js

348 lines
10 KiB
JavaScript

/*
* 6502 Based Virtual Computer
* Copyright (C) 2015 Scott C. Duensing <scott@jaegertech.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
// Our display adapter object.
function textDisplayCanvas() {
// This display adapter can render text in 16 foreground colors
// on 16 background colors. A simple "BIOS" is available by
// writing command data into the byte directly following the
// display memory and then the command into the byte after that.
// You may also write directly to display RAM.
// Font texture is 144x256 pixels.
// Characters are 9x16
// --- Private variables.
var self = this;
// Memory Information.
var MEMORY = null;
var MEMORY_START = 0;
var DATA_BYTE = 0;
var DATA_LSB = 0;
var DATA_BYTE_ADDRESS = 0;
var COMMAND_BYTE_ADDRESS = 0;
// Font atlas size.
var ATLAS_WIDTH = 16;
// Character cell size.
var CELL_WIDTH = 9;
var CELL_HEIGHT = 16;
// Default display size.
var WIDTH = 80;
var HEIGHT = 25;
// Is this a color display?
var IS_COLOR = true;
// These are good old PC DOS colors.
// black, dark red, dark green, brown,
// dark blue, dark magenta, dark cyan, gray
// dim gray, red, green, yellow,
// blue, magenta, cyan, white
var COLOR_HEX = ["#000000", "#8B0000", "#006400", "#8B8B00",
"#00008B", "#8B008B", "#008B8B", "#808080",
"#696969", "#FF0000", "#00FF00", "#FFFF00",
"#0000FF", "#FF00FF", "#00FFFF", "#FFFFFF"];
var COLOR_RGB = new Array();
// Cursor position, blink state, and ASCII symbol.
var CURSOR_X = 0;
var CURSOR_Y = 0;
var CURSOR_B = false;
var CURSOR_C = 177;
// Current color attribute. Boring white on black. (LSN=FG, MSN=BG)
var COLOR_VALUE = 7;
// The actual DOM canvases.
var CANVAS = null;
var CANVAS_CONTEXT = null;
var FONT = null;
var FONT_CONTEXT = null;
// --- Private methods.
var blinkCursor = function() {
// Change cursor state first so it is correct for the rest of the code.
CURSOR_B = !CURSOR_B;
if (CURSOR_B) {
renderCell(CURSOR_X, CURSOR_Y, CURSOR_C, 15, 0, true);
} else {
self.refresh(CURSOR_X, CURSOR_Y);
}
};
// Clear the screen to the current color attributes.
var clearDisplay = function() {
var byte = MEMORY_START;
for (var y=0; y<HEIGHT; y++) {
for (var x=0; x<WIDTH; x++) {
MEMORY.writeByte(byte++, 32); // [SPACE] in ASCII.
if (IS_COLOR) {
MEMORY.writeByte(byte++, COLOR_VALUE);
}
}
}
moveCursor(0, 0);
var MSN = (COLOR_VALUE & 0xf0) >> 4;
CANVAS_CONTEXT.fillStyle = COLOR_HEX[MSN];
CANVAS_CONTEXT.fillRect(0, 0, CANVAS.width, CANVAS.height);
};
// Move the cursor to a new location.
var moveCursor = function(x, y) {
// Be sure old character cell has been restored if the cursor is currently visible.
if (CURSOR_B) self.refresh(CURSOR_X, CURSOR_Y);
// Move it.
CURSOR_X = x;
CURSOR_Y = y;
};
// Draw one character to the display & update cursor position.
var drawCharacter = function(c) {
var color = (IS_COLOR ? 2 : 1);
var byte = MEMORY_START + (CURSOR_Y * WIDTH + CURSOR_X) * color;
var x = CURSOR_X;
var y = CURSOR_Y;
if (c == 8) {
// Backspace
if (x > 0)
x--;
} else if (c == 9) {
// TAB
x = (x + 8) & ~7;
} else if (c == 10) {
// Line Feed
y++;
} else if (c == 13) {
// Carriage Return
x = 0;
} else {
// Other characters.
MEMORY.writeByte(byte++, c);
if (IS_COLOR) MEMORY.writeByte(byte++, COLOR_VALUE);
self.refresh(x, y);
x++;
}
// Line wrap?
if (x >= WIDTH - 1) {
x = 0;
y++;
}
// Scroll required?
var didScroll = false;
while (y >= HEIGHT) {
didScroll = true;
byte = MEMORY_START;
for (var i=0; i<(HEIGHT - 1) * WIDTH; i++) {
MEMORY.writeByte(byte, MEMORY.readByte(byte + WIDTH * color));
byte++;
if (IS_COLOR) {
MEMORY.writeByte(byte, MEMORY.readByte(byte + WIDTH * color));
byte++;
}
}
for (var i=0; i<WIDTH; i++) {
MEMORY.writeByte(byte++, 32);
if (IS_COLOR) MEMORY.writeByte(byte++, COLOR_VALUE);
}
y--;
}
// Update display if we scrolled.
if (didScroll) {
var display = CANVAS_CONTEXT.getImageData(0, CELL_HEIGHT, CANVAS.width, CANVAS.height - CELL_HEIGHT);
CANVAS_CONTEXT.putImageData(display, 0, 0);
var MSN = (COLOR_VALUE & 0xf0) >> 4;
CANVAS_CONTEXT.fillStyle = COLOR_HEX[MSN];
CANVAS_CONTEXT.fillRect(0, CANVAS.height - CELL_HEIGHT, CANVAS.width, CELL_HEIGHT);
}
// Reposition cursor.
moveCursor(x, y);
};
// Draw a zero-terminated string to the display & update cursor position.
var drawString = function(address) {
var byte = address;
do {
var c = MEMORY.readByte(byte++);
if (c != 0) drawCharacter(c);
} while (c != 0);
};
// Finish attaching this display to the VM.
var finishAttaching = function(newMemory, newStartPosition, divId) {
MEMORY = newMemory;
MEMORY_START = newStartPosition;
// Pre-convert our hex colors to RGB values.
for (c=0; c<COLOR_HEX.length; ++c) {
COLOR_RGB.push(util.hexToRgb(COLOR_HEX[c]));
}
// Create canvas and context for display.
CANVAS = document.createElement('canvas');
CANVAS.height = CELL_HEIGHT * HEIGHT;
CANVAS.width = CELL_WIDTH * WIDTH;
CANVAS.style = 'border: 1px solid black';
CANVAS_CONTEXT = CANVAS.getContext('2d');
document.getElementById(divId).appendChild(CANVAS);
var byte = MEMORY_START + HEIGHT * WIDTH * (IS_COLOR ? 2 : 1);
DATA_BYTE_ADDRESS = byte++;
COMMAND_BYTE_ADDRESS = byte++;
clearDisplay();
// Set up memory mapped hardware ports.
MEMORY.addWriteCallback(MEMORY_START, MEMORY_START + self.getMemoryNeeded() - 1, null, function(address, value, data){
MEMORY.callbacksEnabled(false);
var offset = address - MEMORY_START;
// Is this a write to one of the command ports or direct to video memory?
if (address >= DATA_BYTE_ADDRESS) {
// Command byte?
if (address == COMMAND_BYTE_ADDRESS) {
if (value == 0) DATA_LSB = DATA_BYTE;
if (value == 1) clearDisplay();
if (value == 2) moveCursor(DATA_BYTE, CURSOR_Y);
if (value == 3) moveCursor(CURSOR_X, DATA_BYTE);
if (value == 4) MEMORY.writeByte(DATA_BYTE_ADDRESS, CURSOR_X);
if (value == 5) MEMORY.writeByte(DATA_BYTE_ADDRESS, CURSOR_Y);
if (value == 6) COLOR_VALUE = DATA_BYTE;
if (value == 7) drawCharacter(DATA_BYTE);
if (value == 8) drawString((DATA_BYTE << 8) + DATA_LSB);
} else {
// Data byte.
DATA_BYTE = value;
}
} else {
if (IS_COLOR) {
// Is this the character byte or the color byte?
if (offset % 2 == 0)
offset = offset / 2; // Character.
else
offset = ((offset + 1) / 2) - 1; // Color.
}
var y = Math.floor(offset / WIDTH);
var x = offset - y * WIDTH;
self.refresh(x, y);
}
MEMORY.callbacksEnabled(true);
});
};
var renderCell = function(x, y, c, f, b, t) {
// Find pixel offset in display canvas.
var cx = x * CELL_WIDTH;
var cy = y * CELL_HEIGHT;
// Locate this character in the font atlas.
var ax = (c % ATLAS_WIDTH) * CELL_WIDTH;
var ay = Math.floor(c / ATLAS_WIDTH) * CELL_HEIGHT;
// Render character.
var cell = CANVAS_CONTEXT.getImageData(cx, cy, CELL_WIDTH, CELL_HEIGHT);
var pixelColor = null;
for (rx=0; rx<CELL_WIDTH; rx++) {
for (ry=0; ry<CELL_HEIGHT; ry++) {
var pixel = (FONT_CONTEXT.getImageData(ax + rx, ay + ry, 1, 1).data[0] != 0);
// If we've requested transparent backgrounds, skip drawing them.
if (pixel || !t) {
pixelColor = (pixel ? COLOR_RGB[f] : COLOR_RGB[b]);
var index = (rx + ry * cell.width) * 4;
cell.data[index ] = pixelColor.r;
cell.data[index + 1] = pixelColor.g;
cell.data[index + 2] = pixelColor.b;
cell.data[index + 3] = 255;
}
}
}
CANVAS_CONTEXT.putImageData(cell, cx, cy);
//if (t) console.log("Rendered " + c + " at " + x + ", " + y);
}
// --- Public methods.
// Begin attaching this display to the VM.
this.attach = function(newMemory, newStartPosition, divId) {
// Create hidden canvas and load our font bitmap into it.
var fontImage = new Image();
fontImage.onload = function() {
FONT = document.createElement('canvas');
FONT.width = fontImage.width;
FONT.height = fontImage.height;
FONT_CONTEXT = FONT.getContext('2d');
FONT_CONTEXT.drawImage(fontImage, 0, 0);
// After the font loads, finish attaching to the computer.
finishAttaching(newMemory, newStartPosition, divId);
window.setInterval(blinkCursor.bind(self), 250);
//FONT.style = 'visibility: false';
//document.getElementById(divId).appendChild(FONT);
}
fontImage.src = 'VGAFont.png';
}
// Deserialize display state.
this.deserialize = function(state) {
DDATA_LSB = state.D;
IS_COLOR = state.C;
COLOR_VALUE = state.A;
moveCursor(state.X, state.Y);
for (x=0; x<WIDTH; x++)
for (y=0; y<HEIGHT; y++)
self.refresh(x, y);
};
// Refresh a single character cell.
this.refresh = function(x, y) {
var byte = MEMORY_START + (y * WIDTH + x) * (IS_COLOR ? 2 : 1);
// Assume B&W display for now.
var foreground = 7;
var background = 0;
// Get the character code we are going to render.
var c = MEMORY.readByte(byte);
// If we're using a color display, get the color byte.
if (IS_COLOR) {
// LSN=FG, MSN=BG
var value = MEMORY.readByte(byte + 1);
var LSN = value & 0x0f;
var MSN = (value & 0xf0) >> 4;
foreground = LSN;
background = MSN;
}
renderCell(x, y, c, foreground, background, false);
};
// Fetch how much RAM the current settings will require.
this.getMemoryNeeded = function() {
return WIDTH * HEIGHT * (IS_COLOR ? 2 : 1) + 2;
};
// Serialize display state.
this.serialize = function() {
var state = {
X: CURSOR_X,
Y: CURSOR_Y,
C: IS_COLOR,
A: COLOR_VALUE,
D: DATA_LSB
};
return state;
};
}