Simple minimap implementation

This commit is contained in:
tk338g 2021-02-07 20:45:37 +01:00
parent 74e82d0d7c
commit 4c3544df4a
3 changed files with 162 additions and 0 deletions

View File

@ -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", {

View File

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

155
src/components/MiniMap.tsx Normal file
View File

@ -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>
);
}