Selaa lähdekoodia

Simple minimap implementation

tk338g 4 vuotta sitten
vanhempi
commit
4c3544df4a
3 muutettua tiedostoa jossa 162 lisäystä ja 0 poistoa
  1. 2 0
      src/components/LayerUI.tsx
  2. 5 0
      src/components/MiniMap.scss
  3. 155 0
      src/components/MiniMap.tsx

+ 2 - 0
src/components/LayerUI.tsx

@@ -41,6 +41,7 @@ import Stack from "./Stack";
 import { ToolButton } from "./ToolButton";
 import { Tooltip } from "./Tooltip";
 import { UserList } from "./UserList";
+import { MiniMap } from "./MiniMap";
 
 interface LayerUIProps {
   actionManager: ActionManager;
@@ -602,6 +603,7 @@ const LayerUI = ({
       >
         {renderCustomFooter?.(false)}
         {actionManager.renderAction("toggleShortcuts")}
+        <MiniMap appState={appState} elements={elements} />
       </div>
       <button
         className={clsx("disable-zen-mode", {

+ 5 - 0
src/components/MiniMap.scss

@@ -0,0 +1,5 @@
+.Island.MiniMap {
+  position: absolute;
+  bottom: 50px;
+  right: calc(var(--space-factor) * 4);
+}

+ 155 - 0
src/components/MiniMap.tsx

@@ -0,0 +1,155 @@
+import "./MiniMap.scss";
+
+import React, { useEffect, useRef } from "react";
+import { unmountComponentAtNode, render } from "react-dom";
+import { canvasToBlob } from "../data/blob";
+import { getCommonBounds } from "../element";
+import { NonDeletedExcalidrawElement } from "../element/types";
+import { CanvasError } from "../errors";
+import { exportToCanvas } from "../scene/export";
+import { AppState } from "../types";
+import { distance, viewportCoordsToSceneCoords } from "../utils";
+import { ErrorCanvasPreview } from "./ExportDialog";
+import { Island } from "./Island";
+
+const RATIO = 1.2;
+const MINIMAP_HEIGHT = 150;
+const MINIMAP_WIDTH = MINIMAP_HEIGHT * RATIO;
+
+const renderPreview = (
+  content: HTMLCanvasElement | Error,
+  previewNode: HTMLDivElement,
+) => {
+  unmountComponentAtNode(previewNode);
+  previewNode.innerHTML = "";
+  if (content instanceof HTMLCanvasElement) {
+    previewNode.appendChild(content);
+  } else {
+    render(<ErrorCanvasPreview />, previewNode);
+  }
+};
+
+const MinimapViewport = ({
+  elements,
+  appState,
+}: {
+  elements: readonly NonDeletedExcalidrawElement[];
+  appState: AppState;
+}) => {
+  if (elements.length === 0) {
+    return null;
+  }
+
+  const [minX, minY, canvasWidth, canvasHeight] = getCanvasSize(elements);
+  const minimapScale = Math.min(
+    MINIMAP_WIDTH / canvasWidth,
+    MINIMAP_HEIGHT / canvasHeight,
+  );
+
+  const leftTop = viewportCoordsToSceneCoords(
+    { clientX: 0, clientY: 0 },
+    appState,
+  );
+  const rightBot = viewportCoordsToSceneCoords(
+    { clientX: appState.width, clientY: appState.height },
+    appState,
+  );
+
+  const top = (leftTop.y - minY) * minimapScale;
+  const left = (leftTop.x - minX) * minimapScale;
+  const width = (rightBot.x - leftTop.x) * minimapScale;
+  const height = (rightBot.y - leftTop.y) * minimapScale;
+
+  return (
+    <div
+      style={{
+        border: "2px solid orange",
+        boxSizing: "border-box",
+        position: "absolute",
+        pointerEvents: "none",
+        top: Math.max(0, top),
+        left: Math.max(0, left),
+        width: Math.min(MINIMAP_WIDTH - Math.max(0, left), width),
+        height: Math.min(MINIMAP_HEIGHT - Math.max(0, top), height),
+      }}
+    />
+  );
+};
+
+const getCanvasSize = (
+  elements: readonly NonDeletedExcalidrawElement[],
+): [number, number, number, number] => {
+  const [minX, minY, maxX, maxY] = getCommonBounds(elements);
+  const width = distance(minX, maxX);
+  const height = distance(minY, maxY);
+
+  return [minX, minY, width, height];
+};
+
+export function MiniMap({
+  appState,
+  elements,
+}: {
+  appState: AppState;
+  elements: readonly NonDeletedExcalidrawElement[];
+}) {
+  const previewRef = useRef<HTMLDivElement>(null);
+  const appStateRef = useRef<AppState>(appState);
+  appStateRef.current = appState;
+
+  useEffect(() => {
+    const previewNode = previewRef.current;
+    if (!previewNode) {
+      return;
+    }
+    if (elements.length === 0) {
+      unmountComponentAtNode(previewNode);
+      previewNode.innerHTML = "";
+    }
+    const canvas = exportToCanvas(
+      elements,
+      appStateRef.current,
+      {
+        exportBackground: true,
+        viewBackgroundColor: appStateRef.current.viewBackgroundColor,
+        shouldAddWatermark: false,
+      },
+      (width, height) => {
+        const tempCanvas = document.createElement("canvas");
+        const scale = Math.min(MINIMAP_WIDTH / width, MINIMAP_HEIGHT / height);
+        tempCanvas.width = width * scale;
+        tempCanvas.height = height * scale;
+
+        return {
+          canvas: tempCanvas,
+          scale,
+        };
+      },
+    );
+
+    canvasToBlob(canvas)
+      .then(() => {
+        renderPreview(canvas, previewNode);
+      })
+      .catch((error) => {
+        console.error(error);
+        renderPreview(new CanvasError(), previewNode);
+      });
+  }, [elements]);
+
+  return (
+    <Island padding={1} className="MiniMap">
+      <div
+        style={{
+          width: MINIMAP_WIDTH,
+          height: MINIMAP_HEIGHT,
+          position: "relative",
+          backgroundColor: appState.viewBackgroundColor,
+        }}
+      >
+        <div ref={previewRef} />
+        <MinimapViewport elements={elements} appState={appState} />
+      </div>
+    </Island>
+  );
+}