UNPKG

@motion-core/motion-gpu

Version:

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

292 lines (291 loc) 10 kB
import { createCurrentWritable } from "../core/current-value.js"; import { createInitialPointerState, getPointerCoordinates, getPointerNowSeconds, normalizePointerKind, resolvePointerFrameRequestMode } from "../core/pointer.js"; import { useMotionGPU } from "./motiongpu-context.js"; import { onMount } from "svelte"; //#region src/lib/svelte/use-pointer.ts /** * Normalizes click button configuration with a primary-button fallback. */ function normalizeClickButtons(buttons) { const source = buttons && buttons.length > 0 ? buttons : [0]; return new Set(source); } /** * Resolves a valid click duration threshold in milliseconds. */ function resolveClickMaxDurationMs(value) { if (typeof value !== "number" || Number.isNaN(value) || value <= 0) return 350; return value; } /** * Resolves a valid click travel threshold in pixels. */ function resolveClickMaxMovePx(value) { if (typeof value !== "number" || Number.isNaN(value) || value < 0) return 8; return value; } /** * Tracks normalized pointer coordinates and click/tap snapshots for the active `FragCanvas`. */ function usePointer(options = {}) { const motiongpu = useMotionGPU(); const pointerState = createCurrentWritable(createInitialPointerState()); const lastClick = createCurrentWritable(null); const enabled = options.enabled ?? true; const requestFrameMode = options.requestFrame ?? "auto"; const capturePointer = options.capturePointer ?? true; const trackOutside = options.trackWhilePressedOutsideCanvas ?? true; const clickEnabled = options.clickEnabled ?? true; const clickMaxDurationMs = resolveClickMaxDurationMs(options.clickMaxDurationMs); const clickMaxMovePx = resolveClickMaxMovePx(options.clickMaxMovePx); const clickButtons = normalizeClickButtons(options.clickButtons); let activePointerId = null; let downSnapshot = null; let clickCounter = 0; let previousPx = null; let previousUv = null; let previousTimeSeconds = 0; const requestFrame = () => { const mode = resolvePointerFrameRequestMode(requestFrameMode, motiongpu.renderMode.current); if (mode === "invalidate") { motiongpu.invalidate(); return; } if (mode === "advance") motiongpu.advance(); }; /** * Commits a full pointer state snapshot with computed delta and velocity vectors. */ 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 }; pointerState.set(nextState); previousPx = input.point.px; previousUv = input.point.uv; previousTimeSeconds = nowSeconds; requestFrame(); return nextState; }; /** * Updates only the `inside` flag while keeping the latest pointer coordinates. */ const updateInsideState = (inside) => { const nextState = { ...pointerState.current, inside, time: getPointerNowSeconds(), deltaPx: [0, 0], deltaUv: [0, 0], velocityPx: [0, 0], velocityUv: [0, 0] }; pointerState.set(nextState); requestFrame(); return nextState; }; /** * Checks whether an event belongs to the active tracked pointer. */ const isTrackedPointer = (event) => activePointerId === null || event.pointerId === activePointerId; onMount(() => { if (!enabled) return; const canvas = motiongpu.canvas; if (!canvas) return; const handlePointerDown = (event) => { 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 (capturePointer) try { canvas.setPointerCapture(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 handleMove = (event) => { if (!isTrackedPointer(event)) return; 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; let dragging = false; if (pressed && downPx) { const dx = point.px[0] - downPx[0]; const dy = point.px[1] - downPx[1]; dragging = Math.hypot(dx, dy) > 0; } 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 handleWindowMove = (event) => { if (!trackOutside || 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 dx = point.px[0] - downPx[0]; const dy = point.px[1] - downPx[1]; const nextState = updatePointerState({ point, inside: false, pressed: true, dragging: Math.hypot(dx, dy) > 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 (activePointerId === null || event.pointerId !== activePointerId) return; const point = getPointerCoordinates(event.clientX, event.clientY, canvas.getBoundingClientRect()); const pointerType = downSnapshot?.pointerType ?? normalizePointerKind(event.pointerType); const previous = downSnapshot; 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); if (capturePointer && canvas.hasPointerCapture(event.pointerId)) try { canvas.releasePointerCapture(event.pointerId); } catch {} if (emitClick && clickEnabled && previous && clickButtons.has(previous.button)) { const durationMs = getPointerNowSeconds() * 1e3 - previous.timeMs; const dx = point.px[0] - previous.px[0]; const dy = point.px[1] - previous.px[1]; const moveDistance = Math.hypot(dx, dy); if (previous.inside && point.inside && durationMs <= clickMaxDurationMs && moveDistance <= 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) return; updateInsideState(false); }; canvas.addEventListener("pointerdown", handlePointerDown); canvas.addEventListener("pointermove", handleMove); canvas.addEventListener("pointerup", handlePointerUp); canvas.addEventListener("pointercancel", handlePointerCancel); canvas.addEventListener("pointerleave", handlePointerLeave); if (trackOutside) { window.addEventListener("pointermove", handleWindowMove); window.addEventListener("pointerup", handlePointerUp); window.addEventListener("pointercancel", handlePointerCancel); } return () => { canvas.removeEventListener("pointerdown", handlePointerDown); canvas.removeEventListener("pointermove", handleMove); canvas.removeEventListener("pointerup", handlePointerUp); canvas.removeEventListener("pointercancel", handlePointerCancel); canvas.removeEventListener("pointerleave", handlePointerLeave); if (trackOutside) { window.removeEventListener("pointermove", handleWindowMove); window.removeEventListener("pointerup", handlePointerUp); window.removeEventListener("pointercancel", handlePointerCancel); } }; }); return { state: pointerState, lastClick, resetClick: () => { lastClick.set(null); } }; } //#endregion export { usePointer }; //# sourceMappingURL=use-pointer.js.map