Files
grungnet/src/helpers/drawing.js
Mitchell McCaffrey 9975f564fa Added map context to scale simplification by map scale
Added distance to quadratic functions to vector
2020-04-29 18:21:44 +10:00

362 lines
9.6 KiB
JavaScript

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,
toolSettings,
gridSize,
shapes
) {
let position = brushPosition;
if (tool === "shape") {
const snapped = Vector2.roundTo(position, gridSize);
const minGrid = Vector2.min(gridSize);
const distance = Vector2.length(Vector2.subtract(snapped, position));
if (distance < minGrid * snappingThreshold) {
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 {
distance: distanceToLine,
point: pointOnLine,
} = Vector2.distanceToLine(position, a, b);
const isCloseToShape = distanceToLine < minGrid * snappingThreshold;
if (
(isInShape || isCloseToShape) &&
distanceToLine < closestDistance
) {
closestPosition = pointOnLine;
closestDistance = distanceToLine;
}
}
}
}
position = closestPosition;
}
}
return position;
}
export function getDefaultShapeData(type, brushPosition) {
if (type === "circle") {
return { x: brushPosition.x, y: brushPosition.y, radius: 0 };
} else if (type === "rectangle") {
return {
x: brushPosition.x,
y: brushPosition.y,
width: 0,
height: 0,
};
} else if (type === "triangle") {
return {
points: [
{ x: brushPosition.x, y: brushPosition.y },
{ x: brushPosition.x, y: brushPosition.y },
{ x: brushPosition.x, y: brushPosition.y },
],
};
}
}
export function getGridScale(gridSize) {
if (gridSize.x < gridSize.y) {
return { x: gridSize.y / gridSize.x, y: 1 };
} else if (gridSize.y < gridSize.x) {
return { x: 1, y: gridSize.x / gridSize.y };
} else {
return { x: 1, y: 1 };
}
}
export function getUpdatedShapeData(type, data, brushPosition, gridSize) {
const gridScale = getGridScale(gridSize);
if (type === "circle") {
const dif = Vector2.subtract(brushPosition, {
x: data.x,
y: data.y,
});
const scaled = Vector2.multiply(dif, gridScale);
const distance = Vector2.length(scaled);
return {
...data,
radius: distance,
};
} else if (type === "rectangle") {
const dif = Vector2.subtract(brushPosition, { x: data.x, y: data.y });
return {
...data,
width: dif.x,
height: dif.y,
};
} else if (type === "triangle") {
const points = data.points;
const dif = Vector2.subtract(brushPosition, points[0]);
// Scale the distance by the grid scale then unscale before adding
const scaled = Vector2.multiply(dif, gridScale);
const length = Vector2.length(scaled);
const direction = Vector2.normalize(scaled);
// Get the angle for a triangle who's width is the same as it's length
const angle = Math.atan(length / 2 / length);
const sideLength = length / Math.cos(angle);
const leftDir = Vector2.rotateDirection(direction, toDegrees(angle));
const rightDir = Vector2.rotateDirection(direction, -toDegrees(angle));
const leftDirUnscaled = Vector2.divide(leftDir, gridScale);
const rightDirUnscaled = Vector2.divide(rightDir, gridScale);
return {
points: [
points[0],
Vector2.add(Vector2.multiply(leftDirUnscaled, sideLength), points[0]),
Vector2.add(Vector2.multiply(rightDirUnscaled, sideLength), points[0]),
],
};
}
}
const defaultStrokeSize = 1 / 10;
export function getStrokeSize(multiplier, gridSize, canvasWidth, canvasHeight) {
const gridPixelSize = Vector2.multiply(gridSize, {
x: canvasWidth,
y: canvasHeight,
});
return Vector2.min(gridPixelSize) * defaultStrokeSize * multiplier;
}
export function shapeHasFill(shape) {
return (
shape.type === "fog" ||
shape.type === "shape" ||
(shape.type === "path" && shape.pathType === "fill")
);
}
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) {
return path;
}
path.moveTo(points[0].x * canvasWidth, points[0].y * canvasHeight);
const quadraticPoints = pointsToQuadraticBezier(points);
for (let quadPoint of quadraticPoints) {
const pointScaled = Vector2.multiply(quadPoint.end, {
x: canvasWidth,
y: canvasHeight,
});
const controlScaled = Vector2.multiply(quadPoint.controlPoint, {
x: canvasWidth,
y: canvasHeight,
});
path.quadraticCurveTo(
controlScaled.x,
controlScaled.y,
pointScaled.x,
pointScaled.y
);
}
if (close) {
path.closePath();
}
return path;
}
export function pointsToPathSharp(points, close, 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);
}
if (close) {
path.closePath();
}
return path;
}
export function circleToPath(x, y, radius, canvasWidth, canvasHeight) {
const path = new Path2D();
const minSide = canvasWidth < canvasHeight ? canvasWidth : canvasHeight;
path.arc(
x * canvasWidth,
y * canvasHeight,
radius * minSide,
0,
2 * Math.PI,
true
);
return path;
}
export function rectangleToPath(
x,
y,
width,
height,
canvasWidth,
canvasHeight
) {
const path = new Path2D();
path.rect(
x * canvasWidth,
y * canvasHeight,
width * canvasWidth,
height * canvasHeight
);
return path;
}
export function shapeToPath(shape, canvasWidth, canvasHeight) {
const data = shape.data;
if (shape.type === "path") {
return pointsToPathSmooth(
data.points,
shape.pathType === "fill",
canvasWidth,
canvasHeight
);
} else if (shape.type === "shape") {
if (shape.shapeType === "circle") {
return circleToPath(
data.x,
data.y,
data.radius,
canvasWidth,
canvasHeight
);
} else if (shape.shapeType === "rectangle") {
return rectangleToPath(
data.x,
data.y,
data.width,
data.height,
canvasWidth,
canvasHeight
);
} else if (shape.shapeType === "triangle") {
return pointsToPathSharp(data.points, true, canvasWidth, canvasHeight);
}
} else if (shape.type === "fog") {
return pointsToPathSharp(
shape.data.points,
true,
canvasWidth,
canvasHeight
);
}
}
export function isShapeHovered(
shape,
context,
hoverPosition,
canvasWidth,
canvasHeight
) {
const path = shapeToPath(shape, canvasWidth, canvasHeight);
if (shapeHasFill(shape)) {
return context.isPointInPath(
path,
hoverPosition.x * canvasWidth,
hoverPosition.y * canvasHeight
);
} else {
return context.isPointInStroke(
path,
hoverPosition.x * canvasWidth,
hoverPosition.y * canvasHeight
);
}
}
export function drawShape(shape, context, gridSize, canvasWidth, canvasHeight) {
const path = shapeToPath(shape, canvasWidth, canvasHeight);
const color = colors[shape.color] || shape.color;
const fill = shapeHasFill(shape);
context.globalAlpha = shape.blend ? 0.5 : 1.0;
context.fillStyle = color;
context.strokeStyle = color;
if (shape.strokeWidth > 0) {
context.lineCap = "round";
context.lineWidth = getStrokeSize(
shape.strokeWidth,
gridSize,
canvasWidth,
canvasHeight
);
context.stroke(path);
}
if (fill) {
context.fill(path);
}
}
const defaultSimplifySize = 1 / 100;
export function simplifyPoints(points, gridSize, scale) {
return simplify(
points,
(Vector2.min(gridSize) * defaultSimplifySize) / scale
);
}
export function getRelativePointerPosition(event, container) {
if (container) {
const containerRect = container.getBoundingClientRect();
const x = (event.clientX - containerRect.x) / containerRect.width;
const y = (event.clientY - containerRect.y) / containerRect.height;
return { x, y };
}
}