Added multiple map layout and basic data flow

This commit is contained in:
Mitchell McCaffrey 2020-04-23 11:54:29 +10:00
parent f2a92f2ccd
commit 22c5b5cf75
13 changed files with 213 additions and 140 deletions

View File

@ -1,11 +1,9 @@
import React, { useRef, useState, useEffect } from "react"; import React, { useState } from "react";
import { IconButton } from "theme-ui"; import { IconButton } from "theme-ui";
import AddMapModal from "../../modals/AddMapModal"; import AddMapModal from "../../modals/AddMapModal";
import AddMapIcon from "../../icons/AddMapIcon"; import AddMapIcon from "../../icons/AddMapIcon";
const defaultMapSize = 22;
function AddMapButton({ onMapChange }) { function AddMapButton({ onMapChange }) {
const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false);
function openModal() { function openModal() {
@ -15,49 +13,11 @@ function AddMapButton({ onMapChange }) {
setIsAddModalOpen(false); setIsAddModalOpen(false);
} }
const [imageLoaded, setImageLoaded] = useState(false); function handleDone(map) {
onMapChange(map);
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);
}
closeModal(); 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 ( return (
<> <>
<IconButton aria-label="Add Map" title="Add Map" onClick={openModal}> <IconButton aria-label="Add Map" title="Add Map" onClick={openModal}>
@ -67,13 +27,6 @@ function AddMapButton({ onMapChange }) {
isOpen={isAddModalOpen} isOpen={isAddModalOpen}
onRequestClose={closeModal} onRequestClose={closeModal}
onDone={handleDone} onDone={handleDone}
onImageUpload={handleImageUpload}
gridX={gridX}
onGridXChange={setGridX}
gridY={gridY}
onGridYChange={setGridY}
imageLoaded={imageLoaded}
mapSource={mapSource}
/> />
</> </>
); );

View File

@ -17,8 +17,7 @@ const minZoom = 0.1;
const maxZoom = 5; const maxZoom = 5;
function Map({ function Map({
mapSource, map,
mapData,
tokens, tokens,
onMapTokenChange, onMapTokenChange,
onMapTokenRemove, onMapTokenRemove,
@ -80,7 +79,7 @@ function Map({
}, [drawActions, drawActionIndex]); }, [drawActions, drawActionIndex]);
const disabledTools = []; const disabledTools = [];
if (!mapData) { if (!map) {
disabledTools.push("pan"); disabledTools.push("pan");
disabledTools.push("brush"); disabledTools.push("brush");
} }
@ -134,7 +133,7 @@ function Map({
move: (e) => handleMove(e, false), move: (e) => handleMove(e, false),
}, },
cursorChecker: () => { cursorChecker: () => {
return selectedTool === "pan" && mapData ? "move" : "default"; return selectedTool === "pan" && map ? "move" : "default";
}, },
}) })
.on("doubletap", (event) => { .on("doubletap", (event) => {
@ -147,12 +146,12 @@ function Map({
return () => { return () => {
mapInteract.unset(); mapInteract.unset();
}; };
}, [selectedTool, mapData]); }, [selectedTool, map]);
// Reset map transform when map changes // Reset map transform when map changes
useEffect(() => { useEffect(() => {
setTranslateAndScale({ x: 0, y: 0 }, 1); setTranslateAndScale({ x: 0, y: 0 }, 1);
}, [mapSource]); }, [map]);
// Bind the wheel event of the map via a ref // Bind the wheel event of the map via a ref
// in order to support non-passive event listening // in order to support non-passive event listening
@ -194,11 +193,11 @@ function Map({
const mapRef = useRef(null); const mapRef = useRef(null);
const mapContainerRef = useRef(); const mapContainerRef = useRef();
const gridX = mapData && mapData.gridX; const gridX = map && map.gridX;
const gridY = mapData && mapData.gridY; const gridY = map && map.gridY;
const gridSizeNormalized = { x: 1 / gridX || 0, y: 1 / gridY || 0 }; const gridSizeNormalized = { x: 1 / gridX || 0, y: 1 / gridY || 0 };
const tokenSizePercent = gridSizeNormalized.x * 100; const tokenSizePercent = gridSizeNormalized.x * 100;
const aspectRatio = (mapData && mapData.width / mapData.height) || 1; const aspectRatio = (map && map.width / map.height) || 1;
const mapImage = ( const mapImage = (
<Box <Box
@ -218,7 +217,7 @@ function Map({
userSelect: "none", userSelect: "none",
touchAction: "none", touchAction: "none",
}} }}
src={mapSource} src={map && map.source}
/> />
</Box> </Box>
); );
@ -278,8 +277,8 @@ function Map({
/> />
{mapImage} {mapImage}
<MapDrawing <MapDrawing
width={mapData ? mapData.width : 0} width={map ? map.width : 0}
height={mapData ? mapData.height : 0} height={map ? map.height : 0}
selectedTool={selectedTool} selectedTool={selectedTool}
shapes={drawnShapes} shapes={drawnShapes}
onShapeAdd={handleShapeAdd} onShapeAdd={handleShapeAdd}

View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

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
View 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",
};

View File

@ -1,31 +1,41 @@
import React, { useRef, useState } from "react"; import React, { useRef, useState, useEffect } from "react";
import { import { Box, Button, Flex, Label, Input, Text } from "theme-ui";
Box,
Button,
Image as UIImage,
Flex,
Label,
Input,
Text,
} from "theme-ui";
import Modal from "../components/Modal"; 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(); const fileInputRef = useRef();
function handleImageUpload(file) { function handleImageUpload(file) {
if (!file) {
return;
}
let fileGridX = defaultMapSize;
let fileGridY = defaultMapSize;
if (file.name) { if (file.name) {
// Match against a regex to find the grid size in the 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"]] // e.g. Cave 22x23 will return [["22x22", "22", "x", "23"]]
@ -35,12 +45,35 @@ function AddMapModal({
const matchX = parseInt(lastMatch[1]); const matchX = parseInt(lastMatch[1]);
const matchY = parseInt(lastMatch[3]); const matchY = parseInt(lastMatch[3]);
if (!isNaN(matchX) && !isNaN(matchY)) { if (!isNaN(matchX) && !isNaN(matchY)) {
onImageUpload(file, matchX, matchY); fileGridX = matchX;
return; 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() { function openImageDialog() {
@ -78,7 +111,7 @@ function AddMapModal({
as="form" as="form"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
onDone(); onDone(maps[currentMap]);
}} }}
onDragEnter={handleImageDragEnter} onDragEnter={handleImageDragEnter}
> >
@ -89,7 +122,6 @@ function AddMapModal({
style={{ display: "none" }} style={{ display: "none" }}
ref={fileInputRef} ref={fileInputRef}
/> />
<Flex <Flex
sx={{ sx={{
flexDirection: "column", flexDirection: "column",
@ -98,19 +130,7 @@ function AddMapModal({
<Label pt={2} pb={1}> <Label pt={2} pb={1}>
Add map Add map
</Label> </Label>
<UIImage <MapSelect maps={maps} onMapAdd={openImageDialog} />
my={2}
sx={{
width: "500px",
minHeight: "200px",
maxHeight: "300px",
objectFit: "contain",
borderRadius: "4px",
}}
src={mapSource}
onClick={openImageDialog}
bg="muted"
/>
<Flex> <Flex>
<Box mb={2} mr={1} sx={{ flexGrow: 1 }}> <Box mb={2} mr={1} sx={{ flexGrow: 1 }}>
<Label htmlFor="gridX">Columns</Label> <Label htmlFor="gridX">Columns</Label>
@ -118,7 +138,7 @@ function AddMapModal({
type="number" type="number"
name="gridX" name="gridX"
value={gridX} value={gridX}
onChange={(e) => onGridXChange(e.target.value)} onChange={(e) => setGridX(e.target.value)}
/> />
</Box> </Box>
<Box mb={2} ml={1} sx={{ flexGrow: 1 }}> <Box mb={2} ml={1} sx={{ flexGrow: 1 }}>
@ -127,25 +147,13 @@ function AddMapModal({
type="number" type="number"
name="gridY" name="gridY"
value={gridY} value={gridY}
onChange={(e) => onGridYChange(e.target.value)} onChange={(e) => setGridY(e.target.value)}
/> />
</Box> </Box>
</Flex> </Flex>
{mapSource ? ( <Button variant="primary" disabled={imageLoading}>
<Button variant="primary" disabled={!imageLoaded}> Done
Done </Button>
</Button>
) : (
<Button
varient="primary"
onClick={(e) => {
e.preventDefault();
openImageDialog();
}}
>
Select Image
</Button>
)}
{dragging && ( {dragging && (
<Flex <Flex
bg="muted" bg="muted"

View File

@ -1,10 +1,4 @@
import React, { import React, { useState, useEffect, useCallback, useContext } from "react";
useState,
useRef,
useEffect,
useCallback,
useContext,
} from "react";
import { Flex, Box, Text, Link } from "theme-ui"; import { Flex, Box, Text, Link } from "theme-ui";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
@ -40,21 +34,19 @@ function Game() {
* Map state * Map state
*/ */
const [mapSource, setMapSource] = useState(null); const [map, setMap] = useState(null);
const mapDataRef = useRef(null);
function handleMapChange(mapData, mapSource) { function handleMapChange(newMap) {
mapDataRef.current = mapData; setMap(newMap);
setMapSource(mapSource);
for (let peer of Object.values(peers)) { 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({}); const [mapTokens, setMapTokens] = useState({});
function handleMapTokenChange(token) { function handleMapTokenChange(token) {
if (!mapSource) { if (!map.source) {
return; return;
} }
setMapTokens((prevMapTokens) => ({ setMapTokens((prevMapTokens) => ({
@ -150,8 +142,8 @@ function Game() {
function handlePeerData({ data, peer }) { function handlePeerData({ data, peer }) {
if (data.id === "sync") { if (data.id === "sync") {
if (mapSource) { if (map) {
peer.connection.send({ id: "map", data: mapDataRef.current }); peer.connection.send({ id: "map", data: map });
} }
if (mapTokens) { if (mapTokens) {
peer.connection.send({ id: "tokenEdit", data: mapTokens }); peer.connection.send({ id: "tokenEdit", data: mapTokens });
@ -164,9 +156,9 @@ function Game() {
} }
} }
if (data.id === "map") { if (data.id === "map") {
const blob = new Blob([data.data.file]); const file = new Blob([data.data.file]);
mapDataRef.current = { ...data.data, file: blob }; const source = URL.createObjectURL(file);
setMapSource(URL.createObjectURL(mapDataRef.current.file)); setMap({ ...data.data, file, source });
} }
if (data.id === "tokenEdit") { if (data.id === "tokenEdit") {
setMapTokens((prevMapTokens) => ({ setMapTokens((prevMapTokens) => ({
@ -302,8 +294,7 @@ function Game() {
onStreamEnd={handleStreamEnd} onStreamEnd={handleStreamEnd}
/> />
<Map <Map
mapSource={mapSource} map={map}
mapData={mapDataRef.current}
tokens={mapTokens} tokens={mapTokens}
onMapTokenChange={handleMapTokenChange} onMapTokenChange={handleMapTokenChange}
onMapTokenRemove={handleMapTokenRemove} onMapTokenRemove={handleMapTokenRemove}