Skip to content

Commit

Permalink
Merge pull request #31 from JollyGrin/feat/token-customization
Browse files Browse the repository at this point in the history
Feat/token customization
  • Loading branch information
JollyGrin authored Apr 1, 2024
2 parents 87c0838 + 0d9f3dd commit 859b9b8
Show file tree
Hide file tree
Showing 16 changed files with 265 additions and 55 deletions.
28 changes: 28 additions & 0 deletions components/BoardCanvas/Tokens/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const Image = ({ imageUrl }: { imageUrl: string }) =>
`<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 24 24">
<image href="${imageUrl}" height='100%' >
</svg>`;

const Circle = ({
color,
size,
}: {
color?: string;
size?: number;
}) => `<svg fill="${color ?? "black"}" stroke="2" viewBox="0 0 100 100" width="${size ?? 72}" height="${size ?? 72}" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50" />
</svg>`;

export const TokenIcon = {
Image,
Circle,
};

/**
* WIP
*
* - cannot have more than 2 props with defaults. If one is not empty, then the default on the other will break
* - need a cleaner version for adding svgs with this function wrapper. Too much repetition
* - how can I create an enum for this
*
* */
21 changes: 21 additions & 0 deletions components/BoardCanvas/defaultTokenImages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Default Tokens for the board!
*
* To add to this list, add the file to /public/tokens/...
*
* Then add the filename to IMAGES
* */

const IMAGES = [
"Alien.svg",
"BrickWall.svg",
"CloudFog.svg",
"Fire.svg",
"Flag.svg",
"HandShield.svg",
"ShieldHalf.svg",
"Totem.svg",
"Trap.svg",
];

export const DEFAULT_TOKEN_IMAGES = IMAGES.map((img) => `/tokens/${img}`);
9 changes: 8 additions & 1 deletion components/BoardCanvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,14 @@ export const BoardCanvas: React.FC<BoardProps> = ({
backgroundColor: "ghostwhite",
}}
>
<image xlinkHref={src} width={1200} height={1000} x={1} y={1} />
<image
className="background"
xlinkHref={src}
width={1200}
height={1000}
x={1}
y={1}
/>
</svg>
</Box>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as d3 from "d3";
import { MutableRefObject, RefObject, useEffect } from "react";
import { PositionType } from "../Positions/position.type";
import { TokenIcon } from "./Tokens";

type CanvasProps = {
canvasRef: RefObject<SVGSVGElement>;
Expand Down Expand Up @@ -44,14 +45,16 @@ export const useCanvas = ({
}
const g = d3.select(gRef.current);

g.selectAll<SVGCircleElement, PositionType>("circle")
g.selectAll<SVGCircleElement, PositionType>("g")
.data(data)
.join("circle")
.attr("cx", ({ x }) => x)
.attr("cy", ({ y }) => y)
.attr("r", ({ r }) => (r ? r : 15))
.attr("fill", ({ color }) => color ?? "black")
// TODO: replace this to limit which token the user can control
.join((enter) => enter.append("g"))
.attr("transform", (d) => `translate(${d.x}, ${d.y})`)
.html((props) =>
props?.imageUrl
? TokenIcon.Image({ imageUrl: props.imageUrl })
: TokenIcon.Circle({ color: props.color, size: props.r }),
)
// NOTE: Bellow attr & filter shows and limits user to moving their own tokens
.attr("opacity", ({ id }) => (id.includes(self as string) ? 1 : 0.75))
.filter(({ id }) => {
const isSidekick = id.includes("_");
Expand All @@ -60,7 +63,7 @@ export const useCanvas = ({
})
.call(
d3
.drag<SVGCircleElement, PositionType>()
.drag<SVGGElement, PositionType>()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
Expand All @@ -82,7 +85,13 @@ export const useCanvas = ({
}),
);

function dragstarted(e: { target: any }) {
function dragstarted(e: any, d: PositionType) {
const isSelf = d?.id === self;
const isSidekick = d?.id.includes("_");
const isSidekickSelf = d?.id.split("_")[0] === self;
if (isSidekick && !isSidekickSelf) return;
if (!isSelf) return;

d3.select(e.target).raise();
g.attr("cursor", "grabbing");
//@ts-expect-error: implicit any
Expand All @@ -98,11 +107,6 @@ export const useCanvas = ({
event: DragEvent & { subject: PositionType },
d: PositionType,
) {
//@ts-expect-error: implicit any
d3.select<SVGCircleElement, PositionType>(this)
.attr("cx", (d.x = event.x))
.attr("cy", (d.y = event.y));

if (!move) return;

move({
Expand Down
1 change: 0 additions & 1 deletion components/Game/game.modal-template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ export const ModalContainer: React.FC<ModalTemplateType> = ({
}),
);

console.log({ modalType, isOpen });
return (
<>
<Modal isOpen={isOpen} onClose={() => !isCommit && onClose()}>
Expand Down
183 changes: 144 additions & 39 deletions components/Positions/position.modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,30 @@ import {
Flex,
ModalFooter,
Button,
VStack,
Divider,
Grid,
Input,
Menu,
MenuButton,
MenuList,
MenuItem,
Image,
FormLabel,
HStack,
} from "@chakra-ui/react";
import { FC, useCallback, useState } from "react";
import { Dispatch, FC, SetStateAction, useCallback, useState } from "react";
import { MoonIcon, PlusSquareIcon, SunIcon } from "@chakra-ui/icons";
import { useWebGame } from "@/lib/contexts/WebGameProvider";

import { MdUpload as IconUpload } from "react-icons/md";

//@ts-ignore
import { CirclePicker } from "react-color";
import { PositionType, Size } from "./position.type";
import { PositionType } from "./position.type";
import { useRouter } from "next/router";
import { toast } from "react-hot-toast";
import { DEFAULT_TOKEN_IMAGES } from "../BoardCanvas/defaultTokenImages";

export const PositionModal: FC<{
isOpen: boolean;
Expand All @@ -37,15 +52,15 @@ export const PositionModal: FC<{
name as keyof typeof gamePositions.content
] as PositionType;

const [images, setImages] = useState<{ id: string; url: string }[]>([]);

const [selectedColor, setSelectedColor] = useState<string>(
selectedPosition?.color ?? "#000",
);
const [selectedSize, setSelectedSize] = useState<Size>("lg");
const [sidekicks, setSidekicks] = useState<
PositionType["sidekicks"] | undefined
>(selectedPosition?.sidekicks);
const setSize = (size: Size) =>
size === "lg" ? 2 : size === "md" ? 1.65 : 1.35;

const handleColorChange = ({ hex }: { hex: string }) => setSelectedColor(hex);

const _setGamePosition = (props: PositionType) => {
Expand All @@ -60,10 +75,18 @@ export const PositionModal: FC<{
setGamePosition({
...selected,
color: selectedColor,
r: selectedSize === "lg" ? 20 : selectedSize === "md" ? 15 : 10,
imageUrl:
images?.find((img) => img.id === selected?.id)?.url ??
selected?.imageUrl ??
undefined,
sidekicks: sidekicks?.map((kick) => ({
...kick,
color: selectedColor,
r: kick?.r ?? 50,
imageUrl:
images?.find((img) => img.id === kick?.id)?.url ??
kick?.imageUrl ??
undefined,
})),
});
}
Expand All @@ -82,10 +105,21 @@ export const PositionModal: FC<{
},
];
});

toast.success("Preparing new token. Click apply to confirm changes");
}

const gameKickIds = selectedPosition?.sidekicks?.map((kick) => kick.id);

return (
<Modal isOpen={isOpen} onClose={onClose}>
<Modal
isOpen={isOpen}
onClose={() => {
setSidekicks(selectedPosition?.sidekicks);
setImages([]);
onClose();
}}
>
<ModalOverlay />
<ModalContent bg={bg} color={color} transition="all 0.25s ease-in-out">
<ModalHeader as={Flex} gap="1rem">
Expand All @@ -98,46 +132,117 @@ export const PositionModal: FC<{
<ModalBody>
<Box position="relative">
<Flex alignItems="center" gap="1rem" minH="2.5rem">
<PlusSquareIcon onClick={addSidekick} />
<Box
bg={selectedColor}
h={setSize(selectedSize) + "rem"}
w={setSize(selectedSize) + "rem"}
cursor="pointer"
borderRadius="100%"
transition="all 0.25s ease-in-out"
onClick={() =>
setSelectedSize((prev) =>
prev === "lg" ? "md" : prev === "md" ? "sm" : "lg",
)
}
/>
{sidekicks?.map((kick) => (
<Box
key={kick.id}
boxSize="1rem"
bg={selectedColor}
borderRadius="100%"
<VStack alignItems="start">
<CirclePicker onChangeComplete={handleColorChange} />
<Divider />
<TokenPreview
token={selectedPosition}
selectedColor={selectedColor}
setImages={setImages}
/>
))}
{selectedPosition?.sidekicks?.map((kick) => (
<TokenPreview
key={kick.id}
token={kick}
selectedColor={selectedColor}
setImages={setImages}
/>
))}
<Divider />
{sidekicks
?.filter((kick) => !gameKickIds?.includes(kick.id))
.map((kick) => (
<TokenPreview
key={kick.id}
token={kick}
selectedColor={selectedColor}
setImages={setImages}
/>
))}

<PlusSquareIcon onClick={addSidekick} />
</VStack>
</Flex>
<Box
position="absolute"
bg={bg}
p="0.5rem"
borderRadius="1rem"
filter="drop-shadow(0 5px 3px rgba(0,0,0,0.5))"
>
<CirclePicker onChangeComplete={handleColorChange} />
</Box>
</Box>
</ModalBody>
<ModalFooter>
<Button variant="outline" bg="primary" onClick={updateYourColor}>
<ModalFooter flexDirection="column" alignItems="end">
<Button
variant="outline"
bg="brand.primary"
onClick={updateYourColor}
>
Apply
</Button>
<FormLabel fontSize="0.75rem">
Clicking apply will reset token positions
</FormLabel>
</ModalFooter>
</ModalContent>
</Modal>
);
};

const TokenPreview = ({
token,
selectedColor,
setImages,
}: {
token: PositionType;
selectedColor: string;
setImages: Dispatch<SetStateAction<{ id: string; url: string }[]>>;
}) => {
const setImage = (url: string) => {
setImages((prev) => {
return [...prev?.filter((p) => p.id !== token.id), { id: token.id, url }];
});
toast.success("New Image Prepared. Click Apply to confirm changes");
};

const [imageUrl, setImageUrl] = useState("");

return (
<Grid templateColumns="1fr 2fr" w="100%" gap="1rem" alignItems="center">
{token?.imageUrl ? (
<Image src={token.imageUrl} />
) : (
<Box
bg={selectedColor}
w="5rem"
h="5rem"
cursor="pointer"
borderRadius="100%"
transition="all 0.25s ease-in-out"
/>
)}

<Menu>
<MenuButton as={Button}>Tokens</MenuButton>
<MenuList maxH="400px" overflowY="auto">
<FormLabel fontSize="0.75rem" pl="0.75rem" color="black">
Add Token (via url)
</FormLabel>
{imageUrl && <Image src={imageUrl} w="3rem" />}
<HStack mb="0.5rem" px="0.5rem">
<Input
placeholder="image url (.svg, .png, .jpeg)"
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
/>
<Button onClick={() => setImage(imageUrl)}>
<IconUpload />
</Button>
</HStack>
<Divider />
<FormLabel fontSize="0.75rem" pl="0.75rem" color="black">
Default Tokens
</FormLabel>
{DEFAULT_TOKEN_IMAGES.map((img) => (
<MenuItem key={img} onClick={() => setImage(img)}>
<Image src={img} fill="red" color="red" w="100px" />
</MenuItem>
))}
</MenuList>
</Menu>
</Grid>
);
};
1 change: 1 addition & 0 deletions components/Positions/position.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type PositionType = {
tokenSize?: Size;
color?: string;
sidekicks?: Sidekick[];
imageUrl?: string;
};

type Sidekick = Omit<PositionType, "sidekicks">;
Loading

0 comments on commit 859b9b8

Please sign in to comment.