Question

I am developing an HTML5 Canvas project and while adding multitouch support for mobile devices, I seem to have run into an issue on Mobile Safari in iOS 7.1.

The main way that users interact with my app is by clicking and dragging. On the desktop, I add the appropriate mouse event listeners to my canvas...

c.addEventListener('mousedown', mouseDown, false);
c.addEventListener('mousemove', mouseMoved, false);
c.addEventListener('mouseup', mouseUp, false);

... have these event handlers calculate mouse position and call out to a more generic function that just accepts the X and Y position of the event...

function mouseDown(evt) 
{
    var pos = getMousePos(evt);
    inputDown(pos);
}

function mouseUp(evt) 
{
    var pos = getMousePos(evt);
    inputUp(pos);
}

function mouseMoved(evt) 
{
    var pos = getMousePos(evt);
    inputMoved(pos);
}

function getMousePos(evt) 
{
    var rect = c.getBoundingClientRect();
    return { 'x': evt.clientX - rect.left, 'y': evt.clientY - rect.top }
}

... and everything works great. I designed this level of abstraction to allow for easy expansion to other input methods. So now I attempt to use the Touch API to expand this. First I add my handlers to the canvas right next to the mouse handlers...

c.addEventListener("touchstart", touchDown, false);
c.addEventListener("touchmove", touchMoved, false);
c.addEventListener("touchend", touchUp, false);
c.addEventListener("touchcancel", touchUp, false);

... then add my thin event handlers to abstract away the touch events ...

function touchDown(evt) 
{
    evt.preventDefault();

    // Ignore touches after the first.
    if (activeTouch != null)
        return;

    if (evt.changedTouches.length > 0)
    {
        activeTouch = evt.changedTouches[0].identifier;
        var pos = getTouchPos(evt);
        inputDown(pos);
    }
}

function touchUp(evt) 
{
    var pos = getTouchPos(evt);
    activeTouch = null;
    inputUp(pos);
}

function touchMoved(evt) 
{
    var pos = getTouchPos(evt);
    inputMoved(pos);
}

function getTouchPos(evt) 
{
    var canX = 0;
    var canY = 0;

    for( var i = 0; i < evt.touches.length; i++ )
    {
        if (evt.touches[i].identifier == activeTouch)
        {
            canX = evt.touches[i].pageX - c.offsetLeft;
            canY = evt.touches[i].pageY - c.offsetTop;
            break;
        }
    }

    return { 'x': canX, 'y': canY }
}

... and this is where I run into trouble! You can see things get a little hairy but inefficiencies aside, I think I am using everything correctly. The problem is that getTouchPos(evt) seems unable to get the correct X, Y position of a touch on touchstart. On my first touch, I get a position of 0,0. If I drag my finger, the touchmove fires multiple times and the X,Y coordinates reported seem fine. If I lift my finger, touchend fires and I also get a correct location. However, if I touch again, touchstart fires and rather than where I am touching, I get the X,Y coordinates of where my last touch ended!

I've gone in with a debugger and sure enough, the stale/uninitialized values are right there in the Touch objects I get in the event data. Am I doing anything obviously wrong? I don't understand why I seem to be getting bad coordinates from touchstart events. What can I do to work around this issue? Is there another source I can use to get the correct position? A subtle tweak to event order?

Here's a link to an online demo of the full code if you'd like to run some in-browser debug.

And thanks in advance for any help.

Was it helpful?

Solution

I'm not sure if it's obvious, but it was a fun issue to debug. So basically, the confusion comes down to this guy:

function mouseMoved(evt) {
    var pos = getMousePos(evt);
    inputMoved(pos);
}

For your mouse events, everything works great, because you're bound to mousemove.

c.addEventListener('mousemove', mouseMoved, false);

Which calls

function mouseMoved(evt) {
    var pos = getMousePos(evt);
    inputMoved(pos);
}

So, as you move your mouse around, your posX/posY values are updated continuously. (Hopefully you see the issue now :)

For TouchEvents, when a user touches (but does not move), you only trigger touchstart and touchend. It isn't until a user drags, that touchmove gets called, which in turn calls the function touchMoved (that calls inputMoved - which updates your posX/posY values).

The easiest way to address this issue on your page is just to call inputMoved in both touchstart and touchmove so your MassHaver instance has the latest state.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top