Haxe Code Cookbook
Haxe programming cookbookOtherVGA text renderer

VGA text renderer

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