chore: pixi.js rewrite

This commit is contained in:
trafficlunar 2024-12-06 15:45:58 +00:00
parent a5bc500553
commit b30ba187e9
6 changed files with 780 additions and 152 deletions

View file

@ -10,16 +10,16 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@pixi/react": "7",
"@radix-ui/react-menubar": "^1.1.2", "@radix-ui/react-menubar": "^1.1.2",
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"konva": "^9.3.16",
"lucide-react": "^0.464.0", "lucide-react": "^0.464.0",
"pixi.js": "7",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-konva": "^18.2.10",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7"
}, },

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,9 @@
import { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Layer, Stage } from "react-konva";
import { Eraser, Hand, Pencil } from "lucide-react"; import { Eraser, Hand, Pencil } from "lucide-react";
import * as PIXI from "pixi.js";
import { Container, Stage } from "@pixi/react";
import { import {
Menubar, Menubar,
MenubarContent, MenubarContent,
@ -20,80 +22,62 @@ import Blocks from "./components/blocks";
import Cursor from "./components/cursor"; import Cursor from "./components/cursor";
import CursorInformation from "./components/cursor-information"; import CursorInformation from "./components/cursor-information";
// Set scale mode to NEAREST
PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
function App() { function App() {
const stageContainerRef = useRef<HTMLDivElement>(null); const stageContainerRef = useRef<HTMLDivElement>(null);
const stageRef = useRef(null);
const [stageSize, setStageSize] = useState({ const [stageSize, setStageSize] = useState({ width: 0, height: 0 });
width: 0, const [coords, setCoords] = useState<Position>({ x: 0, y: 0 });
height: 0,
});
const [stageScale, setStageScale] = useState(1);
const [stageCoords, setStageCoords] = useState<Position>({ x: 0, y: 0 });
const [mousePosition, setMousePosition] = 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 [tool, setTool] = useState<Tool>("hand"); const [dragging, setDragging] = useState(false);
const [selectedBlock, setSelectedBlock] = useState("stone"); const [scale, setScale] = useState(1);
const [blocks, setBlocks] = useState<Block[]>([]); const [blocks, setBlocks] = useState<Block[]>([]);
const [cssCursor, setCssCursor] = useState("grab"); const [cssCursor, setCssCursor] = useState("grab");
const [mouseDown, setMouseDown] = useState(false); const [tool, setTool] = useState<Tool>("hand");
const [selectedBlock, setSelectedBlock] = useState("stone");
const onToolChange = (value) => { const onToolChange = (value: Tool) => {
setTool(value); setTool(value);
setCssCursor(value === "hand" ? "grab" : "pointer");
switch (value) {
case "hand":
setCssCursor("grab");
break;
default:
setCssCursor("auto");
break;
}
}; };
const onMouseMove = (e) => { const onMouseMove = (e: React.MouseEvent) => {
const stage = e.target.getStage(); if (dragging) {
const oldScale = stage.scaleX(); if (tool === "hand") {
const pointer = stage.getPointerPosition(); setCoords((prevCoords) => ({
x: prevCoords.x + e.movementX,
y: prevCoords.y + e.movementY,
}));
}
onMouseDown(e);
}
const stageRect = stageContainerRef.current?.getBoundingClientRect();
if (!stageRect) return;
const mouseX = e.clientX - stageRect.left;
const mouseY = e.clientY - stageRect.top;
setMousePosition({ setMousePosition({
x: (pointer.x - stage.x()) / oldScale, x: mouseX,
y: (pointer.y - stage.y()) / oldScale, y: mouseY
}); });
setLocalMousePosition({
if (mouseDown) { x: (mouseX - coords.x) / scale,
onClick(e); y: (mouseY - coords.y) / scale
}
};
const onMouseUp = (e) => {
setMouseDown(false);
if (tool == "hand") {
setCssCursor("grab");
}
};
const onWheel = (e) => {
const stage = e.target.getStage();
const oldScale = stage.scaleX();
const pointer = stage.getPointerPosition();
const newScale = e.evt.deltaY < 0 ? oldScale * 1.05 : oldScale / 1.05;
setStageScale(newScale);
setStageCoords({
x: pointer.x - mousePosition.x * newScale,
y: pointer.y - mousePosition.y * newScale,
}); });
}; };
const onClick = (e) => { const onMouseDown = (e: React.MouseEvent) => {
const blockX = Math.floor(mousePosition.x / 16); setDragging(true);
const blockY = Math.floor(mousePosition.y / 16);
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)); const updatedBlocks = blocks.filter((b) => !(b.x === blockX && b.y === blockY));
switch (tool) { switch (tool) {
@ -111,15 +95,32 @@ function App() {
]); ]);
break; break;
} }
case "eraser": { case "eraser":
setBlocks(updatedBlocks); setBlocks(updatedBlocks);
break; 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, 0.25), 16);
setScale(newScale);
setCoords({
x: mousePosition.x - localMousePosition.x * newScale,
y: mousePosition.y - localMousePosition.y * newScale,
});
}; };
useEffect(() => { useEffect(() => {
if (stageContainerRef.current && stageRef.current) { if (stageContainerRef.current) {
setStageSize({ setStageSize({
width: stageContainerRef.current.offsetWidth, width: stageContainerRef.current.offsetWidth,
height: stageContainerRef.current.offsetHeight, height: stageContainerRef.current.offsetHeight,
@ -182,26 +183,19 @@ function App() {
<Stage <Stage
width={stageSize.width} width={stageSize.width}
height={stageSize.height} height={stageSize.height}
draggable={tool == "hand"}
ref={stageRef}
x={stageCoords.x}
y={stageCoords.y}
scaleX={stageScale}
scaleY={stageScale}
onMouseMove={onMouseMove} onMouseMove={onMouseMove}
onMouseDown={() => setMouseDown(true)} onMouseDown={onMouseDown}
onMouseUp={onMouseUp} onMouseUp={onMouseUp}
onWheel={onWheel} onWheel={onWheel}
onClick={onClick}
style={{ cursor: cssCursor }} style={{ cursor: cssCursor }}
> >
<Layer imageSmoothingEnabled={false}> <Container x={coords.x} y={coords.y} scale={scale}>
<Blocks blocks={blocks} setBlocks={setBlocks} /> <Blocks blocks={blocks} setBlocks={setBlocks} />
<Cursor mousePosition={mousePosition} /> <Cursor localMousePosition={localMousePosition} />
</Layer> </Container>
</Stage> </Stage>
<CursorInformation mousePosition={mousePosition} blocks={blocks} /> <CursorInformation localMousePosition={localMousePosition} blocks={blocks} />
</div> </div>
</main> </main>
); );

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Image as KonvaImage } from "react-konva";
import blocksData from "@/lib/blocks/programmer-art/average_colors.json"; import blocksData from "@/lib/blocks/programmer-art/average_colors.json";
import { Sprite } from "@pixi/react";
function Blocks({ blocks, setBlocks }: { blocks: Block[]; setBlocks: React.Dispatch<React.SetStateAction<Block[]>> }) { function Blocks({ blocks, setBlocks }: { blocks: Block[]; setBlocks: React.Dispatch<React.SetStateAction<Block[]>> }) {
const [images, setImages] = useState<{ [key: string]: HTMLImageElement }>({}); const [images, setImages] = useState<{ [key: string]: HTMLImageElement }>({});
@ -43,9 +43,9 @@ function Blocks({ blocks, setBlocks }: { blocks: Block[]; setBlocks: React.Dispa
if (ctx) { if (ctx) {
canvas.width = image.width; canvas.width = image.width;
canvas.height = image.height; canvas.height = image.height;
ctx.drawImage(image, 0, 0, image.width / 8, image.height / 8); ctx.drawImage(image, 0, 0, image.width / 4, image.height / 4);
const imageData = ctx.getImageData(0, 0, image.width / 8, image.height / 8); const imageData = ctx.getImageData(0, 0, image.width / 4, image.height / 4);
const newBlocks: Block[] = []; const newBlocks: Block[] = [];
for (let i = 0; i < imageData.data.length; i += 4) { for (let i = 0; i < imageData.data.length; i += 4) {
@ -76,7 +76,7 @@ function Blocks({ blocks, setBlocks }: { blocks: Block[]; setBlocks: React.Dispa
return ( return (
<> <>
{blocks.map((block, index) => ( {blocks.map((block, index) => (
<KonvaImage key={index} image={images[block.name]} x={block.x * 16} y={block.y * 16} /> <Sprite key={index} image={images[block.name]} x={block.x * 16} y={block.y * 16} />
))} ))}
</> </>
); );

View file

@ -1,22 +1,22 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
function CursorInformation({ mousePosition, blocks }: { mousePosition: Position; blocks: Block[] }) { function CursorInformation({ localMousePosition, blocks }: { localMousePosition: Position; blocks: Block[] }) {
const [position, setPosition] = useState({ x: 0, y: 0 }); const [position, setPosition] = useState({ x: 0, y: 0 });
const [block, setBlock] = useState<Block>(); const [block, setBlock] = useState<Block>();
useEffect(() => { useEffect(() => {
if (mousePosition) { if (localMousePosition) {
const snappedX = Math.floor(mousePosition.x / 16); const x = Math.floor(localMousePosition.x / 16);
const snappedY = Math.floor(mousePosition.y / 16); const y = Math.floor(localMousePosition.y / 16);
setPosition({ setPosition({
x: snappedX, x,
y: snappedY, y,
}); });
setBlock(blocks.find((b) => b.x === snappedX && b.y === snappedY)); setBlock(blocks.find((b) => b.x === x && b.y === y));
} }
}, [mousePosition]); }, [localMousePosition]);
return ( return (
<div className="absolute left-4 bottom-4 flex flex-col gap-1"> <div className="absolute left-4 bottom-4 flex flex-col gap-1">

View file

@ -1,22 +1,29 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Rect } from "react-konva"; import { Graphics } from "@pixi/react";
function Cursor({ mousePosition }: { mousePosition: Position }) { function Cursor({ localMousePosition }: { localMousePosition: Position }) {
const [position, setPosition] = useState({ x: 0, y: 0 }); const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => { useEffect(() => {
if (mousePosition) { if (localMousePosition) {
const snappedX = Math.floor(mousePosition.x / 16) * 16; const x = Math.floor(localMousePosition.x / 16) * 16;
const snappedY = Math.floor(mousePosition.y / 16) * 16; const y = Math.floor(localMousePosition.y / 16) * 16;
setPosition({ setPosition({ x, y });
x: snappedX,
y: snappedY,
});
} }
}, [mousePosition]); }, [localMousePosition]);
return <Rect x={position.x} y={position.y} width={16} height={16} stroke={"white"} strokeWidth={0.5} dash={[2.5]} />; return (
<Graphics
x={position.x}
y={position.y}
draw={(g) => {
g.clear();
g.lineStyle(1, 0xffffff, 1);
g.drawRect(0, 0, 16, 16);
}}
/>
);
} }
export default Cursor; export default Cursor;