Feat/minimap (#44)

* feat: New minimap

* imporved preview

* better start values

* smarter destructure

* getNodesInside refactor

* refactor(minimap): add maskColor and nodeBorderRadius props

* refactor(gitignore): add dist

* refactor(minimap): show empty minimap when there are no nodes

closes #39
This commit is contained in:
AndyLnd
2019-10-22 14:15:36 +02:00
committed by Moritz
parent 99dcc775b4
commit dcc38b23d8
14 changed files with 2525 additions and 504 deletions
+98 -76
View File
@@ -1,15 +1,21 @@
import React, { useRef, useEffect, CSSProperties } from 'react';
import React, { CSSProperties } from 'react';
import classnames from 'classnames';
import { useStoreState } from '../../store/hooks';
import { getNodesInside } from '../../utils/graph';
import { Node } from '../../types';
import { getRectOfNodes, getBoundsofRects } from '../../utils/graph';
import { Node, Rect } from '../../types';
type StringFunc = (node: Node) => string;
interface MiniMapProps extends React.HTMLAttributes<HTMLCanvasElement> {
bgColor?: string;
nodeColor?: string | StringFunc;
interface MiniMapProps extends React.HTMLAttributes<SVGSVGElement> {
nodeColor: string | StringFunc;
nodeBorderRadius: number;
maskColor: string;
}
interface MiniMapNodeProps {
node: Node;
color: string;
borderRadius: number;
}
const baseStyle: CSSProperties = {
@@ -18,81 +24,97 @@ const baseStyle: CSSProperties = {
bottom: 10,
right: 10,
width: 200,
height: 150,
};
export default ({
style = {},
className,
bgColor = '#f8f8f8',
nodeColor = '#ddd',
}: MiniMapProps) => {
const canvasNode = useRef<HTMLCanvasElement>(null);
const state = useStoreState(s => ({
width: s.width,
height: s.height,
nodes: s.nodes,
transform: s.transform,
}));
const mapClasses = classnames('react-flow__minimap', className);
const nodePositions = state.nodes.map(n => n.__rg.position);
const width: number = +(style.width || baseStyle.width || 0);
const height = (state.height / (state.width || 1)) * width;
const bbox = { x: 0, y: 0, width: state.width, height: state.height };
const scaleFactor = width / state.width;
const nodeColorFunc = (nodeColor instanceof Function
? nodeColor
: () => nodeColor) as StringFunc;
useEffect(() => {
if (!canvasNode || !canvasNode.current) {
return;
}
const ctx = canvasNode.current.getContext('2d');
if (!ctx) {
return;
}
const nodesInside = getNodesInside(
state.nodes,
bbox,
state.transform,
true
);
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, width, height);
nodesInside.forEach(n => {
const pos = n.__rg.position;
const transformX = state.transform[0];
const transformY = state.transform[1];
const x = pos.x * state.transform[2] + transformX;
const y = pos.y * state.transform[2] + transformY;
ctx.fillStyle = nodeColorFunc(n);
ctx.fillRect(
x * scaleFactor,
y * scaleFactor,
n.__rg.width * scaleFactor * state.transform[2],
n.__rg.height * scaleFactor * state.transform[2]
);
});
}, [canvasNode.current, nodePositions, state.transform, height]);
const MiniMapNode = ({ node, color, borderRadius }: MiniMapNodeProps) => {
const {
position: { x, y },
width,
height,
} = node.__rg;
const { background, backgroundColor } = node.style || {};
const fill = (background || backgroundColor || color) as string;
return (
<canvas
style={{
...baseStyle,
...style,
height,
}}
<rect
className="react-flow__minimap-node"
x={x}
y={y}
rx={borderRadius}
ry={borderRadius}
width={width}
height={height}
className={mapClasses}
ref={canvasNode}
fill={fill}
/>
);
};
export default ({
style = { backgroundColor: '#f8f8f8' },
className,
nodeColor = '#ddd',
nodeBorderRadius = 5,
maskColor = 'rgba(10, 10, 10, .25)',
}: MiniMapProps) => {
const state = useStoreState(({ width, height, nodes, transform: [tX, tY, tScale] }) => ({
width,
height,
nodes,
tX,
tY,
tScale,
}));
const mapClasses = classnames('react-flow__minimap', className);
const elementWidth = (style.width || baseStyle.width)! as number;
const elementHeight = (style.height || baseStyle.height)! as number;
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc;
const hasNodes = state.nodes && state.nodes.length;
const bb = getRectOfNodes(state.nodes);
const viewBB: Rect = {
x: -state.tX / state.tScale,
y: -state.tY / state.tScale,
width: state.width / state.tScale,
height: state.height / state.tScale,
};
const boundingRect = hasNodes ? getBoundsofRects(bb, viewBB) : viewBB;
const scaledWidth = boundingRect.width / elementWidth;
const scaledHeight = boundingRect.height / elementHeight;
const viewScale = Math.max(scaledWidth, scaledHeight);
const viewWidth = viewScale * elementWidth;
const viewHeight = viewScale * elementHeight;
const offset = 5 * viewScale;
const x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset;
const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
const width = viewWidth + offset * 2;
const height = viewHeight + offset * 2;
return (
<svg
width={elementWidth}
height={elementHeight}
viewBox={`${x} ${y} ${width} ${height}`}
style={{
...baseStyle,
...style,
}}
className={mapClasses}
>
{state.nodes.map(node => (
<MiniMapNode key={node.id} node={node} color={nodeColorFunc(node)} borderRadius={nodeBorderRadius} />
))}
<path
className="react-flow__minimap-mask"
d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`}
fill={maskColor}
fillRule="evenodd"
/>
</svg>
);
};