UNPKG

@thi.ng/rstream-gestures

Version:

Unified mouse, mouse wheel & multi-touch event stream abstraction

187 lines (186 loc) 5.44 kB
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 (let 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 (let 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 };