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 { compare as comparePoints } from "../../helpers/vector2";
import {
getBrushPositionForTool,
getDefaultShapeData,
@ -13,6 +12,8 @@ import {
getRelativePointerPosition,
} from "../../helpers/drawing";
import MapInteractionContext from "../../contexts/MapInteractionContext";
function MapDrawing({
width,
height,
@ -36,6 +37,8 @@ function MapDrawing({
selectedTool === "shape" ||
selectedTool === "erase";
const { scaleRef } = useContext(MapInteractionContext);
// Reset pointer position when tool changes
useEffect(() => {
setPointerPosition({ x: -1, y: -1 });
@ -128,7 +131,8 @@ function MapDrawing({
}
const simplified = simplifyPoints(
[...prevPoints, brushPosition],
gridSize
gridSize,
scaleRef.current
);
return {
...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 { compare as comparePoints } from "../../helpers/vector2";
import {
getBrushPositionForTool,
isShapeHovered,
@ -11,6 +10,8 @@ import {
getRelativePointerPosition,
} from "../../helpers/drawing";
import MapInteractionContext from "../../contexts/MapInteractionContext";
function MapFog({
width,
height,
@ -32,6 +33,8 @@ function MapFog({
isEditing &&
(toolSettings.type === "toggle" || toolSettings.type === "remove");
const { scaleRef } = useContext(MapInteractionContext);
// Reset pointer position when tool changes
useEffect(() => {
setPointerPosition({ x: -1, y: -1 });
@ -65,7 +68,6 @@ function MapFog({
color: "black",
blend: true, // Blend while drawing
id: shortid.generate(),
fogType: toolSettings.useGridSnapping ? "sharp" : "smooth",
});
}
}
@ -124,7 +126,14 @@ function MapFog({
if (drawingShape.data.points.length > 1) {
const shape = {
...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,
};
onShapeAdd(shape);

View File

@ -2,6 +2,8 @@ import React, { useRef, useEffect } from "react";
import { Box } from "theme-ui";
import interact from "interactjs";
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
const zoomSpeed = -0.005;
const minZoom = 0.1;
const maxZoom = 5;
@ -134,7 +136,14 @@ function MapInteraction({ map, aspectRatio, isEnabled, children, controls }) {
paddingBottom: `${(1 / aspectRatio) * 100}%`,
}}
/>
<MapInteractionProvider
value={{
translateRef: mapTranslateRef,
scaleRef: mapScaleRef,
}}
>
{children}
</MapInteractionProvider>
</Box>
</Box>
{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") {
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 {
distance: distanceToLine,
point: pointOnLine,
} = Vector2.distanceToLine(position, a, b);
const isCloseToShape = distanceToLine < minGrid * snappingThreshold;
if (
(isInShape || isCloseToShape) &&
distanceToLine < closestDistance
) {
closestPosition = Vector2.closestPointOnLine(position, a, b);
closestPosition = pointOnLine;
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) {
const path = new Path2D();
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);
// Draw a smooth curve between the points
for (let i = 1; i < points.length - 2; i++) {
const pointScaled = Vector2.multiply(points[i], {
const quadraticPoints = pointsToQuadraticBezier(points);
for (let quadPoint of quadraticPoints) {
const pointScaled = Vector2.multiply(quadPoint.end, {
x: canvasWidth,
y: canvasHeight,
});
const nextPointScaled = Vector2.multiply(points[i + 1], {
const controlScaled = Vector2.multiply(quadPoint.controlPoint, {
x: canvasWidth,
y: canvasHeight,
});
var xc = (pointScaled.x + nextPointScaled.x) / 2;
var yc = (pointScaled.y + nextPointScaled.y) / 2;
path.quadraticCurveTo(pointScaled.x, pointScaled.y, xc, yc);
}
// 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
controlScaled.x,
controlScaled.y,
pointScaled.x,
pointScaled.y
);
}
if (close) {
path.closePath();
@ -265,14 +288,6 @@ export function shapeToPath(shape, canvasWidth, canvasHeight) {
return pointsToPathSharp(data.points, true, canvasWidth, canvasHeight);
}
} else if (shape.type === "fog") {
if (shape.fogType === "smooth") {
return pointsToPathSmooth(
shape.data.points,
true,
canvasWidth,
canvasHeight
);
} else if (shape.fogType === "sharp") {
return pointsToPathSharp(
shape.data.points,
true,
@ -281,7 +296,6 @@ export function shapeToPath(shape, canvasWidth, canvasHeight) {
);
}
}
}
export function isShapeHovered(
shape,
@ -330,8 +344,11 @@ export function drawShape(shape, context, gridSize, canvasWidth, canvasHeight) {
}
const defaultSimplifySize = 1 / 100;
export function simplifyPoints(points, gridSize) {
return simplify(points, Vector2.min(gridSize) * defaultSimplifySize);
export function simplifyPoints(points, gridSize, scale) {
return simplify(
points,
(Vector2.min(gridSize) * defaultSimplifySize) / scale
);
}
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
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)));
const distance = length(subtract(pa, multiply(ba, h)));
const point = add(a, multiply(ba, h));
return { distance, point };
}
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));
// TODO: Fix the robustness of this to allow smoothing on fog layers
// https://www.shadertoy.com/view/MlKcDD
export function distanceToQuadraticBezier(pos, A, B, C) {
let distance = 0;
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) {