mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
/**
|
|
* Telling a click that should put something on the map from the end of a pan,
|
|
* a rotation or a tilt. Both map engines fire their click after short drags
|
|
* and long holds too, which in 3D, where every look around is a drag, kept
|
|
* dropping waypoints nobody meant to set.
|
|
*/
|
|
|
|
/** How far the pointer may travel during a press that still counts as a click. */
|
|
export const TAP_MAX_TRAVEL_PX = 4;
|
|
/** How long a press that still counts as a click may last. */
|
|
export const TAP_MAX_DURATION_MS = 300;
|
|
|
|
/** One finished press on the map. */
|
|
export interface MapPress {
|
|
/** The farthest the first pointer got from where it went down. */
|
|
travelPx: number;
|
|
durationMs: number;
|
|
/** The most pointers down at once: two fingers pinch or rotate, they never place. */
|
|
pointers: number;
|
|
}
|
|
|
|
/**
|
|
* Whether a press places a point. A press that was never seen (a keyboard
|
|
* activation, a synthetic click) is left to the map, which only fires a click
|
|
* for it on purpose.
|
|
*/
|
|
export function isPlacementTap(press: MapPress | null): boolean {
|
|
if (!press) return true;
|
|
return press.pointers <= 1 && press.travelPx < TAP_MAX_TRAVEL_PX && press.durationMs < TAP_MAX_DURATION_MS;
|
|
}
|
|
|
|
export interface PressTracker {
|
|
/** The press that ended last, handed out once: the click it caused reads it, the next one does not. */
|
|
take: () => MapPress | null;
|
|
dispose: () => void;
|
|
}
|
|
|
|
interface Active {
|
|
id: number;
|
|
x: number;
|
|
y: number;
|
|
at: number;
|
|
travelPx: number;
|
|
pointers: Set<number>;
|
|
most: number;
|
|
cancelled: boolean;
|
|
}
|
|
|
|
/**
|
|
* Follows the presses on an element in the capture phase, so neither engine
|
|
* can swallow them first, and keeps the last finished one for the click that
|
|
* follows it.
|
|
*/
|
|
export function createPressTracker(target: EventTarget, now: () => number = () => performance.now()): PressTracker {
|
|
let active: Active | null = null;
|
|
let last: MapPress | null = null;
|
|
|
|
const down = (event: Event) => {
|
|
const e = event as PointerEvent;
|
|
if (active) {
|
|
active.pointers.add(e.pointerId);
|
|
active.most = Math.max(active.most, active.pointers.size);
|
|
return;
|
|
}
|
|
last = null;
|
|
active = {
|
|
id: e.pointerId,
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
at: now(),
|
|
travelPx: 0,
|
|
pointers: new Set([e.pointerId]),
|
|
most: 1,
|
|
cancelled: false,
|
|
};
|
|
};
|
|
const move = (event: Event) => {
|
|
const e = event as PointerEvent;
|
|
if (!active || e.pointerId !== active.id) return;
|
|
active.travelPx = Math.max(active.travelPx, Math.hypot(e.clientX - active.x, e.clientY - active.y));
|
|
};
|
|
const up = (event: Event) => {
|
|
const e = event as PointerEvent;
|
|
if (!active) return;
|
|
if (e.pointerId === active.id && !active.cancelled) {
|
|
move(e);
|
|
}
|
|
if (event.type === 'pointercancel') active.cancelled = true;
|
|
active.pointers.delete(e.pointerId);
|
|
if (active.pointers.size > 0) return;
|
|
last = {
|
|
travelPx: active.cancelled ? Number.POSITIVE_INFINITY : active.travelPx,
|
|
durationMs: now() - active.at,
|
|
pointers: active.most,
|
|
};
|
|
active = null;
|
|
};
|
|
|
|
const options = { capture: true, passive: true } as const;
|
|
target.addEventListener('pointerdown', down, options);
|
|
target.addEventListener('pointermove', move, options);
|
|
target.addEventListener('pointerup', up, options);
|
|
target.addEventListener('pointercancel', up, options);
|
|
|
|
return {
|
|
take: () => {
|
|
const press = last;
|
|
last = null;
|
|
return press;
|
|
},
|
|
dispose: () => {
|
|
target.removeEventListener('pointerdown', down, options);
|
|
target.removeEventListener('pointermove', move, options);
|
|
target.removeEventListener('pointerup', up, options);
|
|
target.removeEventListener('pointercancel', up, options);
|
|
},
|
|
};
|
|
}
|