Browse Source

scroll the closest element to center (#1670)

Co-authored-by: Sanghyeon Lee <yongdamsh@gmail.com>
Aakansha Doshi 4 years ago
parent
commit
fa359034c5

+ 12 - 5
src/actions/actionCanvas.tsx

@@ -4,7 +4,7 @@ import { getDefaultAppState } from "../appState";
 import { trash, zoomIn, zoomOut, resetZoom } from "../components/icons";
 import { ToolButton } from "../components/ToolButton";
 import { t } from "../i18n";
-import { getNormalizedZoom, calculateScrollCenter } from "../scene";
+import { getNormalizedZoom, normalizeScroll } from "../scene";
 import { KEYS } from "../keys";
 import { getShortcutKey } from "../utils";
 import useIsMobile from "../is-mobile";
@@ -202,15 +202,22 @@ export const actionZoomToFit = register({
   name: "zoomToFit",
   perform: (elements, appState) => {
     const nonDeletedElements = elements.filter((element) => !element.isDeleted);
-    const scrollCenter = calculateScrollCenter(nonDeletedElements);
     const commonBounds = getCommonBounds(nonDeletedElements);
-    const zoom = calculateZoom(commonBounds, appState.zoom, scrollCenter);
+    const [x1, y1, x2, y2] = commonBounds;
+    const centerX = (x1 + x2) / 2;
+    const centerY = (y1 + y2) / 2;
+    const scrollX = normalizeScroll(window.innerWidth / 2 - centerX);
+    const scrollY = normalizeScroll(window.innerHeight / 2 - centerY);
+    const zoom = calculateZoom(commonBounds, appState.zoom, {
+      scrollX,
+      scrollY,
+    });
 
     return {
       appState: {
         ...appState,
-        scrollX: scrollCenter.scrollX,
-        scrollY: scrollCenter.scrollY,
+        scrollX,
+        scrollY,
         zoom,
       },
       commitToHistory: false,

+ 2 - 0
src/components/App.tsx

@@ -847,6 +847,8 @@ class App extends React.Component<any, AppState> {
               remoteElements.filter((element: { isDeleted: boolean }) => {
                 return !element.isDeleted;
               }),
+              this.state,
+              this.canvas,
             ),
           });
         }

+ 4 - 1
src/components/LayerUI.tsx

@@ -256,7 +256,9 @@ const LayerUI = ({
         <button
           className="scroll-back-to-content"
           onClick={() => {
-            setAppState({ ...calculateScrollCenter(elements) });
+            setAppState({
+              ...calculateScrollCenter(elements, appState, canvas),
+            });
           }}
         >
           {t("buttons.scrollBackToContent")}
@@ -276,6 +278,7 @@ const LayerUI = ({
       onRoomCreate={onRoomCreate}
       onRoomDestroy={onRoomDestroy}
       onLockToggle={onLockToggle}
+      canvas={canvas}
     />
   ) : (
     <div className="layer-ui__wrapper">

+ 5 - 1
src/components/MobileMenu.tsx

@@ -27,6 +27,7 @@ type MobileMenuProps = {
   onUsernameChange: (username: string) => void;
   onRoomDestroy: () => void;
   onLockToggle: () => void;
+  canvas: HTMLCanvasElement | null;
 };
 
 export const MobileMenu = ({
@@ -39,6 +40,7 @@ export const MobileMenu = ({
   onUsernameChange,
   onRoomDestroy,
   onLockToggle,
+  canvas,
 }: MobileMenuProps) => (
   <>
     {appState.isLoading && <LoadingMessage />}
@@ -131,7 +133,9 @@ export const MobileMenu = ({
             <button
               className="scroll-back-to-content"
               onClick={() => {
-                setAppState({ ...calculateScrollCenter(elements) });
+                setAppState({
+                  ...calculateScrollCenter(elements, appState, canvas),
+                });
               }}
             >
               {t("buttons.scrollBackToContent")}

+ 1 - 1
src/data/index.ts

@@ -271,7 +271,7 @@ export const importFromBackend = async (
     }
 
     elements = data.elements || elements;
-    appState = data.appState || appState;
+    appState = { ...appState, ...data.appState };
   } catch (error) {
     window.alert(t("alerts.importBackendFailed"));
     console.error(error);

+ 0 - 1
src/data/localStorage.ts

@@ -84,6 +84,5 @@ export const restoreFromLocalStorage = () => {
       // Do nothing because appState is already null
     }
   }
-
   return restore(elements, appState);
 };

+ 4 - 1
src/data/restore.ts

@@ -121,7 +121,10 @@ export const restore = (
   }, [] as ExcalidrawElement[]);
 
   if (opts?.scrollToContent && savedState) {
-    savedState = { ...savedState, ...calculateScrollCenter(elements) };
+    savedState = {
+      ...savedState,
+      ...calculateScrollCenter(elements, savedState, null),
+    };
   }
 
   return {

+ 25 - 1
src/element/bounds.ts

@@ -1,5 +1,5 @@
 import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
-import { rotate } from "../math";
+import { distance2d, rotate } from "../math";
 import rough from "roughjs/bin/rough";
 import { Drawable, Op } from "roughjs/bin/core";
 import { Point } from "../types";
@@ -342,3 +342,27 @@ export const getResizedElementAbsoluteCoords = (
     maxY + element.y,
   ];
 };
+
+export const getClosestElementBounds = (
+  elements: readonly ExcalidrawElement[],
+  from: { x: number; y: number },
+): [number, number, number, number] => {
+  if (!elements.length) {
+    return [0, 0, 0, 0];
+  }
+
+  let minDistance = Infinity;
+  let closestElement = elements[0];
+
+  elements.forEach((element) => {
+    const [x1, y1, x2, y2] = getElementBounds(element);
+    const distance = distance2d((x1 + x2) / 2, (y1 + y2) / 2, from.x, from.y);
+
+    if (distance < minDistance) {
+      minDistance = distance;
+      closestElement = element;
+    }
+  });
+
+  return getElementBounds(closestElement);
+};

+ 1 - 0
src/element/index.ts

@@ -17,6 +17,7 @@ export {
   getCommonBounds,
   getDiamondPoints,
   getArrowPoints,
+  getClosestElementBounds,
 } from "./bounds";
 
 export {

+ 46 - 4
src/scene/scroll.ts

@@ -1,12 +1,43 @@
-import { FlooredNumber } from "../types";
+import { AppState, FlooredNumber } from "../types";
 import { ExcalidrawElement } from "../element/types";
-import { getCommonBounds } from "../element";
+import { getCommonBounds, getClosestElementBounds } from "../element";
+
+import {
+  sceneCoordsToViewportCoords,
+  viewportCoordsToSceneCoords,
+} from "../utils";
 
 export const normalizeScroll = (pos: number) =>
   Math.floor(pos) as FlooredNumber;
 
+function isOutsideViewPort(
+  appState: AppState,
+  canvas: HTMLCanvasElement | null,
+  cords: Array<number>,
+) {
+  const [x1, y1, x2, y2] = cords;
+  const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords(
+    { sceneX: x1, sceneY: y1 },
+    appState,
+    canvas,
+    window.devicePixelRatio,
+  );
+  const { x: viewportX2, y: viewportY2 } = sceneCoordsToViewportCoords(
+    { sceneX: x2, sceneY: y2 },
+    appState,
+    canvas,
+    window.devicePixelRatio,
+  );
+  return (
+    viewportX2 - viewportX1 > window.innerWidth ||
+    viewportY2 - viewportY1 > window.innerHeight
+  );
+}
+
 export const calculateScrollCenter = (
   elements: readonly ExcalidrawElement[],
+  appState: AppState,
+  canvas: HTMLCanvasElement | null,
 ): { scrollX: FlooredNumber; scrollY: FlooredNumber } => {
   if (!elements.length) {
     return {
@@ -14,8 +45,19 @@ export const calculateScrollCenter = (
       scrollY: normalizeScroll(0),
     };
   }
-
-  const [x1, y1, x2, y2] = getCommonBounds(elements);
+  const scale = window.devicePixelRatio;
+  let [x1, y1, x2, y2] = getCommonBounds(elements);
+  if (isOutsideViewPort(appState, canvas, [x1, y1, x2, y2])) {
+    [x1, y1, x2, y2] = getClosestElementBounds(
+      elements,
+      viewportCoordsToSceneCoords(
+        { clientX: appState.scrollX, clientY: appState.scrollY },
+        appState,
+        canvas,
+        scale,
+      ),
+    );
+  }
 
   const centerX = (x1 + x2) / 2;
   const centerY = (y1 + y2) / 2;