blockmatic/src/App.tsx
2024-12-08 14:36:57 +00:00

219 lines
5.9 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import { Eraser, Hand, Pencil } from "lucide-react";
import * as PIXI from "pixi.js";
import { Container, Stage } from "@pixi/react";
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from "@/components/ui/menubar";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import ThemeChanger from "./components/menubar/theme-changer";
import Blocks from "./components/blocks";
import Cursor from "./components/cursor";
import CursorInformation from "./components/cursor-information";
import spritesheet from "@/lib/data/blocks/programmer-art/spritesheet.json";
// Set scale mode to NEAREST
PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
function App() {
const stageContainerRef = useRef<HTMLDivElement>(null);
const [stageSize, setStageSize] = useState({ width: 0, height: 0 });
const [coords, setCoords] = useState<Position>({ x: 0, y: 0 });
const [mousePosition, setMousePosition] = useState<Position>({ x: 0, y: 0 });
const [localMousePosition, setLocalMousePosition] = useState<Position>({ x: 0, y: 0 });
const [dragging, setDragging] = useState(false);
const [scale, setScale] = useState(1);
const [blocks, setBlocks] = useState<Block[]>([]);
const [textures, setTextures] = useState<Record<string, PIXI.Texture>>({});
const [cssCursor, setCssCursor] = useState("grab");
const [tool, setTool] = useState<Tool>("hand");
const [selectedBlock, setSelectedBlock] = useState("stone");
const onToolChange = (value: Tool) => {
setTool(value);
setCssCursor(value === "hand" ? "grab" : "pointer");
};
const onMouseMove = (e: React.MouseEvent) => {
if (dragging) {
if (tool === "hand") {
setCoords((prevCoords) => ({
x: prevCoords.x + e.movementX,
y: prevCoords.y + e.movementY,
}));
}
onMouseDown();
}
const stageRect = stageContainerRef.current?.getBoundingClientRect();
if (!stageRect) return;
const mouseX = e.clientX - stageRect.left;
const mouseY = e.clientY - stageRect.top;
setMousePosition({
x: mouseX,
y: mouseY,
});
setLocalMousePosition({
x: (mouseX - coords.x) / scale,
y: (mouseY - coords.y) / scale,
});
};
const onMouseDown = () => {
setDragging(true);
const blockX = Math.floor(localMousePosition.x / 16);
const blockY = Math.floor(localMousePosition.y / 16);
const updatedBlocks = blocks.filter((b) => !(b.x === blockX && b.y === blockY));
switch (tool) {
case "hand":
setCssCursor("grabbing");
break;
case "pencil": {
setBlocks([
...updatedBlocks,
{
name: selectedBlock,
x: blockX,
y: blockY,
},
]);
break;
}
case "eraser":
setBlocks(updatedBlocks);
break;
}
};
const onMouseUp = () => {
setDragging(false);
setCssCursor(tool === "hand" ? "grab" : "pointer");
};
const onWheel = (e: React.WheelEvent) => {
e.preventDefault();
const scaleChange = e.deltaY > 0 ? -0.1 : 0.1;
const newScale = Math.min(Math.max(scale + scaleChange * scale, 0.25), 32);
setScale(newScale);
setCoords({
x: mousePosition.x - localMousePosition.x * newScale,
y: mousePosition.y - localMousePosition.y * newScale,
});
};
useEffect(() => {
const resizeCanvas = () => {
if (stageContainerRef.current) {
setStageSize({
width: stageContainerRef.current.offsetWidth,
height: stageContainerRef.current.offsetHeight,
});
}
};
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
const loadSpritesheet = async () => {
const sheet = new PIXI.Spritesheet(PIXI.BaseTexture.from("/blocks/programmer-art/spritesheet.png"), spritesheet);
await sheet.parse();
setTextures(sheet.textures);
};
loadSpritesheet();
}, []);
return (
<main className="h-screen grid grid-rows-[2.5rem_1fr] grid-cols-[2.5rem_1fr]">
<Menubar className="rounded-none border-t-0 border-x-0 col-span-2">
<MenubarMenu>
<a href="https://github.com/trafficlunar/blockmatic" className="ml-4 mr-2">
blockmatic
</a>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>Open Schematic</MenubarItem>
<MenubarItem>Open Image</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>Export to...</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem>.schematic</MenubarItem>
<MenubarItem>.litematic</MenubarItem>
<MenubarItem>image</MenubarItem>
</MenubarSubContent>
</MenubarSub>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>More</MenubarTrigger>
<MenubarContent>
<ThemeChanger />
</MenubarContent>
</MenubarMenu>
</Menubar>
<ToggleGroup
type="single"
value={tool}
onValueChange={onToolChange}
className="flex flex-col justify-start py-1 border-r border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950"
>
<ToggleGroupItem value="hand" className="!p-0 !h-8 !min-w-8">
<Hand />
</ToggleGroupItem>
<ToggleGroupItem value="pencil" className="!p-0 !h-8 !min-w-8">
<Pencil />
</ToggleGroupItem>
<ToggleGroupItem value="eraser" className="!p-0 !h-8 !min-w-8">
<Eraser />
</ToggleGroupItem>
</ToggleGroup>
<div ref={stageContainerRef} className="relative w-full h-full">
<Stage
width={stageSize.width}
height={stageSize.height}
onMouseMove={onMouseMove}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onWheel={onWheel}
style={{ cursor: cssCursor }}
>
<Container x={coords.x} y={coords.y} scale={scale}>
{textures && <Blocks blocks={blocks} setBlocks={setBlocks} textures={textures} />}
<Cursor localMousePosition={localMousePosition} />
</Container>
</Stage>
<CursorInformation localMousePosition={localMousePosition} blocks={blocks} />
</div>
</main>
);
}
export default App;