This entry came to be after Haxe user anniz shared his experiment which aimed to recreate VGA text rendering in Flash. Soon after, it was proposed and discussed as a potential Code Cookbook entry. You can read more about its history in this repository issue.
Why VGA?
For the uninitiated, VGA (Video Graphics Array) is an analog computer display standard developed by IBM, and first introduced in 1987. It may seem strange to talk about it in this context, but in reality it's just a backdrop to what we're actually doing - rendering text as a raster graphic.
How does VGA text work?
The characters in a VGA font are defined as a monochromatic raster graphic - a rectangular grid of pixels, where the color of every pixel depends on the value of a single bit.
Let's consider a monospaced VGA font with an 8x8 character size. Accordingly, we'll draw an 8x8 grid. The top left corner of the grid will be the grid's origin point, set at (0, 0). Now let's fill out grid fields in such a way that the grid represents the 'A' character.
If we take a better look at the grid, we can see that the character can be stored in eight 8-bit binary (base-2) numbers, where every row of the character raster is represented by its own binary number. These numbers are written out relative to the origin point of the grid. In this case, we're writing the numbers down left-to-right, so the convention is that the least significant bit comes first. With that in mind, we can simplify things by performing a conversion to hex (base-16).
0 1 2 3 4 5 6 7 BINARY HEX
0 . # # # # # # . => 01111110 => 0x7E
1 . # # # # # # . => 01111110 => 0x7E
2 . # # . . # # . => 01100110 => 0x66
3 . # # # # # # . => 01111110 => 0x7E
4 . # # # # # # . => 01111110 => 0x7E
5 . # # . . # # . => 01100110 => 0x66
6 . # # . . # # . => 01100110 => 0x66
7 . . . . . . . . => 00000000 => 0x00
The 'A' character raster can thus be described by an array of its rows, as [0x7E, 0x7E, 0x66, 0x7E, 0x7E, 0x66, 0x66, 0x00]
(from top to bottom).
A VGA font is an array of such character rasters. Ideally, their order in the font array should be determined by their character codes, which makes it easier to fetch the proper character raster for drawing. For example, an ASCII font character set could be placed in an array like this:
var font = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0, null
...,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 32, space
0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x00, // 33, !
...
0x7E, 0X7E, 0X66, 0X66, 0X7E, 0X66, 0X66, 0X00, // 65, A
...
];
Keep in mind that convention is important. Whether the least significant bit comes first or last has an impact on how the character is drawn - specifically, the character will appear to be mirrored.
How does one draw VGA text?
VGA text is drawn character by character. For a given text string, its characters are iterated over, and their character codes are retrieved. These character codes are then used as indexes for the font array, to get the first row of the character raster that is to be drawn. The number of raster rows drawn depends on the character size.
For every column (bit) in the row, a check is made to determine whether the bit is set to 1 or 0. Depending on the value, a white or black color is assigned to the pixel of the image (screen).
A function which demonstrates the principle of drawing VGA characters is given below:
/** * Renders a character from a provided ASCII character set at (x, y) position on image. * @param charCode ASCII character code * @param charSize Character size (assumed monospaced font) * @param font Font as ASCII character array * @param x Horizontal position of character's top-left corner on image * @param y Vertical position of character's top-left corner on image * @param image The image the character will be rendered to */ function renderAsciiChar(charCode : Int, charSize : Int, font : Array<Int>, x : Int, y : Int, image : Image) : Void { // Compute index of the character raster's first row in the font array var index = charCode * charSize; // Iterate over character raster rows for (charRow in 0...charSize) { // Read character raster row bits var rowBits = font[index + charRow]; // Iterate over character raster row bits (columns) for (charColumn in 0...charSize) { // Isolate a single bit from the row bits // Note: this depends on convention! var bit = (rowBits << charcolumn) & 0x80; // 1 = white, 0 = black var color = (bit == 0x80) ? 0xffffffff : 0xff000000; // Set pixel on image image.setPixel(x + charColumn, y + charRow, color); } } }
The actual implementation is only slightly different from the above function, as changes have to be made to accomodate the drawing API of a given platform.
Example
import js.html.CanvasRenderingContext2D; import js.html.ImageData; import js.Browser; class Test { // ASCII character set, as monospaced 8x8 characters static var Font = [ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x7E,0x81,0xA5,0x81,0xBD,0x99,0x81,0x7E, 0x7E,0xFF,0x00,0xFF,0xC3,0xE7,0xFF,0x7E, 0x6C,0xFE,0xFE,0xFE,0x7C,0x38,0x10,0x00, 0x10,0x38,0x7C,0xFE,0x7C,0x38,0x10,0x00, 0x38,0x7C,0x38,0xFE,0xFE,0x92,0x10,0x7C, 0x00,0x10,0x38,0x7C,0xFE,0x7C,0x38,0x7C, 0x00,0x00,0x18,0x3C,0x3C,0x18,0x00,0x00, 0xFF,0xFF,0xE7,0xC3,0xC3,0xE7,0xFF,0xFF, 0x00,0x3C,0x66,0x42,0x42,0x66,0x3C,0x00, 0xFF,0xC3,0x99,0xBD,0xBD,0x99,0xC3,0xFF, 0x0F,0x07,0x0F,0x7D,0xCC,0xCC,0xCC,0x78, 0x3C,0x66,0x66,0x66,0x3C,0x18,0x7E,0x18, 0x3F,0x33,0x3F,0x30,0x30,0x70,0xF0,0xE0, 0x7F,0x63,0x7F,0x63,0x63,0x67,0xE6,0xC0, 0x99,0x5A,0x3C,0xE7,0xE7,0x3C,0x5A,0x99, 0x80,0xE0,0xF8,0xFE,0xF8,0xE0,0x80,0x00, 0x02,0x0E,0x3E,0xFE,0x3E,0x0E,0x02,0x00, 0x18,0x3C,0x7E,0x18,0x18,0x7E,0x3C,0x18, 0x66,0x66,0x66,0x66,0x66,0x00,0x66,0x00, 0x7F,0x00,0x00,0x7B,0x1B,0x1B,0x1B,0x00, 0x3E,0x63,0x38,0x6C,0x6C,0x38,0x86,0xFC, 0x00,0x00,0x00,0x00,0x7E,0x7E,0x7E,0x00, 0x18,0x3C,0x7E,0x18,0x7E,0x3C,0x18,0xFF, 0x18,0x3C,0x7E,0x18,0x18,0x18,0x18,0x00, 0x18,0x18,0x18,0x18,0x7E,0x3C,0x18,0x00, 0x00,0x18,0x0C,0xFE,0x0C,0x18,0x00,0x00, 0x00,0x30,0x60,0xFE,0x60,0x30,0x00,0x00, 0x00,0x00,0xC0,0xC0,0xC0,0xFE,0x00,0x00, 0x00,0x24,0x66,0xFF,0x66,0x24,0x00,0x00, 0x00,0x18,0x3C,0x7E,0xFF,0xFF,0x00,0x00, 0x00,0xFF,0xFF,0x7E,0x3C,0x18,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x18,0x3C,0x3C,0x18,0x18,0x00,0x18,0x00, 0x6C,0x6C,0x6C,0x00,0x00,0x00,0x00,0x00, 0x6C,0x6C,0xFE,0x6C,0xFE,0x6C,0x6C,0x00, 0x18,0x7E,0xC0,0x7C,0x06,0xFC,0x18,0x00, 0x00,0xC6,0xCC,0x18,0x30,0x66,0xC6,0x00, 0x38,0x6C,0x38,0x76,0xDC,0xCC,0x76,0x00, 0x30,0x30,0x60,0x00,0x00,0x00,0x00,0x00, 0x18,0x30,0x60,0x60,0x60,0x30,0x18,0x00, 0x60,0x30,0x18,0x18,0x18,0x30,0x60,0x00, 0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00, 0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x30, 0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00, 0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00, 0x7C,0xCE,0xDE,0xF6,0xE6,0xC6,0x7C,0x00, 0x30,0x70,0x30,0x30,0x30,0x30,0xFC,0x00, 0x78,0xCC,0x0C,0x38,0x60,0xCC,0xFC,0x00, 0x78,0xCC,0x0C,0x38,0x0C,0xCC,0x78,0x00, 0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x1E,0x00, 0xFC,0xC0,0xF8,0x0C,0x0C,0xCC,0x78,0x00, 0x38,0x60,0xC0,0xF8,0xCC,0xCC,0x78,0x00, 0xFC,0xCC,0x0C,0x18,0x30,0x30,0x30,0x00, 0x78,0xCC,0xCC,0x78,0xCC,0xCC,0x78,0x00, 0x78,0xCC,0xCC,0x7C,0x0C,0x18,0x70,0x00, 0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x00, 0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x30, 0x18,0x30,0x60,0xC0,0x60,0x30,0x18,0x00, 0x00,0x00,0x7E,0x00,0x7E,0x00,0x00,0x00, 0x60,0x30,0x18,0x0C,0x18,0x30,0x60,0x00, 0x3C,0x66,0x0C,0x18,0x18,0x00,0x18,0x00, 0x7C,0xC6,0xDE,0xDE,0xDC,0xC0,0x7C,0x00, 0x30,0x78,0xCC,0xCC,0xFC,0xCC,0xCC,0x00, 0xFC,0x66,0x66,0x7C,0x66,0x66,0xFC,0x00, 0x3C,0x66,0xC0,0xC0,0xC0,0x66,0x3C,0x00, 0xF8,0x6C,0x66,0x66,0x66,0x6C,0xF8,0x00, 0xFE,0x62,0x68,0x78,0x68,0x62,0xFE,0x00, 0xFE,0x62,0x68,0x78,0x68,0x60,0xF0,0x00, 0x3C,0x66,0xC0,0xC0,0xCE,0x66,0x3A,0x00, 0xCC,0xCC,0xCC,0xFC,0xCC,0xCC,0xCC,0x00, 0x78,0x30,0x30,0x30,0x30,0x30,0x78,0x00, 0x1E,0x0C,0x0C,0x0C,0xCC,0xCC,0x78,0x00, 0xE6,0x66,0x6C,0x78,0x6C,0x66,0xE6,0x00, 0xF0,0x60,0x60,0x60,0x62,0x66,0xFE,0x00, 0xC6,0xEE,0xFE,0xFE,0xD6,0xC6,0xC6,0x00, 0xC6,0xE6,0xF6,0xDE,0xCE,0xC6,0xC6,0x00, 0x38,0x6C,0xC6,0xC6,0xC6,0x6C,0x38,0x00, 0xFC,0x66,0x66,0x7C,0x60,0x60,0xF0,0x00, 0x7C,0xC6,0xC6,0xC6,0xD6,0x7C,0x0E,0x00, 0xFC,0x66,0x66,0x7C,0x6C,0x66,0xE6,0x00, 0x7C,0xC6,0xE0,0x78,0x0E,0xC6,0x7C,0x00, 0xFC,0xB4,0x30,0x30,0x30,0x30,0x78,0x00, 0xCC,0xCC,0xCC,0xCC,0xCC,0xCC,0xFC,0x00, 0xCC,0xCC,0xCC,0xCC,0xCC,0x78,0x30,0x00, 0xC6,0xC6,0xC6,0xC6,0xD6,0xFE,0x6C,0x00, 0xC6,0xC6,0x6C,0x38,0x6C,0xC6,0xC6,0x00, 0xCC,0xCC,0xCC,0x78,0x30,0x30,0x78,0x00, 0xFE,0xC6,0x8C,0x18,0x32,0x66,0xFE,0x00, 0x78,0x60,0x60,0x60,0x60,0x60,0x78,0x00, 0xC0,0x60,0x30,0x18,0x0C,0x06,0x02,0x00, 0x78,0x18,0x18,0x18,0x18,0x18,0x78,0x00, 0x10,0x38,0x6C,0xC6,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF, 0x30,0x30,0x18,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x78,0x0C,0x7C,0xCC,0x76,0x00, 0xE0,0x60,0x60,0x7C,0x66,0x66,0xDC,0x00, 0x00,0x00,0x78,0xCC,0xC0,0xCC,0x78,0x00, 0x1C,0x0C,0x0C,0x7C,0xCC,0xCC,0x76,0x00, 0x00,0x00,0x78,0xCC,0xFC,0xC0,0x78,0x00, 0x38,0x6C,0x64,0xF0,0x60,0x60,0xF0,0x00, 0x00,0x00,0x76,0xCC,0xCC,0x7C,0x0C,0xF8, 0xE0,0x60,0x6C,0x76,0x66,0x66,0xE6,0x00, 0x30,0x00,0x70,0x30,0x30,0x30,0x78,0x00, 0x0C,0x00,0x1C,0x0C,0x0C,0xCC,0xCC,0x78, 0xE0,0x60,0x66,0x6C,0x78,0x6C,0xE6,0x00, 0x70,0x30,0x30,0x30,0x30,0x30,0x78,0x00, 0x00,0x00,0xCC,0xFE,0xFE,0xD6,0xD6,0x00, 0x00,0x00,0xB8,0xCC,0xCC,0xCC,0xCC,0x00, 0x00,0x00,0x78,0xCC,0xCC,0xCC,0x78,0x00, 0x00,0x00,0xDC,0x66,0x66,0x7C,0x60,0xF0, 0x00,0x00,0x76,0xCC,0xCC,0x7C,0x0C,0x1E, 0x00,0x00,0xDC,0x76,0x62,0x60,0xF0,0x00, 0x00,0x00,0x7C,0xC0,0x70,0x1C,0xF8,0x00, 0x10,0x30,0xFC,0x30,0x30,0x34,0x18,0x00, 0x00,0x00,0xCC,0xCC,0xCC,0xCC,0x76,0x00, 0x00,0x00,0xCC,0xCC,0xCC,0x78,0x30,0x00, 0x00,0x00,0xC6,0xC6,0xD6,0xFE,0x6C,0x00, 0x00,0x00,0xC6,0x6C,0x38,0x6C,0xC6,0x00, 0x00,0x00,0xCC,0xCC,0xCC,0x7C,0x0C,0xF8, 0x00,0x00,0xFC,0x98,0x30,0x64,0xFC,0x00, 0x1C,0x30,0x30,0xE0,0x30,0x30,0x1C,0x00, 0x18,0x18,0x18,0x00,0x18,0x18,0x18,0x00, 0xE0,0x30,0x30,0x1C,0x30,0x30,0xE0,0x00, 0x76,0xDC,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x10,0x38,0x6C,0xC6,0xC6,0xFE,0x00 ]; /** * Renders a letter to the image. * @param charCode Letter character code * @param baseRow Base row of image * @param baseCol Base column of image * @param imageData Image data */ public static function pcRenderLetter(charCode:Int, baseRow:Int, baseCol:Int, imageData:ImageData) { // Calculate character index in font character array // Shifting by 3 to the left is equal to multiplying by 2^3 var baseIndex = charCode << 3; // Iterate over character raster rows for (curRow in 0...8) { // Get character raster row bits var rowBits = Font[baseIndex + curRow]; // Iterate over character raster row bits (columns) for (curCol in 0...8) { // Calculate position of pixel on image var x = baseCol+curCol + 2; var y = baseRow+curRow + 2; // Calculate pixel index in image data array var index = (y * 320 + x) * 4; // Determine pixel color based on current bit value var channel = (((rowBits << curCol) & 0x80) == 0x80)? 255 : 0; // Set pixel imageData.data[index + 0] = channel; imageData.data[index + 1] = channel; imageData.data[index + 2] = channel; imageData.data[index + 3] = 255; } } } /** * Main function, the application's entry point. */ public static function main() { // Create canvas element var canvas = js.Browser.document.createCanvasElement(); canvas.width = 320; canvas.height = 200; canvas.style.background = "#000"; // Get canvas' 2D rendering context var ctx:CanvasRenderingContext2D = canvas.getContext2d(); // Set message lines to be drawn to canvas var msgLines = [ 'echo " _ _ _ __ _____ "', 'echo " | || | /_\\ \\ \\/ / __|"', 'echo " | __ |/ _ \\ > <| __|"', 'echo " |_||_/_/ \\_\\/_/\\_\\___|"', 'echo " "', ]; // Get image data from rendering context var imageData = ctx.getImageData(0,0, canvas.width, canvas.height); // Set delay before initiating text rendering var delay = 0; // Iterate over all message lines for(max in 0...msgLines.length) { // Render line with a delay haxe.Timer.delay(function() { // Clear canvas ctx.clearRect(0,0,canvas.width,canvas.height); // Iterate over message lines that were drawn up to now // We're redrawing them because we're clearing the canvas for (j in 0...max+1) { // Get the message line string var msgStr = msgLines[j]; // Iterate over message line characters for (i in 0...msgStr.length) { // Get character code var c = msgStr.charCodeAt(i); // Render letter pcRenderLetter(c, (j>>1) + (j<<3), (i>>1) + (i<<3), imageData); } } // Draw to canvas ctx.putImageData(imageData, 0,0); }, delay += 650); } // Append canvas to document body Browser.document.body.appendChild(canvas); } }
Result
Read more about VGA fonts here.