Files
grungnet/src/components/Map.js

297 lines
7.2 KiB
JavaScript
Raw Normal View History

2020-03-20 21:46:52 +11:00
import React, { useRef, useEffect, useState } from "react";
import { Box, Image } from "theme-ui";
2020-03-20 21:46:52 +11:00
import interact from "interactjs";
2020-04-18 18:11:21 +10:00
import ProxyToken from "./ProxyToken";
import TokenMenu from "./TokenMenu";
import MapToken from "./MapToken";
import MapDrawing from "./MapDrawing";
import MapControls from "./MapControls";
2020-03-20 13:33:12 +11:00
import { omit } from "../helpers/shared";
2020-03-20 13:33:12 +11:00
const mapTokenClassName = "map-token";
2020-03-20 21:46:52 +11:00
const zoomSpeed = -0.005;
const minZoom = 0.1;
const maxZoom = 5;
2020-03-20 13:33:12 +11:00
function Map({
mapSource,
mapData,
tokens,
onMapTokenChange,
onMapTokenRemove,
onMapChange,
2020-04-19 15:15:48 +10:00
onMapDraw,
onMapDrawUndo,
onMapDrawRedo,
drawActions,
drawActionIndex,
}) {
2020-03-20 13:33:12 +11:00
function handleProxyDragEnd(isOnMap, token) {
if (isOnMap && onMapTokenChange) {
onMapTokenChange(token);
2020-03-20 13:33:12 +11:00
}
if (!isOnMap && onMapTokenRemove) {
onMapTokenRemove(token);
}
}
/**
* Map drawing
*/
2020-03-20 21:46:52 +11:00
2020-04-19 00:24:06 +10:00
const [selectedTool, setSelectedTool] = useState("pan");
const [drawnShapes, setDrawnShapes] = useState([]);
function handleShapeAdd(shape) {
2020-04-19 15:15:48 +10:00
onMapDraw({ type: "add", shape });
}
function handleShapeRemove(shapeId) {
2020-04-19 15:15:48 +10:00
onMapDraw({ type: "remove", shapeId });
}
2020-04-19 15:15:48 +10:00
// Replay the draw actions and convert them to shapes for the map drawing
useEffect(() => {
let shapesById = {};
for (let i = 0; i <= drawActionIndex; i++) {
const action = drawActions[i];
if (action.type === "add") {
shapesById[action.shape.id] = action.shape;
}
if (action.type === "remove") {
shapesById = omit(shapesById, [action.shapeId]);
}
}
setDrawnShapes(Object.values(shapesById));
}, [drawActions, drawActionIndex]);
const disabledTools = [];
if (!mapData) {
disabledTools.push("pan");
disabledTools.push("brush");
}
if (drawnShapes.length === 0) {
disabledTools.push("erase");
}
/**
* Map movement
*/
const mapTranslateRef = useRef({ x: 0, y: 0 });
const mapScaleRef = useRef(1);
const mapMoveContainerRef = useRef();
function setTranslateAndScale(newTranslate, newScale) {
const moveContainer = mapMoveContainerRef.current;
moveContainer.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px) scale(${newScale})`;
mapScaleRef.current = newScale;
mapTranslateRef.current = newTranslate;
}
2020-03-20 21:46:52 +11:00
useEffect(() => {
function handleMove(event, isGesture) {
const scale = mapScaleRef.current;
const translate = mapTranslateRef.current;
let newScale = scale;
let newTranslate = translate;
if (isGesture) {
newScale = Math.max(Math.min(scale + event.ds, maxZoom), minZoom);
}
if (selectedTool === "pan" || isGesture) {
newTranslate = {
x: translate.x + event.dx,
y: translate.y + event.dy,
};
}
setTranslateAndScale(newTranslate, newScale);
}
interact(".map")
.gesturable({
listeners: {
move: (e) => handleMove(e, true),
2020-04-07 11:47:06 +10:00
},
})
.draggable({
inertia: true,
listeners: {
move: (e) => handleMove(e, false),
},
cursorChecker: () => {
return selectedTool === "pan" && mapData ? "move" : "default";
2020-04-07 11:47:06 +10:00
},
});
2020-04-07 11:47:06 +10:00
interact(".map").on("doubletap", (event) => {
2020-03-20 21:46:52 +11:00
event.preventDefault();
if (selectedTool === "pan") {
setTranslateAndScale({ x: 0, y: 0 }, 1);
}
2020-03-20 21:46:52 +11:00
});
}, [selectedTool, mapData]);
2020-03-20 21:46:52 +11:00
// Reset map transform when map changes
useEffect(() => {
setTranslateAndScale({ x: 0, y: 0 }, 1);
}, [mapSource]);
// Bind the wheel event of the map via a ref
// in order to support non-passive event listening
// to allow the track pad zoom to be interrupted
// see https://github.com/facebook/react/issues/14856
useEffect(() => {
const mapContainer = mapContainerRef.current;
function handleZoom(event) {
// Stop overscroll on chrome and safari
// also stop pinch to zoom on chrome
event.preventDefault();
const scale = mapScaleRef.current;
const translate = mapTranslateRef.current;
const deltaY = event.deltaY * zoomSpeed;
const newScale = Math.max(Math.min(scale + deltaY, maxZoom), minZoom);
setTranslateAndScale(translate, newScale);
}
if (mapContainer) {
mapContainer.addEventListener("wheel", handleZoom, {
passive: false,
});
}
return () => {
if (mapContainer) {
mapContainer.removeEventListener("wheel", handleZoom);
}
};
}, []);
/**
* Member setup
*/
const mapRef = useRef(null);
const mapContainerRef = useRef();
const rows = mapData && mapData.rows;
const tokenSizePercent = (1 / rows) * 100;
const aspectRatio = (mapData && mapData.width / mapData.height) || 1;
const mapImage = (
<Box
sx={{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
}}
>
<Image
ref={mapRef}
className="mapImage"
sx={{
width: "100%",
userSelect: "none",
touchAction: "none",
}}
src={mapSource}
/>
</Box>
);
const mapTokens = (
<Box
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
>
{Object.values(tokens).map((token) => (
<MapToken
key={token.id}
token={token}
tokenSizePercent={tokenSizePercent}
className={mapTokenClassName}
/>
))}
</Box>
);
return (
2020-03-20 13:33:12 +11:00
<>
<Box
2020-03-20 13:33:12 +11:00
className="map"
sx={{
flexGrow: 1,
position: "relative",
overflow: "hidden",
backgroundColor: "rgba(0, 0, 0, 0.1)",
userSelect: "none",
2020-04-07 11:47:06 +10:00
touchAction: "none",
}}
2020-03-20 13:33:12 +11:00
bg="background"
ref={mapContainerRef}
>
2020-03-20 13:33:12 +11:00
<Box
sx={{
position: "relative",
top: "50%",
left: "50%",
2020-04-07 11:47:06 +10:00
transform: "translate(-50%, -50%)",
2020-03-20 13:33:12 +11:00
}}
>
<Box ref={mapMoveContainerRef}>
2020-03-20 21:46:52 +11:00
<Box
sx={{
width: "100%",
height: 0,
2020-04-07 11:47:06 +10:00
paddingBottom: `${(1 / aspectRatio) * 100}%`,
2020-03-20 21:46:52 +11:00
}}
/>
{mapImage}
{mapTokens}
2020-04-18 18:11:21 +10:00
<MapDrawing
width={mapData ? mapData.width : 0}
height={mapData ? mapData.height : 0}
2020-04-19 00:24:06 +10:00
selectedTool={selectedTool}
shapes={drawnShapes}
onShapeAdd={handleShapeAdd}
onShapeRemove={handleShapeRemove}
2020-04-18 18:11:21 +10:00
/>
</Box>
2020-03-20 13:33:12 +11:00
</Box>
2020-04-19 00:24:06 +10:00
<MapControls
onMapChange={onMapChange}
onToolChange={setSelectedTool}
selectedTool={selectedTool}
disabledTools={disabledTools}
2020-04-19 15:15:48 +10:00
onUndo={onMapDrawUndo}
onRedo={onMapDrawRedo}
undoDisabled={drawActionIndex < 0}
redoDisabled={drawActionIndex === drawActions.length - 1}
2020-04-19 00:24:06 +10:00
/>
</Box>
2020-03-20 13:33:12 +11:00
<ProxyToken
tokenClassName={mapTokenClassName}
onProxyDragEnd={handleProxyDragEnd}
/>
<TokenMenu
tokenClassName={mapTokenClassName}
onTokenChange={onMapTokenChange}
/>
2020-03-20 13:33:12 +11:00
</>
);
}
export default Map;