From 68c1c6db0c0e615ff5a8a1e0081956d07816d813 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Tue, 13 Jul 2021 18:50:18 +1000 Subject: [PATCH] Typescript --- src/components/map/Map.tsx | 6 +- .../map/{MapDrawing.js => MapDrawing.tsx} | 170 ++++++++++++------ src/components/party/PartyState.ts | 39 ---- src/contexts/MapInteractionContext.tsx | 49 +++-- src/contexts/MapStageContext.tsx | 7 +- src/helpers/colors.ts | 1 + src/helpers/konva.tsx | 2 - ...useGridSnapping.js => useGridSnapping.tsx} | 13 +- src/routes/Game.tsx | 3 +- src/types/Drawing.ts | 19 +- 10 files changed, 186 insertions(+), 123 deletions(-) rename src/components/map/{MapDrawing.js => MapDrawing.tsx} (61%) delete mode 100644 src/components/party/PartyState.ts rename src/hooks/{useGridSnapping.js => useGridSnapping.tsx} (86%) 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" + ); +}