Frontier Software

Event Driven Programming

An event can be defined as “a significant change in state”. — Event-driven architecture

Using sprites from Warlock’s Gauntlet

Event Loop

aka message dispatcher, message loop, message pump, or run loop

Recursively calling JavaScript’s requestAnimationFrame seems the standard way to write these:

function eventLoop() {
  ...
  window.requestAnimationFrame(eventLoop);
}

document.addEventListener("DOMContentLoaded", eventLoop);

Event Handlers

Event Listeners

In addition to writing the event handlers, event handlers also need to be bound to events so that the correct function is called when the event takes place. — Wikipedia

EventTarget.addEventListener(type, handler)

Event Types

Window events

resize

Pointer Events

At time of writing there are 11 pointer events.

Properties common to all of these include:

1. pointerdown

For input devices that do not support hover, a user agent MUST also fire a pointer event named pointerover followed by a pointer event named pointerenter prior to dispatching the pointerdown event. — W3 spec

2. pointerup
3. pointermove
4. pointercancel

On smartphones, this is fired after pointerdown to free the browser to respond to users pinching to change size etc. To overide, Mozilla’s drawing example includes in the css for the canvas touch-action: none;

In the multi-touch interaction example, it is given the same handler as pointerup.

After the pointercancel event is fired, the browser will also send pointerout followed by pointerleave.

5. pointerout
6. pointerleave

As far as I understand the W3 spec, this duplication with pointerout is for compatibility with mouseleave.

7. pointerover

When a user touches a touchscreen (ie doesn’t support hover) the following three events are fired:

  1. pointerover
  2. pointerenter
  3. pointerdown
8. pointerenter

This event type is similar to pointerover, but differs in that it does not bubble. — W3 spec

9. pointerrawupdate

Experimental, constantly sent by Samsung but not by Firefox. No idea what it’s for.

10. gotpointercapture
11. lostpointercapture

Drawing Example

The touch-action property is set to none to prevent the browser from applying its default touch behavior to the application.

canvas {
  border: solid black 1px;
  touch-action: none;
  display: block;
}
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const container = document.getElementById("log");

// Mapping from the pointerId to the current finger position
const ongoingTouches = new Map();
const colors = ["red", "green", "blue"];

document.getElementById("clear").addEventListener("click", () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
});

function log(msg) {
  container.textContent = `${msg} \n${container.textContent}`;
}

function over_handler(event) {
//  log(`over_handler ${event.offsetX} ${event.offsetY}`);
}

function enter_handler(event) {
//  log(`enter_handler ${event.offsetX} ${event.offsetY}`);
}

function down_handler(event) {
  log(`Down pointerId ${event.pointerId} width ${event.width} height ${event.height} twist ${event.twist} tiltX ${event.tiltX} tiltY ${event.tiltY}`);
  const touch = {
    offsetX: event.offsetX,
    offsetY: event.offsetY,
    color: colors[ongoingTouches.size % colors.length],
  };
  ongoingTouches.set(event.pointerId, touch);

  ctx.beginPath();
  ctx.arc(touch.offsetX, touch.offsetY, 4, 0, 2 * Math.PI, false);
  ctx.fillStyle = touch.color;
  ctx.fill();
}

function move_handler(event) {
//  log(`Move pointerId ${event.pointerId} width ${event.width} height ${event.height} twist ${event.twist} tiltX ${event.tiltX} tiltY ${event.tiltY}`);
  const touch = ongoingTouches.get(event.pointerId);

  // Event was not started
  if (!touch) {
    return;
  }

  ctx.beginPath();
  ctx.moveTo(touch.pageX, touch.pageY);
  ctx.lineTo(event.pageX, event.pageY);
  ctx.lineWidth = 4;
  ctx.strokeStyle = touch.color;
  ctx.stroke();

  const newTouch = {
    pageX: event.pageX,
    pageY: event.pageY,
    color: touch.color,
  };

  ongoingTouches.set(event.pointerId, newTouch);
}

function up_handler(event) {
  log(`Up pointerId ${event.pointerId} width ${event.width} height ${event.height} twist ${event.twist} tiltX ${event.tiltX} tiltY ${event.tiltY}`);
  const touch = ongoingTouches.get(event.pointerId);

  if (!touch) {
    console.error(`End: Could not find touch ${event.pointerId}`);
    return;
  }

  ctx.lineWidth = 4;
  ctx.fillStyle = touch.color;
  ctx.beginPath();
  ctx.moveTo(touch.pageX, touch.pageY);
  ctx.lineTo(event.pageX, event.pageY);
  ctx.fillRect(event.pageX - 4, event.pageY - 4, 8, 8);
  ongoingTouches.delete(event.pointerId);
}

function cancel_handler(event) {
  log(`Cancel pointerId ${event.pointerId} width ${event.width} height ${event.height} twist ${event.twist} tiltX ${event.tiltX} tiltY ${event.tiltY}`);
  const touch = ongoingTouches.get(event.pointerId);

  if (!touch) {
    log(`Cancel: Could not find touch ${event.pointerId}`);
    return;
  }

  ongoingTouches.delete(event.pointerId);
}

function out_handler(event) {
//  log(`out_handler ${event.offsetX} ${event.offsetY}`);
}

function leave_handler(event) {
//  log(`leave_handler ${event.offsetX} ${event.offsetY}`);
}

function rawupdate_handler(event) {
//  log(`rawupdate_handler ${event.offsetX} ${event.offsetY}`);
}

function gotcapture_handler(event) {
//  log(`gotcapture_handler ${event.offsetX} ${event.offsetY}`);
}

function lostcapture_handler(event) {
//  log(`lostcapture_handler ${event.offsetX} ${event.offsetY}`);
}

canvas.addEventListener("pointerover", over_handler);
canvas.addEventListener("pointerenter", enter_handler);
canvas.addEventListener("pointerdown", down_handler);
canvas.addEventListener("pointermove", move_handler);
canvas.addEventListener("pointerup", up_handler);
canvas.addEventListener("pointercancel", cancel_handler);
canvas.addEventListener("pointerout", out_handler);
canvas.addEventListener("pointerleave", leave_handler);
canvas.addEventListener("pointerrawupdate", rawupdate_handler);
canvas.addEventListener("gotpointercapture", gotcapture_handler);
canvas.addEventListener("lostpointercapture", lostcapture_handler);

index.html

<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Touch Event Tutorial</title>
    <link rel="stylesheet" href="./css/main.css" />
  </head>
  <body>
    <canvas id="canvas" width="600" height="600" style="border:solid black 1px;">
Your browser does not support canvas element.
    </canvas>
    <br />
Log:
    <pre id="log" style="border: 1px solid #ccc;"></pre>
    <script src="./js/main.js"></script>
  </body>
</html>

A lesson I learnt here was the names of the js and css files need to be changed to get the smartphone browser to use the updated version.

Multi-touch interaction

This example uses the same handler for pointerup, pointercancel, pointerout and pointerleave.

Gestures

Of the above pointer events, pointerdown and pointerup are common to both mouse and touchscreen and seem to be the best to simplify the UI to. pointermove could possibly be used to drag objects selected by pointerdown.

There are pointer events which inherit everything from mouse events to avoid having to use touch events which only work on mobile device, not with a mouse.

Touch events are typically available on devices with a touch screen, but many browsers make the touch events API unavailable on all desktop devices, even those with touch screens.

There is a choice of 4 coordinate systems, each with their own X and Y value.

1. Offset

This is relative to the EventTarget (eg canvas). This seems usually the best choice to me. This is not offered for touch events, only mouse and pointer have offsetX and offsetY for some reason.

2. Client aka viewport

Relative to the browser window and scrolling the document changes the viewport coordinates of a given position within the document.

clientX and clientY is returned by pointer, mouse and touch events, and has the shorthand mouse.x, mouse.y …

3. Page

Relative to the document, meaning a point in an element within the document will have the same coordinates after the user scrolls horizontally or vertically.

This is used in Mozilla’s touch events example.

4. Screen

Relative to the user’s screen.

Click

click fires after both the mousedown and mouseup events have fired, in that order.

Pointer vs Mouse

altKey, ctrlKey, metaKey, shiftKey, button and buttons are all lost on

PointerEvent
├── clientX (alias x, viewport coordiantes)
├── clientY (alias y)
├── offsetX (relative to say canvas, one I use)
├── offsetY (relative to say canvas, one I use)
├── pageX (relative to document)
├── pointerType
│   ├── "mouse"
│   ├── "pen"
│   └── "mouse"
Pointer Touch Mouse
clientX clientX clientX
clientY clientY clientY
offsetX ? offsetX
offsetY ? offsetY
pointerdown touchstart
pointerup touchend
pointercancel touchcancel
touchmove