"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useCamera = useCamera; var _react = require("react"); var _reactRedux = require("react-redux"); var _side_effect_context = require("./side_effect_context"); var selectors = _interopRequireWildcard(require("../store/selectors")); var _action = require("../store/camera/action"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ function useCamera({ id }) { const dispatch = (0, _reactRedux.useDispatch)(); const sideEffectors = (0, _react.useContext)(_side_effect_context.SideEffectContext); const [ref, setRef] = (0, _react.useState)(null); /** * The position of a thing, as a `Vector2`, is multiplied by the projection matrix * to determine where it belongs on the screen. * The projection matrix changes over time if the camera is currently animating. */ const projectionMatrixAtTime = (0, _reactRedux.useSelector)((0, _react.useCallback)(state => { return selectors.projectionMatrix(state.analyzer[id]); }, [id])); /** * Use a ref to refer to the `projectionMatrixAtTime` function. The rAF loop * accesses this and sets state during the rAF cycle. If the rAF loop * effect read this directly from the selector, the rAF loop would need to * be re-inited each time this function changed. The `projectionMatrixAtTime` function * changes each frame during an animation, so the rAF loop would be causing * itself to reinit on each frame. This would necessarily cause a drop in FPS as there * would be a dead zone between when the rAF loop stopped and restarted itself. */ const projectionMatrixAtTimeRef = (0, _react.useRef)(); /** * The projection matrix is stateful, depending on the current time. * When the projection matrix changes, the component should be rerendered. */ const [projectionMatrix, setProjectionMatrix] = (0, _react.useState)(projectionMatrixAtTime(sideEffectors.timestamp())); const userIsPanning = (0, _reactRedux.useSelector)(state => selectors.userIsPanning(state.analyzer[id])); const isAnimatingAtTime = (0, _reactRedux.useSelector)(state => selectors.isAnimating(state.analyzer[id])); const [elementBoundingClientRect, clientRectCallback] = useAutoUpdatingClientRect(); /** * For an event with clientX and clientY, return [clientX, clientY] - the top left corner of the `ref` element */ const relativeCoordinatesFromMouseEvent = (0, _react.useCallback)(event => { if (elementBoundingClientRect === null) { return null; } return [event.clientX - elementBoundingClientRect.x, event.clientY - elementBoundingClientRect.y]; }, [elementBoundingClientRect]); const handleMouseDown = (0, _react.useCallback)(event => { const maybeCoordinates = relativeCoordinatesFromMouseEvent(event); if (maybeCoordinates !== null) { dispatch((0, _action.userStartedPanning)({ id, screenCoordinates: maybeCoordinates, time: sideEffectors.timestamp() })); } }, [dispatch, relativeCoordinatesFromMouseEvent, sideEffectors, id]); const handleMouseMove = (0, _react.useCallback)(event => { const maybeCoordinates = relativeCoordinatesFromMouseEvent(event); if (maybeCoordinates) { dispatch((0, _action.userMovedPointer)({ id, screenCoordinates: maybeCoordinates, time: sideEffectors.timestamp() })); } }, [dispatch, relativeCoordinatesFromMouseEvent, sideEffectors, id]); const handleMouseUp = (0, _react.useCallback)(() => { if (userIsPanning) { dispatch((0, _action.userStoppedPanning)({ id, time: sideEffectors.timestamp() })); } }, [dispatch, sideEffectors, userIsPanning, id]); const handleWheel = (0, _react.useCallback)(event => { if (elementBoundingClientRect !== null && event.ctrlKey && event.deltaY !== 0 && event.deltaMode === 0) { event.preventDefault(); dispatch((0, _action.userZoomed)({ id, /** * * we use elementBoundingClientRect to interpret pixel deltas as a fraction of the element's height * when pinch-zooming in on a mac, deltaY is a negative number but we want the payload to be positive */ zoomChange: event.deltaY / -elementBoundingClientRect.height, time: sideEffectors.timestamp() })); } }, [elementBoundingClientRect, dispatch, sideEffectors, id]); const refCallback = (0, _react.useCallback)(node => { setRef(node); clientRectCallback(node); }, [clientRectCallback]); (0, _react.useEffect)(() => { window.addEventListener('mouseup', handleMouseUp, { passive: true }); return () => { window.removeEventListener('mouseup', handleMouseUp); }; }, [handleMouseUp]); (0, _react.useEffect)(() => { window.addEventListener('mousemove', handleMouseMove, { passive: true }); return () => { window.removeEventListener('mousemove', handleMouseMove); }; }, [handleMouseMove]); /** * Register an event handler directly on `elementRef` for the `wheel` event, with no options * React sets native event listeners on the `window` and calls provided handlers via event propagation. * As of Chrome 73, `'wheel'` events on `window` are automatically treated as 'passive'. * If you don't need to call `event.preventDefault` then you should use regular React event handling instead. */ (0, _react.useEffect)(() => { if (ref !== null) { ref.addEventListener('wheel', handleWheel); return () => { ref.removeEventListener('wheel', handleWheel); }; } }, [ref, handleWheel]); /** * Allow rAF loop to indirectly read projectionMatrixAtTime via a ref. Since it also * sets projectionMatrixAtTime, relying directly on it causes considerable jank. */ (0, _react.useLayoutEffect)(() => { projectionMatrixAtTimeRef.current = projectionMatrixAtTime; }, [projectionMatrixAtTime]); /** * Keep the projection matrix state in sync with the selector. * This isn't needed during animation. */ (0, _react.useLayoutEffect)(() => { // Update the projection matrix that we return, rerendering any component that uses this. setProjectionMatrix(projectionMatrixAtTime(sideEffectors.timestamp())); }, [projectionMatrixAtTime, sideEffectors]); /** * When animation is happening, run a rAF loop, when it is done, stop. */ (0, _react.useLayoutEffect)(() => { const startDate = sideEffectors.timestamp(); if (isAnimatingAtTime(startDate)) { let rafRef = null; const handleFrame = () => { // Get the current timestamp, now that the frame is ready const date = sideEffectors.timestamp(); if (projectionMatrixAtTimeRef.current !== undefined) { // Update the projection matrix, triggering a rerender setProjectionMatrix(projectionMatrixAtTimeRef.current(date)); } // If we are still animating, request another frame, continuing the loop if (isAnimatingAtTime(date)) { rafRef = sideEffectors.requestAnimationFrame(handleFrame); } else { /** * `isAnimatingAtTime` was false, meaning that the animation is complete. * Do not request another animation frame. */ rafRef = null; } }; // Kick off the loop by requestion an animation frame rafRef = sideEffectors.requestAnimationFrame(handleFrame); /** * This function cancels the animation frame request. The cancel function * will occur when the component is unmounted. It will also occur when a dependency * changes. * * The `isAnimatingAtTime` dependency will be changed if the animation state changes. The animation * state only changes when the user animates again (e.g. brings a different node into view, or nudges the * camera.) */ return () => { // Cancel the animation frame. if (rafRef !== null) { sideEffectors.cancelAnimationFrame(rafRef); } }; } }, /** * `isAnimatingAtTime` is a function created with `reselect`. The function reference will be changed when * the animation state changes. When the function reference has changed, you *might* be animating. */ [isAnimatingAtTime, sideEffectors]); (0, _react.useEffect)(() => { if (elementBoundingClientRect !== null) { dispatch((0, _action.userSetRasterSize)({ id, dimensions: [elementBoundingClientRect.width, elementBoundingClientRect.height] })); } }, [dispatch, elementBoundingClientRect, id]); return { ref: refCallback, onMouseDown: handleMouseDown, projectionMatrix }; } /** * Returns a nullable DOMRect and a ref callback. Pass the refCallback to the * `ref` property of a native element and this hook will return a DOMRect for * it by calling `getBoundingClientRect`. This hook will observe the element * with a resize observer and call getBoundingClientRect again after resizes. * * Note that the changes to the position of the element aren't automatically * tracked. So if the element's position moves for some reason, be sure to * handle that. */ function useAutoUpdatingClientRect() { // Access `getBoundingClientRect` via the `SideEffectContext` (for testing.) const { getBoundingClientRect } = (0, _react.useContext)(_side_effect_context.SideEffectContext); // This hooks returns `rect`. const [rect, setRect] = (0, _react.useState)(null); const { ResizeObserver, requestAnimationFrame } = (0, _react.useContext)(_side_effect_context.SideEffectContext); // Keep the current DOM node in state so that we can create a ResizeObserver for it via `useEffect`. const [currentNode, setCurrentNode] = (0, _react.useState)(null); // `ref` will be used with a react element. When the element is available, this function will be called. const ref = (0, _react.useCallback)(node => { // track the node in state setCurrentNode(node); }, []); /** * Any time the DOM node changes (to something other than `null`) recalculate the DOMRect and set it (which will cause it to be returned from the hook. * This effect re-runs when the DOM node has changed. */ (0, _react.useEffect)(() => { if (currentNode !== null) { // When the DOM node is received, immedaiately calculate its DOM Rect and return that setRect(getBoundingClientRect(currentNode)); } }, [currentNode, getBoundingClientRect]); /** * When scroll events occur, recalculate the DOMRect. DOMRect represents the position of an element relative to the viewport, so that may change during scroll (depending on the layout.) * This effect re-runs when the DOM node has changed. */ (0, _react.useEffect)(() => { // the last scrollX and scrollY values that we handled let previousX = window.scrollX; let previousY = window.scrollY; const handleScroll = () => { requestAnimationFrame(() => { // synchronously read from the DOM const currentX = window.scrollX; const currentY = window.scrollY; if (currentNode !== null && (previousX !== currentX || previousY !== currentY)) { setRect(getBoundingClientRect(currentNode)); } previousX = currentX; previousY = currentY; }); }; window.addEventListener('scroll', handleScroll, { passive: true }); return () => { window.removeEventListener('scroll', handleScroll); }; }, [currentNode, requestAnimationFrame, getBoundingClientRect]); (0, _react.useEffect)(() => { if (currentNode !== null) { const resizeObserver = new ResizeObserver(entries => { if (currentNode !== null && currentNode === entries[0].target) { setRect(getBoundingClientRect(currentNode)); } }); resizeObserver.observe(currentNode); return () => { resizeObserver.disconnect(); }; } }, [ResizeObserver, currentNode, getBoundingClientRect]); return [rect, ref]; }