feat: add bare-bones cursor and rectangle select tool
they do not currently move blocks
This commit is contained in:
parent
246afd9120
commit
776ff73bf1
8 changed files with 169 additions and 36 deletions
|
|
@ -10,6 +10,7 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pixi/graphics-smooth": "^1.1.1",
|
||||
"@pixi/react": "^7.1.2",
|
||||
"@pixi/tilemap": "4.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ importers:
|
|||
|
||||
.:
|
||||
dependencies:
|
||||
'@pixi/graphics-smooth':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(@pixi/core@7.4.2)(@pixi/display@7.4.2(@pixi/core@7.4.2))(@pixi/graphics@7.4.2(@pixi/core@7.4.2)(@pixi/display@7.4.2(@pixi/core@7.4.2))(@pixi/sprite@7.4.2(@pixi/core@7.4.2)(@pixi/display@7.4.2(@pixi/core@7.4.2))))
|
||||
'@pixi/react':
|
||||
specifier: ^7.1.2
|
||||
version: 7.1.2(rbk2pa5kjfujbi7gk4qxenqa5a)
|
||||
|
|
@ -690,6 +693,13 @@ packages:
|
|||
peerDependencies:
|
||||
'@pixi/core': 7.4.2
|
||||
|
||||
'@pixi/graphics-smooth@1.1.1':
|
||||
resolution: {integrity: sha512-9xIFWZhHGEb6KCnyWL6TVPYG/QkF0YDM/yDU5EvjTQbaj/1cITrXtI5P3tBkB5H0DQi+8J8/QS38MjfqNEJAYQ==}
|
||||
peerDependencies:
|
||||
'@pixi/core': ^7.2.0
|
||||
'@pixi/display': ^7.2.0
|
||||
'@pixi/graphics': ^7.2.0
|
||||
|
||||
'@pixi/graphics@7.4.2':
|
||||
resolution: {integrity: sha512-jH4/Tum2RqWzHGzvlwEr7HIVduoLO57Ze705N2zQPkUD57TInn5911aGUeoua7f/wK8cTLGzgB9BzSo2kTdcHw==}
|
||||
peerDependencies:
|
||||
|
|
@ -3499,6 +3509,12 @@ snapshots:
|
|||
dependencies:
|
||||
'@pixi/core': 7.4.2
|
||||
|
||||
'@pixi/graphics-smooth@1.1.1(@pixi/core@7.4.2)(@pixi/display@7.4.2(@pixi/core@7.4.2))(@pixi/graphics@7.4.2(@pixi/core@7.4.2)(@pixi/display@7.4.2(@pixi/core@7.4.2))(@pixi/sprite@7.4.2(@pixi/core@7.4.2)(@pixi/display@7.4.2(@pixi/core@7.4.2))))':
|
||||
dependencies:
|
||||
'@pixi/core': 7.4.2
|
||||
'@pixi/display': 7.4.2(@pixi/core@7.4.2)
|
||||
'@pixi/graphics': 7.4.2(@pixi/core@7.4.2)(@pixi/display@7.4.2(@pixi/core@7.4.2))(@pixi/sprite@7.4.2(@pixi/core@7.4.2)(@pixi/display@7.4.2(@pixi/core@7.4.2)))
|
||||
|
||||
'@pixi/graphics@7.4.2(@pixi/core@7.4.2)(@pixi/display@7.4.2(@pixi/core@7.4.2))(@pixi/sprite@7.4.2(@pixi/core@7.4.2)(@pixi/display@7.4.2(@pixi/core@7.4.2)))':
|
||||
dependencies:
|
||||
'@pixi/core': 7.4.2
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { useTextures } from "@/hooks/useTextures";
|
|||
|
||||
import Blocks from "./Blocks";
|
||||
import Cursor from "./Cursor";
|
||||
import SelectionBox from "./SelectionBox";
|
||||
import Grid from "./Grid";
|
||||
import CanvasBorder from "./CanvasBorder";
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ function Canvas() {
|
|||
|
||||
const [holdingAlt, setHoldingAlt] = useState(false);
|
||||
const [oldTool, setOldTool] = useState<Tool>("hand");
|
||||
const [selectionBoxBounds, setSelectionBoxBounds] = useState<BoundingBox>({ minX: 0, minY: 0, maxX: 0, maxY: 0 });
|
||||
|
||||
const visibleArea = useMemo(() => {
|
||||
const blockSize = 16 * scale;
|
||||
|
|
@ -64,14 +66,15 @@ function Canvas() {
|
|||
);
|
||||
}, [blocks, visibleArea]);
|
||||
|
||||
const zoomToMousePosition = useCallback(
|
||||
const zoom = useCallback(
|
||||
(newScale: number) => {
|
||||
setScale(newScale);
|
||||
setCoords({
|
||||
x: mousePosition.x - ((mousePosition.x - coords.x) / scale) * newScale,
|
||||
y: mousePosition.y - ((mousePosition.y - coords.y) / scale) * newScale,
|
||||
});
|
||||
},
|
||||
[coords, mousePosition, scale, setCoords]
|
||||
[coords, mousePosition, scale, setCoords, setScale]
|
||||
);
|
||||
|
||||
const updateCssCursor = useCallback(() => {
|
||||
|
|
@ -148,39 +151,71 @@ function Canvas() {
|
|||
|
||||
const onMouseMove = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (dragging) {
|
||||
if (tool === "hand") {
|
||||
setCoords((prevCoords) => ({
|
||||
x: prevCoords.x + e.movementX,
|
||||
y: prevCoords.y + e.movementY,
|
||||
}));
|
||||
}
|
||||
onToolUse();
|
||||
}
|
||||
|
||||
if (!stageContainerRef.current) return;
|
||||
|
||||
const oldMouseCoords = mouseCoords;
|
||||
|
||||
const rect = stageContainerRef.current.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
const newMouseCoords = {
|
||||
x: Math.floor((mouseX - coords.x) / (16 * scale)),
|
||||
y: Math.floor((mouseY - coords.y) / (16 * scale)),
|
||||
};
|
||||
|
||||
setMousePosition({
|
||||
x: mouseX,
|
||||
y: mouseY,
|
||||
});
|
||||
setMouseCoords({
|
||||
x: Math.floor((mouseX - coords.x) / (16 * scale)),
|
||||
y: Math.floor((mouseY - coords.y) / (16 * scale)),
|
||||
});
|
||||
setMouseCoords(newMouseCoords);
|
||||
|
||||
if (dragging) {
|
||||
switch (tool) {
|
||||
case "hand":
|
||||
setCoords((prevCoords) => ({
|
||||
x: prevCoords.x + e.movementX,
|
||||
y: prevCoords.y + e.movementY,
|
||||
}));
|
||||
break;
|
||||
case "move": {
|
||||
setSelectionBoxBounds((prev) => ({
|
||||
minX: prev.minX + (newMouseCoords.x - oldMouseCoords.x),
|
||||
minY: prev.minY + (newMouseCoords.y - oldMouseCoords.y),
|
||||
maxX: prev.maxX + (newMouseCoords.x - oldMouseCoords.x),
|
||||
maxY: prev.maxY + (newMouseCoords.y - oldMouseCoords.y),
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case "rectangle-select":
|
||||
setSelectionBoxBounds((prev) => ({
|
||||
...prev,
|
||||
maxX: mouseCoords.x + 1,
|
||||
maxY: mouseCoords.y + 1,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
onToolUse();
|
||||
}
|
||||
},
|
||||
[dragging, coords, scale, tool, onToolUse, setCoords]
|
||||
[dragging, coords, scale, tool, onToolUse, setCoords, setSelectionBoxBounds, mouseCoords]
|
||||
);
|
||||
|
||||
const onMouseDown = useCallback(() => {
|
||||
setDragging(true);
|
||||
onToolUse();
|
||||
updateCssCursor();
|
||||
}, [onToolUse, updateCssCursor]);
|
||||
|
||||
if (tool == "rectangle-select") {
|
||||
setSelectionBoxBounds({
|
||||
minX: mouseCoords.x,
|
||||
minY: mouseCoords.y,
|
||||
maxX: mouseCoords.x,
|
||||
maxY: mouseCoords.y,
|
||||
});
|
||||
}
|
||||
}, [onToolUse, updateCssCursor, tool, setSelectionBoxBounds, mouseCoords]);
|
||||
|
||||
const onMouseUp = useCallback(() => {
|
||||
setDragging(false);
|
||||
|
|
@ -192,10 +227,9 @@ function Canvas() {
|
|||
e.preventDefault();
|
||||
const scaleChange = e.deltaY > 0 ? -0.1 : 0.1;
|
||||
const newScale = Math.min(Math.max(scale + scaleChange * scale, 0.1), 32);
|
||||
setScale(newScale);
|
||||
zoomToMousePosition(newScale);
|
||||
zoom(newScale);
|
||||
},
|
||||
[scale, zoomToMousePosition, setScale]
|
||||
[scale, zoom]
|
||||
);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
|
|
@ -208,15 +242,14 @@ function Canvas() {
|
|||
case "zoom": {
|
||||
const scaleChange = holdingAlt ? -0.1 : 0.1;
|
||||
const newScale = Math.min(Math.max(scale + scaleChange * scale, 0.1), 32);
|
||||
setScale(newScale);
|
||||
zoomToMousePosition(newScale);
|
||||
zoom(newScale);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [tool, holdingAlt, scale, mouseCoords, blocks, zoomToMousePosition, setScale, setSelectedBlock]);
|
||||
}, [tool, holdingAlt, scale, mouseCoords, blocks, setSelectedBlock, zoom]);
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
|
|
@ -230,15 +263,21 @@ function Canvas() {
|
|||
setTool("hand");
|
||||
break;
|
||||
case "2":
|
||||
setTool("pencil");
|
||||
setTool("move");
|
||||
break;
|
||||
case "3":
|
||||
setTool("eraser");
|
||||
setTool("rectangle-select");
|
||||
break;
|
||||
case "4":
|
||||
setTool("eyedropper");
|
||||
setTool("pencil");
|
||||
break;
|
||||
case "5":
|
||||
setTool("eraser");
|
||||
break;
|
||||
case "6":
|
||||
setTool("eyedropper");
|
||||
break;
|
||||
case "7":
|
||||
setTool("zoom");
|
||||
break;
|
||||
case "Alt":
|
||||
|
|
@ -327,6 +366,7 @@ function Canvas() {
|
|||
<Container x={coords.x} y={coords.y} scale={scale}>
|
||||
{settings.canvasBorder && <CanvasBorder canvasSize={canvasSize} isDark={isDark} />}
|
||||
<Cursor mouseCoords={mouseCoords} radius={radius} isDark={isDark} />
|
||||
<SelectionBox bounds={selectionBoxBounds} coords={coords} scale={scale} isDark={isDark} />
|
||||
</Container>
|
||||
|
||||
{settings.grid && (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Graphics } from "@pixi/react";
|
||||
|
||||
interface Props {
|
||||
canvasSize: CanvasSize;
|
||||
canvasSize: BoundingBox;
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
52
src/components/canvas/SelectionBox.tsx
Normal file
52
src/components/canvas/SelectionBox.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { useApp } from "@pixi/react";
|
||||
import { DashLineShader, SmoothGraphics } from "@pixi/graphics-smooth";
|
||||
|
||||
interface Props {
|
||||
bounds: BoundingBox;
|
||||
coords: Position;
|
||||
scale: number;
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
const shader = new DashLineShader({ dash: 8, gap: 5 });
|
||||
|
||||
function SelectionBox({ bounds, coords, scale, isDark }: Props) {
|
||||
const app = useApp();
|
||||
const selectionRef = useRef<SmoothGraphics>();
|
||||
|
||||
const drawSelection = () => {
|
||||
if (!selectionRef.current) return;
|
||||
const graphics = selectionRef.current;
|
||||
graphics.clear();
|
||||
graphics.lineStyle({ width: 1, color: isDark ? 0xffffff : 0x000000, shader });
|
||||
graphics.drawRect(
|
||||
bounds.minX * 16 * scale,
|
||||
bounds.minY * 16 * scale,
|
||||
(bounds.maxX - bounds.minX) * 16 * scale,
|
||||
(bounds.maxY - bounds.minY) * 16 * scale
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const graphics = new SmoothGraphics();
|
||||
selectionRef.current = graphics;
|
||||
drawSelection();
|
||||
app.stage.addChild(graphics);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectionRef.current) return;
|
||||
const graphics = selectionRef.current;
|
||||
|
||||
graphics.x = coords.x;
|
||||
graphics.y = coords.y;
|
||||
drawSelection();
|
||||
}, [coords]);
|
||||
|
||||
useEffect(drawSelection, [bounds]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default SelectionBox;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useContext } from "react";
|
||||
import { EraserIcon, HandIcon, PencilIcon, PipetteIcon, ZoomInIcon } from "lucide-react";
|
||||
import { EraserIcon, HandIcon, MousePointer2Icon, PencilIcon, PipetteIcon, SquareDashedIcon, ZoomInIcon } from "lucide-react";
|
||||
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
|
@ -33,6 +33,30 @@ function Toolbar() {
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Move */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<ToggleGroupItem value="move" className="!p-0 !h-8 !min-w-8">
|
||||
<MousePointer2Icon />
|
||||
</ToggleGroupItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Move (2)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Rectangle Select */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<ToggleGroupItem value="rectangle-select" className="!p-0 !h-8 !min-w-8">
|
||||
<SquareDashedIcon />
|
||||
</ToggleGroupItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Rectangle Select (3)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Pencil */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
|
|
@ -41,7 +65,7 @@ function Toolbar() {
|
|||
</ToggleGroupItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Pencil (2)</p>
|
||||
<p>Pencil (4)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -53,7 +77,7 @@ function Toolbar() {
|
|||
</ToggleGroupItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Eraser (3)</p>
|
||||
<p>Eraser (5)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -65,7 +89,7 @@ function Toolbar() {
|
|||
</ToggleGroupItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Eyedropper (4)</p>
|
||||
<p>Eyedropper (6)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -77,7 +101,7 @@ function Toolbar() {
|
|||
</ToggleGroupItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Zoom (5)</p>
|
||||
<p>Zoom (7)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { createContext, ReactNode, useMemo, useState } from "react";
|
|||
|
||||
interface Context {
|
||||
stageSize: Dimension;
|
||||
canvasSize: CanvasSize;
|
||||
canvasSize: BoundingBox;
|
||||
blocks: Block[];
|
||||
coords: Position;
|
||||
scale: number;
|
||||
|
|
|
|||
4
src/types.d.ts
vendored
4
src/types.d.ts
vendored
|
|
@ -10,7 +10,7 @@ interface Dimension {
|
|||
height: number;
|
||||
}
|
||||
|
||||
interface CanvasSize {
|
||||
interface BoundingBox {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
|
|
@ -21,7 +21,7 @@ interface Block extends Position {
|
|||
name: string;
|
||||
}
|
||||
|
||||
type Tool = "hand" | "pencil" | "eraser" | "eyedropper" | "zoom";
|
||||
type Tool = "hand" | "move" | "rectangle-select" | "pencil" | "eraser" | "eyedropper" | "zoom";
|
||||
|
||||
interface Settings {
|
||||
grid: boolean;
|
||||
|
|
|
|||
Loading…
Reference in a new issue