Added map context to scale simplification by map scale
Added distance to quadratic functions to vector
This commit is contained in:
parent
3e5a80e7d1
commit
9975f564fa
@ -1,8 +1,7 @@
|
|||||||
import React, { useRef, useEffect, useState } from "react";
|
import React, { useRef, useEffect, useState, useContext } from "react";
|
||||||
import shortid from "shortid";
|
import shortid from "shortid";
|
||||||
|
|
||||||
import { compare as comparePoints } from "../../helpers/vector2";
|
import { compare as comparePoints } from "../../helpers/vector2";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getBrushPositionForTool,
|
getBrushPositionForTool,
|
||||||
getDefaultShapeData,
|
getDefaultShapeData,
|
||||||
@ -13,6 +12,8 @@ import {
|
|||||||
getRelativePointerPosition,
|
getRelativePointerPosition,
|
||||||
} from "../../helpers/drawing";
|
} from "../../helpers/drawing";
|
||||||
|
|
||||||
|
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||||
|
|
||||||
function MapDrawing({
|
function MapDrawing({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@ -36,6 +37,8 @@ function MapDrawing({
|
|||||||
selectedTool === "shape" ||
|
selectedTool === "shape" ||
|
||||||
selectedTool === "erase";
|
selectedTool === "erase";
|
||||||
|
|
||||||
|
const { scaleRef } = useContext(MapInteractionContext);
|
||||||
|
|
||||||
// Reset pointer position when tool changes
|
// Reset pointer position when tool changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPointerPosition({ x: -1, y: -1 });
|
setPointerPosition({ x: -1, y: -1 });
|
||||||
@ -128,7 +131,8 @@ function MapDrawing({
|
|||||||
}
|
}
|
||||||
const simplified = simplifyPoints(
|
const simplified = simplifyPoints(
|
||||||
[...prevPoints, brushPosition],
|
[...prevPoints, brushPosition],
|
||||||
gridSize
|
gridSize,
|
||||||
|
scaleRef.current
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
...prevShape,
|
...prevShape,
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import React, { useRef, useEffect, useState } from "react";
|
import React, { useRef, useEffect, useState, useContext } from "react";
|
||||||
import shortid from "shortid";
|
import shortid from "shortid";
|
||||||
|
|
||||||
import { compare as comparePoints } from "../../helpers/vector2";
|
import { compare as comparePoints } from "../../helpers/vector2";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getBrushPositionForTool,
|
getBrushPositionForTool,
|
||||||
isShapeHovered,
|
isShapeHovered,
|
||||||
@ -11,6 +10,8 @@ import {
|
|||||||
getRelativePointerPosition,
|
getRelativePointerPosition,
|
||||||
} from "../../helpers/drawing";
|
} from "../../helpers/drawing";
|
||||||
|
|
||||||
|
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||||
|
|
||||||
function MapFog({
|
function MapFog({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@ -32,6 +33,8 @@ function MapFog({
|
|||||||
isEditing &&
|
isEditing &&
|
||||||
(toolSettings.type === "toggle" || toolSettings.type === "remove");
|
(toolSettings.type === "toggle" || toolSettings.type === "remove");
|
||||||
|
|
||||||
|
const { scaleRef } = useContext(MapInteractionContext);
|
||||||
|
|
||||||
// Reset pointer position when tool changes
|
// Reset pointer position when tool changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPointerPosition({ x: -1, y: -1 });
|
setPointerPosition({ x: -1, y: -1 });
|
||||||
@ -65,7 +68,6 @@ function MapFog({
|
|||||||
color: "black",
|
color: "black",
|
||||||
blend: true, // Blend while drawing
|
blend: true, // Blend while drawing
|
||||||
id: shortid.generate(),
|
id: shortid.generate(),
|
||||||
fogType: toolSettings.useGridSnapping ? "sharp" : "smooth",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,7 +126,14 @@ function MapFog({
|
|||||||
if (drawingShape.data.points.length > 1) {
|
if (drawingShape.data.points.length > 1) {
|
||||||
const shape = {
|
const shape = {
|
||||||
...drawingShape,
|
...drawingShape,
|
||||||
data: { points: simplifyPoints(drawingShape.data.points, gridSize) },
|
data: {
|
||||||
|
points: simplifyPoints(
|
||||||
|
drawingShape.data.points,
|
||||||
|
gridSize,
|
||||||
|
// Downscale fog as smoothing doesn't currently work with edge snapping
|
||||||
|
scaleRef.current / 2
|
||||||
|
),
|
||||||
|
},
|
||||||
blend: false,
|
blend: false,
|
||||||
};
|
};
|
||||||
onShapeAdd(shape);
|
onShapeAdd(shape);
|
||||||
|
@ -2,6 +2,8 @@ import React, { useRef, useEffect } from "react";
|
|||||||
import { Box } from "theme-ui";
|
import { Box } from "theme-ui";
|
||||||
import interact from "interactjs";
|
import interact from "interactjs";
|
||||||
|
|
||||||
|
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
|
||||||
|
|
||||||
const zoomSpeed = -0.005;
|
const zoomSpeed = -0.005;
|
||||||
const minZoom = 0.1;
|
const minZoom = 0.1;
|
||||||
const maxZoom = 5;
|
const maxZoom = 5;
|
||||||
@ -134,7 +136,14 @@ function MapInteraction({ map, aspectRatio, isEnabled, children, controls }) {
|
|||||||
paddingBottom: `${(1 / aspectRatio) * 100}%`,
|
paddingBottom: `${(1 / aspectRatio) * 100}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{children}
|
<MapInteractionProvider
|
||||||
|
value={{
|
||||||
|
translateRef: mapTranslateRef,
|
||||||
|
scaleRef: mapScaleRef,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MapInteractionProvider>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{controls}
|
{controls}
|
||||||
|
9
src/contexts/MapInteractionContext.js
Normal file
9
src/contexts/MapInteractionContext.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const MapInteractionContext = React.createContext({
|
||||||
|
translateRef: null,
|
||||||
|
scaleRef: null,
|
||||||
|
});
|
||||||
|
export const MapInteractionProvider = MapInteractionContext.Provider;
|
||||||
|
|
||||||
|
export default MapInteractionContext;
|
@ -34,19 +34,22 @@ export function getBrushPositionForTool(
|
|||||||
if (shape.type === "fog") {
|
if (shape.type === "fog") {
|
||||||
const points = shape.data.points;
|
const points = shape.data.points;
|
||||||
const isInShape = Vector2.pointInPolygon(position, points);
|
const isInShape = Vector2.pointInPolygon(position, points);
|
||||||
|
|
||||||
// Find the closest point to each line of the shape
|
// Find the closest point to each line of the shape
|
||||||
for (let i = 0; i < points.length; i++) {
|
for (let i = 0; i < points.length; i++) {
|
||||||
const a = points[i];
|
const a = points[i];
|
||||||
// Wrap around points to the start to account for closed shape
|
// Wrap around points to the start to account for closed shape
|
||||||
const b = points[(i + 1) % points.length];
|
const b = points[(i + 1) % points.length];
|
||||||
const distanceToLine = Vector2.distanceToLine(position, a, b);
|
|
||||||
|
const {
|
||||||
|
distance: distanceToLine,
|
||||||
|
point: pointOnLine,
|
||||||
|
} = Vector2.distanceToLine(position, a, b);
|
||||||
const isCloseToShape = distanceToLine < minGrid * snappingThreshold;
|
const isCloseToShape = distanceToLine < minGrid * snappingThreshold;
|
||||||
if (
|
if (
|
||||||
(isInShape || isCloseToShape) &&
|
(isInShape || isCloseToShape) &&
|
||||||
distanceToLine < closestDistance
|
distanceToLine < closestDistance
|
||||||
) {
|
) {
|
||||||
closestPosition = Vector2.closestPointOnLine(position, a, b);
|
closestPosition = pointOnLine;
|
||||||
closestDistance = distanceToLine;
|
closestDistance = distanceToLine;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,6 +157,30 @@ export function shapeHasFill(shape) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pointsToQuadraticBezier(points) {
|
||||||
|
const quadraticPoints = [];
|
||||||
|
|
||||||
|
// Draw a smooth curve between the points where each control point
|
||||||
|
// is the current point in the array and the next point is the center of
|
||||||
|
// the current point and the next point
|
||||||
|
for (let i = 1; i < points.length - 2; i++) {
|
||||||
|
const start = points[i - 1];
|
||||||
|
const controlPoint = points[i];
|
||||||
|
const next = points[i + 1];
|
||||||
|
const end = Vector2.divide(Vector2.add(controlPoint, next), 2);
|
||||||
|
|
||||||
|
quadraticPoints.push({ start, controlPoint, end });
|
||||||
|
}
|
||||||
|
// Curve through the last two points
|
||||||
|
quadraticPoints.push({
|
||||||
|
start: points[points.length - 2],
|
||||||
|
controlPoint: points[points.length - 1],
|
||||||
|
end: points[points.length - 1],
|
||||||
|
});
|
||||||
|
|
||||||
|
return quadraticPoints;
|
||||||
|
}
|
||||||
|
|
||||||
export function pointsToPathSmooth(points, close, canvasWidth, canvasHeight) {
|
export function pointsToPathSmooth(points, close, canvasWidth, canvasHeight) {
|
||||||
const path = new Path2D();
|
const path = new Path2D();
|
||||||
if (points.length < 2) {
|
if (points.length < 2) {
|
||||||
@ -161,27 +188,23 @@ export function pointsToPathSmooth(points, close, canvasWidth, canvasHeight) {
|
|||||||
}
|
}
|
||||||
path.moveTo(points[0].x * canvasWidth, points[0].y * canvasHeight);
|
path.moveTo(points[0].x * canvasWidth, points[0].y * canvasHeight);
|
||||||
|
|
||||||
// Draw a smooth curve between the points
|
const quadraticPoints = pointsToQuadraticBezier(points);
|
||||||
for (let i = 1; i < points.length - 2; i++) {
|
for (let quadPoint of quadraticPoints) {
|
||||||
const pointScaled = Vector2.multiply(points[i], {
|
const pointScaled = Vector2.multiply(quadPoint.end, {
|
||||||
x: canvasWidth,
|
x: canvasWidth,
|
||||||
y: canvasHeight,
|
y: canvasHeight,
|
||||||
});
|
});
|
||||||
const nextPointScaled = Vector2.multiply(points[i + 1], {
|
const controlScaled = Vector2.multiply(quadPoint.controlPoint, {
|
||||||
x: canvasWidth,
|
x: canvasWidth,
|
||||||
y: canvasHeight,
|
y: canvasHeight,
|
||||||
});
|
});
|
||||||
var xc = (pointScaled.x + nextPointScaled.x) / 2;
|
path.quadraticCurveTo(
|
||||||
var yc = (pointScaled.y + nextPointScaled.y) / 2;
|
controlScaled.x,
|
||||||
path.quadraticCurveTo(pointScaled.x, pointScaled.y, xc, yc);
|
controlScaled.y,
|
||||||
|
pointScaled.x,
|
||||||
|
pointScaled.y
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Curve through the last two points
|
|
||||||
path.quadraticCurveTo(
|
|
||||||
points[points.length - 2].x * canvasWidth,
|
|
||||||
points[points.length - 2].y * canvasHeight,
|
|
||||||
points[points.length - 1].x * canvasWidth,
|
|
||||||
points[points.length - 1].y * canvasHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
if (close) {
|
if (close) {
|
||||||
path.closePath();
|
path.closePath();
|
||||||
@ -265,21 +288,12 @@ export function shapeToPath(shape, canvasWidth, canvasHeight) {
|
|||||||
return pointsToPathSharp(data.points, true, canvasWidth, canvasHeight);
|
return pointsToPathSharp(data.points, true, canvasWidth, canvasHeight);
|
||||||
}
|
}
|
||||||
} else if (shape.type === "fog") {
|
} else if (shape.type === "fog") {
|
||||||
if (shape.fogType === "smooth") {
|
return pointsToPathSharp(
|
||||||
return pointsToPathSmooth(
|
shape.data.points,
|
||||||
shape.data.points,
|
true,
|
||||||
true,
|
canvasWidth,
|
||||||
canvasWidth,
|
canvasHeight
|
||||||
canvasHeight
|
);
|
||||||
);
|
|
||||||
} else if (shape.fogType === "sharp") {
|
|
||||||
return pointsToPathSharp(
|
|
||||||
shape.data.points,
|
|
||||||
true,
|
|
||||||
canvasWidth,
|
|
||||||
canvasHeight
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,8 +344,11 @@ export function drawShape(shape, context, gridSize, canvasWidth, canvasHeight) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultSimplifySize = 1 / 100;
|
const defaultSimplifySize = 1 / 100;
|
||||||
export function simplifyPoints(points, gridSize) {
|
export function simplifyPoints(points, gridSize, scale) {
|
||||||
return simplify(points, Vector2.min(gridSize) * defaultSimplifySize);
|
return simplify(
|
||||||
|
points,
|
||||||
|
(Vector2.min(gridSize) * defaultSimplifySize) / scale
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRelativePointerPosition(event, container) {
|
export function getRelativePointerPosition(event, container) {
|
||||||
|
@ -78,19 +78,93 @@ export function roundTo(p, to) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sign(a) {
|
||||||
|
return { x: Math.sign(a.x), y: Math.sign(a.y) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function abs(a) {
|
||||||
|
return { x: Math.abs(a.x), y: Math.abs(a.y) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pow(a, b) {
|
||||||
|
if (typeof b === "number") {
|
||||||
|
return { x: Math.pow(a.x, b), y: Math.pow(a.y, b) };
|
||||||
|
} else {
|
||||||
|
return { x: Math.pow(a.x, b.x), y: Math.pow(a.y, b.y) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dot2(a) {
|
||||||
|
return dot(a, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clamp(a, min, max) {
|
||||||
|
return {
|
||||||
|
x: Math.min(Math.max(a.x, min), max),
|
||||||
|
y: Math.min(Math.max(a.y, min), max),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d
|
// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d
|
||||||
export function distanceToLine(p, a, b) {
|
export function distanceToLine(p, a, b) {
|
||||||
const pa = subtract(p, a);
|
const pa = subtract(p, a);
|
||||||
const ba = subtract(b, a);
|
const ba = subtract(b, a);
|
||||||
const h = Math.min(Math.max(dot(pa, ba) / dot(ba, ba), 0), 1);
|
const h = Math.min(Math.max(dot(pa, ba) / dot(ba, ba), 0), 1);
|
||||||
return length(subtract(pa, multiply(ba, h)));
|
const distance = length(subtract(pa, multiply(ba, h)));
|
||||||
|
const point = add(a, multiply(ba, h));
|
||||||
|
return { distance, point };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closestPointOnLine(p, a, b) {
|
// TODO: Fix the robustness of this to allow smoothing on fog layers
|
||||||
const pa = subtract(p, a);
|
// https://www.shadertoy.com/view/MlKcDD
|
||||||
const ba = subtract(b, a);
|
export function distanceToQuadraticBezier(pos, A, B, C) {
|
||||||
const h = dot(pa, ba) / lengthSquared(ba);
|
let distance = 0;
|
||||||
return add(a, multiply(ba, h));
|
let point = { x: pos.x, y: pos.y };
|
||||||
|
|
||||||
|
const a = subtract(B, A);
|
||||||
|
const b = add(subtract(A, multiply(B, 2)), C);
|
||||||
|
const c = multiply(a, 2);
|
||||||
|
const d = subtract(A, pos);
|
||||||
|
|
||||||
|
// Solve cubic roots to find closest points
|
||||||
|
const kk = 1 / dot(b, b);
|
||||||
|
const kx = kk * dot(a, b);
|
||||||
|
const ky = (kk * (2 * dot(a, a) + dot(d, b))) / 3;
|
||||||
|
const kz = kk * dot(d, a);
|
||||||
|
|
||||||
|
const p = ky - kx * kx;
|
||||||
|
const p3 = p * p * p;
|
||||||
|
const q = kx * (2 * kx * kx - 3 * ky) + kz;
|
||||||
|
let h = q * q + 4 * p3;
|
||||||
|
|
||||||
|
if (h >= 0) {
|
||||||
|
// 1 root
|
||||||
|
h = Math.sqrt(h);
|
||||||
|
const x = divide(subtract({ x: h, y: -h }, q), 2);
|
||||||
|
const uv = multiply(sign(x), pow(abs(x), 1 / 3));
|
||||||
|
const t = Math.min(Math.max(uv.x + uv.y - kx, 0), 1);
|
||||||
|
point = add(A, multiply(add(c, multiply(b, t)), t));
|
||||||
|
distance = dot2(add(d, multiply(add(c, multiply(b, t)), t)));
|
||||||
|
} else {
|
||||||
|
// 3 roots but ignore the 3rd one as it will never be closest
|
||||||
|
// https://www.shadertoy.com/view/MdXBzB
|
||||||
|
const z = Math.sqrt(-p);
|
||||||
|
const v = Math.acos(q / (p * z * 2)) / 3;
|
||||||
|
const m = Math.cos(v);
|
||||||
|
const n = Math.sin(v) * 1.732050808;
|
||||||
|
|
||||||
|
const t = clamp(subtract(multiply({ x: m + m, y: -n - m }, z), kx), 0, 1);
|
||||||
|
const d1 = dot2(add(d, multiply(add(c, multiply(b, t.x)), t.x)));
|
||||||
|
const d2 = dot2(add(d, multiply(add(c, multiply(b, t.y)), t.y)));
|
||||||
|
distance = Math.min(d1, d2);
|
||||||
|
if (d1 < d2) {
|
||||||
|
point = add(d, multiply(add(c, multiply(b, t.x)), t.x));
|
||||||
|
} else {
|
||||||
|
point = add(d, multiply(add(c, multiply(b, t.y)), t.y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { distance: Math.sqrt(distance), point: point };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBounds(points) {
|
export function getBounds(points) {
|
||||||
|
Loading…
Reference in New Issue
Block a user