Typescript

This commit is contained in:
Mitchell McCaffrey 2021-07-13 18:50:18 +10:00
parent 569ed696fc
commit 68c1c6db0c
10 changed files with 186 additions and 123 deletions

View File

@ -255,9 +255,9 @@ function Map({
const mapDrawing = ( const mapDrawing = (
<MapDrawing <MapDrawing
map={map} map={map}
shapes={drawShapes} drawings={drawShapes}
onShapeAdd={handleMapShapeAdd} onDrawingAdd={handleMapShapeAdd}
onShapesRemove={handleMapShapesRemove} onDrawingsRemove={handleMapShapesRemove}
active={selectedToolId === "drawing"} active={selectedToolId === "drawing"}
toolSettings={settings.drawing} toolSettings={settings.drawing}
/> />

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import shortid from "shortid"; import shortid from "shortid";
import { Group, Line, Rect, Circle } from "react-konva"; import { Group, Line, Rect, Circle } from "react-konva";
@ -25,14 +25,34 @@ import { getRelativePointerPosition } from "../../helpers/konva";
import useGridSnapping from "../../hooks/useGridSnapping"; 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({ function MapDrawing({
map, map,
shapes, drawings,
onShapeAdd, onDrawingAdd: onShapeAdd,
onShapesRemove, onDrawingsRemove: onShapesRemove,
active, active,
toolSettings, toolSettings,
}) { }: MapDrawingProps) {
const stageScale = useDebouncedStageScale(); const stageScale = useDebouncedStageScale();
const mapWidth = useMapWidth(); const mapWidth = useMapWidth();
const mapHeight = useMapHeight(); const mapHeight = useMapHeight();
@ -42,11 +62,12 @@ function MapDrawing({
const gridStrokeWidth = useGridStrokeWidth(); const gridStrokeWidth = useGridStrokeWidth();
const mapStageRef = useMapStage(); const mapStageRef = useMapStage();
const [drawingShape, setDrawingShape] = useState(null); const [drawing, setDrawing] = useState<Drawing | null>(null);
const [isBrushDown, setIsBrushDown] = useState(false); const [isBrushDown, setIsBrushDown] = useState(false);
const [erasingShapes, setErasingShapes] = useState([]); const [erasingDrawings, setErasingDrawings] = useState<Drawing[]>([]);
const shouldHover = toolSettings.type === "erase" && active; const shouldHover = toolSettings.type === "erase" && active;
const isBrush = const isBrush =
toolSettings.type === "brush" || toolSettings.type === "paint"; toolSettings.type === "brush" || toolSettings.type === "paint";
const isShape = const isShape =
@ -64,8 +85,14 @@ function MapDrawing({
const mapStage = mapStageRef.current; const mapStage = mapStageRef.current;
function getBrushPosition() { function getBrushPosition() {
if (!mapStage) {
return;
}
const mapImage = mapStage.findOne("#mapImage"); const mapImage = mapStage.findOne("#mapImage");
let position = getRelativePointerPosition(mapImage); let position = getRelativePointerPosition(mapImage);
if (!position) {
return;
}
if (map.snapToGrid && isShape) { if (map.snapToGrid && isShape) {
position = snapPositionToGrid(position); position = snapPositionToGrid(position);
} }
@ -77,36 +104,46 @@ function MapDrawing({
function handleBrushDown() { function handleBrushDown() {
const brushPosition = getBrushPosition(); const brushPosition = getBrushPosition();
if (!brushPosition) {
return;
}
const commonShapeData = { const commonShapeData = {
color: toolSettings.color, color: toolSettings.color,
blend: toolSettings.useBlending, blend: toolSettings.useBlending,
id: shortid.generate(), id: shortid.generate(),
}; };
const type = toolSettings.type;
if (isBrush) { if (isBrush) {
setDrawingShape({ setDrawing({
type: "path", type: "path",
pathType: toolSettings.type === "brush" ? "stroke" : "fill", pathType: type === "brush" ? "stroke" : "fill",
data: { points: [brushPosition] }, data: { points: [brushPosition] },
strokeWidth: toolSettings.type === "brush" ? 1 : 0, strokeWidth: type === "brush" ? 1 : 0,
...commonShapeData, ...commonShapeData,
}); });
} else if (isShape) { } else if (isShape && drawingToolIsShape(type)) {
setDrawingShape({ setDrawing({
type: "shape", type: "shape",
shapeType: toolSettings.type, shapeType: type,
data: getDefaultShapeData(toolSettings.type, brushPosition), data: getDefaultShapeData(type, brushPosition),
strokeWidth: toolSettings.type === "line" ? 1 : 0, strokeWidth: toolSettings.type === "line" ? 1 : 0,
...commonShapeData, ...commonShapeData,
}); } as Shape);
} }
setIsBrushDown(true); setIsBrushDown(true);
} }
function handleBrushMove() { function handleBrushMove() {
const brushPosition = getBrushPosition(); const brushPosition = getBrushPosition();
if (isBrushDown && drawingShape) { if (!brushPosition) {
return;
}
if (isBrushDown && drawing) {
if (isBrush) { if (isBrush) {
setDrawingShape((prevShape) => { setDrawing((prevShape) => {
if (prevShape?.type !== "path") {
return prevShape;
}
const prevPoints = prevShape.data.points; const prevPoints = prevShape.data.points;
if ( if (
Vector2.compare( Vector2.compare(
@ -127,63 +164,68 @@ function MapDrawing({
}; };
}); });
} else if (isShape) { } else if (isShape) {
setDrawingShape((prevShape) => ({ setDrawing((prevShape) => {
...prevShape, if (prevShape?.type !== "shape") {
data: getUpdatedShapeData( return prevShape;
prevShape.shapeType, }
prevShape.data, return {
brushPosition, ...prevShape,
gridCellNormalizedSize, data: getUpdatedShapeData(
mapWidth, prevShape.shapeType,
mapHeight prevShape.data,
), brushPosition,
})); gridCellNormalizedSize,
mapWidth,
mapHeight
),
} as Shape;
});
} }
} }
} }
function handleBrushUp() { function handleBrushUp() {
if (isBrush && drawingShape) { if (isBrush && drawing && drawing.type === "path") {
if (drawingShape.data.points.length > 1) { if (drawing.data.points.length > 1) {
onShapeAdd(drawingShape); onShapeAdd(drawing);
} }
} else if (isShape && drawingShape) { } else if (isShape && drawing) {
onShapeAdd(drawingShape); onShapeAdd(drawing);
} }
eraseHoveredShapes(); eraseHoveredShapes();
setDrawingShape(null); setDrawing(null);
setIsBrushDown(false); setIsBrushDown(false);
} }
interactionEmitter.on("dragStart", handleBrushDown); interactionEmitter?.on("dragStart", handleBrushDown);
interactionEmitter.on("drag", handleBrushMove); interactionEmitter?.on("drag", handleBrushMove);
interactionEmitter.on("dragEnd", handleBrushUp); interactionEmitter?.on("dragEnd", handleBrushUp);
return () => { return () => {
interactionEmitter.off("dragStart", handleBrushDown); interactionEmitter?.off("dragStart", handleBrushDown);
interactionEmitter.off("drag", handleBrushMove); interactionEmitter?.off("drag", handleBrushMove);
interactionEmitter.off("dragEnd", handleBrushUp); interactionEmitter?.off("dragEnd", handleBrushUp);
}; };
}); });
function handleShapeOver(shape, isDown) { function handleShapeOver(shape: Drawing, isDown: boolean) {
if (shouldHover && isDown) { if (shouldHover && isDown) {
if (erasingShapes.findIndex((s) => s.id === shape.id) === -1) { if (erasingDrawings.findIndex((s) => s.id === shape.id) === -1) {
setErasingShapes((prevShapes) => [...prevShapes, shape]); setErasingDrawings((prevShapes) => [...prevShapes, shape]);
} }
} }
} }
function eraseHoveredShapes() { function eraseHoveredShapes() {
if (erasingShapes.length > 0) { if (erasingDrawings.length > 0) {
onShapesRemove(erasingShapes.map((shape) => shape.id)); onShapesRemove(erasingDrawings.map((shape) => shape.id));
setErasingShapes([]); setErasingDrawings([]);
} }
} }
function renderShape(shape) { function renderDrawing(shape: Drawing) {
const defaultProps = { const defaultProps = {
key: shape.id, key: shape.id,
onMouseMove: () => handleShapeOver(shape, isBrushDown), onMouseMove: () => handleShapeOver(shape, isBrushDown),
@ -200,7 +242,11 @@ function MapDrawing({
return ( return (
<Line <Line
points={shape.data.points.reduce( points={shape.data.points.reduce(
(acc, point) => [...acc, point.x * mapWidth, point.y * mapHeight], (acc: number[], point) => [
...acc,
point.x * mapWidth,
point.y * mapHeight,
],
[] []
)} )}
stroke={colors[shape.color] || shape.color} stroke={colors[shape.color] || shape.color}
@ -238,7 +284,11 @@ function MapDrawing({
return ( return (
<Line <Line
points={shape.data.points.reduce( points={shape.data.points.reduce(
(acc, point) => [...acc, point.x * mapWidth, point.y * mapHeight], (acc: number[], point) => [
...acc,
point.x * mapWidth,
point.y * mapHeight,
],
[] []
)} )}
closed={true} closed={true}
@ -249,7 +299,11 @@ function MapDrawing({
return ( return (
<Line <Line
points={shape.data.points.reduce( points={shape.data.points.reduce(
(acc, point) => [...acc, point.x * mapWidth, point.y * mapHeight], (acc: number[], point) => [
...acc,
point.x * mapWidth,
point.y * mapHeight,
],
[] []
)} )}
strokeWidth={gridStrokeWidth * shape.strokeWidth} strokeWidth={gridStrokeWidth * shape.strokeWidth}
@ -262,19 +316,19 @@ function MapDrawing({
} }
} }
function renderErasingShape(shape) { function renderErasingDrawing(drawing: Drawing) {
const eraseShape = { const eraseShape: Drawing = {
...shape, ...drawing,
color: "#BB99FF", color: "primary",
}; };
return renderShape(eraseShape); return renderDrawing(eraseShape);
} }
return ( return (
<Group> <Group>
{shapes.map(renderShape)} {drawings.map(renderDrawing)}
{drawingShape && renderShape(drawingShape)} {drawing && renderDrawing(drawing)}
{erasingShapes.length > 0 && erasingShapes.map(renderErasingShape)} {erasingDrawings.length > 0 && erasingDrawings.map(renderErasingDrawing)}
</Group> </Group>
); );
} }

View File

@ -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 }

View File

@ -1,16 +1,45 @@
import React, { ReactChild, useContext } from "react"; import React, { useContext } from "react";
import { EventEmitter } from "stream";
import useDebounce from "../hooks/useDebounce"; import useDebounce from "../hooks/useDebounce";
export const StageScaleContext = React.createContext(undefined) as any; type MapInteraction = {
export const DebouncedStageScaleContext = React.createContext(undefined) as any; stageScale: number;
export const StageWidthContext = React.createContext(undefined) as any; stageWidth: number;
export const StageHeightContext = React.createContext(undefined) as any; stageHeight: number;
export const SetPreventMapInteractionContext = React.createContext(undefined) as any; setPreventMapInteraction: React.Dispatch<React.SetStateAction<boolean>>;
export const MapWidthContext = React.createContext(undefined) as any; mapWidth: number;
export const MapHeightContext = React.createContext(undefined) as any; mapHeight: number;
export const InteractionEmitterContext = React.createContext(undefined) as any; interactionEmitter: EventEmitter | null;
};
export function MapInteractionProvider({ value, children }: { value: any, children: ReactChild[]}) { export const StageScaleContext =
React.createContext<MapInteraction["stageScale"] | undefined>(undefined);
export const DebouncedStageScaleContext =
React.createContext<MapInteraction["stageScale"] | undefined>(undefined);
export const StageWidthContext =
React.createContext<MapInteraction["stageWidth"] | undefined>(undefined);
export const StageHeightContext =
React.createContext<MapInteraction["stageHeight"] | undefined>(undefined);
export const SetPreventMapInteractionContext =
React.createContext<MapInteraction["setPreventMapInteraction"] | undefined>(
undefined
);
export const MapWidthContext =
React.createContext<MapInteraction["mapWidth"] | undefined>(undefined);
export const MapHeightContext =
React.createContext<MapInteraction["mapHeight"] | undefined>(undefined);
export const InteractionEmitterContext =
React.createContext<MapInteraction["interactionEmitter"] | undefined>(
undefined
);
export function MapInteractionProvider({
value,
children,
}: {
value: MapInteraction;
children: React.ReactNode;
}) {
const { const {
stageScale, stageScale,
stageWidth, stageWidth,

View File

@ -1,7 +1,10 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import { Stage } from "konva/types/Stage";
const MapStageContext = React.createContext({ current: null }); type MapStage = React.MutableRefObject<Stage | null>;
export const MapStageProvider: any = MapStageContext.Provider;
const MapStageContext = React.createContext<MapStage | undefined>(undefined);
export const MapStageProvider = MapStageContext.Provider;
export function useMapStage() { export function useMapStage() {
const context = useContext(MapStageContext); const context = useContext(MapStageContext);

View File

@ -12,6 +12,7 @@ const colors = {
darkGray: "rgb(90, 90, 90)", darkGray: "rgb(90, 90, 90)",
lightGray: "rgb(179, 179, 179)", lightGray: "rgb(179, 179, 179)",
white: "rgb(255, 255, 255)", white: "rgb(255, 255, 255)",
primary: "hsl(260, 100%, 80%)",
}; };
export type Colors = typeof colors; export type Colors = typeof colors;

View File

@ -331,10 +331,8 @@ export function getRelativePointerPosition(
): { x: number; y: number } | undefined { ): { x: number; y: number } | undefined {
let transform = node.getAbsoluteTransform().copy(); let transform = node.getAbsoluteTransform().copy();
transform.invert(); transform.invert();
// TODO: handle possible null value
let position = node.getStage()?.getPointerPosition(); let position = node.getStage()?.getPointerPosition();
if (!position) { if (!position) {
// TODO: handle possible null value
return; return;
} }
return transform.point(position); return transform.point(position);

View File

@ -19,11 +19,14 @@ import {
* @param {number=} snappingSensitivity 1 = Always snap, 0 = never snap if undefined the default user setting will be used * @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 * @param {boolean=} useCorners Snap to grid cell corners
*/ */
function useGridSnapping(snappingSensitivity, useCorners = true) { function useGridSnapping(
const [defaultSnappingSensitivity] = useSetting( snappingSensitivity: number | undefined = undefined,
useCorners: boolean = true
) {
const [defaultSnappingSensitivity] = useSetting<number>(
"map.gridSnappingSensitivity" "map.gridSnappingSensitivity"
); );
snappingSensitivity = let gridSnappingSensitivity =
snappingSensitivity === undefined snappingSensitivity === undefined
? defaultSnappingSensitivity ? defaultSnappingSensitivity
: snappingSensitivity; : snappingSensitivity;
@ -36,7 +39,7 @@ function useGridSnapping(snappingSensitivity, useCorners = true) {
/** /**
* @param {Vector2} node The node to snap * @param {Vector2} node The node to snap
*/ */
function snapPositionToGrid(position) { function snapPositionToGrid(position: Vector2) {
// Account for grid offset // Account for grid offset
let offsetPosition = Vector2.subtract( let offsetPosition = Vector2.subtract(
Vector2.subtract(position, gridOffset), Vector2.subtract(position, gridOffset),
@ -70,7 +73,7 @@ function useGridSnapping(snappingSensitivity, useCorners = true) {
const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint); const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint);
if ( if (
distanceToSnapPoint < distanceToSnapPoint <
Vector2.min(gridCellPixelSize) * snappingSensitivity (Vector2.min(gridCellPixelSize) as number) * gridSnappingSensitivity
) { ) {
// Reverse grid offset // Reverse grid offset
let offsetSnapPoint = Vector2.add( let offsetSnapPoint = Vector2.add(

View File

@ -28,6 +28,7 @@ import NetworkedMapAndTokens from "../network/NetworkedMapAndTokens";
import NetworkedParty from "../network/NetworkedParty"; import NetworkedParty from "../network/NetworkedParty";
import Session from "../network/Session"; import Session from "../network/Session";
import { Stage } from "konva/types/Stage";
function Game() { function Game() {
const { id: gameId }: { id: string } = useParams(); const { id: gameId }: { id: string } = useParams();
@ -110,7 +111,7 @@ function Game() {
// A ref to the Konva stage // A ref to the Konva stage
// the ref will be assigned in the MapInteraction component // the ref will be assigned in the MapInteraction component
const mapStageRef: React.MutableRefObject<any> = useRef(); const mapStageRef = useRef<Stage | null>(null);
return ( return (
<AssetsProvider> <AssetsProvider>

View File

@ -37,7 +37,7 @@ export type ShapeData = PointsData | RectData | CircleData;
export type BaseDrawing = { export type BaseDrawing = {
blend: boolean; blend: boolean;
color: string; color: Color;
id: string; id: string;
strokeWidth: number; strokeWidth: number;
}; };
@ -46,8 +46,6 @@ export type BaseShape = BaseDrawing & {
type: "shape"; type: "shape";
}; };
export type ShapeType = "line" | "rectangle" | "circle" | "triangle";
export type Line = BaseShape & { export type Line = BaseShape & {
shapeType: "line"; shapeType: "line";
data: PointsData; data: PointsData;
@ -68,6 +66,12 @@ export type Triangle = BaseShape & {
data: PointsData; data: PointsData;
}; };
export type ShapeType =
| Line["shapeType"]
| Rectangle["shapeType"]
| Circle["shapeType"]
| Triangle["shapeType"];
export type Shape = Line | Rectangle | Circle | Triangle; export type Shape = Line | Rectangle | Circle | Triangle;
export type Path = BaseDrawing & { export type Path = BaseDrawing & {
@ -77,3 +81,12 @@ export type Path = BaseDrawing & {
}; };
export type Drawing = Shape | Path; export type Drawing = Shape | Path;
export function drawingToolIsShape(type: DrawingToolType): type is ShapeType {
return (
type === "line" ||
type === "rectangle" ||
type === "circle" ||
type === "triangle"
);
}