From b34a7df4433b1c2371daa020d74b0c2569158f20 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Tue, 28 Apr 2020 17:04:31 +1000 Subject: [PATCH] Added fog edge snapping --- src/components/map/MapDrawing.js | 86 +++++++++++++++++++++++++++----- src/helpers/drawing.js | 63 ++++++++++++++++++++++- src/helpers/vector2.js | 65 ++++++++++++++++++++++++ 3 files changed, 200 insertions(+), 14 deletions(-) diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.js index 42b2e25..a76a29c 100644 --- a/src/components/map/MapDrawing.js +++ b/src/components/map/MapDrawing.js @@ -1,14 +1,15 @@ import React, { useRef, useEffect, useState } from "react"; -import simplify from "simplify-js"; import shortid from "shortid"; +import { compare as comparePoints } from "../../helpers/vector2"; + import { getBrushPositionForTool, getDefaultShapeData, getUpdatedShapeData, - getStrokeSize, isShapeHovered, drawShape, + simplifyPoints, } from "../../helpers/drawing"; function MapDrawing({ @@ -24,11 +25,15 @@ function MapDrawing({ const canvasRef = useRef(); const containerRef = useRef(); - // const [brushPoints, setBrushPoints] = useState([]); const [isDrawing, setIsDrawing] = useState(false); const [drawingShape, setDrawingShape] = useState(null); const [pointerPosition, setPointerPosition] = useState({ x: -1, y: -1 }); + const shouldHover = + selectedTool === "erase" || + (selectedTool === "fog" && + (toolSettings.type === "toggle" || toolSettings.type === "remove")); + // Reset pointer position when tool changes useEffect(() => { setPointerPosition({ x: -1, y: -1 }); @@ -57,13 +62,12 @@ function MapDrawing({ const brushPosition = getBrushPositionForTool( position, selectedTool, + toolSettings, gridSize, shapes ); const commonShapeData = { id: shortid.generate(), - color: toolSettings && toolSettings.color, - blend: toolSettings && toolSettings.useBlending, }; if (selectedTool === "brush") { setDrawingShape({ @@ -71,6 +75,8 @@ function MapDrawing({ pathType: toolSettings.type, data: { points: [brushPosition] }, strokeWidth: toolSettings.type === "stroke" ? 1 : 0, + color: toolSettings && toolSettings.color, + blend: toolSettings && toolSettings.useBlending, ...commonShapeData, }); } else if (selectedTool === "shape") { @@ -79,6 +85,17 @@ function MapDrawing({ shapeType: toolSettings.type, data: getDefaultShapeData(toolSettings.type, brushPosition), strokeWidth: 0, + color: toolSettings && toolSettings.color, + blend: toolSettings && toolSettings.useBlending, + ...commonShapeData, + }); + } else if (selectedTool === "fog" && toolSettings.type === "add") { + setDrawingShape({ + type: "fog", + data: { points: [brushPosition] }, + strokeWidth: 0.1, + color: "black", + blend: true, // Blend while drawing ...commonShapeData, }); } @@ -90,7 +107,8 @@ function MapDrawing({ } const pointer = event.touches ? event.touches[0] : event; const position = getRelativePointerPosition(pointer); - if (selectedTool === "erase") { + // Set pointer position every frame for erase tool and fog + if (shouldHover) { setPointerPosition(position); } if (isDrawing) { @@ -98,18 +116,25 @@ function MapDrawing({ const brushPosition = getBrushPositionForTool( position, selectedTool, + toolSettings, gridSize, shapes ); if (selectedTool === "brush") { setDrawingShape((prevShape) => { const prevPoints = prevShape.data.points; - if (prevPoints[prevPoints.length - 1] === brushPosition) { - return prevPoints; + if ( + comparePoints( + prevPoints[prevPoints.length - 1], + brushPosition, + 0.001 + ) + ) { + return prevShape; } - const simplified = simplify( + const simplified = simplifyPoints( [...prevPoints, brushPosition], - getStrokeSize(drawingShape.strokeWidth, gridSize, 1, 1) * 0.1 + gridSize ); return { ...prevShape, @@ -125,6 +150,23 @@ function MapDrawing({ brushPosition ), })); + } else if (selectedTool === "fog" && toolSettings.type === "add") { + setDrawingShape((prevShape) => { + const prevPoints = prevShape.data.points; + if ( + comparePoints( + prevPoints[prevPoints.length - 1], + brushPosition, + 0.001 + ) + ) { + return prevShape; + } + return { + ...prevShape, + data: { points: [...prevPoints, brushPosition] }, + }; + }); } } } @@ -140,6 +182,15 @@ function MapDrawing({ } } else if (selectedTool === "shape") { onShapeAdd(drawingShape); + } else if (selectedTool === "fog" && toolSettings.type === "add") { + if (drawingShape.data.points.length > 1) { + const shape = { + ...drawingShape, + data: { points: simplifyPoints(drawingShape.data.points, gridSize) }, + blend: false, + }; + onShapeAdd(shape); + } } setDrawingShape(null); @@ -181,13 +232,22 @@ function MapDrawing({ context.clearRect(0, 0, width, height); let hoveredShape = null; for (let shape of shapes) { - // Detect hover - if (selectedTool === "erase") { + if (shouldHover) { if (isShapeHovered(shape, context, pointerPosition, width, height)) { hoveredShape = shape; } } - drawShape(shape, context, gridSize, width, height); + if (selectedTool === "fog") { + drawShape( + { ...shape, blend: true }, + context, + gridSize, + width, + height + ); + } else { + drawShape(shape, context, gridSize, width, height); + } } if (drawingShape) { drawShape(drawingShape, context, gridSize, width, height); diff --git a/src/helpers/drawing.js b/src/helpers/drawing.js index e0b8f5f..dbed9e8 100644 --- a/src/helpers/drawing.js +++ b/src/helpers/drawing.js @@ -1,9 +1,17 @@ +import simplify from "simplify-js"; + import * as Vector2 from "./vector2"; import { toDegrees } from "./shared"; import colors from "./colors"; const snappingThreshold = 1 / 5; -export function getBrushPositionForTool(brushPosition, tool, gridSize, shapes) { +export function getBrushPositionForTool( + brushPosition, + tool, + toolSettings, + gridSize, + shapes +) { let position = brushPosition; if (tool === "shape") { const snapped = Vector2.roundTo(position, gridSize); @@ -13,6 +21,40 @@ export function getBrushPositionForTool(brushPosition, tool, gridSize, shapes) { position = snapped; } } + if (tool === "fog" && toolSettings.type === "add") { + if (toolSettings.useGridSnapping) { + position = Vector2.roundTo(position, gridSize); + } + if (toolSettings.useEdgeSnapping) { + const minGrid = Vector2.min(gridSize); + let closestDistance = Number.MAX_VALUE; + let closestPosition = position; + // Find the closest point on all fog shapes + for (let shape of shapes) { + if (shape.type === "fog") { + const points = shape.data.points; + const isInShape = Vector2.pointInPolygon(position, points); + + // Find the closest point to each line of the shape + for (let i = 0; i < points.length; i++) { + const a = points[i]; + // Wrap around points to the start to account for closed shape + const b = points[(i + 1) % points.length]; + const distanceToLine = Vector2.distanceToLine(position, a, b); + const isCloseToShape = distanceToLine < minGrid * snappingThreshold; + if ( + (isInShape || isCloseToShape) && + distanceToLine < closestDistance + ) { + closestPosition = Vector2.closestPointOnLine(position, a, b); + closestDistance = distanceToLine; + } + } + } + } + position = closestPosition; + } + } return position; } @@ -86,6 +128,7 @@ export function getStrokeSize(multiplier, gridSize, canvasWidth, canvasHeight) { export function shapeHasFill(shape) { return ( + shape.type === "fog" || shape.type === "shape" || (shape.type === "path" && shape.pathType === "fill") ); @@ -169,6 +212,17 @@ export function triangleToPath(points, canvasWidth, canvasHeight) { return path; } +export function fogToPath(points, canvasWidth, canvasHeight) { + const path = new Path2D(); + path.moveTo(points[0].x * canvasWidth, points[0].y * canvasHeight); + for (let point of points.slice(1)) { + path.lineTo(point.x * canvasWidth, point.y * canvasHeight); + } + path.closePath(); + + return path; +} + export function shapeToPath(shape, canvasWidth, canvasHeight) { const data = shape.data; if (shape.type === "path") { @@ -199,6 +253,8 @@ export function shapeToPath(shape, canvasWidth, canvasHeight) { } else if (shape.shapeType === "triangle") { return triangleToPath(data.points, canvasWidth, canvasHeight); } + } else if (shape.type === "fog") { + return fogToPath(shape.data.points, canvasWidth, canvasHeight); } } @@ -247,3 +303,8 @@ export function drawShape(shape, context, gridSize, canvasWidth, canvasHeight) { context.fill(path); } } + +const defaultSimplifySize = 1 / 100; +export function simplifyPoints(points, gridSize) { + return simplify(points, Vector2.min(gridSize) * defaultSimplifySize); +} diff --git a/src/helpers/vector2.js b/src/helpers/vector2.js index 3e3e7eb..56e529c 100644 --- a/src/helpers/vector2.js +++ b/src/helpers/vector2.js @@ -77,3 +77,68 @@ export function roundTo(p, to) { y: roundToNumber(p.y, to.y), }; } + +// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d +export function distanceToLine(p, a, b) { + const pa = subtract(p, a); + const ba = subtract(b, a); + const h = Math.min(Math.max(dot(pa, ba) / dot(ba, ba), 0), 1); + return length(subtract(pa, multiply(ba, h))); +} + +export function closestPointOnLine(p, a, b) { + const pa = subtract(p, a); + const ba = subtract(b, a); + const h = dot(pa, ba) / lengthSquared(ba); + return add(a, multiply(ba, h)); +} + +export function getBounds(points) { + let minX = Number.MAX_VALUE; + let maxX = Number.MIN_VALUE; + let minY = Number.MAX_VALUE; + let maxY = Number.MIN_VALUE; + for (let point of points) { + minX = point.x < minX ? point.x : minX; + maxX = point.x > maxX ? point.x : maxX; + minY = point.y < minY ? point.y : minY; + maxY = point.y > maxY ? point.y : maxY; + } + return { minX, maxX, minY, maxY }; +} + +// Check bounds then use ray casting algorithm +// https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm +// https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon/2922778 +export function pointInPolygon(p, points) { + const { minX, maxX, minY, maxY } = getBounds(points); + if (p.x < minX || p.x > maxX || p.y < minY || p.y > maxY) { + return false; + } + + let isInside = false; + for (let i = 0, j = points.length - 1; i < points.length; j = i++) { + const a = points[i].y > p.y; + const b = points[j].y > p.y; + if ( + a !== b && + p.x < + ((points[j].x - points[i].x) * (p.y - points[i].y)) / + (points[j].y - points[i].y) + + points[i].x + ) { + isInside = !isInside; + } + } + return isInside; +} + +/** + * Returns true if a the distance between a and b is under threshold + * @param {Vector2} a + * @param {Vector2} b + * @param {number} threshold + */ +export function compare(a, b, threshold) { + return lengthSquared(subtract(a, b)) < threshold * threshold; +}