Event Driven Programming
Event
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
Pointer Events
At time of writing there are 11 pointer events, some of which behave differently for with hover and without hover screens.
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
- pointing device is moved out of the hit test boundaries of an element.
- firing the pointerup event for a device that does not support hover.
- after firing the pointercancel event.
6. pointerleave
- pointing device is moved out of the hit test boundaries of an element (same as pointerout?)
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:
- pointerover
- pointerenter
- 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 |