mkweb

PDP-8 Emulator written in Javascipt with HTML5 Canvas based frontend.


const KEYBOARD_BUFSIZE = 3;     // # of chars
const PRINTER_BUFSIZE = 4096;   // # of chars

const KEYBOARD_BUFFER = [];
const PRINTER_BUFFER  = [];

class Teletype extends Phaser.Scene {
    
    TelePrinter = null;

    constructor () {
        super('teletype');
    }

    preload () {
        this.load.image('retrofont', 'assets/5x7.png');
    }

    create () {

        let background = this.add.graphics();

        background.fillStyle(0xffffff); // White background
        background.fillRect(0,0,1200,700);

        const stand = this.add.graphics();

        // Top of PDP-8
        stand.fillStyle(0x0);
        stand.fillTriangle(0,700, 600,0, 1200,700)
        
        // Stand 
        stand.fillGradientStyle(0xACA688, 0xcac29f, 0xACA688, 0xACA688); // light to dark beige top to bottom
        stand.fillTriangle(105, 650, 600, 0, 1095, 650);

        //Highlight
        stand.fillStyle(0xe8e3cb);
        stand.fillRoundedRect(100,648,1000,50,5);

        // Base
        stand.fillStyle(0xcac29f);
        stand.fillRoundedRect(100,650,1000,50,5);

        let backdrop = this.add.graphics();

        backdrop.fillGradientStyle(0x827e6d, 0x827e6d, 0xcac29f, 0xcac29f); // Dark to light beige top to bottom
        backdrop.fillRect(0,0,1200,600);


        // White hightlights in corners
        this.add.ellipse(65,50,150,25, 0xe8e3cb, 0.2).setAngle(35);    // Top left
        this.add.ellipse(1135,50,150,25, 0xe8e3cb, 0.2).setAngle(-35);    // Top right
        this.add.ellipse(65,550,150,25, 0xe8e3cb, 0.4).setAngle(-35);    // Bottom left
        this.add.ellipse(1135,550,150,25, 0xe8e3cb, 0.4).setAngle(35);    // Bottom right
        

        let screen = this.add.graphics();

        screen.fillStyle(0x0);   // Black        
        screen.fillRoundedRect(95,70,1010,460,20); // Base screen
        screen.fillEllipse(96,300,20,429);   // Left side bulge
        screen.fillEllipse(1104,300,20,429);   // Right side bulge
        screen.fillEllipse(600,70,980,20);  // Top Bulge
        screen.fillEllipse(600,530,980,20);  // Bottom Bulge

        screen.fillStyle(0x6f7374);   // Grey
        screen.fillRoundedRect(100,75,1000,450,20); // Base screen
        screen.fillEllipse(101,300,20,419);   // Left side bulge
        screen.fillEllipse(1099,300,20,419);   // Right side bulge
        screen.fillEllipse(600,75,970,20);  // Top Bulge
        screen.fillEllipse(600,525,970,20);  // Bottom Bulge


        let bezel = this.add.graphics();
        
        // Rounded bezel outside
        bezel.lineStyle(20,0xffffff); // 20 px width in white
        bezel.strokeRect(10, 10, 1180, 580); // background

        bezel.lineStyle(20, 0xcac29f); // 20px stroke width in light beige
        bezel.strokeRoundedRect(10, 10, 1180, 580, {tl:32,tr:32, bl:1,br:1});    // Outer bezel radius
        bezel.strokeRoundedRect(10, 10, 1180, 580, 32);    // Inner bezel radius

        bezel.lineStyle(5, 0xe8e3cb, 0.5);   // 5px stroke width in highlight color 50% opacity
        bezel.strokeRoundedRect(20, 20, 1160, 560, 22);

        
        const fcfg = {
            image: 'retrofont',
            width: 6,
            height: 8,
            chars: Phaser.GameObjects.RetroFont.TEXT_SET1,
            charsPerRow: 18,
            offset: { x: 1, y: 1 },
            spacing: { x: 1, y: 1 },
            lineSpacing: 1
        };
        const retrofontConfig = Phaser.GameObjects.RetroFont.Parse(this, fcfg);
        
        this.cache.bitmapFont.add('retrofont', retrofontConfig);
        
        this.TelePrinter = this.add.bitmapText(120, 86, 'retrofont', '');
        
        this.TelePrinter.setScale(2);
        
        /// Keyboard Events ///
        
        this.input.keyboard.on('keydown', event =>
        {
            if (event.key == ' ') {
                // Prevent space from scrolling down screen (default behavior)
                event.preventDefault();
            }

            if (KEYBOARD_BUFFER.length >= KEYBOARD_BUFSIZE) {
                // Keyboard buffer is full
                // We should beep or something

            } else if (event.key.length == 1) {
                let charCode = event.key.charCodeAt(0);
                let keypress = false;   // true if we recorded a keypress
                if (event.ctrlKey) {    // If ctrl key is down it eats all keystrokes.
                    if (event.key == ' ') {
                        // Ctrl + Space executes single instruction cycle
                        COMPUTER.cycle();
                    }
                } else if (charCode >= 32 && charCode <= 126) { // Printable ascii characters
                    // Add character to keyboard buffer
                    KEYBOARD_BUFFER.push(charCode);
                    keypress = true;
                }

            } else if (event.key == "Enter") {
                if (event.ctrlKey) {
                    // Ctrl + Enter starts the computer
                    COMPUTER.start();
                } else {
                    // Enter emits a CR
                    KEYBOARD_BUFFER.push("\r".charCodeAt(0));
                }

            } else if (event.key == "Backspace") {
                if (event.ctrlKey) {
                    // Ctrl + Backspace halts computer
                    COMPUTER.halt();
                } else {
                    // Backspace emits a ASCII BS (8)
                    KEYBOARD_BUFFER.push("\b".charCodeAt(0));
                }

            } else if (event.key == "Escape") {
                if (event.ctrlKey) {
                    // Ctrl + Esc resets computer
                    // Power cycle should halt if not, clear registers, and load reset vector
                    // Also clear teletype screen on reset
                    COMPUTER.off();
                    this.TelePrinter.text = '';
                    COMPUTER.on();
                }

            } else if (event.key == 'Delete') {
                // Ctrl + Delete clears teletype screen
                if (event.ctrlKey) {
                    this.TelePrinter.text = '';
                }
            }
        });


        /// Pointer Events ///

        this.input.on('pointerdown', function () {
            document.getElementById("keyboardtrigger").focus();
        });
    }

    update () {
        let bufLen = PRINTER_BUFFER.length;
        if (bufLen > 0) {
            // Display characters in printer buffer
            for (var i = 0; i < bufLen; i++) {
                // Get length of last line
                let lineLength = this.TelePrinter.text.length - this.TelePrinter.text.lastIndexOf("\n") - 1;
                // Next character to print
                let nextChar = PRINTER_BUFFER.shift();

                // Wrap lines @ 80 characters
                if (lineLength >= 80 && nextChar != '\n') {
                    this.TelePrinter.text += '\n';
                    lineLength = 0;
                }

                // Scroll display @ 24 lines
                let lines = this.TelePrinter.text.split("\n");
                if (lines.length > 24) {
                    lines.shift();
                    this.TelePrinter.setText(lines);
                }

                // Add character to line
                this.TelePrinter.text += nextChar;
                // lineLength++;
            }

        }
    }
}