179 lines
5.1 KiB
TypeScript
179 lines
5.1 KiB
TypeScript
import { FederatedMouseEvent, FederatedWheelEvent } from "pixi.js";
|
|
import { Component, ReactNode, useEffect, useState } from "react";
|
|
import { useTransform } from "./TransformContext";
|
|
import { useMapData } from "./MapDataContext";
|
|
import { SCALE_FACTOR } from "./Constants";
|
|
import { useApplication } from "@pixi/react";
|
|
|
|
class ErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
|
|
state = { hasError: false };
|
|
|
|
static getDerivedStateFromError() {
|
|
return { hasError: true };
|
|
}
|
|
|
|
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
console.error("Error caught:", error, info);
|
|
}
|
|
|
|
render() {
|
|
return this.state.hasError ? <p>Whoopsie Daisy!</p> : this.props.children;
|
|
}
|
|
}
|
|
|
|
|
|
export function InfiniteCanvas({children} : Readonly<{children?: ReactNode}>) {
|
|
const { position, setPosition, scale, setScale, rotation, setRotation, setScreenCenter, screenCenter } = useTransform();
|
|
const { routeData, originalRouteData } = useMapData();
|
|
|
|
const applicationRef = useApplication();
|
|
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
|
|
const [startRotation, setStartRotation] = useState(0);
|
|
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
|
|
|
useEffect(() => {
|
|
const canvas = applicationRef?.app.canvas;
|
|
if (!canvas) return;
|
|
const canvasRect = canvas.getBoundingClientRect();
|
|
const canvasLeft = canvasRect?.left ?? 0;
|
|
const canvasTop = canvasRect?.top ?? 0;
|
|
const centerX = window.innerWidth / 2 - canvasLeft;
|
|
const centerY = window.innerHeight / 2 - canvasTop;
|
|
setScreenCenter({x: centerX, y: centerY});
|
|
}, [applicationRef?.app.canvas, window.innerWidth, window.innerHeight]);
|
|
|
|
const handlePointerDown = (e: FederatedMouseEvent) => {
|
|
setIsDragging(true);
|
|
setStartPosition({
|
|
x: position.x,
|
|
y: position.y
|
|
});
|
|
setStartMousePosition({
|
|
x: e.globalX,
|
|
y: e.globalY
|
|
});
|
|
setStartRotation(rotation);
|
|
e.stopPropagation();
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
setRotation((originalRouteData?.rotate ?? 0) * Math.PI / 180);
|
|
}, [originalRouteData?.rotate]);
|
|
// Get canvas element and its dimensions/position
|
|
const handlePointerMove = (e: FederatedMouseEvent) => {
|
|
if (!isDragging) return;
|
|
|
|
if (e.shiftKey) {
|
|
const center = screenCenter ?? {x: 0, y: 0};
|
|
const startAngle = Math.atan2(startMousePosition.y - center.y, startMousePosition.x - center.x);
|
|
const currentAngle = Math.atan2(e.globalY - center.y, e.globalX - center.x);
|
|
|
|
// Calculate rotation difference in radians
|
|
const rotationDiff = currentAngle - startAngle;
|
|
|
|
// Update rotation
|
|
setRotation(startRotation + rotationDiff);
|
|
|
|
const cosDelta = Math.cos(rotationDiff);
|
|
const sinDelta = Math.sin(rotationDiff);
|
|
|
|
setPosition({
|
|
x: center.x * (1 - cosDelta) + startPosition.x * cosDelta + (center.y - startPosition.y) * sinDelta,
|
|
y: center.y * (1 - cosDelta) + startPosition.y * cosDelta + (startPosition.x - center.x) * sinDelta
|
|
});
|
|
|
|
} else {
|
|
setRotation(startRotation);
|
|
setPosition({
|
|
x: startPosition.x - startMousePosition.x + e.globalX,
|
|
y: startPosition.y - startMousePosition.y + e.globalY
|
|
});
|
|
}
|
|
e.stopPropagation();
|
|
};
|
|
|
|
// Handle mouse up
|
|
const handlePointerUp = (e: FederatedMouseEvent) => {
|
|
setIsDragging(false);
|
|
e.stopPropagation();
|
|
};
|
|
// Handle mouse wheel for zooming
|
|
const handleWheel = (e: FederatedWheelEvent) => {
|
|
e.stopPropagation();
|
|
|
|
// Get mouse position relative to canvas
|
|
const mouseX = e.globalX - position.x;
|
|
const mouseY = e.globalY - position.y;
|
|
|
|
// Calculate new scale
|
|
const scaleMin = (routeData?.scale_min ?? 10)/SCALE_FACTOR;
|
|
const scaleMax = (routeData?.scale_max ?? 20)/SCALE_FACTOR;
|
|
|
|
let zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; // Zoom out/in
|
|
//const newScale = scale * zoomFactor;
|
|
const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor));
|
|
zoomFactor = newScale / scale;
|
|
|
|
if (scale === newScale) {
|
|
return;
|
|
}
|
|
|
|
// Update position to zoom towards mouse cursor
|
|
setPosition({
|
|
x: position.x + mouseX * (1 - zoomFactor),
|
|
y: position.y + mouseY * (1 - zoomFactor)
|
|
});
|
|
|
|
setScale(newScale);
|
|
};
|
|
|
|
useEffect(() => {
|
|
applicationRef?.app.render();
|
|
console.log(position, scale, rotation);
|
|
}, [position, scale, rotation]);
|
|
|
|
|
|
return (
|
|
<ErrorBoundary>
|
|
{applicationRef?.app && (
|
|
<pixiGraphics
|
|
draw={(g) => {
|
|
const canvas = applicationRef.app.canvas;
|
|
g.clear();
|
|
g.rect(0, 0, canvas?.width ?? 0, canvas?.height ?? 0);
|
|
g.fill("#111");
|
|
}}
|
|
eventMode={'static'}
|
|
interactive
|
|
onPointerDown={handlePointerDown}
|
|
onGlobalPointerMove={handlePointerMove}
|
|
onPointerUp={handlePointerUp}
|
|
onPointerUpOutside={handlePointerUp}
|
|
onWheel={handleWheel}
|
|
/>
|
|
)}
|
|
<pixiContainer
|
|
x={position.x}
|
|
y={position.y}
|
|
scale={scale}
|
|
rotation={rotation}
|
|
>
|
|
{children}
|
|
</pixiContainer>
|
|
{/* Show center of the screen.
|
|
<pixiGraphics
|
|
eventMode="none"
|
|
|
|
draw={(g) => {
|
|
g.clear();
|
|
const center = screenCenter ?? {x: 0, y: 0};
|
|
g.circle(center.x, center.y, 1);
|
|
g.fill("#fff");
|
|
}}
|
|
/> */}
|
|
</ErrorBoundary>
|
|
);
|
|
} |