2020-03-20 21:46:52 +11:00
|
|
|
import React, { useRef, useEffect, useState } from "react";
|
2020-03-20 17:56:34 +11:00
|
|
|
import { Box, Image } from "theme-ui";
|
2020-03-20 21:46:52 +11:00
|
|
|
import interact from "interactjs";
|
2020-03-19 17:33:57 +11:00
|
|
|
|
2020-03-20 13:33:12 +11:00
|
|
|
import ProxyToken from "../components/ProxyToken";
|
2020-03-20 18:06:24 +11:00
|
|
|
import AddMapButton from "../components/AddMapButton";
|
2020-04-13 00:24:03 +10:00
|
|
|
import TokenMenu from "../components/TokenMenu";
|
|
|
|
|
import MapToken from "../components/MapToken";
|
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
|
|
|
|
2020-03-20 18:06:24 +11:00
|
|
|
function Map({
|
|
|
|
|
mapSource,
|
|
|
|
|
mapData,
|
|
|
|
|
tokens,
|
2020-04-13 00:24:03 +10:00
|
|
|
onMapTokenChange,
|
2020-03-20 18:06:24 +11:00
|
|
|
onMapTokenRemove,
|
2020-04-13 00:24:03 +10:00
|
|
|
onMapChange,
|
2020-03-20 18:06:24 +11:00
|
|
|
}) {
|
2020-03-20 13:33:12 +11:00
|
|
|
function handleProxyDragEnd(isOnMap, token) {
|
2020-04-13 00:24:03 +10:00
|
|
|
if (isOnMap && onMapTokenChange) {
|
|
|
|
|
onMapTokenChange(token);
|
2020-03-20 13:33:12 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isOnMap && onMapTokenRemove) {
|
|
|
|
|
onMapTokenRemove(token);
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-03-20 11:05:40 +11:00
|
|
|
|
2020-03-20 21:46:52 +11:00
|
|
|
const [mapTranslate, setMapTranslate] = useState({ x: 0, y: 0 });
|
|
|
|
|
const [mapScale, setMapScale] = useState(1);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2020-03-26 14:53:02 +11:00
|
|
|
interact(".map")
|
|
|
|
|
.gesturable({
|
|
|
|
|
listeners: {
|
2020-04-07 11:47:06 +10:00
|
|
|
move: (event) => {
|
|
|
|
|
setMapScale((previousMapScale) =>
|
2020-03-26 14:53:02 +11:00
|
|
|
Math.max(Math.min(previousMapScale + event.ds, maxZoom), minZoom)
|
|
|
|
|
);
|
2020-04-07 11:47:06 +10:00
|
|
|
setMapTranslate((previousMapTranslate) => ({
|
2020-03-26 14:53:02 +11:00
|
|
|
x: previousMapTranslate.x + event.dx,
|
2020-04-07 11:47:06 +10:00
|
|
|
y: previousMapTranslate.y + event.dy,
|
2020-03-26 14:53:02 +11:00
|
|
|
}));
|
2020-04-07 11:47:06 +10:00
|
|
|
},
|
|
|
|
|
},
|
2020-03-26 14:53:02 +11:00
|
|
|
})
|
|
|
|
|
.draggable({
|
|
|
|
|
inertia: true,
|
|
|
|
|
listeners: {
|
2020-04-07 11:47:06 +10:00
|
|
|
move: (event) => {
|
|
|
|
|
setMapTranslate((previousMapTranslate) => ({
|
2020-03-26 14:53:02 +11:00
|
|
|
x: previousMapTranslate.x + event.dx,
|
2020-04-07 11:47:06 +10:00
|
|
|
y: previousMapTranslate.y + event.dy,
|
2020-03-26 14:53:02 +11:00
|
|
|
}));
|
2020-04-07 11:47:06 +10:00
|
|
|
},
|
|
|
|
|
},
|
2020-03-26 14:53:02 +11:00
|
|
|
});
|
2020-04-07 11:47:06 +10:00
|
|
|
interact(".map").on("doubletap", (event) => {
|
2020-03-20 21:46:52 +11:00
|
|
|
event.preventDefault();
|
|
|
|
|
setMapTranslate({ x: 0, y: 0 });
|
|
|
|
|
setMapScale(1);
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
2020-03-26 15:43:52 +11:00
|
|
|
// Reset map transform when map changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setMapTranslate({ x: 0, y: 0 });
|
|
|
|
|
setMapScale(1);
|
|
|
|
|
}, [mapSource]);
|
|
|
|
|
|
2020-04-09 18:20:10 +10:00
|
|
|
// 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) {
|
2020-04-13 10:33:49 +10:00
|
|
|
// Stop overscroll on chrome and safari
|
|
|
|
|
// also stop pinch to zoom on chrome
|
|
|
|
|
event.preventDefault();
|
2020-04-09 18:20:10 +10:00
|
|
|
|
|
|
|
|
const deltaY = event.deltaY * zoomSpeed;
|
|
|
|
|
setMapScale((previousMapScale) =>
|
|
|
|
|
Math.max(Math.min(previousMapScale + deltaY, maxZoom), minZoom)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mapContainer) {
|
|
|
|
|
mapContainer.addEventListener("wheel", handleZoom, {
|
|
|
|
|
passive: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
if (mapContainer) {
|
|
|
|
|
mapContainer.removeEventListener("wheel", handleZoom);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2020-03-20 17:56:34 +11:00
|
|
|
const mapRef = useRef(null);
|
2020-04-09 18:20:10 +10:00
|
|
|
const mapContainerRef = useRef();
|
2020-03-20 17:56:34 +11:00
|
|
|
const rows = mapData && mapData.rows;
|
|
|
|
|
const tokenSizePercent = (1 / rows) * 100;
|
|
|
|
|
const aspectRatio = (mapData && mapData.width / mapData.height) || 1;
|
|
|
|
|
|
2020-04-13 00:24:03 +10:00
|
|
|
const mapImage = (
|
|
|
|
|
<Box
|
|
|
|
|
sx={{
|
|
|
|
|
position: "absolute",
|
|
|
|
|
top: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
bottom: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Image
|
|
|
|
|
ref={mapRef}
|
|
|
|
|
id="map"
|
|
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const mapActions = (
|
|
|
|
|
<Box
|
|
|
|
|
p={2}
|
|
|
|
|
sx={{
|
|
|
|
|
position: "absolute",
|
|
|
|
|
top: "0",
|
|
|
|
|
left: "50%",
|
|
|
|
|
transform: "translateX(-50%)",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<AddMapButton onMapChanged={onMapChange} />
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
|
2020-03-19 17:33:57 +11:00
|
|
|
return (
|
2020-03-20 13:33:12 +11:00
|
|
|
<>
|
2020-03-20 17:56:34 +11:00
|
|
|
<Box
|
2020-03-20 13:33:12 +11:00
|
|
|
className="map"
|
2020-03-20 18:06:24 +11:00
|
|
|
sx={{
|
|
|
|
|
flexGrow: 1,
|
|
|
|
|
position: "relative",
|
|
|
|
|
overflow: "hidden",
|
2020-03-26 14:53:02 +11:00
|
|
|
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
|
|
|
|
userSelect: "none",
|
2020-04-07 11:47:06 +10:00
|
|
|
touchAction: "none",
|
2020-03-20 18:06:24 +11:00
|
|
|
}}
|
2020-03-20 13:33:12 +11:00
|
|
|
bg="background"
|
2020-04-09 18:20:10 +10:00
|
|
|
ref={mapContainerRef}
|
2020-03-20 11:05:40 +11:00
|
|
|
>
|
2020-03-20 13:33:12 +11:00
|
|
|
<Box
|
|
|
|
|
sx={{
|
2020-03-20 17:56:34 +11:00
|
|
|
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
|
|
|
}}
|
|
|
|
|
>
|
2020-03-20 17:56:34 +11:00
|
|
|
<Box
|
2020-03-20 21:46:52 +11:00
|
|
|
style={{
|
2020-04-07 11:47:06 +10:00
|
|
|
transform: `translate(${mapTranslate.x}px, ${mapTranslate.y}px) scale(${mapScale})`,
|
2020-03-20 17:56:34 +11:00
|
|
|
}}
|
|
|
|
|
>
|
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
|
|
|
}}
|
|
|
|
|
/>
|
2020-04-13 00:24:03 +10:00
|
|
|
{mapImage}
|
|
|
|
|
{mapTokens}
|
2020-03-20 17:56:34 +11:00
|
|
|
</Box>
|
2020-03-20 13:33:12 +11:00
|
|
|
</Box>
|
2020-04-13 00:24:03 +10:00
|
|
|
{mapActions}
|
2020-03-20 17:56:34 +11:00
|
|
|
</Box>
|
2020-03-20 13:33:12 +11:00
|
|
|
<ProxyToken
|
|
|
|
|
tokenClassName={mapTokenClassName}
|
|
|
|
|
onProxyDragEnd={handleProxyDragEnd}
|
|
|
|
|
/>
|
2020-04-13 00:24:03 +10:00
|
|
|
<TokenMenu
|
|
|
|
|
tokenClassName={mapTokenClassName}
|
|
|
|
|
onTokenChange={onMapTokenChange}
|
|
|
|
|
/>
|
2020-03-20 13:33:12 +11:00
|
|
|
</>
|
2020-03-19 17:33:57 +11:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default Map;
|