Share via


Creating an universal virtual touch joystick working for all Touch models thanks to Hand.JS

I’m currently working on several gaming projects for modern browsers and Windows 8 Store projects. Some of them are using HTML5 as a base in order to simplify multi-devices targeting. I was then looking for a unified way to address all inputs on all platforms: Windows 8/RT, Windows Phone 8, iPad, Android & FirefoxOS.

As you’ve maybe discovered in my previous article Unifying touch and mouse- how Pointer Events will make cross-browsers touch support easy , IE10 on Windows 8/RT & Windows Phone 8 implement the Pointer Events model we’ve submitted to the W3C. In order to address in a unified way this Pointer Events model and the one implemented in WebKit based browsers, we’re going to use David Catuhe’s HandJS library. Check out his blog’s post here: HandJS a polyfill for supporting pointer events on every browser. This idea is to target the Pointer model and the library will propagate the touch events to all platforms specifics.

Once I had all the technical pieces in hand, I was looking for a great way to implement a virtual touch joystick in my game. I’m not a huge fan of arrow keys buttons to be touched. On another side, the virtual analogic pad are often not very well placed. But I’ve finally discovered that Seb Lee-Delisle had already digested this and has created this awesome concept described in Multi-touch game controller in JavaScript/HTML5 for iPad . The code is available on GitHub here: JSTouchController

The idea was then to take his code and refactor the touch part to target the Pointer model instead of the original WebKit Touch approach. While working on that several months ago, I’ve discovered that Boris Smus from Google had already started to more or less do it. It’s been done while he was working on his own Pointer.js library as described in his article Generalized input on the cross-device web. However, at that time, Boris was mimicking an old version of the IE10 Pointer Events implementation and his library wasn’t working in IE10. That’s why, even if Boris’ work was awesome, we’ve decided to work on our own version. Indeed, David’s library is currently targeting the last and very recent W3C version currently in last call draft. If you’re having a look to both librairies, you’ll see also that HandJS is using some different approaches in several parts of the code. We will then use HandJS in this article to build our touch joystick.

Sample 1: Pointers tracker

This sample helps you tracking the various inputs on the screen. It tracks and follow the various fingers pressing the canvas element. It’s based on Seb’s sample available on GitHub here: Touches.html

Thanks to Hand.js, we’re going to make it compatible for all browsers. It’s even also going to track the stylus and/or the mouse based on the type of hardware you’re currently testing on!

Here is an HTML5 video of the result in IE10 running under Windows 8. You’ll find see some cyan circles tracking the fingers, then a red circle tracking the mouse and a green circle tracking the pen:

Poster Image

Download Video: MP4, WebM, HTML5 Video Player by VideoJS

The same webpage provides the very same result under Chrome on Windows 8 or on an iOS/Android/FirefoxOS devices (except that pen is only supported by IE10). Thanks to HandJS, write it once and it will run everywhere! Sourire 

You’ve seen in the video that the cyan pointers are of type “TOUCH” whereas the red one is of type “MOUSE”. If you have a touch screen, you can experience the same result by testing this page embedded in this iframe:

This sample works fine on a Windows 8/RT touch device, a Windows Phone 8, an iPad/iPhone or Android/FirefoxOS device! If you don’t have a touch device, HandJS will automatically fallback to mouse. You should then be able to track at least 1 pointer with your mouse. 

Let’s see how to obtain this result in a unified way. All the code lives in Touches.js:

 "use strict";

// shim layer with setTimeout fallback
window.requestAnimFrame = (function () {
    return window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.oRequestAnimationFrame ||
    window.msRequestAnimationFrame ||
    function (callback) {
        window.setTimeout(callback, 1000 / 60);
    };
})();

var pointers; // collections of pointers

var canvas,
c; // c is the canvas' context 2D

document.addEventListener("DOMContentLoaded", init);

window.onorientationchange = resetCanvas;
window.onresize = resetCanvas;

function init() {
    setupCanvas();
    pointers = new Collection();
    canvas.addEventListener('pointerdown', onPointerDown, false);
    canvas.addEventListener('pointermove', onPointerMove, false);
    canvas.addEventListener('pointerup', onPointerUp, false);
    canvas.addEventListener('pointerout', onPointerUp, false);
    requestAnimFrame(draw);
}

function resetCanvas(e) {
    // resize the canvas - but remember - this clears the canvas too.
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    //make sure we scroll to the top left.
    window.scrollTo(0, 0);
}

function draw() {
    c.clearRect(0, 0, canvas.width, canvas.height);

    pointers.forEach(function (pointer) {
        c.beginPath();
        c.fillStyle = "white";
        c.fillText(pointer.type + " id : " + pointer.identifier + " x:" + pointer.x + " y:" + 
                   pointer.y, pointer.x + 30, pointer.y - 30);

        c.beginPath();
        c.strokeStyle = pointer.color;
        c.lineWidth = "6";
        c.arc(pointer.x, pointer.y, 40, 0, Math.PI * 2, true);
        c.stroke();
    });

    requestAnimFrame(draw);
}

function createPointerObject(event) {
    var type;
    var color;
    switch (event.pointerType) {
        case event.POINTER_TYPE_MOUSE:
            type = "MOUSE";
            color = "red";
            break;
        case event.POINTER_TYPE_PEN:
            type = "PEN";
            color = "lime";
            break;
        case event.POINTER_TYPE_TOUCH:
            type = "TOUCH";
            color = "cyan";
            break;
    }
    return { identifier: event.pointerId, x: event.clientX, y: event.clientY, type: type, color: color };
}

function onPointerDown(e) {
    pointers.add(e.pointerId, createPointerObject(e));
}

function onPointerMove(e) {
    if (pointers.item(e.pointerId)) {
        pointers.item(e.pointerId).x = e.clientX;
        pointers.item(e.pointerId).y = e.clientY;
    }
}

function onPointerUp(e) {
    pointers.remove(e.pointerId);
}

function setupCanvas() {
    canvas = document.getElementById('canvasSurface');
    c = canvas.getContext('2d');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    c.strokeStyle = "#ffffff";
    c.lineWidth = 2;
}

Well, I think the code is pretty straightforward. I’m registering to the pointerdown/move/up event like described in my introduction article on MSPointer Events. In the pointerdown handler, I’m catching the ID, the X & Y coordinates and the type of pointers (touch, pen or mouse) inside an object generated on the fly pushed in the pointers collection object. This collection is indexed by the id of the pointers. The collection object is described in Collection.js. The draw() function is then enumerating this collection to draw some cyan/red/lime circles based on the type at the exact position where you’re touching the screen. It also adds some text on each circle’s side to display the pointer’s details. The pointermove handler updates the coordinates of the associated pointer in the collection and the pointerup/out simply removes it from the collection. Hand.JS makes this code compatible for IE10 by propagating pointerdown/move/up/out to the associated MSPointerDown/Move/Up/Out events and to the touchstart/move/end events for WebKit’s browsers. 

If you wish, you can view the complete source code here: https://david.blob.core.windows.net/html5/touchjoystick/Touches.html

Sample 2: Video game controller with a simple spaceship game

Let’s now see the sample I was the most interested in. You will probably also if you’re looking for a virtual analog touch pad for your HTML5 games. The idea is to touch anywhere on the left side of the screen. At the exact position where you’ll touch the screen, it will display a simple but very efficient pad. Moving your finger will update the virtual touch pad and will move a simple spaceship. Touching the right side of the screen will display some red circles and those circles will generate some bullets getting out of the spaceship. Once again, It’s based on Seb’s sample available on GitHub here: TouchControl.html

Here is a video of the updated sample result in IE10 under Windows 8:

Poster Image

Download Video: MP4, WebM, HTML5 Video Player by VideoJS

If you have a touch screen, you can live test the page in this iframe:

Otherwise, you will only be able to move the ship using your mouse by clicking on the left of the screen or firing by clicking on the right side, but you won’t be able to achieve both actions simultaneously. Indeed, HandJS is providing a mouse fallback if the browser or platform doesn’t support touch.

Note: the iPad seems to suffer from an unknown bug which prevents this second iframe to work correctly. Open the sample directly in another tab to make it works on your iPad.

Let’s see again how to obtain this result in a unified way. All the code lives this time in TouchControl.js:

 // shim layer with setTimeout fallback
window.requestAnimFrame = (function () {
    return window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.oRequestAnimationFrame ||
    window.msRequestAnimationFrame ||
    function (callback) {
        window.setTimeout(callback, 1000 / 60);
    };
})();

var canvas,
c, // c is the canvas' context 2D
container,
halfWidth,
halfHeight,
leftPointerID = -1,
leftPointerPos = new Vector2(0, 0),
leftPointerStartPos = new Vector2(0, 0),
leftVector = new Vector2(0, 0);

var pointers; // collections of pointers
var ship;
bullets = [],
spareBullets = [];

document.addEventListener("DOMContentLoaded", init);

window.onorientationchange = resetCanvas;
window.onresize = resetCanvas;

function init() {
    setupCanvas();
    pointers = new Collection();
    ship = new ShipMoving(halfWidth, halfHeight);
    document.body.appendChild(ship.canvas);
    canvas.addEventListener('pointerdown', onPointerDown, false);
    canvas.addEventListener('pointermove', onPointerMove, false);
    canvas.addEventListener('pointerup', onPointerUp, false);
    canvas.addEventListener('pointerout', onPointerUp, false);
    requestAnimFrame(draw);
}

function resetCanvas(e) {
    // resize the canvas - but remember - this clears the canvas too. 
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    halfWidth = canvas.width / 2;
    halfHeight = canvas.height / 2;

    //make sure we scroll to the top left. 
    window.scrollTo(0, 0);
}

function draw() {
    c.clearRect(0, 0, canvas.width, canvas.height);

    ship.targetVel.copyFrom(leftVector);
    ship.targetVel.multiplyEq(0.15);
    ship.update();

    with (ship.pos) {
        if (x < 0) x = canvas.width;
        else if (x > canvas.width) x = 0;
        if (y < 0) y = canvas.height;
        else if (y > canvas.height) y = 0;
    }

    ship.draw();

    for (var i = 0; i < bullets.length; i++) {
        var bullet = bullets[i];
        if (!bullet.enabled) continue;
        bullet.update();
        bullet.draw(c);
        if (!bullet.enabled) {
            spareBullets.push(bullet);

        }
    }

    pointers.forEach(function (pointer) {
        if (pointer.identifier == leftPointerID) {
            c.beginPath();
            c.strokeStyle = "cyan";
            c.lineWidth = 6;
            c.arc(leftPointerStartPos.x, leftPointerStartPos.y, 40, 0, Math.PI * 2, true);
            c.stroke();
            c.beginPath();
            c.strokeStyle = "cyan";
            c.lineWidth = 2;
            c.arc(leftPointerStartPos.x, leftPointerStartPos.y, 60, 0, Math.PI * 2, true);
            c.stroke();
            c.beginPath();
            c.strokeStyle = "cyan";
            c.arc(leftPointerPos.x, leftPointerPos.y, 40, 0, Math.PI * 2, true);
            c.stroke();

        } else {

            c.beginPath();
            c.fillStyle = "white";
            c.fillText("type : " + pointer.type + " id : " + pointer.identifier + " x:" + pointer.x + 
                       " y:" + pointer.y, pointer.x + 30, pointer.y - 30);

            c.beginPath();
            c.strokeStyle = "red";
            c.lineWidth = "6";
            c.arc(pointer.x, pointer.y, 40, 0, Math.PI * 2, true);
            c.stroke();
        }
    });

    requestAnimFrame(draw);
}

function makeBullet() {
    var bullet;

    if (spareBullets.length > 0) {

        bullet = spareBullets.pop();
        bullet.reset(ship.pos.x, ship.pos.y, ship.angle);

    } else {

        bullet = new Bullet(ship.pos.x, ship.pos.y, ship.angle);
        bullets.push(bullet);

    }

    bullet.vel.plusEq(ship.vel);
}

function givePointerType(event) {
    switch (event.pointerType) {
        case event.POINTER_TYPE_MOUSE:
            return "MOUSE";
            break;
        case event.POINTER_TYPE_PEN:
            return "PEN";
            break;
        case event.POINTER_TYPE_TOUCH:
            return "TOUCH";
            break;
    }
}

function onPointerDown(e) {
    var newPointer = { identifier: e.pointerId, x: e.clientX, y: e.clientY, type: givePointerType(e) };
    if ((leftPointerID < 0) && (e.clientX < halfWidth)) {
        leftPointerID = e.pointerId;
        leftPointerStartPos.reset(e.clientX, e.clientY);
        leftPointerPos.copyFrom(leftPointerStartPos);
        leftVector.reset(0, 0);
    }
    else {
        makeBullet();

    }
    pointers.add(e.pointerId, newPointer);
}

function onPointerMove(e) {
    if (leftPointerID == e.pointerId) {
        leftPointerPos.reset(e.clientX, e.clientY);
        leftVector.copyFrom(leftPointerPos);
        leftVector.minusEq(leftPointerStartPos);
    }
    else {
        if (pointers.item(e.pointerId)) {
            pointers.item(e.pointerId).x = e.clientX;
            pointers.item(e.pointerId).y = e.clientY;
        }
    }
}

function onPointerUp(e) {
    if (leftPointerID == e.pointerId) {
        leftPointerID = -1;
        leftVector.reset(0, 0);

    }
    leftVector.reset(0, 0);

    pointers.remove(e.pointerId);
}

function setupCanvas() {
    canvas = document.getElementById('canvasSurfaceGame');
    c = canvas.getContext('2d');
    resetCanvas();
    c.strokeStyle = "#ffffff";
    c.lineWidth = 2;
}

The code is again very straightforward, I won’t spend time explaining it. You can view the complete source code here: https://david.blob.core.windows.net/html5/touchjoystick/TouchControl.html

In conclusion, thanks to the job done by Seb Lee-Delisle and David Catuhe, you now have all the pieces needed to implement your own virtual touch joypad for your HTML5 games. The result will work on all touch devices supporting HTML5!

Follow the author @davrous

Comments

  • Anonymous
    December 04, 2013
    The comment has been removed

  • Anonymous
    January 10, 2014
    This is awesome, I just tested it on the Acer Iconia W700 i5. The original is a bit slow, but the iPad version works just fine.

  • Anonymous
    April 22, 2014
    Not bad, not bad....

  • Anonymous
    June 18, 2015
    The comment has been removed

  • Anonymous
    June 18, 2015
    The comment has been removed