Added fog edge snapping
This commit is contained in:
parent
c73b099567
commit
b34a7df443
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user