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 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}
/>
</>
);

View File

@ -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}

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 {
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"

View File

@ -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}