Compare commits
2 Commits
8e6abb95db
...
1f3895a025
Author | SHA1 | Date | |
---|---|---|---|
|
1f3895a025 | ||
|
a258592054 |
@ -1,5 +1,13 @@
|
||||
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';
|
||||
|
||||
@ -17,6 +25,14 @@ interface Range {
|
||||
y1: number;
|
||||
}
|
||||
|
||||
interface Line {
|
||||
x0: number;
|
||||
y0: number;
|
||||
x1: number;
|
||||
y1: number;
|
||||
stroke: string;
|
||||
}
|
||||
|
||||
function pointsToRange(p0: Point, p1: Point): Range {
|
||||
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 };
|
||||
}
|
||||
|
||||
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 GAP = 20;
|
||||
|
||||
// const [offset, setOffset] = useState<Point>({ x: GAP / 2, y: GAP / 2 });
|
||||
const [offset, setOffset] = useState<Point>({ x: 10, y: 10 });
|
||||
const [offset, setOffset] = useState<Point>({ x: GAP * 2, y: GAP * 2 });
|
||||
const range = useRange(offset);
|
||||
const mousePoint = useMouseInRange(range);
|
||||
|
||||
const gridLines = useMemo(() => {
|
||||
const lines = [];
|
||||
const lines_thin: Line[] = [];
|
||||
const lines_thick: Line[] = [];
|
||||
const start = snapToGrid({ x: range.x0, y: range.y0 }, GAP);
|
||||
const end = snapToGrid({ x: range.x1, y: range.y1 }, 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) {
|
||||
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]);
|
||||
|
||||
const [userLines, setUserLines] = useState<Range[]>([]);
|
||||
const [userRedoLines, setUserRedoLines] = useState<Range[]>([]);
|
||||
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(() => {
|
||||
return gridLines.map((line, idx) => (
|
||||
<line
|
||||
@ -82,8 +195,8 @@ const Grid: FC = () => {
|
||||
y1={line.y0}
|
||||
x2={line.x1}
|
||||
y2={line.y1}
|
||||
stroke="#123456"
|
||||
strokeWidth="2"
|
||||
stroke={line.stroke}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
));
|
||||
}, [gridLines]);
|
||||
@ -130,28 +243,6 @@ const Grid: FC = () => {
|
||||
));
|
||||
}, [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 (
|
||||
<svg
|
||||
className="grid"
|
||||
@ -159,20 +250,15 @@ const Grid: FC = () => {
|
||||
viewBox={`${range.x0} ${range.y0} ${range.x1 - range.x0} ${
|
||||
range.y1 - range.y0
|
||||
}`}
|
||||
onClick={onGridClick}
|
||||
onMouseDown={onGridClick}
|
||||
onContextMenu={onGridContextMenu}
|
||||
// tabIndex={0}
|
||||
// onKeyDown={onGridKeyDown}
|
||||
>
|
||||
{gridLineElements}
|
||||
{userLineElements}
|
||||
{userLineIndicatorElement}
|
||||
{userPointIndicatorElement}
|
||||
<text
|
||||
x={range.x0 + 20}
|
||||
y={range.y0 + 60} // of baseline
|
||||
fill="#ffffff"
|
||||
fontSize="40px"
|
||||
>
|
||||
GRID
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user