diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 19c856db77..a70cb9808a 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -8787,7 +8787,10 @@ class App extends React.Component { const x = event.clientX; const dx = x - pointerDownState.lastCoords.x; this.translateCanvas({ - scrollX: this.state.scrollX - dx / this.state.zoom.value, + scrollX: + this.state.scrollX - + (dx * (currentScrollBars.horizontal?.deltaMultiplier || 1)) / + this.state.zoom.value, }); pointerDownState.lastCoords.x = x; return true; @@ -8797,7 +8800,10 @@ class App extends React.Component { const y = event.clientY; const dy = y - pointerDownState.lastCoords.y; this.translateCanvas({ - scrollY: this.state.scrollY - dy / this.state.zoom.value, + scrollY: + this.state.scrollY - + (dy * (currentScrollBars.vertical?.deltaMultiplier || 1)) / + this.state.zoom.value, }); pointerDownState.lastCoords.y = y; return true; diff --git a/packages/excalidraw/scene/scrollbars.ts b/packages/excalidraw/scene/scrollbars.ts index 6c1bd8a8c0..35fecba370 100644 --- a/packages/excalidraw/scene/scrollbars.ts +++ b/packages/excalidraw/scene/scrollbars.ts @@ -11,6 +11,7 @@ export const SCROLLBAR_MARGIN = 4; export const SCROLLBAR_WIDTH = 6; export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)"; +// The scrollbar represents where the viewport is in relationship to the scene export const getScrollBars = ( elements: RenderableElementsMap, viewportWidth: number, @@ -31,9 +32,6 @@ export const getScrollBars = ( const viewportWidthWithZoom = viewportWidth / appState.zoom.value; const viewportHeightWithZoom = viewportHeight / appState.zoom.value; - const viewportWidthDiff = viewportWidth - viewportWidthWithZoom; - const viewportHeightDiff = viewportHeight - viewportHeightWithZoom; - const safeArea = { top: parseInt(getGlobalCSSVariable("sat")) || 0, bottom: parseInt(getGlobalCSSVariable("sab")) || 0, @@ -44,10 +42,8 @@ export const getScrollBars = ( const isRTL = getLanguage().rtl; // The viewport is the rectangle currently visible for the user - const viewportMinX = - -appState.scrollX + viewportWidthDiff / 2 + safeArea.left; - const viewportMinY = - -appState.scrollY + viewportHeightDiff / 2 + safeArea.top; + const viewportMinX = -appState.scrollX + safeArea.left; + const viewportMinY = -appState.scrollY + safeArea.top; const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right; const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom; @@ -57,8 +53,43 @@ export const getScrollBars = ( const sceneMaxX = Math.max(elementsMaxX, viewportMaxX); const sceneMaxY = Math.max(elementsMaxY, viewportMaxY); - // The scrollbar represents where the viewport is in relationship to the scene + // the elements-only bbox + const sceneWidth = elementsMaxX - elementsMinX; + const sceneHeight = elementsMaxY - elementsMinY; + // scene (elements) bbox + the viewport bbox that extends outside of it + const extendedSceneWidth = sceneMaxX - sceneMinX; + const extendedSceneHeight = sceneMaxY - sceneMinY; + + const scrollWidthOffset = + Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right) + + SCROLLBAR_WIDTH * 2; + + const scrollbarWidth = + viewportWidth * (viewportWidthWithZoom / extendedSceneWidth) - + scrollWidthOffset; + + const scrollbarHeightOffset = + Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom) + + SCROLLBAR_WIDTH * 2; + + const scrollbarHeight = + viewportHeight * (viewportHeightWithZoom / extendedSceneHeight) - + scrollbarHeightOffset; + // NOTE the delta multiplier calculation isn't quite correct when viewport + // is extended outside the scene (elements) bbox as there's some small + // accumulation error. I'll let this be an exercise for others to fix. ^^ + const horizontalDeltaMultiplier = + extendedSceneWidth > sceneWidth + ? (extendedSceneWidth * appState.zoom.value) / + (scrollbarWidth + scrollWidthOffset) + : viewportWidth / (scrollbarWidth + scrollWidthOffset); + + const verticalDeltaMultiplier = + extendedSceneHeight > sceneHeight + ? (extendedSceneHeight * appState.zoom.value) / + (scrollbarHeight + scrollbarHeightOffset) + : viewportHeight / (scrollbarHeight + scrollbarHeightOffset); return { horizontal: viewportMinX === sceneMinX && viewportMaxX === sceneMaxX @@ -66,18 +97,17 @@ export const getScrollBars = ( : { x: Math.max(safeArea.left, SCROLLBAR_MARGIN) + - ((viewportMinX - sceneMinX) / (sceneMaxX - sceneMinX)) * - viewportWidth, + SCROLLBAR_WIDTH + + ((viewportMinX - sceneMinX) / extendedSceneWidth) * viewportWidth, y: viewportHeight - SCROLLBAR_WIDTH - Math.max(SCROLLBAR_MARGIN, safeArea.bottom), - width: - ((viewportMaxX - viewportMinX) / (sceneMaxX - sceneMinX)) * - viewportWidth - - Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right), + width: scrollbarWidth, height: SCROLLBAR_WIDTH, + deltaMultiplier: horizontalDeltaMultiplier, }, + vertical: viewportMinY === sceneMinY && viewportMaxY === sceneMaxY ? null @@ -88,14 +118,13 @@ export const getScrollBars = ( SCROLLBAR_WIDTH - Math.max(safeArea.right, SCROLLBAR_MARGIN), y: - ((viewportMinY - sceneMinY) / (sceneMaxY - sceneMinY)) * - viewportHeight + - Math.max(safeArea.top, SCROLLBAR_MARGIN), + Math.max(safeArea.top, SCROLLBAR_MARGIN) + + SCROLLBAR_WIDTH + + ((viewportMinY - sceneMinY) / extendedSceneHeight) * + viewportHeight, width: SCROLLBAR_WIDTH, - height: - ((viewportMaxY - viewportMinY) / (sceneMaxY - sceneMinY)) * - viewportHeight - - Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom), + height: scrollbarHeight, + deltaMultiplier: verticalDeltaMultiplier, }, }; }; diff --git a/packages/excalidraw/scene/types.ts b/packages/excalidraw/scene/types.ts index 08b05a57de..12a5e27a8e 100644 --- a/packages/excalidraw/scene/types.ts +++ b/packages/excalidraw/scene/types.ts @@ -130,12 +130,14 @@ export type ScrollBars = { y: number; width: number; height: number; + deltaMultiplier: number; } | null; vertical: { x: number; y: number; width: number; height: number; + deltaMultiplier: number; } | null; };