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.
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.