GraphicsBlast
The Basics | JavaScript | Windows | Command Line

Lesson 2: Becoming Interactive

Our interactive window!
Becoming Interactive: Let's listen for user input and switch to real-time rendering!

Now we have a WebGL canvas on our web page, in this lesson we're going to look at making it interactive. Previously, we rendered (well, cleared...) the canvas just once when the page had loaded. But real-time graphics requires us to render repeatedly, fast enough that the series of images drawn to the canvas look to us like fluid motion.

In this lesson, we'll see how this can be achieved in a browser, and as part of making our canvas interactive, we'll also see how some simple user-input handling is performed.

Drawing

To construct a program which renders repeatedly, we'll create a new function called mainLoop:

44.
function draw()
45.
{
46.
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
47.
}
48.
+ 49.
function mainLoop()
+ 50.
{
+ 51.
    draw();
+ 52.
    window.requestAnimationFrame(mainLoop);
+ 53.
}

The idea is that this function will be called from our initialise function, and then will continue to call itself over-and-over again. Any functions we need to do repeatedly can then be called from this function. For now, we're just going to call our draw function.

The last line of the function though is where the magic happens. The function requestAnimationFrame() informs the browser that before the next screen update, we want the function mainLoop to be called. Usually this is happening around 60 times per second, but varies by device.

Requesting an animation frame only requests a single frame though. The browser will make a single call to whichever function we pass it, and that's it. So by calling requestAnimationFrame from within our mainLoop function, we create a loop. Each time the browser wants to refresh the screen, it calls our code, which finishes by registering itself to be called on the next update.

Note that for performance reasons, browsers don't update the screen for tabs that are in the background. This would kill battery life and give the CPU far too much work to do. If the user changes to a different tab in the browser, our loop will therefore be temporarily suspended until the user comes back to our page. This ordinarily won't be an issue, but if you are doing some network communication for example from this callback, it might be a cause of some stuttering. It's just something that's helpful to be aware of, and might save you some frantic hair-pulling during debugging.

So now we have a function which will loop itself and keep drawing to our canvas. For the loop to start though, we mustn't forget to make the first call to it when we update our initialise function in a moment!

User Input

To capture user input for our canvas, we can register event listeners. When that particular action happens, such as pressing a key on the keyboard, the browser will call a function of our choosing, and pass in some additional information about what happened.

We're going to modify the end of our initialise function to bind some handler functions, handleKeyPressed and handleKeyReleased. These functions will then be called when the user presses or releases a key on their keyboard.

20.
    gl.clearColor(0.04, 0.23, 0.51, 1.0);
21.
+ 22.
    canvas.addEventListener("keydown", handleKeyPressed);
+ 23.
    canvas.addEventListener("keyup", handleKeyReleased);
+ 24.
+ 25.
    window.requestAnimationFrame(mainLoop);
26.
}

I'll just take a moment to remind you here that in JavaScript there are several ways to do add these event callbacks, but this is the recommended way. Using canvas.addEventListener means just that, another event listener is added. Alternative approaches like using canvas.onkeydown will overwrite all existing handlers, and are therefore considered bad practice.

Not only are there several ways to add the handlers here, but there are several things you can bind them to, depending on how you want your page to behave. Here I'm listening for key presses and releases on the canvas element on our page. The user might need to click on the canvas for it to become the active element on the page, at which point any time a key is pressed down or released, the handleKeyPressed or handleKeyReleased functions will be called.

Alternatively, you can bind them to the whole page by using document.addEventListener. Anywhere on the page, if a key is pressed, you will know about it. This can be useful in some circumstances, but can also be annoying to your users if they've scrolled way past your canvas but are still unknowingly interacting with it. It really depends on your usage.

Also, I remembered to put the call to mainLoop at the end of our initialise function, to start our drawing loop!

Now, let's define the handleKeyPressed and handleKeyReleased functions:

25.
    window.requestAnimationFrame(mainLoop);
26.
}
27.
+ 28.
function handleKeyPressed(event)
+ 29.
{
+ 30.
    console.log("Key pressed: " + event.key + " code: " + event.code);
+ 31.
+ 32.
    if(event.code == "Space")
+ 33.
    {
+ 34.
        //spacebar pressed
+ 35.
        console.log("Spacebar was pressed!")
+ 36.
    }
+ 37.
}
+ 38.
+ 39.
function handleKeyReleased(event)
+ 40.
{
+ 41.
    console.log("Key released: " + event.key + " code: " + event.code);
+ 42.
}
43.
44.
function draw()
45.
{

There is nothing particularly exciting going on here right now, but we can build on these functions later.

In either event, I'm simply writing to the console which key was pressed and released. The event.key property gives us a string representing the character of the key pressed, whereas event.code gives us one representing the key pressed. For example, if I hold shift and press the key for the digit one, the event.key will be an exclamation mark, while the event.code will just be the number one. Therefore you should consider whether you're more interested in the physical key on the keyboard or the semantic meaning when deciding which to use.

NOTE: Other widely-used properties such as event.keyCode are deprecated, as they don't handle international keyboards well, and may be removed at some point in the future.

One more thing to consider is that this choice isn't purely affecting international keyboards. It's not so common, but AZERTY keyboards do exist alongside the more common QWERTY keyboard layout. Graphical programs commonly use the WASD keys to move around, but if the user has a non-standard keyboard which has the W key elsewhere, checking the semantic key name might cause them great difficulties and awkward controls, while the event.code would be fine. Conversely, giving users instructions like "Press the 'f' key" should probably still represent the 'f' key on a non-standard keyboard. So it's tricky.

I've added a few extra lines here to demonstrate detecting if a specific key was pressed. If a user presses the spacebar, I display a message in the console. If you want to modify this to detect another key, the simplest way is to use the first console message to get the key in question's key or code property, and then just substitute this in here.

The event object also contains various other parameters which can be useful. Normally, when a key is held down, after around a second the key press event will rapidly repeat to allow users to quickly repeat characters. This can be detected using the event.repeat property, which will be true for every subsequent key press event. You can use this to block user's from holding down a key to perform some event repetitively. Likewise, event.shiftKey exists to tell you if shift was being held when the key was pressed, event.ctrlKey for the control key, and event.altKey for alt.

Anyway, with that we have a brief introduction to handling keyboard events, and we now have an interactive WebGL canvas! Remember to hard-refresh your browser if your have troubles seeing your keypresses appear in the console. Also remember that if you chose to bind you key handlers to the canvas rather than the whole page, you may need to actually click on the canvas before key presses will be registered.