Moving Shapes on the Canvas Using the Keyboard

by kirupa   |   9 March 2016

On the canvas, one of the most common things you'll do with the keyboard is use the arrow keys to move something around. For example, click on the following triangle and click on any of your arrow keys:

Notice that the triangle moves in the direction of whichever arrow key you pressed.

In this tutorial, you will learn how to create this example. You will learn all about how to listen for key presses and move something around the screen as a reaction to whichever key was pressed. Of course, since nothing is ever as straightforward as it seems, we'll touch upon a bunch of other relevant topics along the way :P

Onwards!

OMG! A Canvas Book Written by Kirupa?!!

To kick your canvas skills up a few notches, everything you see here and more (with all its casual clarity!) is available in both paperback and digital editions.

BUY ON AMAZON

The Basic Approach

By now, you've probably seen how to work with the keyboard. You may even have seen the approach we use for having something we draw follow the mouse cursor around. The way we move things using the keyboard combines a little bit of what you've seen in those two tutorials. If you haven't already seen what is in those two tutorials, don't worry. You aren't missing that much, and we'll review the interesting parts again over here :P

Starting at the very top, we have something we've drawn on our canvas:

That something for our example is a triangle. This triangle is made up of three points that are represented by a horizontal and vertical number:

These numbers are the only things that ensure our triangle looks they way it does. More importantly, for what we are trying to do, these numbers are the only things that help specify where our triangle is actually positioned.

At this point, our task should be getting a bit clearer. Because we can adjust where the triangle appears by just fiddling around with a couple of numbers, our job is to figure out how to have each arrow key press be responsible for that fiddling. What we are going to do in the next couple of sections is put all of these English words together into some totally rad JavaScript that works...more or less!

Displaying Our Triangle

Let's start easy by first drawing our triangle. The way we are going to do that is by defining a function called drawTriangle that draws a triangle at a fixed position in our canvas.

Using our usual example where we have a canvas element defined with an id value of myCanvas, ensure the contents of your script tag look as follows:

var canvas = document.querySelector("#myCanvas");
var context = canvas.getContext("2d");

function drawTriangle() {
  // the triangle
  context.beginPath();
  context.moveTo(200, 100);
  context.lineTo(170, 150);
  context.lineTo(230, 150);
  context.closePath();

  // the outline
  context.lineWidth = 10;
  context.strokeStyle = "rgba(102, 102, 102, 1)";
  context.stroke();

  // the fill color
  context.fillStyle = "rgba(255, 204, 0, 1)";
  context.fill();
}
drawTriangle();

Once you have added this code to your document, go ahead and preview your document in your browser. If everything worked out properly, you'll see a yellow triangle displayed. There is nothing exciting going on with this code that you haven't seen before, but there is one thing I want to call out. Notice that our triangle is defined by the following X and Y values:

context.moveTo(200, 100);
context.lineTo(170, 150);
context.lineTo(230, 150);

It is these values that we'll eventually end up adjusting to accommodate our arrow key presses.

Dealing With the Keyboard

With our triangle drawn, our next job is to deal with the keyboard. This involves the following steps:

  1. Listening for the events your keyboard fires
  2. Inside the event handler, accessing the KeyboardEvent's keyCode property.
  3. Handling the cases when the left, right, up, and down arrow keys are pressed.

There are several ways of doing this, but we are going to use a familiar (but less-than-ideal approach). Go ahead and add the following lines of code just above where you defined your drawTriangle function:

window.addEventListener("keydown", moveSomething, false);
 
function moveSomething(e) {
    switch(e.keyCode) {
        case 37:
            // left key pressed
            break;
        case 38:
            // up key pressed
            break;
        case 39:
            // right key pressed
            break;
        case 40:
            // down key pressed
            break;  
    }   
}		

With the code we have just added, we first listen for a key press by listening for the keydown event. When that event gets overheard, we call the moveSomething event handler that deals with each arrow key press. It does this dealing by matching the keyCode property with the appropriate key value each arrow key is known by.

Adjusting the Position

It is time to tie together the triangle we've drawn with the code we just added for dealing with the arrow keys. What we are going to do is define two counter variables called deltaX and deltaY. What these variables will do is keep a count of how far to move our triangle as a result of arrow key presses. This may sound a bit confusing right now, but hang on tight!

First, let's go ahead and define our deltaX and deltaY variables and put them to use inside our moveSomething function. Add the following highlighted lines to your code:

var deltaX = 0;
var deltaY = 0;

function moveSomething(e) {
    switch(e.keyCode) {
        case 37:
            deltaX -= 2;
            break;
        case 38:
            deltaY -= 2;
            break;
        case 39:
            deltaX += 2;
            break;
        case 40:
            deltaY += 2;
            break;
    }
}

Depending on which arrow key was pressed, either the deltaX or deltaY variable will be increased or decreased. These variables changing in isolation has no effect on our triangles. We need to modify our drawTriangle function to actually use the deltaX and deltaY variables. Guess what we are going to do next?

Go ahead and make the following highlighted changes to the drawTriangle function:

function drawTriangle() {
  context.clearRect(0, 0, canvas.width, canvas.height);
  // the triangle
  context.beginPath();
  context.moveTo(200 + deltaX, 100 + deltaY);
  context.lineTo(170 + deltaX, 150 + deltaY);
  context.lineTo(230 + deltaX, 150 + deltaY);
  context.closePath();

  // the outline
  context.lineWidth = 10;
  context.strokeStyle = "rgba(102, 102, 102, 1)";
  context.stroke();

  // the fill color
  context.fillStyle = "rgba(255, 204, 0, 1)";
  context.fill();
}

The code changes should be pretty straightforward to make sense of. The call to clearRect ensures we clear our canvas before attempting to re-draw our triangle. The additions to the context.moveTo and context.lineTo methods take the deltaX and deltaY values into account. This ensures our triangle is always drawn with an offset that is determined by the number of times you pressed each arrow key. Putting that last sentence into human terms, this means you can move your triangle around using the keyboard.

At this point, if you preview your page now, our example still won't work. The reason is because there is one more thing you need to do. We need to call drawTriangle each time a key is pressed to actually draw our triangle in the new position. To make this happen, go back to the moveSomething function and add a call to drawTriangle towards the bottom:

function moveSomething(e) {
    switch(e.keyCode) {
        case 37:
            deltaX -= 2;
            break;
        case 38:
            deltaY -= 2;
            break;
        case 39:
            deltaX += 2;
            break;
        case 40:
            deltaY += 2;
            break;
    }

    drawTriangle();
}		

If you preview your page in your browser this time around, give your canvas element focus by clicking on it, and then use your arrow keys. If everything worked out properly, you'll see our triangle moving around the screen!

Preventing Default Keyboard Behavior

When relying on the keyboard, the thing you need to keep in mind is that everything from your browser to your operating system is listening to the keyboard as well. Strange and unexpected things happen when your keyboard is used. In our case, the arrow keys are used to scroll your page up and down (and occasionally left and right). Even if users have their focus on the canvas, tapping the arrow keys will cause your entire page to scroll if your content gets large enough to display scrollbars. You don't want that.

The way you fix this is very simple. In your moveSomething function (aka the event handler), simply add a call to preventDefault as highlighted:

function moveSomething(e) {
    switch(e.keyCode) {
        case 37:
            deltaX -= 2;
            break;
        case 38:
            deltaY -= 2;
            break;
        case 39:
            deltaX += 2;
            break;
        case 40:
            deltaY += 2;
            break;
    }
    e.preventDefault();

    drawTriangle();
}		

The preventDefault method prevents your browser from reacting to any keyboard events as long as your page has focus. This ensures that you can do all sorts of keyboard-ey things inside your canvas without worrying about your key presses accidentally triggering normal (yet unwanted) browser behavior.

Improved Keyhandling Logic

When talking about our current logic for dealing with the keyboard events, I mentioned that we are using a less-than-ideal solution. To see why, go back to your example and press and hold the Up and Right arrow keys at the same time. What you would expect to see is your triangle moving diagonally. What you actually see is your triangle moving in only one direction - either right or up. That isn't what we want!

The reason for this bizarre behavior is because we are using a switch statement to figure out which arrow key was pressed. There is nothing wrong with this general approach, for switch statements are far less verbose than if/else-if statements for checking which condition happens to equate to true. For many general cases, this is fine. How often are people going to hold down multiple keys at the same time? As it turns out, for the interactive/game-ey things that we are doing, pressing multiple keys will be a common occurrence. Pressing the Up and Right arrows on the keyboard at the same time is the equivalent of pushing a joystick diagonally. Don't tell me you've never done that before!

The solution is to change how we check for which key was pressed. Replace your existing addEventListener call and moveSomething function with the following instead:

var deltaX = 0;
var deltaY = 0;

window.addEventListener("keydown", keysPressed, false);
window.addEventListener("keyup", keysReleased, false);

var keys = [];

function keysPressed(e) {
    // store an entry for every key pressed
    keys[e.keyCode] = true;

    // left
    if (keys[37]) {
      deltaX -= 2;
    }

    // right
    if (keys[39]) {
      deltaX += 2;
    }

    // down
    if (keys[38]) {
      deltaY -= 2;
    }

    // up
    if (keys[40]) {
      deltaY += 2;
    }

    e.preventDefault();

    drawTriangle();
}

function keysReleased(e) {
    // mark keys that were released
    keys[e.keyCode] = false;
}		

This code change looks pretty massive, but it's not that invasive. What we've done is simply take our existing code and combine it with the Detecting Multiple Key Presses code you saw in the Keyboard Events tutorial. This change ensures that we can press multiple keys and get the desired behavior that we want. You can see that if you test your page out again and try pressing multiple arrow keys. Win!

Why we aren't using requestAnimationFrame

Earlier, when having a circle follow our mouse cursor around, we gave requestAnimationFrame the responsibility of drawing (and re-drawing) our circle. We didn't have our draw code be part of the mousemove event handler that fires each time your mouse cursor moves. That is in stark contrast to what we did here where our moveSomething (and keysPressed) event handlers are directly responsible for calling our drawTriangle function. Why did we do two different things in what looks like an identical situation?

The reason has to do with how chatty some events are with respect to your frame rate. The goal is to do work to update our screen at a rate that is consistent with our frame rate - ideally 60 times a second. Your mousemove event fires waaaaaaay too rapidly. Forcing things to get drawn with each mousemove fire would lead to a lot of unnecessary work with only a fraction of that work actually showing up on screen. That is why we defer drawing our circle in the mousemove case to requestAnimationFrame and its insane ability to stay in sync with the frame rate. Our keydown event (kinda like all keyboard events) is not very chatty at all. Having our keydown event handler (moveSomething) be responsible for drawing and shifting our triangle by two pixels is totally an OK call.

With all of this said, we will look at an example later where we will use requestAnimationFrame to smoothly move things using our keyboard!

Conclusion

Even in a world filled with devices where you touch and tap to do things, the keyboard is still very useful. Our goal was to move something around depending on which of the arrow keys was pressed. On the surface, this seemed like a very simple goal. As you saw in the many sections that followed our introduction, getting the basic functionality up and running was easy. Getting the functionality right turned out to be a bit more complicated.

In this tutorial, we uncovered some of the biggest quirks involved with using the keyboard. Note that I didn't say that we covered all of the quirks. That honor is spread across a few more tutorials we'll look at in the future where we touch upon some other keyboard and canvas-related shenanigans.

If you have a question about this or any other topic, the easiest thing is to drop by our forums where a bunch of the friendliest people you'll ever run into will be happy to help you out!

THE KIRUPA NEWSLETTER

Get cool tips, tricks, selfies, and more...personally hand-delivered to your inbox!

( View past issues for an idea of what you've been missing out on all this time! )

WHAT DO YOU THINK?

blog comments powered by Disqus

NEWSLETTER

No spam. No fluff. Just awesome content sent straight to your inbox!

Awesome and high-performance web hosting!
BACK TO TOP
new books - yay!!!