Gestión de la entrada táctil y el ratón en todos los navegadores

La interacción táctil con los sitios y aplicaciones Web nos abre una vía para la mejora de su usabilidad y ubicuidad, teniendo en cuenta que la Web y las aplicaciones de estilo Metro de Windows 8 juegan un papel fundamental de cara a los dispositivos táctiles que podremos disfrutar en el futuro.

En este post explicamos cómo los desarrolladores Web pueden utilizar el nuevo modelo de evento de puntero IE10 junto con el modelo de evento táctil iOS y el modelo de evento de ratón del W3C (en su versión extendida) para crear manejadores de código compatibles con diferentes navegadores con interfaz táctil, de ratón y de puntero.

Como introducción nada más: tengo la suerte de tener un Tablet PC Samsung 700T con la versión Preliminar de Desarrollo de Windows. Con él he podido disfrutar de las demos de interfaz multi-táctil en IE Test Drive llamadas Touch Effects y Lasso Birds. Igual que me ha ocurrido a mí, posiblemente habéis podido comprobar que Lasso Birds funciona en distintos dispositivos y navegadores, aparte de IE10. Por ejemplo, su interfaz táctil funciona también en dispositivos iOS. En este post hemos tomado prestadas ciertas pautas de Lasso Birds y las hemos generalizado y extendido para incluir versiones de navegadores más antiguas.

Podéis ver la demo en la dirección original. Debería funcionar en vuestros navegadores. A continuación hay un análisis de los patrones de código y ciertas lecciones que he podido aprender con esta experiencia.

El código

El algoritmo básico para dibujar con el modelo de ratón es bien sencillo:

var drawingStarted = false;

function DoEvent(eventObject) {

    if (eventObject.type == "mousedown") {

        drawingStarted = true;

        startDraw(eventObject.pageX, eventObject.pageY);

    }

    else if (eventObject.type == "mousemove") {

        if (drawingStarted) {

            extendDraw(eventObject.pageX, eventObject.pageY);

        }

    }

    else if (eventObject.type == "mouseup") {

        drawingStarted = false;

        endDraw();

    }

}

El único cambio que hay que hacer para que funcione con los eventos de puntero de IE10 es tener en cuenta que se pueden tener apoyados en pantalla más de un puntero a la vez, cada uno identificado con un valor “pointerID” distinto. El modelo de puntero de IE10 dispara eventos independientes MSPointerDown, MSPointerMove y MSPointerUp para cada puntero en el momento en que cambian sus estados.

var drawingStarted = {};

function DoEvent(eventObject) {

    eventObject.preventManipulation(); // without this, instead of drawing, you pan

    var pointerId = eventObject.pointerId;

    if (eventObject.type == "MSPointerDown") {

        drawingStarted[pointerId] = true;

        startDraw(pointerId, eventObject.pageX, eventObject.pageY);

    }

    else if (eventObject.type == "MSPointerMove") {

        if (drawingStarted[pointerId]) {

            extendDraw(pointerId, eventObject.pageX, eventObject.pageY);

        }

    }

    else if (eventObject.type == "MSPointerUp") {

        delete drawingStarted[pointerId];

        endDraw(pointerId);

    }

}

Para adaptar el modelo de ratón original al modelo de evento táctil de Apple iOS es preciso recorrer la lista de changedTouches para cada evento touchstart, touchmove y touchend porque en el modelo de iOS los cambios de estado que suceden al mismo tiempo se empaquetan dentro de un solo evento. Igual que con el modelo de puntero de IE10, cada punto de toque se designa con un identificador único.

var drawingStarted = {};

function DoEvent(eventObject) {

    eventObject.preventDefault(); // without this, instead of drawing, you pan

    for (var i = 0; i < eventObject.changedTouches.length; ++i) {

        var touchPoint = eventObject.changedTouches[i];

        var touchPointId = touchPoint.identifier;

        if (eventObject.type == "touchstart") {

            drawingStarted[touchPointId] = true;

            startDraw(touchPointId, touchPoint.pageX, touchPoint.pageY);

        }

        else if (eventObject.type == "touchmove") {

            if (drawingStarted[touchPointId]) {

                extendDraw(touchPointId, touchPoint.pageX, touchPoint.pageY);

            }

        }

        else if (eventObject.type == "touchend") {

            delete drawingStarted[touchPointId];

            endDraw(touchPointId);

        }

    }

}

Si refundimos los tres algoritmos individuales tenemos que tener en cuenta las diferencias entre los nombres de eventos y los nombres de atributo de los identificadores únicos de puntero, así como la falta de un identificador en el caso del modelo de ratón.

En el modelo refundido, que os muestro más abajo, he añadido también una comprobación de que la posición “move” ha cambiado realmente, porque el modelo de puntero de IE10 transmite los eventos MSPointerMove con las mismas coordenadas x, y cuando se establece un punto de contacto con la pantalla pero no se mueve. Al eliminar estos movimientos redundantes, evito las llamadas a extendDraw() que no hacen nada. He resuelto esta comprobación guardando la última posición (valores x e y) de un inicio o un movimiento en el objeto lastXY y luego, tras comprobar que existe una entrada lastXY para un id concreto, lastXY sustituye al objeto drawingStarted utilizado en los dos ejemplos anteriores.

var lastXY = {};

function DoEvent(eventObject) {

    // stop panning and zooming so we can draw

    if (eventObject.preventManipulation)

        eventObject.preventManipulation();

    else

        eventObject.preventDefault();

    // if we have an array of changedTouches, use it, else create an array of one with our eventObject

    var touchPoints = (