Added multiple map layout and basic data flow
This commit is contained in:
parent
f2a92f2ccd
commit
22c5b5cf75
@ -1,11 +1,9 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { IconButton } from "theme-ui";
|
||||
|
||||
import AddMapModal from "../../modals/AddMapModal";
|
||||
import AddMapIcon from "../../icons/AddMapIcon";
|
||||
|
||||
const defaultMapSize = 22;
|
||||
|
||||
function AddMapButton({ onMapChange }) {
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
function openModal() {
|
||||
@ -15,49 +13,11 @@ function AddMapButton({ onMapChange }) {
|
||||
setIsAddModalOpen(false);
|
||||
}
|
||||
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
|
||||
const mapDataRef = useRef(null);
|
||||
const [mapSource, setMapSource] = useState(null);
|
||||
function handleImageUpload(file, fileGridX, fileGridY) {
|
||||
const url = URL.createObjectURL(file);
|
||||
let image = new Image();
|
||||
image.onload = function () {
|
||||
mapDataRef.current = {
|
||||
file,
|
||||
gridX: fileGridX || gridX,
|
||||
gridY: fileGridY || gridY,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
};
|
||||
setImageLoaded(true);
|
||||
};
|
||||
image.src = url;
|
||||
setMapSource(url);
|
||||
if (fileGridX) {
|
||||
setGridX(fileGridX);
|
||||
}
|
||||
if (fileGridY) {
|
||||
setGridY(fileGridY);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDone() {
|
||||
if (mapDataRef.current && mapSource) {
|
||||
onMapChange(mapDataRef.current, mapSource);
|
||||
}
|
||||
function handleDone(map) {
|
||||
onMapChange(map);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
const [gridX, setGridX] = useState(defaultMapSize);
|
||||
const [gridY, setGridY] = useState(defaultMapSize);
|
||||
useEffect(() => {
|
||||
if (mapDataRef.current) {
|
||||
mapDataRef.current.gridX = gridX;
|
||||
mapDataRef.current.gridY = gridY;
|
||||
}
|
||||
}, [gridX, gridY]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton aria-label="Add Map" title="Add Map" onClick={openModal}>
|
||||
@ -67,13 +27,6 @@ function AddMapButton({ onMapChange }) {
|
||||
isOpen={isAddModalOpen}
|
||||
onRequestClose={closeModal}
|
||||
onDone={handleDone}
|
||||
onImageUpload={handleImageUpload}
|
||||
gridX={gridX}
|
||||
onGridXChange={setGridX}
|
||||
gridY={gridY}
|
||||
onGridYChange={setGridY}
|
||||
imageLoaded={imageLoaded}
|
||||
mapSource={mapSource}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -17,8 +17,7 @@ const minZoom = 0.1;
|
||||
const maxZoom = 5;
|
||||
|
||||
function Map({
|
||||
mapSource,
|
||||
mapData,
|
||||
map,
|
||||
tokens,
|
||||
onMapTokenChange,
|
||||
onMapTokenRemove,
|
||||
@ -80,7 +79,7 @@ function Map({
|
||||
}, [drawActions, drawActionIndex]);
|
||||
|
||||
const disabledTools = [];
|
||||
if (!mapData) {
|
||||
if (!map) {
|
||||
disabledTools.push("pan");
|
||||
disabledTools.push("brush");
|
||||
}
|
||||
@ -134,7 +133,7 @@ function Map({
|
||||
move: (e) => handleMove(e, false),
|
||||
},
|
||||
cursorChecker: () => {
|
||||
return selectedTool === "pan" && mapData ? "move" : "default";
|
||||
return selectedTool === "pan" && map ? "move" : "default";
|
||||
},
|
||||
})
|
||||
.on("doubletap", (event) => {
|
||||
@ -147,12 +146,12 @@ function Map({
|
||||
return () => {
|
||||
mapInteract.unset();
|
||||
};
|
||||
}, [selectedTool, mapData]);
|
||||
}, [selectedTool, map]);
|
||||
|
||||
// Reset map transform when map changes
|
||||
useEffect(() => {
|
||||
setTranslateAndScale({ x: 0, y: 0 }, 1);
|
||||
}, [mapSource]);
|
||||
}, [map]);
|
||||
|
||||
// Bind the wheel event of the map via a ref
|
||||
// in order to support non-passive event listening
|
||||
@ -194,11 +193,11 @@ function Map({
|
||||
|
||||
const mapRef = useRef(null);
|
||||
const mapContainerRef = useRef();
|
||||
const gridX = mapData && mapData.gridX;
|
||||
const gridY = mapData && mapData.gridY;
|
||||
const gridX = map && map.gridX;
|
||||
const gridY = map && map.gridY;
|
||||
const gridSizeNormalized = { x: 1 / gridX || 0, y: 1 / gridY || 0 };
|
||||
const tokenSizePercent = gridSizeNormalized.x * 100;
|
||||
const aspectRatio = (mapData && mapData.width / mapData.height) || 1;
|
||||
const aspectRatio = (map && map.width / map.height) || 1;
|
||||
|
||||
const mapImage = (
|
||||
<Box
|
||||
@ -218,7 +217,7 @@ function Map({
|
||||
userSelect: "none",
|
||||
touchAction: "none",
|
||||
}}
|
||||
src={mapSource}
|
||||
src={map && map.source}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
@ -278,8 +277,8 @@ function Map({
|
||||
/>
|
||||
{mapImage}
|
||||
<MapDrawing
|
||||
width={mapData ? mapData.width : 0}
|
||||
height={mapData ? mapData.height : 0}
|
||||
width={map ? map.width : 0}
|
||||
height={map ? map.height : 0}
|
||||
selectedTool={selectedTool}
|
||||
shapes={drawnShapes}
|
||||
onShapeAdd={handleShapeAdd}
|
||||
|
55
src/components/map/MapSelect.js
Normal file
55
src/components/map/MapSelect.js
Normal file
@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import { Flex, Image as UIImage } from "theme-ui";
|
||||
|
||||
import AddIcon from "../../icons/AddIcon";
|
||||
|
||||
function MapSelect({ maps, onMapAdd }) {
|
||||
const tileProps = {
|
||||
m: 2,
|
||||
sx: {
|
||||
width: "150px",
|
||||
height: "150px",
|
||||
borderRadius: "4px",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
bg: "muted",
|
||||
};
|
||||
|
||||
function tile(map) {
|
||||
return (
|
||||
<Flex // TODO: use DB key
|
||||
key={map.source}
|
||||
{...tileProps}
|
||||
>
|
||||
<UIImage
|
||||
sx={{ width: "100%", objectFit: "contain" }}
|
||||
src={map.source}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
my={2}
|
||||
bg="muted"
|
||||
sx={{
|
||||
flexWrap: "wrap",
|
||||
width: "500px",
|
||||
maxHeight: "300px",
|
||||
borderRadius: "4px",
|
||||
// TODO: move to simple scroll
|
||||
overflowY: "scroll",
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{maps.map(tile)}
|
||||
<Flex onClick={onMapAdd} {...tileProps}>
|
||||
<AddIcon />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default MapSelect;
|
18
src/icons/AddIcon.js
Normal file
18
src/icons/AddIcon.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function AddIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4 11h-3v3c0 .55-.45 1-1 1s-1-.45-1-1v-3H8c-.55 0-1-.45-1-1s.45-1 1-1h3V8c0-.55.45-1 1-1s1 .45 1 1v3h3c.55 0 1 .45 1 1s-.45 1-1 1z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddIcon;
|
BIN
src/maps/Blank Grid 22x22.jpg
Executable file
BIN
src/maps/Blank Grid 22x22.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 89 KiB |
BIN
src/maps/Grass Grid 22x22.jpg
Executable file
BIN
src/maps/Grass Grid 22x22.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
BIN
src/maps/Sand Grid 22x22.jpg
Executable file
BIN
src/maps/Sand Grid 22x22.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
BIN
src/maps/Stone Grid 22x22.jpg
Executable file
BIN
src/maps/Stone Grid 22x22.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 169 KiB |
BIN
src/maps/Water Grid 22x22.jpg
Executable file
BIN
src/maps/Water Grid 22x22.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 171 KiB |
BIN
src/maps/Wood Grid 22x22.jpg
Executable file
BIN
src/maps/Wood Grid 22x22.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 180 KiB |
49
src/maps/index.js
Normal file
49
src/maps/index.js
Normal file
@ -0,0 +1,49 @@
|
||||
import blankImage from "./Blank Grid 22x22.jpg";
|
||||
import grassImage from "./Grass Grid 22x22.jpg";
|
||||
import sandImage from "./Sand Grid 22x22.jpg";
|
||||
import stoneImage from "./Stone Grid 22x22.jpg";
|
||||
import waterImage from "./Water Grid 22x22.jpg";
|
||||
import woodImage from "./Wood Grid 22x22.jpg";
|
||||
|
||||
const defaultProps = {
|
||||
gridX: 22,
|
||||
gridY: 22,
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
};
|
||||
|
||||
export const blank = {
|
||||
...defaultProps,
|
||||
source: blankImage,
|
||||
name: "Blank Grid 22x22",
|
||||
};
|
||||
|
||||
export const grass = {
|
||||
...defaultProps,
|
||||
source: grassImage,
|
||||
name: "Grass Grid 22x22",
|
||||
};
|
||||
|
||||
export const sand = {
|
||||
...defaultProps,
|
||||
source: sandImage,
|
||||
name: "Sand Grid 22x22",
|
||||
};
|
||||
|
||||
export const stone = {
|
||||
...defaultProps,
|
||||
source: stoneImage,
|
||||
name: "Stone Grid 22x22",
|
||||
};
|
||||
|
||||
export const water = {
|
||||
...defaultProps,
|
||||
source: waterImage,
|
||||
name: "Water Grid 22x22",
|
||||
};
|
||||
|
||||
export const wood = {
|
||||
...defaultProps,
|
||||
source: woodImage,
|
||||
name: "Wood Grid 22x22",
|
||||
};
|
@ -1,31 +1,41 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Image as UIImage,
|
||||
Flex,
|
||||
Label,
|
||||
Input,
|
||||
Text,
|
||||
} from "theme-ui";
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { Box, Button, Flex, Label, Input, Text } from "theme-ui";
|
||||
|
||||
import Modal from "../components/Modal";
|
||||
import MapSelect from "../components/map/MapSelect";
|
||||
|
||||
import * as defaultMaps from "../maps";
|
||||
|
||||
const defaultMapSize = 22;
|
||||
|
||||
function AddMapModal({ isOpen, onRequestClose, onDone }) {
|
||||
const [imageLoading, setImageLoading] = useState(false);
|
||||
|
||||
const [currentMap, setCurrentMap] = useState(-1);
|
||||
const [maps, setMaps] = useState(Object.values(defaultMaps));
|
||||
|
||||
const [gridX, setGridX] = useState(defaultMapSize);
|
||||
const [gridY, setGridY] = useState(defaultMapSize);
|
||||
useEffect(() => {
|
||||
setMaps((prevMaps) => {
|
||||
const newMaps = [...prevMaps];
|
||||
const changedMap = newMaps[currentMap];
|
||||
if (changedMap) {
|
||||
changedMap.gridX = gridX;
|
||||
changedMap.gridY = gridY;
|
||||
}
|
||||
return newMaps;
|
||||
});
|
||||
}, [gridX, gridY, currentMap]);
|
||||
|
||||
function AddMapModal({
|
||||
isOpen,
|
||||
onRequestClose,
|
||||
onDone,
|
||||
onImageUpload,
|
||||
gridX,
|
||||
onGridXChange,
|
||||
gridY,
|
||||
onGridYChange,
|
||||
imageLoaded,
|
||||
mapSource,
|
||||
}) {
|
||||
const fileInputRef = useRef();
|
||||
|
||||
function handleImageUpload(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
let fileGridX = defaultMapSize;
|
||||
let fileGridY = defaultMapSize;
|
||||
if (file.name) {
|
||||
// Match against a regex to find the grid size in the file name
|
||||
// e.g. Cave 22x23 will return [["22x22", "22", "x", "23"]]
|
||||
@ -35,12 +45,35 @@ function AddMapModal({
|
||||
const matchX = parseInt(lastMatch[1]);
|
||||
const matchY = parseInt(lastMatch[3]);
|
||||
if (!isNaN(matchX) && !isNaN(matchY)) {
|
||||
onImageUpload(file, matchX, matchY);
|
||||
return;
|
||||
fileGridX = matchX;
|
||||
fileGridY = matchY;
|
||||
}
|
||||
}
|
||||
}
|
||||
onImageUpload(file);
|
||||
const url = URL.createObjectURL(file);
|
||||
let image = new Image();
|
||||
setImageLoading(true);
|
||||
image.onload = function () {
|
||||
setMaps((prevMaps) => {
|
||||
const newMaps = [
|
||||
...prevMaps,
|
||||
{
|
||||
file,
|
||||
gridX: fileGridX,
|
||||
gridY: fileGridY,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
source: url,
|
||||
},
|
||||
];
|
||||
setCurrentMap(newMaps.length - 1);
|
||||
return newMaps;
|
||||
});
|
||||
setGridX(fileGridX);
|
||||
setGridY(fileGridY);
|
||||
setImageLoading(false);
|
||||
};
|
||||
image.src = url;
|
||||
}
|
||||
|
||||
function openImageDialog() {
|
||||
@ -78,7 +111,7 @@ function AddMapModal({
|
||||
as="form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
onDone();
|
||||
onDone(maps[currentMap]);
|
||||
}}
|
||||
onDragEnter={handleImageDragEnter}
|
||||
>
|
||||
@ -89,7 +122,6 @@ function AddMapModal({
|
||||
style={{ display: "none" }}
|
||||
ref={fileInputRef}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
@ -98,19 +130,7 @@ function AddMapModal({
|
||||
<Label pt={2} pb={1}>
|
||||
Add map
|
||||
</Label>
|
||||
<UIImage
|
||||
my={2}
|
||||
sx={{
|
||||
width: "500px",
|
||||
minHeight: "200px",
|
||||
maxHeight: "300px",
|
||||
objectFit: "contain",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
src={mapSource}
|
||||
onClick={openImageDialog}
|
||||
bg="muted"
|
||||
/>
|
||||
<MapSelect maps={maps} onMapAdd={openImageDialog} />
|
||||
<Flex>
|
||||
<Box mb={2} mr={1} sx={{ flexGrow: 1 }}>
|
||||
<Label htmlFor="gridX">Columns</Label>
|
||||
@ -118,7 +138,7 @@ function AddMapModal({
|
||||
type="number"
|
||||
name="gridX"
|
||||
value={gridX}
|
||||
onChange={(e) => onGridXChange(e.target.value)}
|
||||
onChange={(e) => setGridX(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
<Box mb={2} ml={1} sx={{ flexGrow: 1 }}>
|
||||
@ -127,25 +147,13 @@ function AddMapModal({
|
||||
type="number"
|
||||
name="gridY"
|
||||
value={gridY}
|
||||
onChange={(e) => onGridYChange(e.target.value)}
|
||||
onChange={(e) => setGridY(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
{mapSource ? (
|
||||
<Button variant="primary" disabled={!imageLoaded}>
|
||||
Done
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
varient="primary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
openImageDialog();
|
||||
}}
|
||||
>
|
||||
Select Image
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="primary" disabled={imageLoading}>
|
||||
Done
|
||||
</Button>
|
||||
{dragging && (
|
||||
<Flex
|
||||
bg="muted"
|
||||
|
@ -1,10 +1,4 @@
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useContext,
|
||||
} from "react";
|
||||
import React, { useState, useEffect, useCallback, useContext } from "react";
|
||||
import { Flex, Box, Text, Link } from "theme-ui";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
@ -40,21 +34,19 @@ function Game() {
|
||||
* Map state
|
||||
*/
|
||||
|
||||
const [mapSource, setMapSource] = useState(null);
|
||||
const mapDataRef = useRef(null);
|
||||
const [map, setMap] = useState(null);
|
||||
|
||||
function handleMapChange(mapData, mapSource) {
|
||||
mapDataRef.current = mapData;
|
||||
setMapSource(mapSource);
|
||||
function handleMapChange(newMap) {
|
||||
setMap(newMap);
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "map", data: mapDataRef.current });
|
||||
peer.connection.send({ id: "map", data: map });
|
||||
}
|
||||
}
|
||||
|
||||
const [mapTokens, setMapTokens] = useState({});
|
||||
|
||||
function handleMapTokenChange(token) {
|
||||
if (!mapSource) {
|
||||
if (!map.source) {
|
||||
return;
|
||||
}
|
||||
setMapTokens((prevMapTokens) => ({
|
||||
@ -150,8 +142,8 @@ function Game() {
|
||||
|
||||
function handlePeerData({ data, peer }) {
|
||||
if (data.id === "sync") {
|
||||
if (mapSource) {
|
||||
peer.connection.send({ id: "map", data: mapDataRef.current });
|
||||
if (map) {
|
||||
peer.connection.send({ id: "map", data: map });
|
||||
}
|
||||
if (mapTokens) {
|
||||
peer.connection.send({ id: "tokenEdit", data: mapTokens });
|
||||
@ -164,9 +156,9 @@ function Game() {
|
||||
}
|
||||
}
|
||||
if (data.id === "map") {
|
||||
const blob = new Blob([data.data.file]);
|
||||
mapDataRef.current = { ...data.data, file: blob };
|
||||
setMapSource(URL.createObjectURL(mapDataRef.current.file));
|
||||
const file = new Blob([data.data.file]);
|
||||
const source = URL.createObjectURL(file);
|
||||
setMap({ ...data.data, file, source });
|
||||
}
|
||||
if (data.id === "tokenEdit") {
|
||||
setMapTokens((prevMapTokens) => ({
|
||||
@ -302,8 +294,7 @@ function Game() {
|
||||
onStreamEnd={handleStreamEnd}
|
||||
/>
|
||||
<Map
|
||||
mapSource={mapSource}
|
||||
mapData={mapDataRef.current}
|
||||
map={map}
|
||||
tokens={mapTokens}
|
||||
onMapTokenChange={handleMapTokenChange}
|
||||
onMapTokenRemove={handleMapTokenRemove}
|
||||
|
Loading…
x
Reference in New Issue
Block a user