UNPKG

react-pixi-plot

Version:

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

222 lines 11 kB
import { AppContext, Container } from 'react-pixi-fiber'; import * as PIXI from 'pixi.js'; import React from 'react'; import { preventGlobalMouseEvents, restoreGlobalMouseEvents } from '../../globalEvents'; import { distance } from '../../utils'; import { PixiPlotContext } from '../../PlotContext'; import SelectionOverlay from './SelectionOverlay'; const MIN_DRAG_DISTANCE = 3; const LEFT_BUTTON = 0; class SelectionContainer extends React.PureComponent { constructor(props) { super(props); /** * 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 = () => { preventGlobalMouseEvents(); document.addEventListener('mouseup', this.mouseUpListener, true); document.addEventListener('mousemove', this.mouseMoveListener, true); }; /** * 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(); if (e.button === LEFT_BUTTON) { this.selectStart = this.eventRendererPosition(e); this.addToSelection = e.shiftKey; this.removeFromSelection = e.ctrlKey; } }; /** * 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) => { 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 (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)); const plotBounds = this.pixelToPlotBounds(pixelBounds); this.setState({ brushRectangle: null, }); this.props.onSelect({ pixelBounds, plotBounds, addToSelection: e.ctrlKey, removeFromSelection: e.shiftKey, isBrushing: true, nativeEvent: e, }); delete this.addToSelection; delete this.removeFromSelection; } else { const localPosition = this.eventLocalPosition(e); this.props.onSelect({ pixelBounds: new PIXI.Rectangle(mousePosition.x, mousePosition.y), plotBounds: new PIXI.Rectangle(localPosition.x, localPosition.y), addToSelection: e.ctrlKey, removeFromSelection: e.shiftKey, isBrushing: false, nativeEvent: e, }); } delete this.selectStart; } }; /** * 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.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)); const plotBounds = this.pixelToPlotBounds(pixelBounds); const { appWidth, appHeight } = this.props; this.setState({ brushRectangle: plotBounds, viewRectangle: this.pixelToPlotBounds(new PIXI.Rectangle(0, 0, appWidth, appHeight)), removingFromSelection: e.ctrlKey, }); this.props.onHover({ pixelBounds, plotBounds, 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 { appHeight, appWidth } = this.props; const x = mousePosition.x; const y = mousePosition.y; if (x < 0) mousePosition.x = 0; else if (x > appWidth) mousePosition.x = appWidth; if (y < 0) mousePosition.y = 0; else if (y > appHeight) mousePosition.y = appHeight; 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); const localPosition = this.eventLocalPosition(e); if (!e.ctrlKey) { this.props.onHover({ pixelBounds: new PIXI.Rectangle(mousePosition.x, mousePosition.y), plotBounds: new PIXI.Rectangle(localPosition.x, localPosition.y), isBrushing: false, nativeEvent: e, }); } }; this.pixelToPlotBounds = (pixelBounds) => { const { draggablePosition, zoomablePosition, zoomableScale } = this.props; const topLeft = new PIXI.Point((pixelBounds.left - draggablePosition.x - zoomablePosition.x) / zoomableScale.x, (pixelBounds.top - draggablePosition.y - zoomablePosition.y) / zoomableScale.y); const width = pixelBounds.width / zoomableScale.x; const height = pixelBounds.height / zoomableScale.y; return new PIXI.Rectangle(topLeft.x, topLeft.y, width, height); }; this.state = {}; } componentDidMount() { this.props.canvas.addEventListener('mousedown', this.handleMouseDown); this.props.canvas.addEventListener('mousemove', this.handleMouseMove); } /** * 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. */ eventLocalPosition(mouseEvent) { const { draggablePosition, zoomablePosition, zoomableScale } = this.props; const rendererPosition = this.eventRendererPosition(mouseEvent); return new PIXI.Point((rendererPosition.x - draggablePosition.x - zoomablePosition.x) / zoomableScale.x, (rendererPosition.y - draggablePosition.y - zoomablePosition.y) / zoomableScale.y); } /** * 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 { interactionManager } = this.props; const mousePosition = new PIXI.Point(); interactionManager.mapPositionToPoint(mousePosition, mouseEvent.clientX, mouseEvent.clientY); return mousePosition; } render() { const { children, showBrushOverlay } = this.props; const { brushRectangle, viewRectangle, removingFromSelection } = this.state; return (React.createElement(Container, null, children, showBrushOverlay && brushRectangle && React.createElement(SelectionOverlay, { selectionRectangle: brushRectangle, viewRectangle: viewRectangle, invert: removingFromSelection }))); } } SelectionContainer.defaultProps = { onHover: () => { }, onSelect: () => { }, }; export default (props) => React.createElement(PixiPlotContext.Consumer, null, ({ state }) => React.createElement(AppContext.Consumer, null, app => React.createElement(SelectionContainer, Object.assign({ appHeight: state.appHeight, appWidth: state.appWidth, draggablePosition: state.draggablePosition, zoomablePosition: state.zoomablePosition, zoomableScale: state.zoomableScale, canvas: app.view, interactionManager: app.renderer.plugins.interaction }, props)))); //# sourceMappingURL=SelectionContainer.js.map