UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

262 lines (261 loc) • 9.72 kB
import * as React from "react"; import { tlenv } from "../globals/environment.mjs"; import { Vec } from "../primitives/Vec.mjs"; import { preventDefault } from "../utils/dom.mjs"; import { isAccelKey } from "../utils/keyboard.mjs"; import { normalizeWheel } from "../utils/normalizeWheel.mjs"; import { useEditor } from "./useEditor.mjs"; function useGestureEvents(ref) { const editor = useEditor(); React.useEffect(() => { const elm = ref.current; if (!elm) return; let pinchState = "not sure"; function onWheel(event) { if (!editor.getInstanceState().isFocused) { return; } pinchState = "not sure"; const editingShapeId = editor.getEditingShapeId(); if (editingShapeId) { const shape = editor.getShape(editingShapeId); if (shape) { const util = editor.getShapeUtil(shape); if (util.canScroll(shape)) { const bounds = editor.getShapePageBounds(editingShapeId); if (bounds?.containsPoint(editor.inputs.getCurrentPagePoint())) { return; } } } } preventDefault(event); event.stopPropagation(); const delta = normalizeWheel(event); if (delta.x === 0 && delta.y === 0) return; const info = { type: "wheel", name: "wheel", delta, point: new Vec(event.clientX, event.clientY), shiftKey: event.shiftKey, altKey: event.altKey, ctrlKey: event.metaKey || event.ctrlKey, metaKey: event.metaKey, accelKey: isAccelKey(event) }; editor.dispatch(info); } let initDistanceBetweenFingers = 1; let initZoom = 1; let currDistanceBetweenFingers = 0; const initPointBetweenFingers = new Vec(); const prevPointBetweenFingers = new Vec(); let activeTouches = []; function getScaleBounds() { const baseZoom = editor.getBaseZoom(); const { zoomSteps, zoomSpeed } = editor.getCameraOptions(); const zoomMin = zoomSteps[0] * baseZoom; const zoomMax = zoomSteps[zoomSteps.length - 1] * baseZoom; return { min: zoomMin ** (1 / zoomSpeed), max: zoomMax ** (1 / zoomSpeed) }; } function getScaleFrom() { const { zoomSpeed } = editor.getCameraOptions(); return editor.getZoomLevel() ** (1 / zoomSpeed); } let scaleOffset = 1; let initScaleFrom = 1; function updatePinchState(isSafariTrackpadPinch) { if (isSafariTrackpadPinch) { pinchState = "zooming"; } if (pinchState === "zooming") { return; } const touchDistance = Math.abs(currDistanceBetweenFingers - initDistanceBetweenFingers); const originDistance = Vec.Dist(initPointBetweenFingers, prevPointBetweenFingers); switch (pinchState) { case "not sure": { if (touchDistance > 24) { pinchState = "zooming"; } else if (originDistance > 16) { pinchState = "panning"; } break; } case "panning": { if (touchDistance > 64) { pinchState = "zooming"; } break; } } } function dispatchPinchEvent(name, origin, delta, zoom, event) { editor.dispatch({ type: "pinch", name, point: { x: origin.x, y: origin.y, z: zoom }, delta, shiftKey: event.shiftKey, altKey: event.altKey, ctrlKey: event.metaKey || event.ctrlKey, metaKey: event.metaKey, accelKey: isAccelKey(event) }); } function getOriginAndDistance(t0, t1) { const origin = { x: (t0.clientX + t1.clientX) / 2, y: (t0.clientY + t1.clientY) / 2 }; const distance = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY); return { origin, distance }; } function onTouchStart(event) { if (!(event.target === elm || elm?.contains(event.target))) return; activeTouches = Array.from(event.touches); if (activeTouches.length === 2) { pinchState = "not sure"; const { origin, distance } = getOriginAndDistance(activeTouches[0], activeTouches[1]); prevPointBetweenFingers.x = origin.x; prevPointBetweenFingers.y = origin.y; initPointBetweenFingers.x = origin.x; initPointBetweenFingers.y = origin.y; initDistanceBetweenFingers = Math.max(distance, 1); currDistanceBetweenFingers = distance; initZoom = editor.getZoomLevel(); initScaleFrom = getScaleFrom(); scaleOffset = initScaleFrom; dispatchPinchEvent("pinch_start", origin, { x: 0, y: 0 }, editor.getZoomLevel(), event); } } function onTouchMove(event) { activeTouches = Array.from(event.touches); if (activeTouches.length < 2) return; const { origin, distance } = getOriginAndDistance(activeTouches[0], activeTouches[1]); currDistanceBetweenFingers = distance; const dx = origin.x - prevPointBetweenFingers.x; const dy = origin.y - prevPointBetweenFingers.y; prevPointBetweenFingers.x = origin.x; prevPointBetweenFingers.y = origin.y; updatePinchState(false); const bounds = getScaleBounds(); const rawScale = initScaleFrom * (distance / initDistanceBetweenFingers); scaleOffset = Math.min(bounds.max, Math.max(bounds.min, rawScale)); switch (pinchState) { case "zooming": { const currZoom = scaleOffset ** editor.getCameraOptions().zoomSpeed; dispatchPinchEvent("pinch", origin, { x: dx, y: dy }, currZoom, event); break; } case "panning": { dispatchPinchEvent("pinch", origin, { x: dx, y: dy }, initZoom, event); break; } } } function onTouchEnd(event) { const wasPinching = activeTouches.length >= 2; activeTouches = Array.from(event.touches); if (wasPinching && activeTouches.length < 2) { const scale = scaleOffset ** editor.getCameraOptions().zoomSpeed; const origin = { ...prevPointBetweenFingers }; pinchState = "not sure"; editor.timers.requestAnimationFrame(() => { dispatchPinchEvent("pinch_end", origin, { x: origin.x, y: origin.y }, scale, event); }); } } let safariGestureInitialScale = 1; function onGestureStart(event) { const e = event; if (!(e.target === elm || elm?.contains(e.target))) return; preventDefault(e); e.stopPropagation(); pinchState = "not sure"; safariGestureInitialScale = getScaleFrom(); scaleOffset = safariGestureInitialScale; initZoom = editor.getZoomLevel(); prevPointBetweenFingers.x = e.clientX; prevPointBetweenFingers.y = e.clientY; initPointBetweenFingers.x = e.clientX; initPointBetweenFingers.y = e.clientY; initDistanceBetweenFingers = 1; currDistanceBetweenFingers = 1; dispatchPinchEvent( "pinch_start", { x: e.clientX, y: e.clientY }, { x: 0, y: 0 }, editor.getZoomLevel(), e ); } function onGestureChange(event) { const e = event; if (!(e.target === elm || elm?.contains(e.target))) return; preventDefault(e); e.stopPropagation(); const dx = e.clientX - prevPointBetweenFingers.x; const dy = e.clientY - prevPointBetweenFingers.y; prevPointBetweenFingers.x = e.clientX; prevPointBetweenFingers.y = e.clientY; const bounds = getScaleBounds(); const rawScale = safariGestureInitialScale * e.scale; scaleOffset = Math.min(bounds.max, Math.max(bounds.min, rawScale)); currDistanceBetweenFingers = e.scale * initDistanceBetweenFingers; updatePinchState(true); const currZoom = scaleOffset ** editor.getCameraOptions().zoomSpeed; dispatchPinchEvent("pinch", { x: e.clientX, y: e.clientY }, { x: dx, y: dy }, currZoom, e); } function onGestureEnd(event) { const e = event; if (!(e.target === elm || elm?.contains(e.target))) return; preventDefault(e); e.stopPropagation(); const scale = scaleOffset ** editor.getCameraOptions().zoomSpeed; pinchState = "not sure"; editor.timers.requestAnimationFrame(() => { dispatchPinchEvent( "pinch_end", { x: e.clientX, y: e.clientY }, { x: e.clientX, y: e.clientY }, scale, e ); }); } elm.addEventListener("wheel", onWheel, { passive: false }); const useGestureEvents2 = !tlenv.isIos && "GestureEvent" in window; if (useGestureEvents2) { elm.addEventListener("gesturestart", onGestureStart); elm.addEventListener("gesturechange", onGestureChange); elm.addEventListener("gestureend", onGestureEnd); } else { elm.addEventListener("touchstart", onTouchStart); elm.addEventListener("touchmove", onTouchMove); elm.addEventListener("touchend", onTouchEnd); elm.addEventListener("touchcancel", onTouchEnd); } return () => { elm.removeEventListener("wheel", onWheel); if (useGestureEvents2) { elm.removeEventListener("gesturestart", onGestureStart); elm.removeEventListener("gesturechange", onGestureChange); elm.removeEventListener("gestureend", onGestureEnd); } else { elm.removeEventListener("touchstart", onTouchStart); elm.removeEventListener("touchmove", onTouchMove); elm.removeEventListener("touchend", onTouchEnd); elm.removeEventListener("touchcancel", onTouchEnd); } }; }, [editor, ref]); } export { useGestureEvents }; //# sourceMappingURL=useGestureEvents.mjs.map