Simple minimap implementation
This commit is contained in:
parent
74e82d0d7c
commit
4c3544df4a
@ -41,6 +41,7 @@ import Stack from "./Stack";
|
|||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
|
import { MiniMap } from "./MiniMap";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@ -602,6 +603,7 @@ const LayerUI = ({
|
|||||||
>
|
>
|
||||||
{renderCustomFooter?.(false)}
|
{renderCustomFooter?.(false)}
|
||||||
{actionManager.renderAction("toggleShortcuts")}
|
{actionManager.renderAction("toggleShortcuts")}
|
||||||
|
<MiniMap appState={appState} elements={elements} />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className={clsx("disable-zen-mode", {
|
className={clsx("disable-zen-mode", {
|
||||||
|
5
src/components/MiniMap.scss
Normal file
5
src/components/MiniMap.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.Island.MiniMap {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 50px;
|
||||||
|
right: calc(var(--space-factor) * 4);
|
||||||
|
}
|
155
src/components/MiniMap.tsx
Normal file
155
src/components/MiniMap.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user