Added map context to scale simplification by map scale

Added distance to quadratic functions to vector
This commit is contained in:
Mitchell McCaffrey 2020-04-29 18:21:44 +10:00
parent 3e5a80e7d1
commit 9975f564fa
6 changed files with 170 additions and 48 deletions

View File

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

View File

@ -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);

View File

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

View 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;

View File

@ -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) {

View File

@ -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) {