Optional Extra Topic 10a: Further Event-Handling Topics: Key events and Timed Functions

Key events

Key events occur when the user presses a key. With the growth of in-browser applications resembling traditional desktop applications, handling key events is becoming more useful. Key events include keydown (when the user presses a key down), keypress (when the user presses a key down and up immediately as you normally would when typing) and keyup (when the user releases a key). Here is an example of using key events:


// .js file

function init()
{
    var canvas = document.getElementById('canvas1');
    canvas.addEventListener ("keyup", handleKeyEvent);
}

function handleKeyEvent(e)
{
    console.log(`keyCode=${e.keyCode} shift?=${e.shiftKey}`);
}
In the HTML, the canvas must have a tabindex attribute set to 0 (details below):
<canvas id='canvas1' width='400' height='400' tabindex='0'>
</canvas>

Note how we make use of two properties of the event object relating to key events, namely keyCode and shiftKey. keyCode gives us a numerical code (not necessarily the ASCII code) for each key while shiftKey gives us a boolean (true/false) value representing whether the Shift key was pressed or not. keyCode relates to the actual key, so pressing A or Shift/A will return the same value (even though lower case 'a' and capital 'A' have different ASCII codes); we use shiftKey to determine whether Shift was pressed at the same time. You can also use ctrlKey and altKey in the same way, but use of CTRL and Alt during in browser applications is generally not used, as these keys have default actions in the browser window itself.

Note also the use of tabindex in the canvas tag. (Ref: Jonathan Snook). Normally, you cannot focus a canvas by clicking the mouse or tabbing to it, because (unlike text fields, for example) they were not intended for text input. Adding the attribute tabindex with a value of 0 (i.e. tabindex=0) to the canvas allows it to be focused by clicking the mouse on it or tabbing to it. It also allows you to force focus in code using the focus() method, for example:

canvas.focus();

Timed functions

In the kind of event-driven programming that we use with JavaScript, we might want to schedule a particular action to occur at a fixed time interval. Imagine, for example, an in-browser game. The hero might move in response to the arrow keys, but we need to make the monsters move independently and continuously without some sort of trigger by the user (if the hero stands still the monsters will still need to chase). This can be done with the setInterval() function. setInterval() is very easy to use: it takes two arguments, the function we would like to run, and the interval to run it in milliseconds. For example (JavaScript only shown, not HTML: we assume that there is a canvas with an ID of canvas1):



var canvas, index;
var styles = ['black','blue','red','magenta','green','cyan','yellow','white'];


function init()
{
    canvas = document.getElementById('canvas1');
    setInterval( doUpdate, 2000);
    index = 0;
}


function doUpdate()
{
    var ctx = canvas.getContext("2d");
    ctx.fillStyle = styles[index];
    ctx.fillRect(0,0,400,400);
    
    index++;
    if (index==styles.length)
    {
        index = 0;
    }
}

Here we define a doUpdate() function. It fills the canvas with the current colour from the array styles and then increases the index variable "count" by one, so that it moves on to the next colour and once it's reached the end, it resets "count" to 0 again. We schedule this function to be called in the doUpdate() function every 2000 milliseconds using setInterval():
setInterval( doUpdate, 2000);

Starting and stopping a scheduled function

At some point we will probably want to stop our scheduled function. We might also want to stop the user trying to set a scheduled function going twice. To do these tasks we need to make use of the return value of setInterval(), which is a "handle" on the function. We can then use clearInterval(), passing the handle in as an argument, to stop the function being scheduled. Also, we can test whether the "handle" already exists, to prevent the function being scheduled twice. The next example demonstrates this:



var canvas, index;
var styles = ['black','blue','red','magenta','green','cyan','yellow','white'];
var handle;

function init()
{
    canvas = document.getElementById('canvas1');
    document.getElementById("startbtn").addEventListener ("click", start);
    document.getElementById("endbtn").addEventListener ("click", end);
}

function start()
{
    if(handle)
    {
        alert("Function already scheduled!");
    }
    else
    {
        handle=setInterval(doUpdate, 2000);
        index = 0;
    }
}

function end()
{
    // Clear the function and set "handle" to null so that the test
    // in "start" will work and we'll be able to schedule the function
    // again.
    if(handle) 
    {
        clearInterval(handle);
        handle=null; 
    }
}

function doUpdate(n)
{
    var ctx = canvas.getContext("2d");
    ctx.fillStyle = styles[index];
    ctx.fillRect(0,0,400,400);
    
    index++;
    if (index==styles.length)
    {
        index = 0;
    }
}

Note how we have two buttons, one to schedule the function and another to cancel it. Note also how we test for the existence of the handle before starting the function, to prevent the function being scheduled more than once which, if done enough times, might crash the browser!

setTimeout()

Very similar to setInterval() is setTimeout(). The only difference is that it schedules a function to run just once, not multiple times.

requestAnimationFrame()

The requestAnimationFrame() function is similar in usage to setInterval() but is designed to sync with the browser frame refresh and can thus lead to smoother updates and animations. You pass it a function to call next time the display refreshes itself, resulting in smoother animations and less flicker. This function will typically perform a screen update of some kind, such as an animation.

This function gets passed a parameter representing the current time since the page loaded in milliseconds. The function is only called once, so you need to call requestAnimationFrame() again from within the function in order to call it repeatedly. So you can use code such as:


function init() {
    requestAnimationFrame(updateFunction);
}

function updateFunction(timeSincePageLoad) {
    // perform your animation...
    requestAnimationFrame(updateFunction);
}
If you are doing an animation and you want to control the speed of movement of a character across the screen so that it moves slower than 1 pixel per requestAnimationFrame() call, you can increase its coordinates by a fractional amount less than one. In fact, using the time parameter passed to the update function, you can set the movement of a character to a specific number of pixels per second, again leading to smoother and more consistent movement. You can find information on requestAnimationFrame() here.

Further exercise

Write code to move an image round the canvas. The user should use the arrow keys to move it. (key codes for the cursor keys: 37=left, 38=up, 39=right, 40=down). If you do not have an image use this one: hero.png

Data URLs

Most current browsers are able to understand data URLs. These are URLs representing data of some sort, typically images although some browsers also understand data URLs of PDF documents. A data URL takes a form such as:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAYAAACAvzb ... etc ...
The image data is encoded in base64 (a text encoding of binary data such as image data). If you entered this data URL in the address box of your browser, the corresponding image would be loaded.

Uses of data URLs

Data URLs are very useful when working with <canvas>. We might want to save the current state of the canvas and restore it later. One example would be an in-browser Microsoft Paint-like application in which users can draw on a canvas and save their drawing to an image element (you cannot right-click on a canvas and save it directly as an image in all browsers). We can use the toDataURL() method of the canvas object to do this. toDataURL() returns a data URL (by default in PNG format) of the contents of a canvas, for example:

function dataURLTest()
{
    var canvas = document.getElementById("canvas1");
    var dataURL = canvas.toDataURL();
    document.getElementById("img1").src = dataURL;
}
What this code is doing is obtaining a canvas with an ID of canvas1, encoding its contents as a data URL, and then setting the src property of the image with the ID of img1 to the data URL. The result will be that the contents of the canvas are copied into the image with the ID of img1, which the user will be able to right-click to save.

Loading a data URL back into a canvas

You can also load a data URL back into a canvas. This is easy to do: simply create a new Image object, set its src to the data URL, and draw it on the canvas. For example:

var img = new Image();
img.src = dataURL; // where "dataURL" is the saved data URL
ctx.drawImage(img, 0, 0);

Data URLs and AJAX

Data URLs are even more powerful when combined with AJAX. We can obtain the contents of a canvas as a data URL and then send that data URL to a server-side script using AJAX. The server-side script can then decode the data and save it server-side as a PNG image, making it possible to reload the image later.

Here is an example of what the client-side code might look like:

var canvas;

function init()
{
    canvas = document.getElementById("canvas1");
    var ctx = canvas.getContext("2d");
    ctx.fillStyle = 'red';
    ctx.fillRect(0,0,400,400);
    ctx.fillStyle = 'green';
    ctx.fillRect(100,100,200,200);
    
    document.getElementById("btn1").addEventListener("click", ajaxsave);
}

function ajaxsave()
{
    var data = canvas.toDataURL().replace("data:image/png;base64,", "");
    var formData = new FormData();
    formData.append("imgdata", data);
    var xhr = new XMLHttpRequest();
    xhr.addEventListener("load", e=> { console.log('uploaded'); });
    xhr.open("POST", "saveimg.php");
    xhr.send(formData);
}

The client-side is fairly simple and includes little new: we get the data using toDataURL() and send it to the server-side script saveimg.php using AJAX. Note that:

Saving canvas data: server-side

Here is what the server-side code to save this data might look like (note: no error-checking!):

<?php
$now = time();
$imgdata_encoded = str_replace(" ","+",$_POST["imgdata"]);
file_put_contents("/var/www/uploads/$now.png", base64_decode($imgdata_encoded));
?>
This code is a little more complex than you might expect, because there is a "gotcha" with POSTing data to a server. When POST requests are made, any + signs in the data are automatically converted to a space. This is because in URLs, plus signs are used to encode spaces. So we have to reverse the conversion by replacing any spaces with plus signs (base64 data never has any genuine spaces):
$imgdata_encoded = str_replace(" ","+",$_POST["imgdata"]);

We then use file_put_contents() to put the file in a publicly-visible folder on the server. Note also that we get the current time (in seconds since January 1st 1970) with time(), to ensure that the saved file has a unique name.


Advanced exercises

Either:

Or: