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 send to 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 = "";
}
```