Moved tokens to be relatively positioned and scaled to the current map

This commit is contained in:
Mitchell McCaffrey 2020-03-20 17:56:34 +11:00
parent c25566f682
commit 33ea19fef6
6 changed files with 136 additions and 66 deletions

View File

@ -1,5 +1,13 @@
import React, { useRef, useState } from "react"; import React, { useRef, useState, useEffect } from "react";
import { IconButton, Box, Button, Image, Flex, Label, Input } from "theme-ui"; import {
IconButton,
Box,
Button,
Image as UIImage,
Flex,
Label,
Input
} from "theme-ui";
import Modal from "./Modal"; import Modal from "./Modal";
@ -22,11 +30,26 @@ function AddMapButton({ handleMapChange }) {
setIsAddModalOpen(false); setIsAddModalOpen(false);
} }
const [imageLoaded, setImageLoaded] = useState(false);
const mapDataRef = useRef(null); const mapDataRef = useRef(null);
const [mapSource, setImageSource] = useState(null); const [mapSource, setMapSource] = useState(null);
function handleImageUpload(event) { function handleImageUpload(event) {
mapDataRef.current = { file: event.target.files[0], rows, columns }; const file = event.target.files[0];
setImageSource(URL.createObjectURL(mapDataRef.current.file)); const url = URL.createObjectURL(file);
let image = new Image();
image.onload = function() {
mapDataRef.current = {
file,
rows,
columns,
width: image.width,
height: image.height
};
setImageLoaded(true);
};
image.src = url;
setMapSource(url);
} }
function handleDone() { function handleDone() {
@ -38,6 +61,12 @@ function AddMapButton({ handleMapChange }) {
const [rows, setRows] = useState(defaultMapSize); const [rows, setRows] = useState(defaultMapSize);
const [columns, setColumns] = useState(defaultMapSize); const [columns, setColumns] = useState(defaultMapSize);
useEffect(() => {
if (mapDataRef.current) {
mapDataRef.current.rows = rows;
mapDataRef.current.columns = columns;
}
}, [rows, columns]);
return ( return (
<> <>
@ -70,7 +99,7 @@ function AddMapButton({ handleMapChange }) {
/> />
<Flex sx={{ flexDirection: "column" }}> <Flex sx={{ flexDirection: "column" }}>
<Image <UIImage
my={2} my={2}
sx={{ sx={{
width: "500px", width: "500px",
@ -104,7 +133,9 @@ function AddMapButton({ handleMapChange }) {
</Box> </Box>
</Flex> </Flex>
{mapSource ? ( {mapSource ? (
<Button variant="primary">Done</Button> <Button variant="primary" disabled={!imageLoaded}>
Done
</Button>
) : ( ) : (
<Button <Button
varient="primary" varient="primary"

View File

@ -1,12 +1,13 @@
import React from "react"; import React, { useRef } from "react";
import { Image, Flex, Box } from "theme-ui"; import { Box, Image } from "theme-ui";
import Token from "../components/Token"; import Token from "../components/Token";
import ProxyToken from "../components/ProxyToken"; import ProxyToken from "../components/ProxyToken";
const mapTokenClassName = "map-token"; const mapTokenClassName = "map-token";
const defaultTokenSize = 48;
function Map({ imageSource, tokens, onMapTokenMove, onMapTokenRemove }) { function Map({ mapSource, mapData, tokens, onMapTokenMove, onMapTokenRemove }) {
function handleProxyDragEnd(isOnMap, token) { function handleProxyDragEnd(isOnMap, token) {
if (isOnMap && onMapTokenMove) { if (isOnMap && onMapTokenMove) {
onMapTokenMove(token); onMapTokenMove(token);
@ -17,43 +18,76 @@ function Map({ imageSource, tokens, onMapTokenMove, onMapTokenRemove }) {
} }
} }
const mapRef = useRef(null);
const rows = mapData && mapData.rows;
const tokenSizePercent = (1 / rows) * 100;
const aspectRatio = (mapData && mapData.width / mapData.height) || 1;
return ( return (
<> <>
<Flex <Box
className="map" className="map"
sx={{ justifyContent: "center", flexGrow: 1 }} sx={{ flexGrow: 1, position: "relative", overflow: "hidden" }}
bg="background" bg="background"
> >
<Box <Box
sx={{ sx={{
position: "absolute", position: "relative",
top: 0, overflow: "hidden",
left: 0, top: "50%",
right: 0, left: "50%",
userSelect: "none" transform: "translate(-50%, -50%)"
}} }}
> >
{Object.values(tokens).map(token => ( <Box
<Box sx={{
key={token.id} width: "100%",
sx={{ height: 0,
position: "absolute", paddingBottom: `${(1 / aspectRatio) * 100}%`
transform: `translate(${token.x}px, ${token.y}px)` }}
}} />
>
<Token <Box
tokenId={token.id} sx={{ position: "absolute", top: 0, right: 0, bottom: 0, left: 0 }}
image={token.image} >
className={mapTokenClassName} <Image ref={mapRef} id="map" src={mapSource} />
/> </Box>
</Box> <Box
))} sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0
}}
>
{Object.values(tokens).map(token => (
<Box
key={token.id}
style={{
left: `${token.x * 100}%`,
top: `${token.y * 100}%`,
width: `${tokenSizePercent}%`
}}
sx={{
position: "absolute",
display: "flex"
}}
>
<Token
tokenId={token.id}
image={token.image}
className={mapTokenClassName}
/>
</Box>
))}
</Box>
</Box> </Box>
<Image src={imageSource} sx={{ objectFit: "contain" }} /> </Box>
</Flex>
<ProxyToken <ProxyToken
tokenClassName={mapTokenClassName} tokenClassName={mapTokenClassName}
onProxyDragEnd={handleProxyDragEnd} onProxyDragEnd={handleProxyDragEnd}
size={defaultTokenSize}
/> />
</> </>
); );

View File

@ -5,7 +5,7 @@ import interact from "interactjs";
import usePortal from "../helpers/usePortal"; import usePortal from "../helpers/usePortal";
function ProxyToken({ tokenClassName, onProxyDragEnd }) { function ProxyToken({ tokenClassName, onProxyDragEnd, size }) {
const proxyContainer = usePortal("root"); const proxyContainer = usePortal("root");
const [imageSource, setImageSource] = useState(""); const [imageSource, setImageSource] = useState("");
@ -67,8 +67,20 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
let proxy = imageRef.current; let proxy = imageRef.current;
if (proxy) { if (proxy) {
if (onProxyDragEnd) { if (onProxyDragEnd) {
const x = parseFloat(proxy.getAttribute("data-x")) || 0; // TODO: look at cleaning this up with a props instead of
const y = parseFloat(proxy.getAttribute("data-y")) || 0; // hard coding an id
const map = document.getElementById("map");
const mapRect = map.getBoundingClientRect();
let x = parseFloat(proxy.getAttribute("data-x")) || 0;
let y = parseFloat(proxy.getAttribute("data-y")) || 0;
// Convert coordiantes to be relative to the map
x = x - mapRect.left;
y = y - mapRect.top;
// Normalize to map width
x = x / (mapRect.right - mapRect.left);
y = y / (mapRect.bottom - mapRect.top);
const id = target.getAttribute("data-token-id"); const id = target.getAttribute("data-token-id");
onProxyDragEnd(proxyOnMap.current, { onProxyDragEnd(proxyOnMap.current, {
image: imageSource, image: imageSource,
@ -109,11 +121,10 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
}} }}
> >
<Image <Image
p={2}
src={imageSource} src={imageSource}
sx={{ sx={{
width: "64px", width: `${size}px`,
height: "64px", height: `${size}px`,
touchAction: "none", touchAction: "none",
userSelect: "none", userSelect: "none",
position: "absolute" position: "absolute"
@ -125,4 +136,6 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
); );
} }
ProxyToken.defaultProps = { size: 48 };
export default ProxyToken; export default ProxyToken;

View File

@ -4,18 +4,7 @@ import { Image } from "theme-ui";
function Token({ image, className, tokenId }) { function Token({ image, className, tokenId }) {
// Store the token id in the html element for the drag code to pick it up // Store the token id in the html element for the drag code to pick it up
const idProp = tokenId ? { "data-token-id": tokenId } : {}; const idProp = tokenId ? { "data-token-id": tokenId } : {};
return ( return <Image className={className} src={image} {...idProp} />;
<Image
p={2}
className={className}
src={image}
sx={{
width: "64px",
height: "64px"
}}
{...idProp}
/>
);
} }
export default Token; export default Token;

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { Flex } from "theme-ui"; import { Flex, Box } from "theme-ui";
import shortid from "shortid"; import shortid from "shortid";
import * as tokens from "../tokens"; import * as tokens from "../tokens";
@ -30,7 +30,9 @@ function Tokens({ onCreateMapToken }) {
px={2} px={2}
> >
{Object.entries(tokens).map(([id, image]) => ( {Object.entries(tokens).map(([id, image]) => (
<Token key={id} image={image} className={listTokenClassName} /> <Box key={id} m={2} sx={{ width: "48px", height: "48px" }}>
<Token image={image} className={listTokenClassName} />
</Box>
))} ))}
</Flex> </Flex>
<ProxyToken <ProxyToken

View File

@ -97,6 +97,19 @@ function Game() {
return ( return (
<Flex sx={{ flexDirection: "column", height: "100vh" }}> <Flex sx={{ flexDirection: "column", height: "100vh" }}>
<Flex
sx={{ justifyContent: "space-between", flexGrow: 1, height: "100%" }}
>
<Party streams={streams} localStreamId={peerId} />
<Map
mapSource={mapSource}
mapData={mapDataRef.current}
tokens={mapTokens}
onMapTokenMove={handleEditMapToken}
onMapTokenRemove={handleRemoveMapToken}
/>
<Tokens onCreateMapToken={handleEditMapToken} />
</Flex>
<Box <Box
p={2} p={2}
sx={{ sx={{
@ -108,18 +121,6 @@ function Game() {
> >
<AddMapButton handleMapChange={handleMapChange} /> <AddMapButton handleMapChange={handleMapChange} />
</Box> </Box>
<Flex
sx={{ justifyContent: "space-between", flexGrow: 1, height: "100%" }}
>
<Party streams={streams} localStreamId={peerId} />
<Map
imageSource={mapSource}
tokens={mapTokens}
onMapTokenMove={handleEditMapToken}
onMapTokenRemove={handleRemoveMapToken}
/>
<Tokens onCreateMapToken={handleEditMapToken} />
</Flex>
</Flex> </Flex>
); );
} }