Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Canvas get touch position on mobile web

I have a code which drags a line from (x,y) co-ordinate to new mouse (x,y) co-ordinate. This works fine in desktop browsers, but for some reason it doesn't work in mobile browsers. I have added touch event listeners but I guess the co-ordinates are some how getting incorrect. Heres my code:

   function getMouse(e) {
     var element = canvas, offsetX = 0, offsetY = 0;
     if (element.offsetParent) {
       do {
         offsetX += element.offsetLeft;
         offsetY += element.offsetTop;
       } while ((element = element.offsetParent));
     }

     mx = (e.pageX - offsetX) - LINE_WIDTH;
     my =( e.pageY - offsetY )- LINE_WIDTH;
   }
   function mouseDown(e){
     getMouse(e);
     clear(fctx);
     var l = lines.length;
     for (var i = l-1; i >= 0; i--) {
       draw(fctx,lines[i]);
       var imageData = fctx.getImageData(mx, my, 1, 1);
       if (imageData.data[3] > 0) {
         selectedObject = lines[i];
         isDrag = true;
         canvas.onmousemove = drag;
         clear(fctx);
       }
     }
   }
   function mouseUp(){
     isDrag = false;
   }
   canvas.onmousedown = mouseDown;
   canvas.onmouseup = mouseUp;
   canvas.addEventListener('touchstart', mouseDown, false);
   canvas.addEventListener('touchend', mouseUp, false);

you can see the working part here: http://codepen.io/nirajmchauhan/pen/yYdMJR

like image 717
jaybutani Avatar asked Nov 27 '15 07:11

jaybutani


1 Answers

Generating mouse events from touch events

OK seen this question up here for a while and no one is coming forward with an answer I will give one.

Touch events unlike mouse events involve many points of contact with the UI. To accommodate this the touch events supply an array of touchpoints. As a mouse can not be in two places at once the two interaction methods should really be handled separately for the best user experience. OP as you do not ask about detecting if the device is touch or mouse driven, I have left that for another person to ask.

Handling Both

Mouse and Touch events can coexist. Adding listeners for mouse or touch events on a device that does no have one or the other is not an issue. The missing input interface simply does not generate any events. This makes it easy to implements a transparent solution for your page.

It comes down to which interface you prefer and emulating that interface when the hardware for it is unavailable. In this case I will emulate the mouse from any touch events that are created.

Creating events programmatically.

The code uses the MouseEvent object to create and dispatch events. It is simple to use and the events are indistinguishable from real mouse events. For a detailed description of MouseEvents goto MDN MouseEvent

At its most basic.

Create a mouse click event and dispatch it to the document

  var event = new MouseEvent( "click", {'view': window, 'bubbles': true,'cancelable': true});
  document.dispatchEvent(event);

You can also dispatch the event to individual elements.

  document.getElementById("someButton").dispatchEvent(event);

To listen to the event it is just the same as listening to the actual mouse.

  document.getElementById("someButton").addEventListener(function(event){
        // your code
  ));

The second argument in the MouseEvent function is where you can add extra information about the event. Say for example clientX and clientY the position of the mouse, or which or buttons for which button/s is being pressed.

If you have ever looked at the mouseEvent you will know there are a lot of properties. So exactly what you send in the mouse event will depend on what your event listener is using.

Touch events.

Touch events are similar to the mouse. There is touchstart, touchmove, and touchend. They differ in that they supply a array of locations, one item for each point of contact. Not sure what the max is but for this answer we are only interested in one. See MDN touchEvent for full details.

What we need to do is for touch events that involve only one contact point we want to generate corresponding mouse events at the same location. If the touch event returns more than one contact point, we can not know which their intended focus is on so we will simply ignore them.

function touchEventHandler(event){
    if (event.touches.length > 1){  // Ignor multi touch events
        return;
    }
}

So now we know that the touch a single contact we can go about creating the mouse events based on the information in the touch events.

At its most basic

touch = event.changedTouches[0]; // get the position information
if(type === "touchmove"){        
    mouseEventType = "mousemove";   // get the name of the mouse event
                                    // this touch will emulate   
}else
if(type === "touchstart"){  
    mouseEventType = "mousedown";     // mouse event to create
}else
if(type === "touchend"){ 
    mouseEventType = "mouseup";     // ignore mouse up if click only
}

var mouseEvent = new MouseEvent( // create event
    mouseEventType,   // type of event
    {
        'view': event.target.ownerDocument.defaultView,
        'bubbles': true,
        'cancelable': true,
        'screenX':touch.screenX,  // get the touch coords 
        'screenY':touch.screenY,  // and add them to the 
        'clientX':touch.clientX,  // mouse event
        'clientY':touch.clientY,
});
// send it to the same target as the touch event contact point.
touch.target.dispatchEvent(mouseEvent);

Now your mouse listeners will receive mousedown, mousemove, mouseup events when a user touches the device at only one location.

Missing the click

All good so far but there is one mouse event missing, that is needed as well. "onClick" I am not sure if there is a equivilant touch event and just as an exercise I saw there is enough information in what we have to decide if a set of touch events could be considered a click.

It will depend on how far apart the start and end touch events are, more than a few pixels and its a drag. It will also depend on how long. (Though not the same as a mouse) I find that people tend to tap for click, while a mouse can be held, in lieu of conformation on the release, or drag away to cancel, this is not how people use the touch interface.

So I record the time the touchStart event happens. event.timeStamp and where it started. Then at the touchEnd event I find the distance it has moved and the time since. If they are both under the limits I have set I also generate a mouse click event along with the mouse up event.

So that is the basics way to convert touch events into mouse events.

Some CODE

Below is a tiny API called mouseTouch that does what I have just explained. It covers the most basic mouse interactions required in a simple drawing app.

//                                _______               _     
//                               |__   __|             | |    
//    _ __ ___   ___  _   _ ___  ___| | ___  _   _  ___| |__  
//   | '_ ` _ \ / _ \| | | / __|/ _ \ |/ _ \| | | |/ __| '_ \ 
//   | | | | | | (_) | |_| \__ \  __/ | (_) | |_| | (__| | | |
//   |_| |_| |_|\___/ \__,_|___/\___|_|\___/ \__,_|\___|_| |_|
//                                                            
//    
// Demonstration of a simple mouse emulation API using touch events.

// Using touch to simulate a mouse.
// Keeping it clean with touchMouse the only pubic reference.
// See Usage instructions at bottom.
var touchMouse = (function(){
    "use strict";
    var timeStart, touchStart, mouseTouch, listeningElement, hypot;


    mouseTouch = {};  // the public object 
    // public properties.
    mouseTouch.clickRadius = 3; // if touch start and end within 3 pixels then may be a click
    mouseTouch.clickTime = 200; // if touch start and end in under this time in ms then may be a click
    mouseTouch.generateClick = true; // if true simulates onClick event
                                     // if false only generate mousedown, mousemove, and mouseup
    mouseTouch.clickOnly = false; // if true on generate click events
    mouseTouch.status = "Started."; // just for debugging



    // ES6 new math function
    // not sure the extent of support for Math.hypot so hav simple poly fill
    if(typeof Math.hypot === 'function'){
        hypot = Math.hypot;
    }else{
        hypot = function(x,y){  // Untested 
            return Math.sqrt(Math.pow(x,2)+Math.pow(y,2));
        };
    }
    // Use the new API and MouseEvent object
    function triggerMouseEvemt(type,fromTouch,fromEvent){
      var mouseEvent = new MouseEvent(
          type, 
          {
              'view': fromEvent.target.ownerDocument.defaultView,
              'bubbles': true,
              'cancelable': true,
                'screenX':fromTouch.screenX,
                'screenY':fromTouch.screenY,
                'clientX':fromTouch.clientX,
                'clientY':fromTouch.clientY,
                'offsetX':fromTouch.clientX, // this is for old Chrome 
                'offsetY':fromTouch.clientY,
                'ctrlKey':fromEvent.ctrlKey,
                'altKey':fromEvent.altKey,
                'shiftKey':fromEvent.shiftKey,
                'metaKey':fromEvent.metaKey,
                'button':0,
                'buttons':1,
          });
        // to do.
        // dispatch returns cancelled you will have to 
        // add code here if needed
        fromTouch.target.dispatchEvent(mouseEvent);
    }

    // touch listener. Listens to Touch start, move and end.
    // dispatches mouse events as needed. Also sends a click event
    // if click falls within supplied thresholds and conditions
    function emulateMouse(event) {

        var type, time, touch, isClick, mouseEventType, x, y, dx, dy, dist;
        event.preventDefault();  // stop any default happenings interfering
        type = event.type ;  // the type.

        // ignore multi touch input
        if (event.touches.length > 1){
            if(touchStart !== undefined){ // don't leave the mouse down
                triggerMouseEvent("mouseup",event.changedTouches[0],event);
            }
            touchStart = undefined;
            return;
        }
        mouseEventType = "";
        isClick = false;  // default no click
        // check for each event type I have the most numorus move event first, Good practice to always think about the efficancy for conditional coding.
        if(type === "touchmove" && !mouseTouch.clickOnly){        // touchMove
            touch = event.changedTouches[0];
            mouseEventType = "mousemove";      // not much to do just move the mouse
        }else
        if(type === "touchstart"){  
            touch = touchStart = event.changedTouches[0]; // save the touch start for dist check
            timeStart = event.timeStamp; // save the start time
            mouseEventType = !mouseTouch.clickOnly?"mousedown":"";     // mouse event to create
        }else
        if(type === "touchend"){  // end check time and distance
            touch =  event.changedTouches[0];
            mouseEventType = !mouseTouch.clickOnly?"mouseup":"";     // ignore mouse up if click only
            // if click generator active
            if(touchStart !== undefined && mouseTouch.generateClick){
                time = event.timeStamp - timeStart;  // how long since touch start
                // if time is right
                if(time < mouseTouch.clickTime){
                    // get the distance from the start touch
                    dx = touchStart.clientX-touch.clientX;
                    dy = touchStart.clientY-touch.clientY;
                    dist = hypot(dx,dy);
                    if(dist < mouseTouch.clickRadius){
                        isClick = true;
                    }
                }
            }
        }
        // send mouse basic events if any
        if(mouseEventType !== ""){
            // send the event
            triggerMouseEvent(mouseEventType,touch,event);
        }
        // if a click also generates a mouse click event
        if(isClick){
            // generate mouse click
            triggerMouseEvent("click",touch,event);
        }
    }

    // remove events
    function removeTouchEvents(){
        listeningElement.removeEventListener("touchstart", emulateMouse);
        listeningElement.removeEventListener("touchend", emulateMouse);
        listeningElement.removeEventListener("touchmove", emulateMouse);
        listeningElement = undefined;  

    }

    // start  adds listeners and makes it all happen.
    // element is optional and will default to document.
    // or will Listen to element.
    function startTouchEvents(element){
        if(listeningElement !== undefined){ // untested
            // throws to stop cut and past useage of this example code.
            // Overwriting the listeningElement can result in a memory leak.
            // You can remove this condition block and it will work
            // BUT IT IS NOT RECOGMENDED

            throw new ReferanceError("touchMouse says!!!! API limits functionality to one element.");
        }
        if(element === undefined){
            element = document;
        }
        listeningElement = element;
        listeningElement.addEventListener("touchstart", emulateMouse);
        listeningElement.addEventListener("touchend", emulateMouse);
        listeningElement.addEventListener("touchmove", emulateMouse);
    }

    // add the start event to public object.
    mouseTouch.start = startTouchEvents;
    // stops event listeners and remove them from the DOM 
    mouseTouch.stop = removeTouchEvents;

    return mouseTouch;

})(); 











// How to use

touchMouse.start(); // done using defaults will emulate mouse on the entier page 

// For one element and only clicks
// HTML
<input value="touch click me" id="touchButton" type="button"></input>
// Script
var el = document.getElementById("touchButton");
if(el !== null){
    touchMouse.clickOnly = true;
    touchMouse.start(el);
}

// For drawing on a canvas
<canvas id="touchCanvas"></canvas> 
// script
var el = document.getElementById("touchButton");
if(el !== null){
    touchMouse.generateClick = false; // no mouse clicks please
    touchMouse.start(el);
}

// For switching elements call stop then call start on the new element
// warning touchMouse retained a reference to the element you
// pass it with start. Dereferencing touchMouse will not delete it.
// Once you have called start you must call stop in order to delete it.

// API
//---------------------------------------------------------------
// To dereference call the stop method if you have called start . Then dereference touchMouse
// Example
touchMouse.stop();
touchMouse = undefined;


// Methods.
//---------------------------------------------------------------
// touchMouse.start(element); // element optional. Element is the element to attach listeners to.
                              // Calling start a second time without calling stop will
                              // throw a reference error. This is to stop memory leaks.
                              // YOU Have been warned...

// touchMouse.stop();          // removes listeners and dereferences any DOM objects held
//---------------------------------------------------------------
// Properties
// mouseTouch.clickRadius = 3; // Number. Default 3. If touch start and end within 3 pixels then may be a click
// mouseTouch.clickTime = 200; // Number. Default 200. If touch start and end in under this time in ms then may be a click
// mouseTouch.generateClick;   // Boolean. Default true. If true simulates onClick event
//                                    // if false only generate mousedown, mousemove, and mouseup
// mouseTouch.clickOnly;      // Boolean.  Default false. If true only generate click events Default false
// mouseTouch.status;         // String. Just for debugging kinda pointless really. 

So hope that helps you with your code.

like image 91
Blindman67 Avatar answered Nov 18 '22 18:11

Blindman67