Compare commits
No commits in common. "1f3895a025cb21e97f95470c134401f68945eec4" and "8e6abb95dbf81fe366b9a8bd31bd6d76345c69d0" have entirely different histories.
1f3895a025
...
8e6abb95db
@ -1,13 +1,5 @@
|
|||||||
import { useMouse, useWindowSize } from '@uidotdev/usehooks';
|
import { useMouse, useWindowSize } from '@uidotdev/usehooks';
|
||||||
import {
|
import { FC, MouseEvent, useCallback, useMemo, useState } from 'react';
|
||||||
FC,
|
|
||||||
KeyboardEvent,
|
|
||||||
MouseEvent,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import './grid.scss';
|
import './grid.scss';
|
||||||
|
|
||||||
@ -25,14 +17,6 @@ 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 };
|
||||||
}
|
}
|
||||||
@ -64,128 +48,31 @@ 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_thin: Line[] = [];
|
const lines = [];
|
||||||
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) {
|
||||||
const thick = x % (GAP * 4) == 0;
|
lines.push({ ...range, x0: x, x1: x });
|
||||||
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) {
|
||||||
const thick = y % (GAP * 4) == 0;
|
lines.push({ ...range, y0: y, y1: y });
|
||||||
const lines = thick ? lines_thick : lines_thin;
|
|
||||||
lines.push({
|
|
||||||
...range,
|
|
||||||
y0: y,
|
|
||||||
y1: y,
|
|
||||||
stroke: thick ? THICK_COLOR : THIN_COLOR,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines_thin.concat(lines_thick);
|
return lines;
|
||||||
}, [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
|
||||||
@ -195,8 +82,8 @@ const Grid: FC = () => {
|
|||||||
y1={line.y0}
|
y1={line.y0}
|
||||||
x2={line.x1}
|
x2={line.x1}
|
||||||
y2={line.y1}
|
y2={line.y1}
|
||||||
stroke={line.stroke}
|
stroke="#123456"
|
||||||
strokeWidth={2}
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}, [gridLines]);
|
}, [gridLines]);
|
||||||
@ -243,6 +130,28 @@ 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"
|
||||||
@ -250,15 +159,20 @@ 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
|
||||||
}`}
|
}`}
|
||||||
onMouseDown={onGridClick}
|
onClick={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…
x
Reference in New Issue
Block a user