Haxe Code Cookbook
Haxe programming cookbookOtherVGA text renderer

VGA text renderer

Reading time: 13 minutes

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.


Last modified:
Created:
Category:  Other