Click here to Skip to main content
13,251,998 members (49,931 online)
Click here to Skip to main content
Add your own
alternative version

Stats

15.5K views
460 downloads
23 bookmarked
Posted 27 Jan 2016

Command Console in your browser via HTML5 Canvas & JavaScript

, 28 Apr 2016
Rate this:
Please Sign up or sign in to vote.
Introduction to HTML5 Canvas which shows you how to create a command line console in your browser.

Introduction

Update Note 2016-04-28: I fixed the problem with handling the backspace and enter keys. For some time it was only FireFox which didn't work, but then Chrome stopped working also.  It's all fixed now. You can see the code change in the last code sample.

I have been learning about the power of the HTML5 Canvas element and what you can do.  I wanted to see if I could duplicate the command-line console in the browser which I could later use for fun and pranks.  Here is how I did it.

In the image below, I've taken a snapshot of my Console in the Browser (it's the one in the back) which includes a snapshot of a real Console window (for comparison).  I was able to capture the image showing both cursors even though the cursors blink -- to show users the console window is ready for input.  My version blinks the cursor also.  

See Live Demo

You can see it live in your browser at: http://raddev.us/console/console.htm (opens in new tab/window)

Console In Browser compared with real console window

Background

The HTML5 Canvas element is quite powerful and easy to get started with, but there is still a lot of Flash out there (unfortunately).  I think this small example of how simple it is to get started with the Canvas element will inspire you to start doing your own projects.  

A great book to go further with is HTML5 Canvas by Steve Fulton and Jeff Fulton (O'Reilly pub) - amazon link (opens in new window/tab).  That is a great book which will lead you into the technology and take you very far.  Written well and has the details you need.

Setting Up Our HTML

The first step is to set up our HTML for HTML5 and Canvas work -- the bulk of the work is actually done in JavaScript (as you probably expected).  Here's the entire listing of the simple the HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>console</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<canvas id="gamescreen">You're browser does not support HTML5.</canvas>
<script src="js/console.js"></script>

</body>
</html>

The first line contains the expected DOCTYPE definition.  This is a message to the browser that it should be expecting what follows to be HTML.

I add the natural language tag, since it will be in English.

Link CSS 

The next thing to notice is that I place link to a stylesheet which I simply name main.css in a subfolder named css.

I like to keep everything separated out as it should be for good clean code.  Even in a short program like this it will be important since it makes it easier to organize everything.

Here's the extremely simple CSS:

/* main.css */
body,html {margin:0;padding:0;}

This CSS does nothing more than remove all the padding and margin distance that a browser automatically assigns.  I don't want any space between my Canvas element and the browser's inner client area if possible.

Finally, there are only two more lines that even matter in the HTML.

Setup Canvas Element

The first one sets up the Canvas element and gives it an id of gamescreen.  That allows me to reference this element from my JavaScript, as we will soon see.

<canvas id="gamescreen">You're browser does not support HTML5.</canvas>

The text between the begin and end Canvas element is only shown if the user's browser doesn't support HTML5. It's a simple way to let them know that the application isn't going to work in their browser.

Next, I include a reference to the JavaScript which will drive the entire program.

<script src="js/console.js"></script>

Again, I place my JavaScript in a subfolder named js to keep things organized.  

Why Is JavaScript Inclusion, After Everything?

It is very important that the JavaScript be loaded after the canvas element, because the code in the JavaScript will reference the Canvas element.  There are other ways to solve this : using jQuery's ready() method or the OnLoad() event of the page.  But, in an effort to keep this an introductory article I've opted to simply add the JavaScript after the Canvas element so I can assure the JavaScript is loaded after the Canvas element.  

Basic Summary of Events

Now that we are set up our little app will behave in the following way:

1. the page will load

2. the css will load

3. the JavaScript will load and take some actions.

Knowing the basic operation of your code is the first and most powerful step to controlling how your code works.  Now, let's jump in and examine the actual code that does the work.  

All of the code can be found in the console.js file.

JavaScript Powers the Canvas Element

The entire console.js is over 220 lines of code -- not bad but quite long to look at all at once -- so I'll break it down and we'll talk about the code in sections. Then, you can download the code and examine the entire listing yourself.  

Application Initialization

First let's look at the basic items I need initialized and why I need them to be set up before the app starts drawing on the Canvas.

//consle.js

var ctx = null;
var theCanvas = null;
var lineHeight = 20;

var widthOffset = 2;
var cursorWidth = 8;
var cursorHeight = 3;
var fontColor = "#C0C0C0";
var outputFont = '12pt Consolas';
var charWidth;

var allUserCmds = [ ]; // array of strings to hold the commands user types
var currentCmd = ""; // string to hold current cmd user is typing

var PROMPT = "c:\\>";
var promptWidth = null;
var promptPad = 3;
var leftWindowMargin = 2;
var cursor = null;
window.addEventListener("load", initApp);
var flashCounter = 1;

How JavaScript Runs

One of the first things to understand is how JavaScript runs.  As you can see, the console.js has a bunch of initialization going on at the top of the file.  But, does that code run? The answer is that once the browser loads the JavaScript file, then any code that is outside of a function runs in a top down order.  That means that the code shown above -- not wrapped in a separate function does run from top to bottom.  

Global Variables: Run & Scream

This is the lazy-coder's way of initializing some items we are going to need.  In a larger application you do not want to do this because those variables will be global variables and should send you running and screaming since anyone could change their values from other modules and totally corrupt your program.  

Global Variables are So Easy : Antipattern Defined

This is a great time to talk about the fact that because creating global variables is so easy in JavaScript it often gets copied by inexperienced developers without them even knowing.  This creates a pattern for the way that JavaScript is often created which is actually an antipattern.  An antipattern is something that becomes extremely prevalent but is still wrong.  If you'll learn that it is wrong to do this from this example then you'll be far ahead of the other many other developers.

Now, let's look at how my console uses those variables.

Canvas Context Object

The first and most important variable in this sample is the one named ctx.

You can see that I just initialize that objec to null.  It's just so I have a named object that I can initialize and use later.

Skip ahead down to the last line which looks like:

window.addEventListener("load", initApp);

That's the standard way to add an EventListener using pure JavaScript.  This tells the browser that anytime the load event (first parameter) fires, then call the method we've defined named initApp.

Notice that the first parameter is a string (surrouded by double-quotes) but the last one is an object (no double-quotes) which is the function named initApp.  If you read that closely, you've just learned that a JavaScript function is a first-class object.  That just means that functions in JavaScript are objects and can be referenced easily using their names.

Who Fires the Load Event?

The load event is fired by the browser itself whenever a document is done loading.  We are telling the browser to notify us when the document is done loading.  There are many other events with other names that we could've supplied as the first parameter.  Also, keep in mind if we had used the word "loaded" or "loads" we'd get an error because the JavaScript interpreter does not know those events.  Events are predefined by JavaScript creators. You can google JavaScript events to find more.

Okay, so when the document loads, I want to be notified and I want my initApp() method to be called.

So, after all of the variables get initialized, I register the Load event which will insure my initApp method is called.

Let's jump down the initApp() function now so we can see what work it does.  

Close Look at initApp() Function

The entire code of the initApp function looks like the following:

function initApp()
{
    theCanvas = document.getElementById("gamescreen");
    ctx = theCanvas.getContext("2d");
    ctx.font = outputFont;
    var metrics = ctx.measureText("W");
    // rounded to nearest int
    charWidth = Math.ceil(metrics.width);
    promptWidth = charWidth * PROMPT.length + promptPad;
    cursor = new appCursor({x:promptWidth,y:lineHeight,width:cursorWidth,height:cursorHeight});

    window.addEventListener("resize", draw);
    window.addEventListener("keydown",keyDownHandler);
    window.addEventListener("keypress",showKey);
    initViewArea();
    setInterval(flashCursor,300);
    function appCursor (cursor){
        this.x = cursor.x;
        this.y = cursor.y;
        this.width = cursor.width;
        this.height = cursor.height;
    }
}

The first thing you can see is that I call a standard document method called getElementById() and pass in the id that we previously defined in the HTML.  This represents the Canvas element in the HTML.

Once we have that element we can call an HTML5 specific method which all browsers which support HTML5 will have defined for us: getContext("2d").

When we call that method, it will return a graphics context object which we will store in our ctx variable, so we can draw on the Canvas.  Every HTML5 app which wants to draw on the Canvas will have a call similar to this one.  This one tells it that we want to do 2d drawing by providing that string as a parameter.  

Leveraging the Power of the Context Object

The entire idea of loading that object is that the HTML5 developers and Browser developers have included a library functionality which we can now use.  Since we've obtained a context object, we are now able to call methods that are already defined for us.  Calling them is as easy as:

1. knowing the functions' names

2. knowing the parameters they expect (if any)

To learn more about the Context object I simply googled: HTML5 context object.

One of the first links was:

https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D^

That's a great Mozilla (Firefox org) developer's site which has a lot of information.

Width of Each Letter Drawn On Canvas

One of the things that I know I am going to need to know -- to handle the text that is drawn on the Canvas -- is the width of each letter that appears.

There is no easy way to get that value.  After a lot of reading I fell upon what I consider to be the best way

1. set the font family and size (outputFont) so that when we measure the width of the character it is based upon the font that will be output.  We call the Canvas context property aptly named, font :

ctx.font = outputFont;

2. take the widest letter in my font set : the letter W will do.

3. call the Context method measureText() to get its width

4. round that value up to the nearest integer and store it for later use

After that I have the basic width of every character which will appear in the Console Window.

Output Font

We've also set the font-family and font size which will be used when we draw the text (more on this later) to the Canvas.  In our case I learned that the font for the Console window is usually Consolas and I've chose 12pt.

Font Color

Of course, we also want the font to be the same color as the Windows console so I did some experimenting and found that it is the following value used in the initialization we previously saw:

var fontColor = "#C0C0C0";

A Lot of Work

That's a lot of work!  Yes, padawan, with great power comes much responsibility.  Note: This was a good-bad mixture of a quote from Spider-man and a reference to Star Wars. I am UberGeek!!  Try not to be jealous.  :)

******* SideBar: Mobile Problems *****************

At this point I loaded the example on my Android Pad and learned there are a couple of problems when this little app loads.

1. The font is extremely small.

2. You cannot type because the Canvas element doesn't initiate the mobile keyboard.

We can fix both of these problems and I may do so in a later update to the article, but for now, please understand that this is a limitation and this application is _only_ built to work in your desktop browser for now.

Okay, now let's really move through the code.

The next thing we need to examine is the appCursor function / object.

appCursor Function

The appCursor function is declared in the initial code, right before our  looks like the following:

function appCursor (cursor)
{
    this.x = cursor.x;
    this.y = cursor.y;
    this.width = cursor.width;
    this.height = cursor.height;
}

This is very simple, but let's break it down.

I've named this function appCursor so any dev users will instantly understand its purpose.  This function will create an object for us in JavaScript and uses a powerful initialization convention.

All you have to do to create a new appCursor object is call it and send in a cursor which has the values you want it to have.  

Now, the nice thing is that you can easily use JSON (JavaScript Object Notation) to create your object on the fly, send it in and get an initialized object that represents the apps cursor (blinking underscore).

When I call this function the one and only time in the console.js it looks like the following:

cursor = new appCursor({x:promptWidth,y:lineHeight,width:cursorWidth,height:cursorHeight});

If you've never seen JSON or you've only worked with it a little it may look odd to you.

JSON is simply one or more name value pairs separated by commas where the name and value are separated by colons.

Here are two simple JSON examples:

{name:value}

{color:green}

Since the open/close brackets { } are object initializers in JavaScript, you are actually creating an object which has a propery with name that has it's value set to value.

Here's another example:

var myFont = {color:green};

Now you could use the following syntax on that object to get it's value for color:

console.log(myFont.color);

Then the output would be : green

Register for DOM Events

The next thing we do in the initApp Function is set up some DOM events.

window.addEventListener("resize", draw);
window.addEventListener("keydown",keyDownHandler);
window.addEventListener("keypress",showKey);

addEventListener is the pure JavaScript method which registers your function as a callback when the browser event fires.

The first parameter is the JavaScript defined event (resize, keydown, keypress) as a string.  The second parameter is the name of your function which should be called, but notice it is not a string.  It is the function reference -- since functions in JavaScript are first-class objects.

Keypress and Keydown

You can see that I've registered to do some special work when the browser window is resized, when a keydown occurs and when a keypress occurs.  Yes, those last two are slightly different.

Since there is a bit more that happens during initApp, we'll come back and show you exactly what each of these functions do after we complete going over the code in initApp.

The next thing that occurs is initViewArea() is called:

function initViewArea() {
    
    
    // the -5 in the two following lines makes the canvas area, just slightly smaller
    // than the entire window.  this helps so the scrollbars do not appear.
    ctx.canvas.width  =  window.innerWidth-5;
    ctx.canvas.height = window.innerHeight-5;
    
    ctx.fillStyle = "#000000";
    ctx.fillRect(0,0,ctx.canvas.width, ctx.canvas.height);
    
    ctx.font = outputFont;
    ctx.fillStyle = fontColor;
    var textOut = PROMPT;

    ctx.fillText  (textOut,leftWindowMargin, cursor.y);
    draw();
}

The first thing we do is initialize the canvas width and height so it takes up the entire window, except for a a small border of 5 pixels wide.

After that we set the fillStyle so it is black and call fillRect that fills the entire area with the black.

That is the background of our console window.

The Font Is Drawn in the Canvas

Finally, I set the font to that outputFont we defined earlier.  I looked up the Windows console font and I use that so that the fake console window will look like the real thing.

Next, I have to draw the text of the prompt (represented by c:\> ).  Keep in mind that the canvas element requires everything to be drawn.  We use the Canvas context function called fillText to draw the text.

draw() Function : Main Loop

And finally, we call the draw() function.  This is the same functon which will run if the user resizes the window.

Every time the window is changed it must be drawn.  This draw() function ends up being somewhat of a main loop.

function draw()
{
    ctx.canvas.width  = window.innerWidth-5;
    ctx.canvas.height = window.innerHeight-5;
    
    ctx.fillStyle = "#000000";
    ctx.fillRect(0,0,ctx.canvas.width, ctx.canvas.height);
    ctx.font = outputFont;
    ctx.fillStyle = fontColor;
    
    for (var i=0;i<allUserCmds.length;i++)
    {
        drawPrompt(i+1);
        if (i == 0)
        {
            xVal = promptWidth;
        }
        else
        {
            xVal = promptWidth-charWidth;
        }
            
        ctx.font = outputFont;
        ctx.fillStyle = fontColor;
        for (var letterCount = 0; letterCount < allUserCmds[i].length;letterCount++)
        {
            ctx.fillText(allUserCmds[i][letterCount], xVal, lineHeight * (i+1));
            xVal+=charWidth;
        }
    }
    if (currentCmd != "")
    {
        drawPrompt(Math.ceil(cursor.y/lineHeight));
        ctx.font = outputFont;
        ctx.fillStyle = fontColor;
        xVal = promptWidth-charWidth;
        for (var letterCount = 0; letterCount < currentCmd.length;letterCount++)
        {
            ctx.fillText(currentCmd[letterCount], xVal, cursor.y);
            xVal += charWidth;
        }
    }
    else
    {
        drawPrompt(Math.ceil(cursor.y/lineHeight));
    }
}

Two Things to Focus On In draw() Function

The interesting code here is the work I have to do to calculate the width of each letter -- so that I can move the cursor far enough to the right.  And, if the user backspaces I need to be able to draw over (blot out) that character and move the cursor back to the correct place.  It's a lot of work and made my respect grow for the real devs who created the console window. :)

A List of Typed Commands

Also, notice that I keep the list of typed commands in an array and then I have to redraw them at any time the windonw is resized or changed.  It's a lot of work but it works fairly well.  The bolded loop of code shows me iterating through that list to draw them on the screen.

Flashing the Cursor, Like the Real Console

This is one of the parts that makes this feel extremely real, I think.  In the initApp I set up a function to run on a specified interval using the JavaScript method setInterval.

The set up looks like:

setInterval(flashCursor,300);

This setups up a callback method -- in this case my function called flashCursor -- which will run every 300ms (0.3 seconds).

flashCursor is a very simple method which looks like :

function flashCursor(){
    
    var flag = flashCounter % 3;

    switch (flag)
    {
        case 1 :
        case 2 :
        {
            ctx.fillStyle = fontColor;
            ctx.fillRect(cursor.x,cursor.y,cursor.width, cursor.height);
            flashCounter++;
            break;
        }
        default:
        {
            ctx.fillStyle = "#000000";
            ctx.fillRect(cursor.x,cursor.y,cursor.width, cursor.height);
            flashCounter= 1;
        }
    }
}

I do a modulo division to generate a value between 1 and 3 and I simply switch on that flag.  This allows me to change the color of the cursor so it is at times black and doesn't appear and then other times it is the normal font color.  It all creates the illusion that the cursor is blinking on and off.

Finally, we'll take a look at the showKey and keydownHandler methods which I wrote and you'll have everything you need to know to use the code for yourself.

function showKey(e){
    blotOutCursor();

    ctx.font = outputFont;
    ctx.fillStyle = fontColor;

    ctx.fillText  (String.fromCharCode(e.charCode),cursor.x, cursor.y);
    cursor.x += charWidth;
    currentCmd += String.fromCharCode(e.charCode);
}

The showKey occurs when the key is pressed by the user.  I get the charCode and draw it using fillText so it shows up on the screen.  But then I have to manage where the cursor is on the screen now too, because it wil be further to the right now.

Why Use KeyDown?

KeyDown allows me to capture non-printable characters such as the backspace key, the enter key, etc.

For each of those I have to do some calculations and then draw the text and cursor in the new correct location.

function keyDownHandler(e){
    
    var currentKey = null;
    if (e.code !== undefined)
    {
        currentKey = e.code;
        console.log("e.code : " + e.code);
    }
    else
    {
        currentKey = e.keyCode;
        console.log("e.keyCode : " + e.keyCode);
    }
    console.log(currentKey);
    // handle backspace key
    if((currentKey === 8 || currentKey === 'Backspace') && document.activeElement !== 'text') {
            e.preventDefault();
            // promptWidth is the beginning of the line with the c:\>
            if (cursor.x > promptWidth)
            {
                blotPrevChar();
                if (currentCmd.length > 0)
                {
                    currentCmd = currentCmd.slice(0,-1);
                }
            }
    }
    // handle <ENTER> key
    if (currentKey == 13 || currentKey == 'Enter')
    {
        blotOutCursor();
        drawNewLine();
        cursor.x=promptWidth-charWidth;
        cursor.y+=lineHeight;
        if (currentCmd.length > 0)
        {
            allUserCmds.push(currentCmd);
            currentCmd = "";
        }
    }
}

Amazing Amount of Work For Something So Simple

Isn't it an amazing amount of work for something that seems to be so simple?  

Limitations : No Scrolling

I didn't implement scrolling so if you type so many commands that they go off screen I haven't handled that.  I'leave that to you. 

Yours For Extending

I created this silly app for fun and learning.  It can be easily extended to do much more.  Next, you could do some funny output when users type specific commands.  It could be a real hoot and it would trick a lot of people.

 

Using the Code

  1. Get the download
  2. unzip it
  3. drop it in a folder
  4. double-click the console.htm file and it will load in your default browser.

Note: Obviously, your browser has to support HTML5 and Canvas.

Have fun.

History

updated aricle (last code listing and code to fix problems handling backspace and enter kesy:  04-28-2016

Article initially published: 01-27-2015

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

raddevus
Software Developer (Senior) RADDev Publishing
United States United States
The CP editors went to Canada and all I got was this crummy sig line.
My web site, blog and other dev projects including C'YaPass : http://raddev.us^

You may also be interested in...

Pro
Pro

Comments and Discussions

 
GeneralMy vote of 5 Pin
Gun Gun Febrianza6-Jun-16 12:30
member Gun Gun Febrianza6-Jun-16 12:30 
GeneralRe: My vote of 5 Pin
raddevus6-Jun-16 14:32
professionalraddevus6-Jun-16 14:32 
QuestionGreat Work! Pin
eslipak3-May-16 12:17
professionaleslipak3-May-16 12:17 
QuestionWhat is it good for? Pin
Jan Zumwalt3-May-16 1:24
memberJan Zumwalt3-May-16 1:24 
AnswerRe: What is it good for? Pin
raddevus3-May-16 4:24
professionalraddevus3-May-16 4:24 
GeneralRe: What is it good for? Pin
GerhardKreuzer3-May-16 7:26
memberGerhardKreuzer3-May-16 7:26 
GeneralRe: What is it good for? Pin
raddevus3-May-16 9:46
professionalraddevus3-May-16 9:46 
BugDoesn't work with Win 7 & firefox Pin
Jan Zumwalt1-Feb-16 18:47
memberJan Zumwalt1-Feb-16 18:47 
GeneralRe: Doesn't work with Win 7 & firefox Pin
raddevus2-Feb-16 2:58
memberraddevus2-Feb-16 2:58 
GeneralRe: Doesn't work with Win 7 & firefox Pin
lobotomy20-Mar-16 7:14
professionallobotomy20-Mar-16 7:14 
GeneralRe: Doesn't work with Win 7 & firefox Pin
raddevus20-Mar-16 10:24
memberraddevus20-Mar-16 10:24 
GeneralRe: Doesn't work with Win 7 & firefox Pin
lobotomy21-Mar-16 3:12
professionallobotomy21-Mar-16 3:12 
GeneralMy vote of 5 Pin
George Tourtsinakis28-Jan-16 0:58
memberGeorge Tourtsinakis28-Jan-16 0:58 
GeneralRe: My vote of 5 Pin
OriginalGriff28-Jan-16 1:04
protectorOriginalGriff28-Jan-16 1:04 
GeneralRe: My vote of 5 Pin
George Tourtsinakis28-Jan-16 2:09
memberGeorge Tourtsinakis28-Jan-16 2:09 
GeneralRe: My vote of 5 Pin
raddevus28-Jan-16 2:38
memberraddevus28-Jan-16 2:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.171114.1 | Last Updated 29 Apr 2016
Article Copyright 2016 by raddevus
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid