Multi-touch interaction

Pointer events extend DOM input events to support various pointing input devices such as pen/stylus and touch screens as well as mouse. The pointer is a hardware-agnostic device that can target a specific set of screen coordinates. Having a single event model for pointers can simplify creating websites, applications and provide a good user experience regardless of the user's hardware.

Pointer events have many similarities to mouse events but they support multiple simultaneous pointers such as multiple fingers on a touch screen. This additional feature can be used to provide richer user interaction models but at the cost of additional complexity in the multi-touch interaction handling. This document demonstrates via example code, using pointer events with different multi-touch interactions.

A live version of this application is available on GitHub. The source code is available on GitHub; pull requests and bug reports are welcome.

Example

This example demonstrates using pointer events' various event types (pointerdown, pointermove, pointerup pointercancel, etc.) for different multi-touch interactions.

Define touch targets

The application uses <div> to define three different touch target areas.

html
<style>
  div {
    margin: 0em;
    padding: 2em;
  }
  #target1 {
    background: white;
    border: 1px solid black;
  }
  #target2 {
    background: white;
    border: 1px solid black;
  }
  #target3 {
    background: white;
    border: 1px solid black;
  }
</style>

Global state

To support multi-touch interaction, preserving a pointer's event state during various event phases is required. This application uses three arrays to cache event state, one cache per target element.

js
// Log events flag
const logEvents = false;

// Event caches, one per touch target
const evCache1 = [];
const evCache2 = [];
const evCache3 = [];

Register event handlers

Event handlers are registered for the following pointer events: pointerdown, pointermove and pointerup. The handler for pointerup is used for the pointercancel, pointerout and pointerleave events, since these four events have the same semantics in this application.

js
function setHandlers(name) {
  // Install event handlers for the given element
  const el = document.getElementById(name);
  el.onpointerdown = pointerdownHandler;
  el.onpointermove = pointermoveHandler;

  // Use same handler for pointer{up,cancel,out,leave} events since
  // the semantics for these events - in this app - are the same.
  el.onpointerup = pointerupHandler;
  el.onpointercancel = pointerupHandler;
  el.onpointerout = pointerupHandler;
  el.onpointerleave = pointerupHandler;
}

function init() {
  setHandlers("target1");
  setHandlers("target2");
  setHandlers("target3");
}

Pointer down

The pointerdown event is fired when a pointer (mouse, pen/stylus or touch point on a touchscreen) makes contact with the contact surface. The event's state must be cached, in case this down event is part of a multi-touch interaction.

In this application, when a pointer is placed down on an element, the background color of the element changes, depending on the number of active touch points the element has. See the update_background function for more details about the color changes.

js
function pointerdownHandler(ev) {
  // The pointerdown event signals the start of a touch interaction.
  // Save this event for later processing (this could be part of a
  // multi-touch interaction) and update the background color
  pushEvent(ev);
  if (logEvents) {
    log(`pointerDown: name = ${ev.target.id}`, ev);
  }
  updateBackground(ev);
}

Pointer move

The pointermove handler is called when the pointer moves. It may be called multiple times (for example, if the user moves the pointer) before a different event type is fired.

In this application, a pointer move is represented by the target's border being set to dashed to provide a clear visual indication that the element has received this event.

js
function pointermoveHandler(ev) {
  // Note: if the user makes more than one "simultaneous" touch, most browsers
  // fire at least one pointermove event and some will fire several pointermoves.
  //
  // This function sets the target element's border to "dashed" to visually
  // indicate the target received a move event.
  if (logEvents) {
    log("pointerMove", ev);
  }
  updateBackground(ev);
  ev.target.style.border = "dashed";
}

Pointer up

The pointerup event is fired when a pointer is raised from the contact surface. When this occurs, the event is removed from the associated event cache.

In this application, this handler is also used for pointercancel, pointerleave and pointerout events.

js
function pointerupHandler(ev) {
  if (logEvents) {
    log(ev.type, ev);
  }
  // Remove this touch point from the cache and reset the target's
  // background and border
  removeEvent(ev);
  updateBackground(ev);
  ev.target.style.border = "1px solid black";
}

Application UI

The application uses <div> elements for the touch areas and provides buttons to enable logging and to clear the log.

To prevent the browser's default touch behavior from overriding this application's pointer handling, the touch-action property is applied to the <body> element.

html
<body onload="init();" style="touch-action:none">
  <div id="target1">Tap, Hold or Swipe me 1</div>
  <div id="target2">Tap, Hold or Swipe me 2</div>
  <div id="target3">Tap, Hold or Swipe me 3</div>

  <!-- UI for logging/debugging -->
  <button id="log" onclick="enableLog(event);">Start/Stop event logging</button>
  <button id="clearlog" onclick="clearLog(event);">Clear the log</button>
  <p></p>
  <output></output>
</body>

Miscellaneous functions

These functions support the application but aren't directly involved with the event flow.

Cache management

These functions manage the global event caches evCache1, evCache2 and evCache3.

js
function getCache(ev) {
  // Return the cache for this event's target element
  switch (ev.target.id) {
    case "target1":
      return evCache1;
    case "target2":
      return evCache2;
    case "target3":
      return evCache3;
    default:
      log("Error with cache handling", ev);
  }
}

function pushEvent(ev) {
  // Save this event in the target's cache
  const evCache = getCache(ev);
  evCache.push(ev);
}

function removeEvent(ev) {
  // Remove this event from the target's cache
  const evCache = getCache(ev);
  const index = evCache.findIndex(
    (cachedEv) => cachedEv.pointerId === ev.pointerId,
  );
  evCache.splice(index, 1);
}

Update background color

The background color of the touch areas will change as follows: no active touches is white; one active touch is yellow; two simultaneous touches is pink and three or more simultaneous touches is lightblue.

js
function updateBackground(ev) {
  // Change background color based on the number of simultaneous touches/pointers
  // currently down:
  //   white - target element has no touch points i.e. no pointers down
  //   yellow - one pointer down
  //   pink - two pointers down
  //   lightblue - three or more pointers down
  const evCache = getCache(ev);
  switch (evCache.length) {
    case 0:
      // Target element has no touch points
      ev.target.style.background = "white";
      break;
    case 1:
      // Single touch point
      ev.target.style.background = "yellow";
      break;
    case 2:
      // Two simultaneous touch points
      ev.target.style.background = "pink";
      break;
    default:
      // Three or more simultaneous touches
      ev.target.style.background = "lightblue";
  }
}

Event logging

These functions are used to send event activity to the application window (to support debugging and learning about the event flow).

js
// Log events flag
let logEvents = false;

function enableLog(ev) {
  logEvents = !logEvents;
}

function log(name, ev) {
  const o = document.getElementsByTagName("output")[0];
  const s =
    `${name}:<br>` +
    `  pointerID   = ${ev.pointerId}<br>` +
    `  pointerType = ${ev.pointerType}<br>` +
    `  isPrimary   = ${ev.isPrimary}`;
  o.innerHTML += `${s}<br>`;
}

function clearLog(event) {
  const o = document.getElementsByTagName("output")[0];
  o.innerHTML = "";
}