Added custom sizing support

This commit is contained in:
Mitchell McCaffrey 2020-04-07 11:47:06 +10:00
parent 43aaf04843
commit 3bd8fce6ab
5 changed files with 130 additions and 57 deletions

View File

@ -17,7 +17,7 @@ function Map({
tokens, tokens,
onMapTokenMove, onMapTokenMove,
onMapTokenRemove, onMapTokenRemove,
onMapChanged onMapChanged,
}) { }) {
function handleProxyDragEnd(isOnMap, token) { function handleProxyDragEnd(isOnMap, token) {
if (isOnMap && onMapTokenMove) { if (isOnMap && onMapTokenMove) {
@ -36,29 +36,29 @@ function Map({
interact(".map") interact(".map")
.gesturable({ .gesturable({
listeners: { listeners: {
move: event => { move: (event) => {
setMapScale(previousMapScale => setMapScale((previousMapScale) =>
Math.max(Math.min(previousMapScale + event.ds, maxZoom), minZoom) Math.max(Math.min(previousMapScale + event.ds, maxZoom), minZoom)
); );
setMapTranslate(previousMapTranslate => ({ setMapTranslate((previousMapTranslate) => ({
x: previousMapTranslate.x + event.dx, x: previousMapTranslate.x + event.dx,
y: previousMapTranslate.y + event.dy y: previousMapTranslate.y + event.dy,
})); }));
} },
} },
}) })
.draggable({ .draggable({
inertia: true, inertia: true,
listeners: { listeners: {
move: event => { move: (event) => {
setMapTranslate(previousMapTranslate => ({ setMapTranslate((previousMapTranslate) => ({
x: previousMapTranslate.x + event.dx, x: previousMapTranslate.x + event.dx,
y: previousMapTranslate.y + event.dy y: previousMapTranslate.y + event.dy,
})); }));
} },
} },
}); });
interact(".map").on("doubletap", event => { interact(".map").on("doubletap", (event) => {
event.preventDefault(); event.preventDefault();
setMapTranslate({ x: 0, y: 0 }); setMapTranslate({ x: 0, y: 0 });
setMapScale(1); setMapScale(1);
@ -67,7 +67,7 @@ function Map({
function handleZoom(event) { function handleZoom(event) {
const deltaY = event.deltaY * zoomSpeed; const deltaY = event.deltaY * zoomSpeed;
setMapScale(previousMapScale => setMapScale((previousMapScale) =>
Math.max(Math.min(previousMapScale + deltaY, maxZoom), minZoom) Math.max(Math.min(previousMapScale + deltaY, maxZoom), minZoom)
); );
} }
@ -93,7 +93,7 @@ function Map({
overflow: "hidden", overflow: "hidden",
backgroundColor: "rgba(0, 0, 0, 0.1)", backgroundColor: "rgba(0, 0, 0, 0.1)",
userSelect: "none", userSelect: "none",
touchAction: "none" touchAction: "none",
}} }}
bg="background" bg="background"
onWheel={handleZoom} onWheel={handleZoom}
@ -103,19 +103,19 @@ function Map({
position: "relative", position: "relative",
top: "50%", top: "50%",
left: "50%", left: "50%",
transform: "translate(-50%, -50%)" transform: "translate(-50%, -50%)",
}} }}
> >
<Box <Box
style={{ style={{
transform: `translate(${mapTranslate.x}px, ${mapTranslate.y}px) scale(${mapScale})` transform: `translate(${mapTranslate.x}px, ${mapTranslate.y}px) scale(${mapScale})`,
}} }}
> >
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
height: 0, height: 0,
paddingBottom: `${(1 / aspectRatio) * 100}%` paddingBottom: `${(1 / aspectRatio) * 100}%`,
}} }}
/> />
<Box <Box
@ -124,7 +124,7 @@ function Map({
top: 0, top: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
left: 0 left: 0,
}} }}
> >
<Image <Image
@ -133,7 +133,7 @@ function Map({
sx={{ sx={{
width: "100%", width: "100%",
userSelect: "none", userSelect: "none",
touchAction: "none" touchAction: "none",
}} }}
src={mapSource} src={mapSource}
/> />
@ -144,24 +144,27 @@ function Map({
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
bottom: 0 bottom: 0,
}} }}
> >
{Object.values(tokens).map(token => ( {Object.values(tokens).map((token) => (
<Box <Box
key={token.id} key={token.id}
style={{ style={{
left: `${token.x * 100}%`, left: `${token.x * 100}%`,
top: `${token.y * 100}%`, top: `${token.y * 100}%`,
width: `${tokenSizePercent}%` width: `${tokenSizePercent * (token.size || 1)}%`,
}} }}
sx={{ sx={{
position: "absolute", position: "absolute",
display: "flex" display: "flex",
}} }}
> >
<Token <Token
tokenId={token.id} data={{
id: token.id,
size: token.size,
}}
image={token.image} image={token.image}
className={mapTokenClassName} className={mapTokenClassName}
sx={{ position: "absolute" }} sx={{ position: "absolute" }}
@ -177,7 +180,7 @@ function Map({
position: "absolute", position: "absolute",
top: "0", top: "0",
left: "50%", left: "50%",
transform: "translateX(-50%)" transform: "translateX(-50%)",
}} }}
> >
<AddMapButton onMapChanged={onMapChanged} /> <AddMapButton onMapChanged={onMapChanged} />

View File

@ -16,7 +16,7 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
useEffect(() => { useEffect(() => {
interact(`.${tokenClassName}`).draggable({ interact(`.${tokenClassName}`).draggable({
listeners: { listeners: {
start: event => { start: (event) => {
let target = event.target; let target = event.target;
// Hide the token and copy it's image to the proxy // Hide the token and copy it's image to the proxy
target.style.opacity = "0.25"; target.style.opacity = "0.25";
@ -39,7 +39,7 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
} }
}, },
move: event => { move: (event) => {
let proxy = imageRef.current; let proxy = imageRef.current;
// Move the proxy based off of the movment of the token // Move the proxy based off of the movment of the token
if (proxy) { if (proxy) {
@ -66,7 +66,7 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
} }
}, },
end: event => { end: (event) => {
let target = event.target; let target = event.target;
let proxy = imageRef.current; let proxy = imageRef.current;
if (proxy) { if (proxy) {
@ -85,12 +85,12 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
x = x / (mapRect.right - mapRect.left); x = x / (mapRect.right - mapRect.left);
y = y / (mapRect.bottom - mapRect.top); y = y / (mapRect.bottom - mapRect.top);
const id = target.getAttribute("data-token-id");
onProxyDragEnd(proxyOnMap.current, { onProxyDragEnd(proxyOnMap.current, {
image: imageSource, image: imageSource,
x, x,
y, y,
id // Pass in props stored as data- in the dom node
...target.dataset,
}); });
} }
@ -103,8 +103,8 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
// Show the token // Show the token
target.style.opacity = "1"; target.style.opacity = "1";
setImageSource(""); setImageSource("");
} },
} },
}); });
}, [imageSource, onProxyDragEnd, tokenClassName, proxyContainer]); }, [imageSource, onProxyDragEnd, tokenClassName, proxyContainer]);
@ -121,7 +121,7 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
top: 0, top: 0,
left: 0, left: 0,
bottom: 0, bottom: 0,
right: 0 right: 0,
}} }}
> >
<Image <Image
@ -129,7 +129,7 @@ function ProxyToken({ tokenClassName, onProxyDragEnd }) {
sx={{ sx={{
touchAction: "none", touchAction: "none",
userSelect: "none", userSelect: "none",
position: "absolute" position: "absolute",
}} }}
ref={imageRef} ref={imageRef}
/> />

View File

@ -0,0 +1,52 @@
import React from "react";
import { Box, Flex, IconButton, Text, Label } from "theme-ui";
function SizeInput({ value, onChange }) {
return (
<Box>
<Text sx={{ textAlign: "center" }} variant="heading">
Size
</Text>
<Flex sx={{ alignItems: "center", justifyContent: "center" }}>
<IconButton
aria-label="Lower token size"
onClick={() => value > 1 && onChange(value - 1)}
>
<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="M18 13H6c-.55 0-1-.45-1-1s.45-1 1-1h12c.55 0 1 .45 1 1s-.45 1-1 1z" />
</svg>
</IconButton>
<Text>{value}</Text>
<IconButton
aria-label="Increase token size"
onClick={() => onChange(value + 1)}
>
<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="M18 13h-5v5c0 .55-.45 1-1 1s-1-.45-1-1v-5H6c-.55 0-1-.45-1-1s.45-1 1-1h5V6c0-.55.45-1 1-1s1 .45 1 1v5h5c.55 0 1 .45 1 1s-.45 1-1 1z" />
</svg>
</IconButton>
</Flex>
</Box>
);
}
SizeInput.defaultProps = {
value: 1,
onChange: () => {},
};
export default SizeInput;

View File

@ -1,21 +1,26 @@
import React from "react"; import React from "react";
import { Image } from "theme-ui"; import { Image } from "theme-ui";
function Token({ image, className, tokenId, sx }) { // The data prop is used to pass data into the dom element
// Store the token id in the html element for the drag code to pick it up // this can be used to pass state to the ProxyToken
const idProp = tokenId ? { "data-token-id": tokenId } : {}; function Token({ image, className, data, sx }) {
// Map the keys in data to have the `data-` prefix
const dataProps = Object.fromEntries(
Object.entries(data).map(([key, value]) => [`data-${key}`, value])
);
return ( return (
<Image <Image
className={className} className={className}
sx={{ userSelect: "none", touchAction: "none", ...sx }} sx={{ userSelect: "none", touchAction: "none", ...sx }}
src={image} src={image}
{...idProp} {...dataProps}
/> />
); );
} }
Token.defaultProps = { Token.defaultProps = {
sx: {} data: {},
sx: {},
}; };
export default Token; export default Token;

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useState } from "react";
import { Flex, Box } from "theme-ui"; import { Flex, Box } from "theme-ui";
import shortid from "shortid"; import shortid from "shortid";
@ -6,34 +6,47 @@ import * as tokens from "../tokens";
import Token from "./Token"; import Token from "./Token";
import ProxyToken from "./ProxyToken"; import ProxyToken from "./ProxyToken";
import SizeInput from "./SizeInput";
const listTokenClassName = "list-token"; const listTokenClassName = "list-token";
function Tokens({ onCreateMapToken }) { function Tokens({ onCreateMapToken }) {
const [tokenSize, setTokenSize] = useState(1);
function handleProxyDragEnd(isOnMap, token) { function handleProxyDragEnd(isOnMap, token) {
if (isOnMap && onCreateMapToken) { if (isOnMap && onCreateMapToken) {
// Give the token an id // Give the token an id
onCreateMapToken({ ...token, id: shortid.generate() }); onCreateMapToken({ ...token, id: shortid.generate(), size: tokenSize });
} }
} }
return ( return (
<> <>
<Flex <Flex sx={{ flexDirection: "column" }}>
bg="background" <Flex
sx={{ bg="background"
width: "80px", sx={{
minWidth: "80px", width: "80px",
flexDirection: "column", minWidth: "80px",
overflowY: "auto" flexDirection: "column",
}} overflowY: "auto",
px={2} }}
> px={2}
{Object.entries(tokens).map(([id, image]) => ( >
<Box key={id} m={2} sx={{ width: "48px", height: "48px" }}> {Object.entries(tokens).map(([id, image]) => (
<Token image={image} className={listTokenClassName} /> <Box key={id} m={2} sx={{ width: "48px", height: "48px" }}>
</Box> <Token image={image} className={listTokenClassName} />
))} </Box>
))}
</Flex>
<Box
pt={1}
sx={{
backgroundColor: "muted",
}}
>
<SizeInput value={tokenSize} onChange={setTokenSize} />
</Box>
</Flex> </Flex>
<ProxyToken <ProxyToken
tokenClassName={listTokenClassName} tokenClassName={listTokenClassName}