@thi.ng/rstream-gestures
Version:
Unified mouse, mouse wheel & multi-touch event stream abstraction
187 lines (186 loc) • 5.44 kB
JavaScript
import { isBoolean } from "@thi.ng/checks/is-boolean";
import { isNumber } from "@thi.ng/checks/is-number";
import { clamp } from "@thi.ng/math/interval";
import { fromDOMEvent } from "@thi.ng/rstream/event";
import { __nextID } from "@thi.ng/rstream/idgen";
import { merge } from "@thi.ng/rstream/merge";
import { map } from "@thi.ng/transducers/map";
const START_EVENTS = /* @__PURE__ */ new Set([
"mousedown",
"touchmove",
"touchstart",
"mousemove"
]);
const END_EVENTS = /* @__PURE__ */ new Set(["mouseup", "touchend", "touchcancel"]);
const BASE_EVENTS = ["mousemove", "mousedown", "touchstart", "wheel"];
const EVENT_GESTURETYPES = {
touchstart: "start",
touchmove: "drag",
touchend: "end",
touchcancel: "end",
mousedown: "start",
mouseup: "end",
wheel: "zoom"
};
const gestureStream = (el, _opts) => {
const opts = {
zoom: 1,
absZoom: true,
minZoom: 0.25,
maxZoom: 4,
smooth: 1,
eventOpts: { capture: true },
preventDefault: true,
preventScrollOnZoom: true,
preventContextMenu: true,
local: true,
scale: false,
..._opts
};
opts.id = opts.id || `gestures-${__nextID()}`;
const active = [];
let zoom = clamp(
isNumber(opts.zoom) ? opts.zoom : opts.zoom.deref() || 1,
opts.minZoom,
opts.maxZoom
);
let zoomDelta = 0;
let numTouches = 0;
let lastPos = [0, 0];
let tempStreams;
const isBody = el === document.body;
const tempEvents = [
"touchend",
"touchcancel",
"touchmove",
"mouseup"
];
!isBody && tempEvents.push("mousemove");
opts.preventContextMenu && el.addEventListener("contextmenu", (e) => e.preventDefault());
const gestureStart = (etype, events, bounds, isTouch) => {
const isStart = etype === "mousedown" || etype === "touchstart";
for (const t of events) {
const id = t.identifier || 0;
const pos = __getPos(t, bounds, opts.local, opts.scale);
let touch = active.find((t2) => t2.id === id);
if (!touch && isStart) {
touch = { id, start: pos };
active.push(touch);
numTouches++;
}
if (touch) {
touch.pos = pos;
touch.delta = [
pos[0] - touch.start[0],
pos[1] - touch.start[1]
];
if (isTouch) {
touch.force = t.force;
}
}
}
if (isStart && !tempStreams) {
tempStreams = tempEvents.map(
(id) => __eventSource(document.body, id, opts, "-temp")
);
stream.addAll(tempStreams);
!isBody && stream.removeID("mousemove");
}
};
const gestureEnd = (events) => {
for (const t of events) {
const id = t.identifier || 0;
const idx = active.findIndex((t2) => t2.id === id);
if (idx !== -1) {
active.splice(idx, 1);
numTouches--;
}
}
if (numTouches === 0) {
stream.removeAll(tempStreams);
!isBody && stream.add(__eventSource(el, "mousemove", opts));
tempStreams = void 0;
}
};
const updateZoom = (e) => {
const zdelta = opts.smooth * ("wheelDeltaY" in e ? -e.wheelDeltaY / 120 : e.deltaY / 40);
zoom = opts.absZoom ? clamp(zoom + zdelta, opts.minZoom, opts.maxZoom) : zdelta;
zoomDelta = zdelta;
};
const stream = merge({
id: opts.id,
src: BASE_EVENTS.map((id) => __eventSource(el, id, opts)),
xform: map((e) => {
const etype = e.type;
if (etype === "$zoom") {
zoomDelta = e.value - zoom;
if (opts.absZoom) {
zoom = clamp(zoom + zoomDelta, opts.minZoom, opts.maxZoom);
} else {
zoom = zoomDelta;
}
return {
pos: lastPos.slice(),
buttons: 0,
type: "zoom",
active,
zoom,
zoomDelta,
isTouch: false
};
}
const type = __classifyEventType(etype, !!tempStreams);
let isTouch = !!e.touches;
let events = isTouch ? Array.from(e.changedTouches) : [e];
const bounds = el.getBoundingClientRect();
if (START_EVENTS.has(etype)) {
gestureStart(etype, events, bounds, isTouch);
} else if (END_EVENTS.has(etype)) {
gestureEnd(events);
} else if (type === "zoom") {
updateZoom(e);
}
lastPos = __getPos(events[0], bounds, opts.local, opts.scale);
opts.preventDefault && e.preventDefault();
return {
event: e,
pos: lastPos,
buttons: isTouch ? active.length : e.buttons,
type,
active,
zoom,
zoomDelta,
isTouch
};
})
});
if (!isNumber(opts.zoom)) {
stream.add(opts.zoom.map((x) => ({ type: "$zoom", value: x })));
}
return stream;
};
const __eventSource = (el, type, opts, suffix = "") => {
let eventOpts = opts.eventOpts;
if (type === "wheel" && opts.preventScrollOnZoom) {
eventOpts = isBoolean(eventOpts) ? { capture: eventOpts, passive: false } : { ...eventOpts, passive: false };
}
return fromDOMEvent(el, type, eventOpts, { id: type + suffix });
};
const __classifyEventType = (etype, isActive) => etype === "mousemove" ? isActive ? "drag" : "move" : EVENT_GESTURETYPES[etype];
const __getPos = (e, bounds, isLocal, doScale) => {
let x = e.clientX;
let y = e.clientY;
if (isLocal) {
x -= bounds.left;
y -= bounds.top;
}
if (doScale) {
const dpr = window.devicePixelRatio || 1;
x *= dpr;
y *= dpr;
}
return [x | 0, y | 0];
};
export {
gestureStream
};