Files
grungnet/src/components/map/Map.js

301 lines
7.4 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";
import ProxyToken from "../token/ProxyToken";
import TokenMenu from "../token/TokenMenu";
2020-04-18 18:11:21 +10:00
import MapToken from "./MapToken";
import MapDrawing from "./MapDrawing";
import MapFog from "./MapFog";
import MapControls from "./MapControls";
2020-03-20 13:33:12 +11:00
import { omit } from "../../helpers/shared";
import useDataSource from "../../helpers/useDataSource";
import MapInteraction from "./MapInteraction";
import { mapSources as defaultMapSources } from "../../maps";
const mapTokenProxyClassName = "map-token__proxy";
const mapTokenMenuClassName = "map-token__menu";
2020-03-20 13:33:12 +11:00
function Map({
map,
2020-04-23 17:23:34 +10:00
mapState,
tokens,
onMapTokenStateChange,
onMapTokenStateRemove,
onMapChange,
onMapStateChange,
2020-04-19 15:15:48 +10:00
onMapDraw,
onFogDraw,
onMapUndo,
onMapRedo,
2020-04-29 21:12:57 +10:00
allowMapDrawing,
allowFogDrawing,
2020-04-26 14:34:27 +10:00
allowTokenChange,
}) {
const mapSource = useDataSource(map, defaultMapSources);
function handleProxyDragEnd(isOnMap, tokenState) {
if (isOnMap && onMapTokenStateChange) {
onMapTokenStateChange(tokenState);
2020-03-20 13:33:12 +11:00
}
if (!isOnMap && onMapTokenStateRemove) {
onMapTokenStateRemove(tokenState);
2020-03-20 13:33:12 +11:00
}
}
/**
* Map drawing
*/
2020-03-20 21:46:52 +11:00
const [selectedToolId, setSelectedToolId] = useState("pan");
const [toolSettings, setToolSettings] = useState({
2020-04-28 17:03:17 +10:00
fog: { type: "add", useEdgeSnapping: true, useGridSnapping: false },
brush: {
color: "darkGray",
type: "stroke",
useBlending: false,
},
shape: {
color: "red",
type: "rectangle",
useBlending: true,
},
});
2020-04-29 20:55:52 +10:00
function handleToolSettingChange(tool, change) {
setToolSettings((prevSettings) => ({
...prevSettings,
[tool]: {
...prevSettings[tool],
...change,
},
}));
}
2020-04-19 00:24:06 +10:00
2020-04-29 20:55:52 +10:00
function handleToolAction(action) {
if (action === "eraseAll") {
onMapDraw({
type: "remove",
shapeIds: mapShapes.map((s) => s.id),
timestamp: Date.now(),
});
}
}
const [mapShapes, setMapShapes] = useState([]);
function handleMapShapeAdd(shape) {
onMapDraw({ type: "add", shapes: [shape], timestamp: Date.now() });
}
function handleMapShapeRemove(shapeId) {
onMapDraw({ type: "remove", shapeIds: [shapeId], timestamp: Date.now() });
}
const [fogShapes, setFogShapes] = useState([]);
function handleFogShapeAdd(shape) {
onFogDraw({ type: "add", shapes: [shape], timestamp: Date.now() });
}
function handleFogShapeRemove(shapeId) {
onFogDraw({ type: "remove", shapeIds: [shapeId], timestamp: Date.now() });
}
2020-04-29 20:40:34 +10:00
function handleFogShapeEdit(shape) {
onFogDraw({ type: "edit", shapes: [shape], timestamp: Date.now() });
}
2020-04-19 15:15:48 +10:00
// Replay the draw actions and convert them to shapes for the map drawing
useEffect(() => {
2020-04-23 17:23:34 +10:00
if (!mapState) {
return;
}
function actionsToShapes(actions, actionIndex) {
let shapesById = {};
for (let i = 0; i <= actionIndex; i++) {
const action = actions[i];
2020-04-29 20:40:34 +10:00
if (action.type === "add" || action.type === "edit") {
for (let shape of action.shapes) {
shapesById[shape.id] = shape;
}
}
if (action.type === "remove") {
shapesById = omit(shapesById, action.shapeIds);
}
}
return Object.values(shapesById);
}
setMapShapes(
actionsToShapes(mapState.mapDrawActions, mapState.mapDrawActionIndex)
);
setFogShapes(
actionsToShapes(mapState.fogDrawActions, mapState.fogDrawActionIndex)
);
2020-04-23 17:23:34 +10:00
}, [mapState]);
const disabledControls = [];
2020-04-29 21:12:57 +10:00
if (!allowMapDrawing) {
disabledControls.push("brush");
disabledControls.push("shape");
disabledControls.push("erase");
}
// If no actions that can be undone
if (!allowFogDrawing && !allowMapDrawing) {
disabledControls.push("undo");
disabledControls.push("redo");
}
if (!map) {
disabledControls.push("pan");
}
if (mapShapes.length === 0) {
disabledControls.push("erase");
}
if (!mapState || mapState.mapDrawActionIndex < 0) {
disabledControls.push("undo");
}
2020-04-29 21:12:57 +10:00
if (!allowFogDrawing) {
disabledControls.push("fog");
}
if (
!mapState ||
mapState.mapDrawActionIndex === mapState.mapDrawActions.length - 1
) {
disabledControls.push("redo");
}
/**
* Member setup
*/
const mapRef = useRef(null);
const gridX = map && map.gridX;
const gridY = map && map.gridY;
2020-04-20 15:17:56 +10:00
const gridSizeNormalized = { x: 1 / gridX || 0, y: 1 / gridY || 0 };
const tokenSizePercent = gridSizeNormalized.x * 100;
const aspectRatio = (map && map.width / map.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,
pointerEvents: "none",
}}
>
2020-04-23 17:23:34 +10:00
{mapState &&
Object.values(mapState.tokens).map((tokenState) => (
2020-04-23 17:23:34 +10:00
<MapToken
key={tokenState.id}
token={tokens.find((token) => token.id === tokenState.tokenId)}
tokenState={tokenState}
2020-04-23 17:23:34 +10:00
tokenSizePercent={tokenSizePercent}
className={`${mapTokenProxyClassName} ${mapTokenMenuClassName}`}
/>
))}
</Box>
);
const mapDrawing = (
<MapDrawing
width={map ? map.width : 0}
height={map ? map.height : 0}
selectedTool={selectedToolId !== "fog" ? selectedToolId : "none"}
toolSettings={toolSettings[selectedToolId]}
shapes={mapShapes}
onShapeAdd={handleMapShapeAdd}
onShapeRemove={handleMapShapeRemove}
gridSize={gridSizeNormalized}
/>
);
const mapFog = (
<MapFog
width={map ? map.width : 0}
height={map ? map.height : 0}
isEditing={selectedToolId === "fog"}
toolSettings={toolSettings["fog"]}
shapes={fogShapes}
onShapeAdd={handleFogShapeAdd}
onShapeRemove={handleFogShapeRemove}
2020-04-29 20:40:34 +10:00
onShapeEdit={handleFogShapeEdit}
gridSize={gridSizeNormalized}
/>
);
const mapControls = (
<MapControls
onMapChange={onMapChange}
onMapStateChange={onMapStateChange}
currentMap={map}
onSelectedToolChange={setSelectedToolId}
selectedToolId={selectedToolId}
toolSettings={toolSettings}
onToolSettingChange={handleToolSettingChange}
2020-04-29 20:55:52 +10:00
onToolAction={handleToolAction}
disabledControls={disabledControls}
onUndo={onMapUndo}
onRedo={onMapRedo}
/>
);
return (
2020-03-20 13:33:12 +11:00
<>
<MapInteraction
map={map}
aspectRatio={aspectRatio}
isEnabled={selectedToolId === "pan"}
controls={mapControls}
>
{map && mapImage}
{map && mapDrawing}
{map && mapFog}
{map && mapTokens}
</MapInteraction>
2020-04-26 14:34:27 +10:00
{allowTokenChange && (
<>
<ProxyToken
tokenClassName={mapTokenProxyClassName}
onProxyDragEnd={handleProxyDragEnd}
tokens={mapState && mapState.tokens}
/>
<TokenMenu
tokenClassName={mapTokenMenuClassName}
onTokenChange={onMapTokenStateChange}
tokens={mapState && mapState.tokens}
/>
</>
)}
2020-03-20 13:33:12 +11:00
</>
);
}
export default Map;