UNPKG

react-pixi-plot

Version:

A React component rendering a zoomable and draggable PIXI.js scene. Intended to render 2d plots

247 lines 11.9 kB
import React from 'react'; import normalizeWheel from 'normalize-wheel'; import * as PIXI from 'pixi.js'; const MIN_DRAG_DISTANCE = 3; const LEFT_BUTTON = 0; const RIGHT_BUTTON = 2; export default class InteractionManager extends React.PureComponent { constructor() { super(...arguments); /** * A simple distance calculation between two cartesian objects with x and y parameters. * @method * @param {object} a The first cartesian point. * @param {object} b The second cartesian point. * @returns {number} The magnitude of the linear distance between a and b. */ this.distance = (a, b) => Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)); /** * Sets DOM body style pointerEvents to 'none' to prevent global mouse events. Paired with {@link InteractionManager#restoreGlobalMouseEvents|restoreGlobalMouseEvents}. * @method * @returns {undefined} */ this.preventGlobalMouseEvents = () => { document.body.style.pointerEvents = 'none'; }; /** * Sets DOM body style pointerEvents to 'auto' to restore global mouse events. Paired with {@link InteractionManager#preventGlobalMouseEvents|preventGlobalMouseEvents}. * @method * @returns {undefined} */ this.restoreGlobalMouseEvents = () => { document.body.style.pointerEvents = 'auto'; }; /** * Handles all mouse down events. * When using the left mouse button checks if shift or control keys (e.shiftKey, e.ctrlKey booleans) are pressed to know if a selection add or remove is occuring. * When using the right mouse button performs a pan of the visualization. * @method * @param {object} e The mouse down event. * @returns {undefined} */ this.handleMouseDown = (e) => { this.captureMouseEvents(e.nativeEvent); if (e.button === LEFT_BUTTON) { this.selectStart = this.eventRendererPosition(e); this.addToSelection = e.shiftKey; this.removeFromSelection = e.ctrlKey; } else if (e.button === RIGHT_BUTTON) { this.panAnchorPoint = this.eventRendererPosition(e); } }; /** * @todo stop the preventDefault for contextmenu event listener from preventing right clicking in the entire Lodestone application. * Isolates mouse events to the visualization by adding event listeners and preventing mouse defaults and event propagation. Handles cancelling the right mouse context menu. * @method * @param {object} e The mouse event. * @returns {undefined} */ this.captureMouseEvents = (e) => { this.preventGlobalMouseEvents(); document.addEventListener('mouseup', this.mouseUpListener, true); document.addEventListener('mousemove', this.mouseMoveListener, true); document.addEventListener('contextmenu', (ev) => ev.preventDefault(), true); e.preventDefault(); e.stopPropagation(); }; /** * Handles mouse button release. Restores event listeners that were removed during {@link PixiVisualization#captureMouseEvents|captureMouseEvents}, * enables holding shift and selecting more points, and removing points from selections with the right mouse button. * @method * @param {object} e The mouse up event. * @returns {undefined} */ this.mouseUpListener = (e) => { this.restoreGlobalMouseEvents(); document.removeEventListener('mouseup', this.mouseUpListener, true); document.removeEventListener('mousemove', this.mouseMoveListener, true); document.removeEventListener('contextmenu', (ev) => ev.preventDefault(), true); e.preventDefault(); if (e.button === LEFT_BUTTON && this.selectStart) { let mousePosition = this.eventRendererPosition(e); if (this.distance(this.selectStart, mousePosition) >= MIN_DRAG_DISTANCE) { // change mousePosition to be within the canvas size mousePosition = this.constrainMousePosition(mousePosition); const pixelBounds = new PIXI.Rectangle(Math.min(this.selectStart.x, mousePosition.x), Math.min(this.selectStart.y, mousePosition.y), Math.abs(this.selectStart.x - mousePosition.x), Math.abs(this.selectStart.y - mousePosition.y)); this.props.onSelect({ pixelBounds, plotBounds: new PIXI.Rectangle(), addToSelection: e.ctrlKey, removeFromSelection: e.shiftKey, isBrushing: true, nativeEvent: e }); delete this.addToSelection; delete this.removeFromSelection; } else { this.props.onSelect({ pixelBounds: new PIXI.Rectangle(mousePosition.x, mousePosition.y), plotBounds: new PIXI.Rectangle(), addToSelection: e.ctrlKey, removeFromSelection: e.shiftKey, isBrushing: false, nativeEvent: e }); } delete this.selectStart; } else if (e.button === RIGHT_BUTTON) { delete this.panAnchorPoint; } }; this.handleTouchStart = (e) => { if (e.targetTouches.length === 1) this.panAnchorPoint = this.eventRendererPosition(e.targetTouches.item(0)); else { if (this.panAnchorPoint) delete this.panAnchorPoint; if (e.targetTouches.length === 2) { const a = this.eventRendererPosition(e.targetTouches.item(0)); const b = this.eventRendererPosition(e.targetTouches.item(1)); this.pinchDistance = this.distance(a, b); } } }; this.handleTouchEnd = (e) => { if (e.targetTouches.length === 0) // no more touches delete this.panAnchorPoint; else if (e.targetTouches.length === 1) { delete this.pinchDistance; } }; this.handleTouchMove = (e) => { if (e.targetTouches.length === 1) { const touch = e.targetTouches.item(0); const touchPosition = this.eventRendererPosition(touch); if (this.panAnchorPoint !== undefined) { this.props.onPan(this.panAnchorPoint, touchPosition); this.panAnchorPoint = touchPosition; } } else if (e.targetTouches.length === 2) { const a = this.eventRendererPosition(e.targetTouches.item(0)); const b = this.eventRendererPosition(e.targetTouches.item(1)); const newPinchDistance = this.distance(a, b); this.props.onZoom(newPinchDistance / this.pinchDistance, a); } }; /** * Handles mouse movement events. If a selection has already been started this handles expanding the selection, and handles panning to the new position if a pan is occuring. * @method * @param {object} e The mouse move event. * @returns {undefined} */ this.mouseMoveListener = (e) => { e.stopPropagation(); e.preventDefault(); let mousePosition = this.eventRendererPosition(e); if (this.panAnchorPoint !== undefined) { this.props.onPan(this.panAnchorPoint, mousePosition); this.panAnchorPoint = mousePosition; } if (this.selectStart !== undefined) { // change mousePosition to be within the canvas size. mousePosition = this.constrainMousePosition(mousePosition); const pixelBounds = new PIXI.Rectangle(Math.min(this.selectStart.x, mousePosition.x), Math.min(this.selectStart.y, mousePosition.y), Math.abs(this.selectStart.x - mousePosition.x), Math.abs(this.selectStart.y - mousePosition.y)); this.props.onHover({ pixelBounds, plotBounds: new PIXI.Rectangle(), isBrushing: true, nativeEvent: e }); } }; /** * Handles keeping the mouse within the canvas if a mouse-based interaction is occuring. * @method * @param {object} mousePosition Where the mouse is currently. * @returns {object} Where we have constrained the mouse to be. */ this.constrainMousePosition = (mousePosition) => { const { plotSize: { width, height } } = this.props; const x = mousePosition.x; const y = mousePosition.y; if (x < 0) mousePosition.x = 0; else if (x > width) mousePosition.x = width; if (y < 0) mousePosition.y = 0; else if (y > height) mousePosition.y = height; return mousePosition; }; /** * Handles moving the mouse. Determines the mouse's position and if the ctrl key isn't pressed we hover the data under the mouse cursor. * @method * @param {object} e The mouse move event. * @returns {undefined} */ this.handleMouseMove = (e) => { const mousePosition = this.eventRendererPosition(e); if (!e.ctrlKey) this.props.onHover({ pixelBounds: new PIXI.Rectangle(mousePosition.x, mousePosition.y), plotBounds: new PIXI.Rectangle(), isBrushing: false, nativeEvent: e.nativeEvent }); }; /** * Handles whenever someone scrolls the mouse wheel. We generate the zoom factor, and prevent the website from reacting normally to a mouse scroll (scolling down the page). * @method * @param {object} e The mouse wheel event. * @returns {undefined} */ this.handleWheel = (e) => { const normalizedEvent = normalizeWheel(e); this.props.onZoom(Math.pow(2, -normalizedEvent.pixelY / 500), this.eventRendererPosition(e)); e.stopPropagation(); e.preventDefault(); }; } /** * Correlates clicks on the renderer that happen in screen space coordinates to renderer coordinates. Used to understand where the mouse event occurred inside the renderer. * @method * @param {MouseEvent} mouseEvent The mouse event. * @returns {PIXI.Point} The Point object containing the coordinates of the mouse, in the renderer's coordinates system. */ eventRendererPosition(mouseEvent) { const mousePosition = new PIXI.Point(); this.props.pixiInteractionManager.mapPositionToPoint(mousePosition, mouseEvent.clientX, mouseEvent.clientY); return mousePosition; } render() { return React.cloneElement(this.props.children, { onMouseDown: this.handleMouseDown, onWheel: this.handleWheel, onMouseMove: this.handleMouseMove, onTouchStart: this.handleTouchStart, onTouchEnd: this.handleTouchEnd, onTouchMove: this.handleTouchMove }); } } //# sourceMappingURL=InteractionManager.js.map