Created
September 11, 2023 05:00
-
-
Save StephenHaney/a4bde95c318c1491bec548f0fa3c04d8 to your computer and use it in GitHub Desktop.
Modulz AutoPan on dragging
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { autorun } from 'mobx'; | |
import { observer } from 'mobx-react-lite'; | |
import React, { useEffect, useRef } from 'react'; | |
import { useAppState } from 'src/store/context/AppStateProvider'; | |
/** Moves the pan of the canvas if a cursor leaves the edges of the canvas during a drag */ | |
export const AutoPan = observer(() => { | |
const { hoverState, dragState, cameraState, toolState, selectionState } = useAppState(); | |
/** A RAF for the autopan function */ | |
const autoPanAnimationFrame = useRef<number>(0); | |
/** | |
* Stores the direction the canvas needs to pan in order to move towards a cursor that's off canvas. | |
* You can set it from 0-1 to get just a little pan or full strength. 0.5 = half speed pan | |
*/ | |
const canvasToCursorVector = useRef<{ dX: number; dY: number }>({ dX: 0, dY: 0 }); | |
useEffect(() => { | |
/** The number of window pixels to move the pan per frame */ | |
const autoPanAmountPerFrame = 15; | |
/** How close the cursor needs to be to the edge before auto pan starts */ | |
const triggerDistanceFromEdge = 30; | |
/** We start the pan slowly, so how far into the trigger do we hit max speed? */ | |
const triggerDistanceUntilMaxSpeed = triggerDistanceFromEdge * 3; | |
/** Schedule up a pan before the next frame */ | |
function schedulePan(e: PointerEvent) { | |
cancelAnimationFrame(autoPanAnimationFrame.current); | |
autoPanAnimationFrame.current = requestAnimationFrame(() => { | |
const { dX, dY } = canvasToCursorVector.current; | |
if ( | |
dragState.isDragging === 'MoveTool' || | |
dragState.isResizing || | |
selectionState.isDrawingSelectionBrush || | |
toolState.drawingNode !== null | |
) { | |
if (dX !== 0 || dY !== 0) { | |
// Set the new pan: | |
const panAmountX = dX * autoPanAmountPerFrame; | |
const panAmountY = dY * autoPanAmountPerFrame; | |
const newPan = { x: cameraState.pan.x - panAmountX, y: cameraState.pan.y - panAmountY }; | |
cameraState.setPan(newPan); | |
// Keep RAFing pan checks until the mouse moves back into the canvas | |
schedulePan(e); | |
} | |
} else if (dX !== 0 || dY !== 0) { | |
// The mouse event ended but we still have dX or dY | |
// maybe due to an escape key or drop while the cursor was off canvas | |
// this is benign, but go ahead and clear them out in prep for the next mouse event | |
canvasToCursorVector.current.dX = 0; | |
canvasToCursorVector.current.dY = 0; | |
} | |
}); | |
} | |
function onPointerMove(e: PointerEvent) { | |
if ( | |
dragState.isDragging === 'MoveTool' || | |
dragState.isResizing || | |
selectionState.isDrawingSelectionBrush || | |
toolState.drawingNode !== null | |
) { | |
// These are all arranged so the number is positive if the cursor is outside the trigger area | |
const deltaLeftTrigger = e.clientX - (cameraState.currentViewableWindowArea.minX + triggerDistanceFromEdge); | |
const deltaRightTrigger = cameraState.currentViewableWindowArea.maxX - triggerDistanceFromEdge - e.clientX; | |
const deltaTopTrigger = e.clientY - (cameraState.currentViewableWindowArea.minY + triggerDistanceFromEdge); | |
const deltaBottomTrigger = cameraState.currentViewableWindowArea.maxY - triggerDistanceFromEdge - e.clientY; | |
// The smoothing works like: | |
// Math.abs(distanceFromEdge) / triggerDistanceUntilMaxSpeed | |
// So the scroll starts very slow and gets progressively faster until you're | |
// however far PAST the edge, at which point you hit max speed | |
// Check for the need to auto pan | |
if (deltaLeftTrigger <= 0) { | |
const multiplier = Math.min(1, Math.abs(deltaLeftTrigger) / triggerDistanceUntilMaxSpeed); | |
canvasToCursorVector.current.dX = -multiplier; | |
} else if (deltaRightTrigger <= 0) { | |
const multiplier = Math.min(1, Math.abs(deltaRightTrigger) / triggerDistanceUntilMaxSpeed); | |
canvasToCursorVector.current.dX = multiplier; | |
} else { | |
canvasToCursorVector.current.dX = 0; | |
} | |
if (deltaTopTrigger <= 0) { | |
const multiplier = Math.min(1, Math.abs(deltaTopTrigger) / triggerDistanceUntilMaxSpeed); | |
canvasToCursorVector.current.dY = -multiplier; | |
} else if (deltaBottomTrigger <= 0) { | |
const multiplier = Math.min(1, Math.abs(deltaBottomTrigger) / triggerDistanceUntilMaxSpeed); | |
canvasToCursorVector.current.dY = multiplier; | |
} else { | |
canvasToCursorVector.current.dY = 0; | |
} | |
if (canvasToCursorVector.current.dX !== 0 || canvasToCursorVector.current.dY !== 0) { | |
// Schedule a pan on the next raf | |
schedulePan(e); | |
} | |
} | |
} | |
// Wire up the mouse watcher | |
window.addEventListener('pointermove', onPointerMove); | |
// Clean up | |
return () => { | |
window.removeEventListener('pointermove', onPointerMove); | |
}; | |
}, [cameraState, dragState, toolState, selectionState, hoverState]); | |
return null; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment