diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx
index 01426e4..2f6618b 100644
--- a/src/components/map/Map.tsx
+++ b/src/components/map/Map.tsx
@@ -255,9 +255,9 @@ function Map({
const mapDrawing = (
diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.tsx
similarity index 61%
rename from src/components/map/MapDrawing.js
rename to src/components/map/MapDrawing.tsx
index ccce9fd..118f5e2 100644
--- a/src/components/map/MapDrawing.js
+++ b/src/components/map/MapDrawing.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from "react";
+import { useState, useEffect } from "react";
import shortid from "shortid";
import { Group, Line, Rect, Circle } from "react-konva";
@@ -25,14 +25,34 @@ import { getRelativePointerPosition } from "../../helpers/konva";
import useGridSnapping from "../../hooks/useGridSnapping";
+import { Map } from "../../types/Map";
+import {
+ Drawing,
+ DrawingToolSettings,
+ drawingToolIsShape,
+ Shape,
+} from "../../types/Drawing";
+
+export type DrawingAddEventHanlder = (drawing: Drawing) => void;
+export type DrawingsRemoveEventHandler = (drawingIds: string[]) => void;
+
+type MapDrawingProps = {
+ map: Map;
+ drawings: Drawing[];
+ onDrawingAdd: DrawingAddEventHanlder;
+ onDrawingsRemove: DrawingsRemoveEventHandler;
+ active: boolean;
+ toolSettings: DrawingToolSettings;
+};
+
function MapDrawing({
map,
- shapes,
- onShapeAdd,
- onShapesRemove,
+ drawings,
+ onDrawingAdd: onShapeAdd,
+ onDrawingsRemove: onShapesRemove,
active,
toolSettings,
-}) {
+}: MapDrawingProps) {
const stageScale = useDebouncedStageScale();
const mapWidth = useMapWidth();
const mapHeight = useMapHeight();
@@ -42,11 +62,12 @@ function MapDrawing({
const gridStrokeWidth = useGridStrokeWidth();
const mapStageRef = useMapStage();
- const [drawingShape, setDrawingShape] = useState(null);
+ const [drawing, setDrawing] = useState(null);
const [isBrushDown, setIsBrushDown] = useState(false);
- const [erasingShapes, setErasingShapes] = useState([]);
+ const [erasingDrawings, setErasingDrawings] = useState([]);
const shouldHover = toolSettings.type === "erase" && active;
+
const isBrush =
toolSettings.type === "brush" || toolSettings.type === "paint";
const isShape =
@@ -64,8 +85,14 @@ function MapDrawing({
const mapStage = mapStageRef.current;
function getBrushPosition() {
+ if (!mapStage) {
+ return;
+ }
const mapImage = mapStage.findOne("#mapImage");
let position = getRelativePointerPosition(mapImage);
+ if (!position) {
+ return;
+ }
if (map.snapToGrid && isShape) {
position = snapPositionToGrid(position);
}
@@ -77,36 +104,46 @@ function MapDrawing({
function handleBrushDown() {
const brushPosition = getBrushPosition();
+ if (!brushPosition) {
+ return;
+ }
const commonShapeData = {
color: toolSettings.color,
blend: toolSettings.useBlending,
id: shortid.generate(),
};
+ const type = toolSettings.type;
if (isBrush) {
- setDrawingShape({
+ setDrawing({
type: "path",
- pathType: toolSettings.type === "brush" ? "stroke" : "fill",
+ pathType: type === "brush" ? "stroke" : "fill",
data: { points: [brushPosition] },
- strokeWidth: toolSettings.type === "brush" ? 1 : 0,
+ strokeWidth: type === "brush" ? 1 : 0,
...commonShapeData,
});
- } else if (isShape) {
- setDrawingShape({
+ } else if (isShape && drawingToolIsShape(type)) {
+ setDrawing({
type: "shape",
- shapeType: toolSettings.type,
- data: getDefaultShapeData(toolSettings.type, brushPosition),
+ shapeType: type,
+ data: getDefaultShapeData(type, brushPosition),
strokeWidth: toolSettings.type === "line" ? 1 : 0,
...commonShapeData,
- });
+ } as Shape);
}
setIsBrushDown(true);
}
function handleBrushMove() {
const brushPosition = getBrushPosition();
- if (isBrushDown && drawingShape) {
+ if (!brushPosition) {
+ return;
+ }
+ if (isBrushDown && drawing) {
if (isBrush) {
- setDrawingShape((prevShape) => {
+ setDrawing((prevShape) => {
+ if (prevShape?.type !== "path") {
+ return prevShape;
+ }
const prevPoints = prevShape.data.points;
if (
Vector2.compare(
@@ -127,63 +164,68 @@ function MapDrawing({
};
});
} else if (isShape) {
- setDrawingShape((prevShape) => ({
- ...prevShape,
- data: getUpdatedShapeData(
- prevShape.shapeType,
- prevShape.data,
- brushPosition,
- gridCellNormalizedSize,
- mapWidth,
- mapHeight
- ),
- }));
+ setDrawing((prevShape) => {
+ if (prevShape?.type !== "shape") {
+ return prevShape;
+ }
+ return {
+ ...prevShape,
+ data: getUpdatedShapeData(
+ prevShape.shapeType,
+ prevShape.data,
+ brushPosition,
+ gridCellNormalizedSize,
+ mapWidth,
+ mapHeight
+ ),
+ } as Shape;
+ });
}
}
}
function handleBrushUp() {
- if (isBrush && drawingShape) {
- if (drawingShape.data.points.length > 1) {
- onShapeAdd(drawingShape);
+ if (isBrush && drawing && drawing.type === "path") {
+ if (drawing.data.points.length > 1) {
+ onShapeAdd(drawing);
}
- } else if (isShape && drawingShape) {
- onShapeAdd(drawingShape);
+ } else if (isShape && drawing) {
+ onShapeAdd(drawing);
}
eraseHoveredShapes();
- setDrawingShape(null);
+ setDrawing(null);
setIsBrushDown(false);
}
- interactionEmitter.on("dragStart", handleBrushDown);
- interactionEmitter.on("drag", handleBrushMove);
- interactionEmitter.on("dragEnd", handleBrushUp);
+ interactionEmitter?.on("dragStart", handleBrushDown);
+ interactionEmitter?.on("drag", handleBrushMove);
+ interactionEmitter?.on("dragEnd", handleBrushUp);
return () => {
- interactionEmitter.off("dragStart", handleBrushDown);
- interactionEmitter.off("drag", handleBrushMove);
- interactionEmitter.off("dragEnd", handleBrushUp);
+ interactionEmitter?.off("dragStart", handleBrushDown);
+ interactionEmitter?.off("drag", handleBrushMove);
+ interactionEmitter?.off("dragEnd", handleBrushUp);
};
});
- function handleShapeOver(shape, isDown) {
+ function handleShapeOver(shape: Drawing, isDown: boolean) {
if (shouldHover && isDown) {
- if (erasingShapes.findIndex((s) => s.id === shape.id) === -1) {
- setErasingShapes((prevShapes) => [...prevShapes, shape]);
+ if (erasingDrawings.findIndex((s) => s.id === shape.id) === -1) {
+ setErasingDrawings((prevShapes) => [...prevShapes, shape]);
}
}
}
function eraseHoveredShapes() {
- if (erasingShapes.length > 0) {
- onShapesRemove(erasingShapes.map((shape) => shape.id));
- setErasingShapes([]);
+ if (erasingDrawings.length > 0) {
+ onShapesRemove(erasingDrawings.map((shape) => shape.id));
+ setErasingDrawings([]);
}
}
- function renderShape(shape) {
+ function renderDrawing(shape: Drawing) {
const defaultProps = {
key: shape.id,
onMouseMove: () => handleShapeOver(shape, isBrushDown),
@@ -200,7 +242,11 @@ function MapDrawing({
return (
[...acc, point.x * mapWidth, point.y * mapHeight],
+ (acc: number[], point) => [
+ ...acc,
+ point.x * mapWidth,
+ point.y * mapHeight,
+ ],
[]
)}
stroke={colors[shape.color] || shape.color}
@@ -238,7 +284,11 @@ function MapDrawing({
return (
[...acc, point.x * mapWidth, point.y * mapHeight],
+ (acc: number[], point) => [
+ ...acc,
+ point.x * mapWidth,
+ point.y * mapHeight,
+ ],
[]
)}
closed={true}
@@ -249,7 +299,11 @@ function MapDrawing({
return (
[...acc, point.x * mapWidth, point.y * mapHeight],
+ (acc: number[], point) => [
+ ...acc,
+ point.x * mapWidth,
+ point.y * mapHeight,
+ ],
[]
)}
strokeWidth={gridStrokeWidth * shape.strokeWidth}
@@ -262,19 +316,19 @@ function MapDrawing({
}
}
- function renderErasingShape(shape) {
- const eraseShape = {
- ...shape,
- color: "#BB99FF",
+ function renderErasingDrawing(drawing: Drawing) {
+ const eraseShape: Drawing = {
+ ...drawing,
+ color: "primary",
};
- return renderShape(eraseShape);
+ return renderDrawing(eraseShape);
}
return (
- {shapes.map(renderShape)}
- {drawingShape && renderShape(drawingShape)}
- {erasingShapes.length > 0 && erasingShapes.map(renderErasingShape)}
+ {drawings.map(renderDrawing)}
+ {drawing && renderDrawing(drawing)}
+ {erasingDrawings.length > 0 && erasingDrawings.map(renderErasingDrawing)}
);
}
diff --git a/src/components/party/PartyState.ts b/src/components/party/PartyState.ts
deleted file mode 100644
index 194de30..0000000
--- a/src/components/party/PartyState.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * @typedef {object} Timer
- * @property {number} current
- * @property {number} max
- */
-export type Timer = {
- current: number,
- max: number
-}
-
-/**
- * @typedef {object} PlayerDice
- * @property {boolean} share
- * @property {[]} rolls
- */
-export type PlayerDice = { share: boolean, rolls: [] }
-
-/**
- * @typedef {object} PlayerInfo
- * @property {string} nickname
- * @property {Timer | null} timer
- * @property {PlayerDice} dice
- * @property {string} sessionId
- * @property {string} userId
- */
-export type PlayerInfo = {
- nickname: string,
- timer: Timer | null,
- dice: PlayerDice,
- sessionId: string,
- userId: string
-}
-
-/**
- * @typedef {object} PartyState
- * @property {string} player
- * @property {PlayerInfo} playerInfo
- */
-export type PartyState = { [player: string]: PlayerInfo }
\ No newline at end of file
diff --git a/src/contexts/MapInteractionContext.tsx b/src/contexts/MapInteractionContext.tsx
index 1c3364b..7362c86 100644
--- a/src/contexts/MapInteractionContext.tsx
+++ b/src/contexts/MapInteractionContext.tsx
@@ -1,16 +1,45 @@
-import React, { ReactChild, useContext } from "react";
+import React, { useContext } from "react";
+import { EventEmitter } from "stream";
import useDebounce from "../hooks/useDebounce";
-export const StageScaleContext = React.createContext(undefined) as any;
-export const DebouncedStageScaleContext = React.createContext(undefined) as any;
-export const StageWidthContext = React.createContext(undefined) as any;
-export const StageHeightContext = React.createContext(undefined) as any;
-export const SetPreventMapInteractionContext = React.createContext(undefined) as any;
-export const MapWidthContext = React.createContext(undefined) as any;
-export const MapHeightContext = React.createContext(undefined) as any;
-export const InteractionEmitterContext = React.createContext(undefined) as any;
+type MapInteraction = {
+ stageScale: number;
+ stageWidth: number;
+ stageHeight: number;
+ setPreventMapInteraction: React.Dispatch>;
+ mapWidth: number;
+ mapHeight: number;
+ interactionEmitter: EventEmitter | null;
+};
-export function MapInteractionProvider({ value, children }: { value: any, children: ReactChild[]}) {
+export const StageScaleContext =
+ React.createContext(undefined);
+export const DebouncedStageScaleContext =
+ React.createContext(undefined);
+export const StageWidthContext =
+ React.createContext(undefined);
+export const StageHeightContext =
+ React.createContext(undefined);
+export const SetPreventMapInteractionContext =
+ React.createContext(
+ undefined
+ );
+export const MapWidthContext =
+ React.createContext(undefined);
+export const MapHeightContext =
+ React.createContext(undefined);
+export const InteractionEmitterContext =
+ React.createContext(
+ undefined
+ );
+
+export function MapInteractionProvider({
+ value,
+ children,
+}: {
+ value: MapInteraction;
+ children: React.ReactNode;
+}) {
const {
stageScale,
stageWidth,
diff --git a/src/contexts/MapStageContext.tsx b/src/contexts/MapStageContext.tsx
index 8f9a30b..684b297 100644
--- a/src/contexts/MapStageContext.tsx
+++ b/src/contexts/MapStageContext.tsx
@@ -1,7 +1,10 @@
import React, { useContext } from "react";
+import { Stage } from "konva/types/Stage";
-const MapStageContext = React.createContext({ current: null });
-export const MapStageProvider: any = MapStageContext.Provider;
+type MapStage = React.MutableRefObject;
+
+const MapStageContext = React.createContext(undefined);
+export const MapStageProvider = MapStageContext.Provider;
export function useMapStage() {
const context = useContext(MapStageContext);
diff --git a/src/helpers/colors.ts b/src/helpers/colors.ts
index a89fd18..946aae2 100644
--- a/src/helpers/colors.ts
+++ b/src/helpers/colors.ts
@@ -12,6 +12,7 @@ const colors = {
darkGray: "rgb(90, 90, 90)",
lightGray: "rgb(179, 179, 179)",
white: "rgb(255, 255, 255)",
+ primary: "hsl(260, 100%, 80%)",
};
export type Colors = typeof colors;
diff --git a/src/helpers/konva.tsx b/src/helpers/konva.tsx
index d26d959..7eb8edc 100644
--- a/src/helpers/konva.tsx
+++ b/src/helpers/konva.tsx
@@ -331,10 +331,8 @@ export function getRelativePointerPosition(
): { x: number; y: number } | undefined {
let transform = node.getAbsoluteTransform().copy();
transform.invert();
- // TODO: handle possible null value
let position = node.getStage()?.getPointerPosition();
if (!position) {
- // TODO: handle possible null value
return;
}
return transform.point(position);
diff --git a/src/hooks/useGridSnapping.js b/src/hooks/useGridSnapping.tsx
similarity index 86%
rename from src/hooks/useGridSnapping.js
rename to src/hooks/useGridSnapping.tsx
index aa52040..3fc68a5 100644
--- a/src/hooks/useGridSnapping.js
+++ b/src/hooks/useGridSnapping.tsx
@@ -19,11 +19,14 @@ import {
* @param {number=} snappingSensitivity 1 = Always snap, 0 = never snap if undefined the default user setting will be used
* @param {boolean=} useCorners Snap to grid cell corners
*/
-function useGridSnapping(snappingSensitivity, useCorners = true) {
- const [defaultSnappingSensitivity] = useSetting(
+function useGridSnapping(
+ snappingSensitivity: number | undefined = undefined,
+ useCorners: boolean = true
+) {
+ const [defaultSnappingSensitivity] = useSetting(
"map.gridSnappingSensitivity"
);
- snappingSensitivity =
+ let gridSnappingSensitivity =
snappingSensitivity === undefined
? defaultSnappingSensitivity
: snappingSensitivity;
@@ -36,7 +39,7 @@ function useGridSnapping(snappingSensitivity, useCorners = true) {
/**
* @param {Vector2} node The node to snap
*/
- function snapPositionToGrid(position) {
+ function snapPositionToGrid(position: Vector2) {
// Account for grid offset
let offsetPosition = Vector2.subtract(
Vector2.subtract(position, gridOffset),
@@ -70,7 +73,7 @@ function useGridSnapping(snappingSensitivity, useCorners = true) {
const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint);
if (
distanceToSnapPoint <
- Vector2.min(gridCellPixelSize) * snappingSensitivity
+ (Vector2.min(gridCellPixelSize) as number) * gridSnappingSensitivity
) {
// Reverse grid offset
let offsetSnapPoint = Vector2.add(
diff --git a/src/routes/Game.tsx b/src/routes/Game.tsx
index 2301c56..8111021 100644
--- a/src/routes/Game.tsx
+++ b/src/routes/Game.tsx
@@ -28,6 +28,7 @@ import NetworkedMapAndTokens from "../network/NetworkedMapAndTokens";
import NetworkedParty from "../network/NetworkedParty";
import Session from "../network/Session";
+import { Stage } from "konva/types/Stage";
function Game() {
const { id: gameId }: { id: string } = useParams();
@@ -110,7 +111,7 @@ function Game() {
// A ref to the Konva stage
// the ref will be assigned in the MapInteraction component
- const mapStageRef: React.MutableRefObject = useRef();
+ const mapStageRef = useRef(null);
return (
diff --git a/src/types/Drawing.ts b/src/types/Drawing.ts
index 868bc4e..3ad60f7 100644
--- a/src/types/Drawing.ts
+++ b/src/types/Drawing.ts
@@ -37,7 +37,7 @@ export type ShapeData = PointsData | RectData | CircleData;
export type BaseDrawing = {
blend: boolean;
- color: string;
+ color: Color;
id: string;
strokeWidth: number;
};
@@ -46,8 +46,6 @@ export type BaseShape = BaseDrawing & {
type: "shape";
};
-export type ShapeType = "line" | "rectangle" | "circle" | "triangle";
-
export type Line = BaseShape & {
shapeType: "line";
data: PointsData;
@@ -68,6 +66,12 @@ export type Triangle = BaseShape & {
data: PointsData;
};
+export type ShapeType =
+ | Line["shapeType"]
+ | Rectangle["shapeType"]
+ | Circle["shapeType"]
+ | Triangle["shapeType"];
+
export type Shape = Line | Rectangle | Circle | Triangle;
export type Path = BaseDrawing & {
@@ -77,3 +81,12 @@ export type Path = BaseDrawing & {
};
export type Drawing = Shape | Path;
+
+export function drawingToolIsShape(type: DrawingToolType): type is ShapeType {
+ return (
+ type === "line" ||
+ type === "rectangle" ||
+ type === "circle" ||
+ type === "triangle"
+ );
+}