UNPKG

@motion-core/motion-gpu

Version:

Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte, React, and Vue adapter entrypoints.

419 lines (418 loc) 13.8 kB
import { createCurrentWritable } from "./current-value.js"; //#region src/lib/core/pointer.ts /** * Returns a monotonic timestamp in seconds. */ function getPointerNowSeconds() { if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now() / 1e3; return Date.now() / 1e3; } /** * Creates the initial pointer state snapshot. */ function createInitialPointerState() { return { px: [0, 0], uv: [0, 0], ndc: [-1, -1], inside: false, pressed: false, dragging: false, pointerType: null, pointerId: null, button: null, buttons: 0, time: getPointerNowSeconds(), downPx: null, downUv: null, deltaPx: [0, 0], deltaUv: [0, 0], velocityPx: [0, 0], velocityUv: [0, 0] }; } /** * Converts client coordinates to canvas-relative pointer coordinates. */ function getPointerCoordinates(clientX, clientY, rect) { const width = Math.max(rect.width, 1); const height = Math.max(rect.height, 1); const nx = (clientX - rect.left) / width; const ny = (clientY - rect.top) / height; const pxX = clientX - rect.left; const pxY = clientY - rect.top; const uvX = nx; const uvY = 1 - ny; return { px: [pxX, pxY], uv: [uvX, uvY], ndc: [nx * 2 - 1, uvY * 2 - 1], inside: nx >= 0 && nx <= 1 && ny >= 0 && ny <= 1 }; } /** * Resolves frame wake-up strategy for pointer-driven updates. */ function resolvePointerFrameRequestMode(mode, renderMode) { if (mode !== "auto") return mode; if (renderMode === "manual") return "advance"; if (renderMode === "on-demand") return "invalidate"; return "none"; } /** * Normalizes unknown pointer kind values to the public `PointerKind`. */ function normalizePointerKind(pointerType) { if (pointerType === "mouse" || pointerType === "pen" || pointerType === "touch") return pointerType; return "mouse"; } function resolveClickMaxDurationMs(value) { if (typeof value !== "number" || Number.isNaN(value) || value <= 0) return 350; return value; } function resolveClickMaxMovePx(value) { if (typeof value !== "number" || Number.isNaN(value) || value < 0) return 8; return value; } function normalizeClickButtons(buttons) { const source = buttons && buttons.length > 0 ? buttons : [0]; return new Set(source); } /** * Creates the shared pointer controller used by every framework adapter. */ function createPointerController(runtime, initialOptions = {}) { const state = createCurrentWritable(createInitialPointerState()); const lastClick = createCurrentWritable(null); let options = initialOptions; let canvas = null; let windowTarget = null; let canvasListenersAttached = false; let windowMoveListenerAttached = false; let windowReleaseListenersAttached = false; let activePointerId = null; let capturedPointerId = null; let downSnapshot = null; let clickCounter = 0; let previousPx = null; let previousUv = null; let previousTimeSeconds = 0; const handledCanvasMoveEvents = /* @__PURE__ */ new WeakSet(); const isEnabled = () => options.enabled ?? true; const shouldTrackOutside = () => options.trackWhilePressedOutsideCanvas ?? true; const requestFrame = () => { const mode = resolvePointerFrameRequestMode(options.requestFrame ?? "auto", runtime.getRenderMode()); if (mode === "invalidate") { runtime.invalidate(); return; } if (mode === "advance") runtime.advance(); }; const updatePointerState = (input) => { const nowSeconds = getPointerNowSeconds(); const dt = previousTimeSeconds > 0 ? Math.max(nowSeconds - previousTimeSeconds, 1e-6) : 0; const deltaPx = input.resetDelta || !previousPx ? [0, 0] : [input.point.px[0] - previousPx[0], input.point.px[1] - previousPx[1]]; const deltaUv = input.resetDelta || !previousUv ? [0, 0] : [input.point.uv[0] - previousUv[0], input.point.uv[1] - previousUv[1]]; const velocityPx = dt > 0 ? [deltaPx[0] / dt, deltaPx[1] / dt] : [0, 0]; const velocityUv = dt > 0 ? [deltaUv[0] / dt, deltaUv[1] / dt] : [0, 0]; const nextState = { px: input.point.px, uv: input.point.uv, ndc: input.point.ndc, inside: input.inside, pressed: input.pressed, dragging: input.dragging, pointerType: input.pointerType, pointerId: input.pointerId, button: input.button, buttons: input.buttons, time: nowSeconds, downPx: input.downPx, downUv: input.downUv, deltaPx, deltaUv, velocityPx, velocityUv }; state.set(nextState); previousPx = input.point.px; previousUv = input.point.uv; previousTimeSeconds = nowSeconds; requestFrame(); return nextState; }; const updateInsideState = (inside) => { const current = state.current; state.set({ ...current, inside, time: getPointerNowSeconds(), deltaPx: [0, 0], deltaUv: [0, 0], velocityPx: [0, 0], velocityUv: [0, 0] }); requestFrame(); }; const releaseCapture = () => { if (!canvas || capturedPointerId === null) { capturedPointerId = null; return; } try { if (typeof canvas.hasPointerCapture !== "function" || canvas.hasPointerCapture(capturedPointerId)) canvas.releasePointerCapture(capturedPointerId); } catch {} capturedPointerId = null; }; const resetTracking = (emitFrame) => { releaseCapture(); activePointerId = null; downSnapshot = null; previousPx = null; previousUv = null; previousTimeSeconds = 0; const current = state.current; if (!(current.inside || current.pressed || current.dragging || current.pointerId !== null || current.button !== null || current.buttons !== 0 || current.downPx !== null || current.downUv !== null)) return; state.set({ ...current, inside: false, pressed: false, dragging: false, pointerId: null, button: null, buttons: 0, time: getPointerNowSeconds(), downPx: null, downUv: null, deltaPx: [0, 0], deltaUv: [0, 0], velocityPx: [0, 0], velocityUv: [0, 0] }); if (emitFrame) requestFrame(); }; const handlePointerDown = (event) => { if (!canvas || activePointerId !== null) return; const point = getPointerCoordinates(event.clientX, event.clientY, canvas.getBoundingClientRect()); const pointerType = normalizePointerKind(event.pointerType); activePointerId = event.pointerId; downSnapshot = { pointerId: event.pointerId, pointerType, button: event.button, timeMs: getPointerNowSeconds() * 1e3, px: point.px, uv: point.uv, inside: point.inside }; if (options.capturePointer ?? true) try { canvas.setPointerCapture(event.pointerId); capturedPointerId = event.pointerId; } catch {} const nextState = updatePointerState({ point, inside: point.inside, pressed: true, dragging: false, pointerType, pointerId: event.pointerId, button: event.button, buttons: event.buttons, downPx: point.px, downUv: point.uv, resetDelta: true }); options.onDown?.(nextState, event); }; const handlePointerMove = (event) => { if (!canvas || activePointerId !== null && event.pointerId !== activePointerId) return; handledCanvasMoveEvents.add(event); const point = getPointerCoordinates(event.clientX, event.clientY, canvas.getBoundingClientRect()); const pressed = activePointerId !== null && event.pointerId === activePointerId; const downPx = pressed ? downSnapshot?.px ?? point.px : null; const downUv = pressed ? downSnapshot?.uv ?? point.uv : null; const dragging = pressed && downPx ? Math.hypot(point.px[0] - downPx[0], point.px[1] - downPx[1]) > 0 : false; const nextState = updatePointerState({ point, inside: point.inside, pressed, dragging, pointerType: normalizePointerKind(event.pointerType), pointerId: event.pointerId, button: pressed ? downSnapshot?.button ?? event.button : null, buttons: event.buttons, downPx, downUv }); options.onMove?.(nextState, event); }; const handleWindowPointerMove = (event) => { if (handledCanvasMoveEvents.has(event) || !canvas || activePointerId === null || event.pointerId !== activePointerId) return; const point = getPointerCoordinates(event.clientX, event.clientY, canvas.getBoundingClientRect()); if (point.inside) return; const downPx = downSnapshot?.px ?? point.px; const downUv = downSnapshot?.uv ?? point.uv; const nextState = updatePointerState({ point, inside: false, pressed: true, dragging: Math.hypot(point.px[0] - downPx[0], point.px[1] - downPx[1]) > 0, pointerType: downSnapshot?.pointerType ?? normalizePointerKind(event.pointerType), pointerId: event.pointerId, button: downSnapshot?.button ?? event.button, buttons: event.buttons, downPx, downUv }); options.onMove?.(nextState, event); }; const releasePointer = (event, emitClick) => { if (!canvas || activePointerId === null || event.pointerId !== activePointerId) return; const point = getPointerCoordinates(event.clientX, event.clientY, canvas.getBoundingClientRect()); const previous = downSnapshot; const pointerType = previous?.pointerType ?? normalizePointerKind(event.pointerType); const nextState = updatePointerState({ point, inside: point.inside, pressed: false, dragging: false, pointerType, pointerId: null, button: null, buttons: event.buttons, downPx: null, downUv: null }); options.onUp?.(nextState, event); releaseCapture(); if (emitClick && (options.clickEnabled ?? true) && previous) { if (normalizeClickButtons(options.clickButtons).has(previous.button)) { const durationMs = getPointerNowSeconds() * 1e3 - previous.timeMs; const moveDistance = Math.hypot(point.px[0] - previous.px[0], point.px[1] - previous.px[1]); if (previous.inside && point.inside && durationMs <= resolveClickMaxDurationMs(options.clickMaxDurationMs) && moveDistance <= resolveClickMaxMovePx(options.clickMaxMovePx)) { clickCounter += 1; const click = { id: clickCounter, time: getPointerNowSeconds(), pointerType, pointerId: event.pointerId, button: previous.button, modifiers: { alt: event.altKey, ctrl: event.ctrlKey, shift: event.shiftKey, meta: event.metaKey }, px: point.px, uv: point.uv, ndc: point.ndc }; lastClick.set(click); options.onClick?.(click, nextState, event); requestFrame(); } } } activePointerId = null; downSnapshot = null; }; const handlePointerUp = (event) => { releasePointer(event, true); }; const handlePointerCancel = (event) => { releasePointer(event, false); }; const handlePointerLeave = () => { if (activePointerId === null) updateInsideState(false); }; const attachWindowMoveListener = () => { if (windowMoveListenerAttached || !windowTarget || !shouldTrackOutside()) return; windowTarget.addEventListener("pointermove", handleWindowPointerMove); windowMoveListenerAttached = true; }; const detachWindowMoveListener = () => { if (!windowMoveListenerAttached || !windowTarget) return; windowTarget.removeEventListener("pointermove", handleWindowPointerMove); windowMoveListenerAttached = false; }; const attachWindowReleaseListeners = () => { if (windowReleaseListenersAttached || !windowTarget) return; windowTarget.addEventListener("pointerup", handlePointerUp); windowTarget.addEventListener("pointercancel", handlePointerCancel); windowReleaseListenersAttached = true; }; const detachWindowReleaseListeners = () => { if (!windowReleaseListenersAttached || !windowTarget) return; windowTarget.removeEventListener("pointerup", handlePointerUp); windowTarget.removeEventListener("pointercancel", handlePointerCancel); windowReleaseListenersAttached = false; }; const attachCanvasListeners = () => { if (canvasListenersAttached || !canvas || !isEnabled()) return; canvas.addEventListener("pointerdown", handlePointerDown); canvas.addEventListener("pointermove", handlePointerMove); canvas.addEventListener("pointerup", handlePointerUp); canvas.addEventListener("pointercancel", handlePointerCancel); canvas.addEventListener("pointerleave", handlePointerLeave); canvasListenersAttached = true; attachWindowReleaseListeners(); attachWindowMoveListener(); }; const detachCanvasListeners = () => { detachWindowMoveListener(); detachWindowReleaseListeners(); if (!canvasListenersAttached || !canvas) return; canvas.removeEventListener("pointerdown", handlePointerDown); canvas.removeEventListener("pointermove", handlePointerMove); canvas.removeEventListener("pointerup", handlePointerUp); canvas.removeEventListener("pointercancel", handlePointerCancel); canvas.removeEventListener("pointerleave", handlePointerLeave); canvasListenersAttached = false; }; const destroy = () => { detachCanvasListeners(); resetTracking(false); canvas = null; windowTarget = null; }; return { state, lastClick, resetClick() { lastClick.set(null); }, mount(nextCanvas) { if (canvas === nextCanvas) { attachCanvasListeners(); return; } destroy(); canvas = nextCanvas; windowTarget = nextCanvas.ownerDocument.defaultView; attachCanvasListeners(); }, updateOptions(nextOptions) { const wasEnabled = isEnabled(); const wasTrackingOutside = shouldTrackOutside(); options = nextOptions; const enabled = isEnabled(); const trackingOutside = shouldTrackOutside(); if (wasEnabled && !enabled) { detachCanvasListeners(); resetTracking(true); return; } if (!wasEnabled && enabled) { attachCanvasListeners(); return; } if (!enabled || wasTrackingOutside === trackingOutside) return; if (trackingOutside) attachWindowMoveListener(); else detachWindowMoveListener(); }, destroy }; } //#endregion export { createInitialPointerState, createPointerController, getPointerCoordinates, getPointerNowSeconds, normalizePointerKind, resolvePointerFrameRequestMode }; //# sourceMappingURL=pointer.js.map