Compare commits
2 Commits
8e6abb95db
...
1f3895a025
Author | SHA1 | Date | |
---|---|---|---|
|
1f3895a025 | ||
|
a258592054 |
@ -1,5 +1,13 @@
|
|||||||
import { useMouse, useWindowSize } from '@uidotdev/usehooks';
|
import { useMouse, useWindowSize } from '@uidotdev/usehooks';
|
||||||
import { FC, MouseEvent, useCallback, useMemo, useState } from 'react';
|
import {
|
||||||
|
FC,
|
||||||
|
KeyboardEvent,
|
||||||
|
MouseEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import './grid.scss';
|
import './grid.scss';
|
||||||
|
|
||||||
@ -17,6 +25,14 @@ interface Range {
|
|||||||
y1: number;
|
y1: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Line {
|
||||||
|
x0: number;
|
||||||
|
y0: number;
|
||||||
|
x1: number;
|
||||||
|
y1: number;
|
||||||
|
stroke: string;
|
||||||
|
}
|
||||||
|
|
||||||
function pointsToRange(p0: Point, p1: Point): Range {
|
function pointsToRange(p0: Point, p1: Point): Range {
|
||||||
return { x0: p0.x, y0: p0.y, x1: p1.x, y1: p1.y };
|
return { x0: p0.x, y0: p0.y, x1: p1.x, y1: p1.y };
|
||||||
}
|
}
|
||||||
@ -48,31 +64,128 @@ function useMouseInRange(range: Range): Point {
|
|||||||
return { x: mouse.x + range.x0, y: mouse.y + range.y0 };
|
return { x: mouse.x + range.x0, y: mouse.y + range.y0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useKeyPress(
|
||||||
|
callback: (event: KeyboardEvent) => void,
|
||||||
|
key: string,
|
||||||
|
ctrlKey: boolean = false
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (eventRaw: Event) => {
|
||||||
|
const event = eventRaw as any as KeyboardEvent;
|
||||||
|
if (event.key == key && event.ctrlKey == ctrlKey) {
|
||||||
|
callback(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keypress', listener);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keypress', listener);
|
||||||
|
};
|
||||||
|
}, [callback, key, ctrlKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const THICK_COLOR = '#123456';
|
||||||
|
const THIN_COLOR = '#0d1c2b';
|
||||||
|
|
||||||
const Grid: FC = () => {
|
const Grid: FC = () => {
|
||||||
const GAP = 20;
|
const GAP = 20;
|
||||||
|
|
||||||
// const [offset, setOffset] = useState<Point>({ x: GAP / 2, y: GAP / 2 });
|
const [offset, setOffset] = useState<Point>({ x: GAP * 2, y: GAP * 2 });
|
||||||
const [offset, setOffset] = useState<Point>({ x: 10, y: 10 });
|
|
||||||
const range = useRange(offset);
|
const range = useRange(offset);
|
||||||
const mousePoint = useMouseInRange(range);
|
const mousePoint = useMouseInRange(range);
|
||||||
|
|
||||||
const gridLines = useMemo(() => {
|
const gridLines = useMemo(() => {
|
||||||
const lines = [];
|
const lines_thin: Line[] = [];
|
||||||
|
const lines_thick: Line[] = [];
|
||||||
const start = snapToGrid({ x: range.x0, y: range.y0 }, GAP);
|
const start = snapToGrid({ x: range.x0, y: range.y0 }, GAP);
|
||||||
const end = snapToGrid({ x: range.x1, y: range.y1 }, GAP);
|
const end = snapToGrid({ x: range.x1, y: range.y1 }, GAP);
|
||||||
for (let x = start.x; x <= end.x; x += GAP) {
|
for (let x = start.x; x <= end.x; x += GAP) {
|
||||||
lines.push({ ...range, x0: x, x1: x });
|
const thick = x % (GAP * 4) == 0;
|
||||||
|
const lines = thick ? lines_thick : lines_thin;
|
||||||
|
lines.push({
|
||||||
|
...range,
|
||||||
|
x0: x,
|
||||||
|
x1: x,
|
||||||
|
stroke: thick ? THICK_COLOR : THIN_COLOR,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
for (let y = start.y; y <= end.y; y += GAP) {
|
for (let y = start.y; y <= end.y; y += GAP) {
|
||||||
lines.push({ ...range, y0: y, y1: y });
|
const thick = y % (GAP * 4) == 0;
|
||||||
|
const lines = thick ? lines_thick : lines_thin;
|
||||||
|
lines.push({
|
||||||
|
...range,
|
||||||
|
y0: y,
|
||||||
|
y1: y,
|
||||||
|
stroke: thick ? THICK_COLOR : THIN_COLOR,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines;
|
return lines_thin.concat(lines_thick);
|
||||||
}, [range, GAP]);
|
}, [range, GAP]);
|
||||||
|
|
||||||
const [userLines, setUserLines] = useState<Range[]>([]);
|
const [userLines, setUserLines] = useState<Range[]>([]);
|
||||||
|
const [userRedoLines, setUserRedoLines] = useState<Range[]>([]);
|
||||||
const [userStartPoint, setUserStartPoint] = useState<Point | null>(null);
|
const [userStartPoint, setUserStartPoint] = useState<Point | null>(null);
|
||||||
|
|
||||||
|
// mouse clicks create lines
|
||||||
|
const onGridClick = useCallback(
|
||||||
|
(event: MouseEvent<SVGSVGElement>) => {
|
||||||
|
// right-click to unset the start point
|
||||||
|
if (event.button === 2) {
|
||||||
|
setUserStartPoint(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const point = snapToGrid(mousePoint, GAP);
|
||||||
|
|
||||||
|
setUserRedoLines([]);
|
||||||
|
|
||||||
|
if (lodash.isEqual(userStartPoint, point)) return;
|
||||||
|
if (lodash.isNull(userStartPoint)) {
|
||||||
|
setUserStartPoint(point);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = pointsToRange(userStartPoint, point);
|
||||||
|
setUserLines([...userLines, line]);
|
||||||
|
|
||||||
|
if (event.shiftKey == false) {
|
||||||
|
setUserStartPoint(null);
|
||||||
|
} else {
|
||||||
|
setUserStartPoint(point);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[userStartPoint, setUserStartPoint, setUserLines, GAP, mousePoint]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onGridContextMenu = useCallback(
|
||||||
|
(event: MouseEvent<SVGSVGElement>) => {
|
||||||
|
event.preventDefault(); // disable default context menu
|
||||||
|
},
|
||||||
|
[setUserStartPoint]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ctrl+z/ctrl+y for undo/redo
|
||||||
|
const onUndo = useCallback(() => {
|
||||||
|
console.log('onUndo', userLines);
|
||||||
|
const line = userLines.at(-1);
|
||||||
|
if (!line) return;
|
||||||
|
|
||||||
|
setUserLines(userLines.slice(0, -1));
|
||||||
|
setUserRedoLines([...userRedoLines, line]);
|
||||||
|
}, [userLines, setUserLines, userRedoLines, setUserRedoLines]);
|
||||||
|
|
||||||
|
const onRedo = useCallback(() => {
|
||||||
|
console.log('onRedo', userRedoLines);
|
||||||
|
const line = userRedoLines.at(-1);
|
||||||
|
if (!line) return;
|
||||||
|
|
||||||
|
setUserRedoLines(userRedoLines.slice(0, -1));
|
||||||
|
setUserLines([...userLines, line]);
|
||||||
|
}, [userLines, setUserLines, userRedoLines, setUserRedoLines]);
|
||||||
|
|
||||||
|
useKeyPress(onUndo, 'z', true);
|
||||||
|
useKeyPress(onRedo, 'y', true);
|
||||||
|
|
||||||
const gridLineElements = useMemo(() => {
|
const gridLineElements = useMemo(() => {
|
||||||
return gridLines.map((line, idx) => (
|
return gridLines.map((line, idx) => (
|
||||||
<line
|
<line
|
||||||
@ -82,8 +195,8 @@ const Grid: FC = () => {
|
|||||||
y1={line.y0}
|
y1={line.y0}
|
||||||
x2={line.x1}
|
x2={line.x1}
|
||||||
y2={line.y1}
|
y2={line.y1}
|
||||||
stroke="#123456"
|
stroke={line.stroke}
|
||||||
strokeWidth="2"
|
strokeWidth={2}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}, [gridLines]);
|
}, [gridLines]);
|
||||||
@ -130,28 +243,6 @@ const Grid: FC = () => {
|
|||||||
));
|
));
|
||||||
}, [userLines]);
|
}, [userLines]);
|
||||||
|
|
||||||
const onGridClick = useCallback(
|
|
||||||
(event: MouseEvent<SVGSVGElement>) => {
|
|
||||||
const point = snapToGrid(mousePoint, GAP);
|
|
||||||
|
|
||||||
if (lodash.isEqual(userStartPoint, point)) return;
|
|
||||||
if (lodash.isNull(userStartPoint)) {
|
|
||||||
setUserStartPoint(point);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const line = pointsToRange(userStartPoint, point);
|
|
||||||
setUserLines([...userLines, line]);
|
|
||||||
|
|
||||||
if (event.shiftKey == false) {
|
|
||||||
setUserStartPoint(null);
|
|
||||||
} else {
|
|
||||||
setUserStartPoint(point);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[userStartPoint, setUserStartPoint, setUserLines, GAP, mousePoint]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className="grid"
|
className="grid"
|
||||||
@ -159,20 +250,15 @@ const Grid: FC = () => {
|
|||||||
viewBox={`${range.x0} ${range.y0} ${range.x1 - range.x0} ${
|
viewBox={`${range.x0} ${range.y0} ${range.x1 - range.x0} ${
|
||||||
range.y1 - range.y0
|
range.y1 - range.y0
|
||||||
}`}
|
}`}
|
||||||
onClick={onGridClick}
|
onMouseDown={onGridClick}
|
||||||
|
onContextMenu={onGridContextMenu}
|
||||||
|
// tabIndex={0}
|
||||||
|
// onKeyDown={onGridKeyDown}
|
||||||
>
|
>
|
||||||
{gridLineElements}
|
{gridLineElements}
|
||||||
{userLineElements}
|
{userLineElements}
|
||||||
{userLineIndicatorElement}
|
{userLineIndicatorElement}
|
||||||
{userPointIndicatorElement}
|
{userPointIndicatorElement}
|
||||||
<text
|
|
||||||
x={range.x0 + 20}
|
|
||||||
y={range.y0 + 60} // of baseline
|
|
||||||
fill="#ffffff"
|
|
||||||
fontSize="40px"
|
|
||||||
>
|
|
||||||
GRID
|
|
||||||
</text>
|
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user