UNPKG

sigma

Version:

A JavaScript library dedicated to graph drawing.

246 lines (201 loc) 8.19 kB
/** * Sigma.js Touch Captor * ====================== * * Sigma's captor dealing with touch. * @module */ import { CameraState, Coordinates, Dimensions } from "../../types"; import Captor, { getPosition, getTouchCoords, getTouchesArray } from "./captor"; import Camera from "../camera"; const DRAG_TIMEOUT = 200; const TOUCH_INERTIA_RATIO = 3; const TOUCH_INERTIA_DURATION = 200; /** * Touch captor class. * * @constructor */ export default class TouchCaptor extends Captor { enabled = true; isMoving = false; startCameraState?: CameraState; touchMode = 0; // number of touches down movingTimeout?: number; startTouchesAngle?: number; startTouchesDistance?: number; startTouchesPositions?: Coordinates[]; lastTouchesPositions?: Coordinates[]; constructor(container: HTMLElement, camera: Camera) { super(container, camera); // Binding methods: this.handleStart = this.handleStart.bind(this); this.handleLeave = this.handleLeave.bind(this); this.handleMove = this.handleMove.bind(this); // Binding events container.addEventListener("touchstart", this.handleStart, false); container.addEventListener("touchend", this.handleLeave, false); container.addEventListener("touchcancel", this.handleLeave, false); container.addEventListener("touchmove", this.handleMove, false); } kill(): void { const container = this.container; container.removeEventListener("touchstart", this.handleStart); container.removeEventListener("touchend", this.handleLeave); container.removeEventListener("touchcancel", this.handleLeave); container.removeEventListener("touchmove", this.handleMove); } getDimensions(): Dimensions { return { width: this.container.offsetWidth, height: this.container.offsetHeight, }; } dispatchRelatedMouseEvent(type: string, e: TouchEvent, position?: Coordinates, emitter?: EventTarget): void { const mousePosition = position || getPosition(e.touches[0]); const mouseEvent = new MouseEvent(type, { clientX: mousePosition.x, clientY: mousePosition.y, altKey: e.altKey, ctrlKey: e.ctrlKey, }); (emitter || this.container).dispatchEvent(mouseEvent); } handleStart(e: TouchEvent): void | boolean { if (!this.enabled) return; // Prevent default to avoid default browser behaviors... e.preventDefault(); // ...but simulate mouse behavior anyway, to get the MouseCaptor working as well: if (e.touches.length === 1) this.dispatchRelatedMouseEvent("mousedown", e); const touches = getTouchesArray(e.touches); this.isMoving = true; this.touchMode = touches.length; this.startCameraState = this.camera.getState(); this.startTouchesPositions = touches.map(getPosition); this.lastTouchesPositions = this.startTouchesPositions; // When there are two touches down, let's record distance and angle as well: if (this.touchMode === 2) { const [{ x: x0, y: y0 }, { x: x1, y: y1 }] = this.startTouchesPositions; this.startTouchesAngle = Math.atan2(y1 - y0, x1 - x0); this.startTouchesDistance = Math.sqrt(Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2)); } this.emit("touchdown", getTouchCoords(e)); } handleLeave(e: TouchEvent): void { if (!this.enabled) return; // Prevent default to avoid default browser behaviors... e.preventDefault(); // ...but simulate mouse behavior anyway, to get the MouseCaptor working as well: if (e.touches.length === 0 && this.lastTouchesPositions && this.lastTouchesPositions.length) { this.dispatchRelatedMouseEvent("mouseup", e, this.lastTouchesPositions[0], document); this.dispatchRelatedMouseEvent("click", e, this.lastTouchesPositions[0]); } if (this.movingTimeout) { this.isMoving = false; clearTimeout(this.movingTimeout); } switch (this.touchMode) { case 2: if (e.touches.length === 1) { this.handleStart(e); e.preventDefault(); break; } /* falls through */ case 1: // TODO // Dispatch event if (this.isMoving) { const cameraState = this.camera.getState(), previousCameraState = this.camera.getPreviousState(); this.camera.animate( { x: cameraState.x + TOUCH_INERTIA_RATIO * (cameraState.x - previousCameraState.x), y: cameraState.y + TOUCH_INERTIA_RATIO * (cameraState.y - previousCameraState.y), }, { duration: TOUCH_INERTIA_DURATION, easing: "quadraticOut", }, ); } this.isMoving = false; this.touchMode = 0; break; } this.emit("touchup", getTouchCoords(e)); } handleMove(e: TouchEvent): void { if (!this.enabled) return; // Prevent default to avoid default browser behaviors... e.preventDefault(); // ...but simulate mouse behavior anyway, to get the MouseCaptor working as well: if (e.touches.length === 1) this.dispatchRelatedMouseEvent("mousemove", e); const startCameraState = this.startCameraState as CameraState; const touches = getTouchesArray(e.touches); const touchesPositions = touches.map(getPosition); this.lastTouchesPositions = touchesPositions; this.isMoving = true; if (this.movingTimeout) clearTimeout(this.movingTimeout); this.movingTimeout = window.setTimeout(() => { this.isMoving = false; }, DRAG_TIMEOUT); switch (this.touchMode) { case 1: { const { x: xStart, y: yStart } = this.camera.viewportToFramedGraph( this.getDimensions(), (this.startTouchesPositions || [])[0] as Coordinates, ); const { x, y } = this.camera.viewportToFramedGraph(this.getDimensions(), touchesPositions[0]); this.camera.setState({ x: startCameraState.x + xStart - x, y: startCameraState.y + yStart - y, }); break; } case 2: { /** * Here is the thinking here: * * 1. We can find the new angle and ratio, by comparing the vector from "touch one" to "touch two" at the start * of the d'n'd and now * * 2. We can use `Camera#viewportToGraph` inside formula to retrieve the new camera position, using the graph * position of a touch at the beginning of the d'n'd (using `startCamera.viewportToGraph`) and the viewport * position of this same touch now */ const newCameraState: Partial<CameraState> = {}; const { x: x0, y: y0 } = touchesPositions[0]; const { x: x1, y: y1 } = touchesPositions[1]; const angleDiff = Math.atan2(y1 - y0, x1 - x0) - (this.startTouchesAngle as number); const ratioDiff = Math.hypot(y1 - y0, x1 - x0) / (this.startTouchesDistance as number); // 1. newCameraState.ratio = startCameraState.ratio / ratioDiff; newCameraState.angle = startCameraState.angle + angleDiff; // 2. const dimensions = this.getDimensions(); const touchGraphPosition = Camera.from(startCameraState).viewportToFramedGraph( dimensions, (this.startTouchesPositions || [])[0] as Coordinates, ); const smallestDimension = Math.min(dimensions.width, dimensions.height); const dx = smallestDimension / dimensions.width; const dy = smallestDimension / dimensions.height; const ratio = newCameraState.ratio / smallestDimension; // Align with center of the graph: let x = x0 - smallestDimension / 2 / dx; let y = y0 - smallestDimension / 2 / dy; // Rotate: [x, y] = [ x * Math.cos(-newCameraState.angle) - y * Math.sin(-newCameraState.angle), y * Math.cos(-newCameraState.angle) + x * Math.sin(-newCameraState.angle), ]; newCameraState.x = touchGraphPosition.x - x * ratio; newCameraState.y = touchGraphPosition.y + y * ratio; this.camera.setState(newCameraState); break; } } this.emit("touchmove", getTouchCoords(e)); } }