Added shape drawing and more vector helper functions
This commit is contained in:
parent
e08dc60f5f
commit
81f84d8a19
@ -60,7 +60,7 @@ function Map({
|
||||
color: "red",
|
||||
type: "rectangle",
|
||||
useBlending: true,
|
||||
useGridSnapping: true,
|
||||
useGridSnapping: false,
|
||||
},
|
||||
});
|
||||
function handleToolSettingChange(tool, change) {
|
||||
|
@ -3,7 +3,13 @@ import simplify from "simplify-js";
|
||||
import shortid from "shortid";
|
||||
|
||||
import colors from "../../helpers/colors";
|
||||
import { snapPositionToGrid } from "../../helpers/shared";
|
||||
import {
|
||||
getBrushPositionForTool,
|
||||
getDefaultShapeData,
|
||||
getUpdatedShapeData,
|
||||
getStrokeSize,
|
||||
shapeHasFill,
|
||||
} from "../../helpers/drawing";
|
||||
|
||||
function MapDrawing({
|
||||
width,
|
||||
@ -18,12 +24,9 @@ function MapDrawing({
|
||||
const canvasRef = useRef();
|
||||
const containerRef = useRef();
|
||||
|
||||
const toolColor = toolSettings && toolSettings.color;
|
||||
const useToolBlending = toolSettings && toolSettings.useBlending;
|
||||
const useGridSnapping = toolSettings && toolSettings.useGridSnapping;
|
||||
|
||||
const [brushPoints, setBrushPoints] = useState([]);
|
||||
// const [brushPoints, setBrushPoints] = useState([]);
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
const [drawingShape, setDrawingShape] = useState(null);
|
||||
const [pointerPosition, setPointerPosition] = useState({ x: -1, y: -1 });
|
||||
|
||||
// Reset pointer position when tool changes
|
||||
@ -44,18 +47,40 @@ function MapDrawing({
|
||||
function handleStart(event) {
|
||||
if (event.touches && event.touches.length !== 1) {
|
||||
setIsDrawing(false);
|
||||
setBrushPoints([]);
|
||||
setDrawingShape(null);
|
||||
return;
|
||||
}
|
||||
const pointer = event.touches ? event.touches[0] : event;
|
||||
const position = getRelativePointerPosition(pointer);
|
||||
setPointerPosition(position);
|
||||
setIsDrawing(true);
|
||||
const brushPosition = getBrushPositionForTool(
|
||||
position,
|
||||
toolSettings,
|
||||
gridSize,
|
||||
shapes
|
||||
);
|
||||
const commonShapeData = {
|
||||
id: shortid.generate(),
|
||||
color: toolSettings && toolSettings.color,
|
||||
blend: toolSettings && toolSettings.useBlending,
|
||||
};
|
||||
if (selectedTool === "brush") {
|
||||
const brushPosition = useGridSnapping
|
||||
? snapPositionToGrid(position, gridSize)
|
||||
: position;
|
||||
setBrushPoints([brushPosition]);
|
||||
setDrawingShape({
|
||||
type: "path",
|
||||
pathType: toolSettings.type,
|
||||
data: { points: [brushPosition] },
|
||||
strokeWidth: toolSettings.type === "stroke" ? 1 : 0,
|
||||
...commonShapeData,
|
||||
});
|
||||
} else if (selectedTool === "shape") {
|
||||
setDrawingShape({
|
||||
type: "shape",
|
||||
shapeType: toolSettings.type,
|
||||
data: getDefaultShapeData(toolSettings.type, brushPosition),
|
||||
strokeWidth: 0,
|
||||
...commonShapeData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,17 +93,39 @@ function MapDrawing({
|
||||
if (selectedTool === "erase") {
|
||||
setPointerPosition(position);
|
||||
}
|
||||
if (isDrawing && selectedTool === "brush") {
|
||||
if (isDrawing) {
|
||||
setPointerPosition(position);
|
||||
const brushPosition = useGridSnapping
|
||||
? snapPositionToGrid(position, gridSize)
|
||||
: position;
|
||||
setBrushPoints((prevPoints) => {
|
||||
if (prevPoints[prevPoints.length - 1] === brushPosition) {
|
||||
return prevPoints;
|
||||
}
|
||||
return [...prevPoints, brushPosition];
|
||||
});
|
||||
const brushPosition = getBrushPositionForTool(
|
||||
position,
|
||||
toolSettings,
|
||||
gridSize,
|
||||
shapes
|
||||
);
|
||||
if (selectedTool === "brush") {
|
||||
setDrawingShape((prevShape) => {
|
||||
const prevPoints = prevShape.data.points;
|
||||
if (prevPoints[prevPoints.length - 1] === brushPosition) {
|
||||
return prevPoints;
|
||||
}
|
||||
const simplified = simplify(
|
||||
[...prevPoints, brushPosition],
|
||||
getStrokeSize(drawingShape.strokeWidth, gridSize, 1, 1) * 0.1
|
||||
);
|
||||
return {
|
||||
...prevShape,
|
||||
data: { points: simplified },
|
||||
};
|
||||
});
|
||||
} else if (selectedTool === "shape") {
|
||||
setDrawingShape((prevShape) => ({
|
||||
...prevShape,
|
||||
data: getUpdatedShapeData(
|
||||
prevShape.shapeType,
|
||||
prevShape.data,
|
||||
brushPosition
|
||||
),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,24 +135,21 @@ function MapDrawing({
|
||||
}
|
||||
setIsDrawing(false);
|
||||
if (selectedTool === "brush") {
|
||||
if (brushPoints.length > 1) {
|
||||
const simplifiedPoints = simplify(brushPoints, 0.001);
|
||||
const type = "path";
|
||||
if (drawingShape.data.points.length > 1) {
|
||||
// const simplifiedPoints = simplify(
|
||||
// drawingShape.data.points,
|
||||
// getStrokeSize(drawingShape.strokeWidth, gridSize, 1, 1) * 0.1
|
||||
// );
|
||||
|
||||
if (type !== null) {
|
||||
const data = { points: simplifiedPoints };
|
||||
onShapeAdd({
|
||||
type,
|
||||
data,
|
||||
id: shortid.generate(),
|
||||
color: toolColor,
|
||||
blend: useToolBlending,
|
||||
});
|
||||
}
|
||||
|
||||
setBrushPoints([]);
|
||||
// const data = { points: simplifiedPoints };
|
||||
// onShapeAdd({ ...drawingShape, data });
|
||||
onShapeAdd(drawingShape);
|
||||
}
|
||||
} else if (selectedTool === "shape") {
|
||||
onShapeAdd(drawingShape);
|
||||
}
|
||||
|
||||
setDrawingShape(null);
|
||||
if (selectedTool === "erase" && hoveredShapeRef.current) {
|
||||
onShapeRemove(hoveredShapeRef.current.id);
|
||||
}
|
||||
@ -134,13 +178,15 @@ function MapDrawing({
|
||||
|
||||
const hoveredShapeRef = useRef(null);
|
||||
useEffect(() => {
|
||||
function pointsToPath(points) {
|
||||
function pointsToPath(points, close) {
|
||||
const path = new Path2D();
|
||||
path.moveTo(points[0].x * width, points[0].y * height);
|
||||
for (let point of points.slice(1)) {
|
||||
path.lineTo(point.x * width, point.y * height);
|
||||
}
|
||||
path.closePath();
|
||||
if (close) {
|
||||
path.closePath();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
@ -160,22 +206,30 @@ function MapDrawing({
|
||||
function shapeToPath(shape) {
|
||||
const data = shape.data;
|
||||
if (shape.type === "path") {
|
||||
return pointsToPath(data.points);
|
||||
} else if (shape.type === "circle") {
|
||||
return circleToPath(data.x, data.y, data.radius);
|
||||
} else if (shape.type === "rectangle") {
|
||||
return rectangleToPath(data.x, data.y, data.width, data.height);
|
||||
} else if (shape.type === "triangle") {
|
||||
return pointsToPath(data.points);
|
||||
return pointsToPath(data.points, shape.pathType === "fill");
|
||||
} else if (shape.type === "shape") {
|
||||
if (shape.shapeType === "circle") {
|
||||
return circleToPath(data.x, data.y, data.radius);
|
||||
} else if (shape.shapeType === "rectangle") {
|
||||
return rectangleToPath(data.x, data.y, data.width, data.height);
|
||||
} else if (shape.shapeType === "triangle") {
|
||||
return pointsToPath(data.points, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawPath(path, color, blend, context) {
|
||||
function drawPath(path, color, fill, strokeWidth, blend, context) {
|
||||
context.globalAlpha = blend ? 0.5 : 1.0;
|
||||
context.fillStyle = color;
|
||||
context.strokeStyle = color;
|
||||
context.stroke(path);
|
||||
context.fill(path);
|
||||
if (strokeWidth > 0) {
|
||||
context.lineCap = "round";
|
||||
context.lineWidth = getStrokeSize(strokeWidth, gridSize, width, height);
|
||||
context.stroke(path);
|
||||
}
|
||||
if (fill) {
|
||||
context.fill(path);
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
@ -198,15 +252,30 @@ function MapDrawing({
|
||||
hoveredShape = shape;
|
||||
}
|
||||
}
|
||||
drawPath(path, colors[shape.color], shape.blend, context);
|
||||
|
||||
drawPath(
|
||||
path,
|
||||
colors[shape.color],
|
||||
shapeHasFill(shape),
|
||||
shape.strokeWidth,
|
||||
shape.blend,
|
||||
context
|
||||
);
|
||||
}
|
||||
if (selectedTool === "brush" && brushPoints.length > 0) {
|
||||
const path = pointsToPath(brushPoints);
|
||||
drawPath(path, colors[toolColor], useToolBlending, context);
|
||||
if (drawingShape) {
|
||||
const path = shapeToPath(drawingShape);
|
||||
drawPath(
|
||||
path,
|
||||
colors[drawingShape.color],
|
||||
shapeHasFill(drawingShape),
|
||||
drawingShape.strokeWidth,
|
||||
drawingShape.blend,
|
||||
context
|
||||
);
|
||||
}
|
||||
if (hoveredShape) {
|
||||
const path = shapeToPath(hoveredShape);
|
||||
drawPath(path, "#BB99FF", true, context);
|
||||
drawPath(path, "#BB99FF", true, 1, true, context);
|
||||
}
|
||||
hoveredShapeRef.current = hoveredShape;
|
||||
}
|
||||
@ -217,9 +286,8 @@ function MapDrawing({
|
||||
pointerPosition,
|
||||
isDrawing,
|
||||
selectedTool,
|
||||
brushPoints,
|
||||
toolColor,
|
||||
useToolBlending,
|
||||
drawingShape,
|
||||
gridSize,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
@ -30,16 +30,16 @@ function ShapeToolSettings({ settings, onSettingChange }) {
|
||||
<RadioIconButton
|
||||
title="Shape Type Circle"
|
||||
onClick={() => onSettingChange({ type: "circle" })}
|
||||
isSelected={settings.type === "cricle"}
|
||||
isSelected={settings.type === "circle"}
|
||||
>
|
||||
<ShapeTriangleIcon />
|
||||
<ShapeCircleIcon />
|
||||
</RadioIconButton>
|
||||
<RadioIconButton
|
||||
title="Shape Type Triangle"
|
||||
onClick={() => onSettingChange({ type: "triangle" })}
|
||||
isSelected={settings.type === "triangle"}
|
||||
>
|
||||
<ShapeCircleIcon />
|
||||
<ShapeTriangleIcon />
|
||||
</RadioIconButton>
|
||||
<Divider vertical />
|
||||
<AlphaBlendToggle
|
||||
|
88
src/helpers/drawing.js
Normal file
88
src/helpers/drawing.js
Normal file
@ -0,0 +1,88 @@
|
||||
import { snapPositionToGrid } from "./shared";
|
||||
import * as Vector2 from "./vector2";
|
||||
import { toDegrees } from "./shared";
|
||||
|
||||
export function getBrushPositionForTool(
|
||||
brushPosition,
|
||||
settings,
|
||||
gridSize,
|
||||
shapes
|
||||
) {
|
||||
let position = brushPosition;
|
||||
if (settings && settings.useGridSnapping) {
|
||||
position = snapPositionToGrid(position, gridSize);
|
||||
}
|
||||
|
||||
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 getUpdatedShapeData(type, data, brushPosition) {
|
||||
if (type === "circle") {
|
||||
const dif = Vector2.subtract(brushPosition, { x: data.x, y: data.y });
|
||||
const distance = Vector2.length(dif);
|
||||
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]);
|
||||
const length = Vector2.length(dif);
|
||||
const direction = Vector2.normalize(dif);
|
||||
// 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));
|
||||
|
||||
return {
|
||||
points: [
|
||||
points[0],
|
||||
Vector2.add(Vector2.multiply(leftDir, sideLength), points[0]),
|
||||
Vector2.add(Vector2.multiply(rightDir, sideLength), points[0]),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const defaultStrokeSize = 1 / 10;
|
||||
export function getStrokeSize(multiplier, gridSize, width, height) {
|
||||
const gridPixelSize = Vector2.multiply(gridSize, { x: width, y: height });
|
||||
return Vector2.min(gridPixelSize) * defaultStrokeSize * multiplier;
|
||||
}
|
||||
|
||||
export function shapeHasFill(shape) {
|
||||
return (
|
||||
shape.type === "shape" ||
|
||||
(shape.type === "path" && shape.pathType === "fill")
|
||||
);
|
||||
}
|
@ -34,3 +34,11 @@ export function snapPositionToGrid(position, gridSize) {
|
||||
y: roundTo(position.y, gridSize.y),
|
||||
};
|
||||
}
|
||||
|
||||
export function toRadians(angle) {
|
||||
return angle * (Math.PI / 180);
|
||||
}
|
||||
|
||||
export function toDegrees(angle) {
|
||||
return angle * (180 / Math.PI);
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { toRadians } from "./shared";
|
||||
|
||||
export function lengthSquared(p) {
|
||||
return p.x * p.x + p.y * p.y;
|
||||
}
|
||||
@ -8,7 +10,7 @@ export function length(p) {
|
||||
|
||||
export function normalize(p) {
|
||||
const l = length(p);
|
||||
return { x: p.x / l, y: p.y / l };
|
||||
return divide(p, l);
|
||||
}
|
||||
|
||||
export function dot(a, b) {
|
||||
@ -16,5 +18,55 @@ export function dot(a, b) {
|
||||
}
|
||||
|
||||
export function subtract(a, b) {
|
||||
return { x: a.x - b.x, y: a.y - b.y };
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x - b, y: a.y - b };
|
||||
} else {
|
||||
return { x: a.x - b.x, y: a.y - b.y };
|
||||
}
|
||||
}
|
||||
|
||||
export function add(a, b) {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x + b, y: a.y + b };
|
||||
} else {
|
||||
return { x: a.x + b.x, y: a.y + b.y };
|
||||
}
|
||||
}
|
||||
|
||||
export function multiply(a, b) {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x * b, y: a.y * b };
|
||||
} else {
|
||||
return { x: a.x * b.x, y: a.y * b.y };
|
||||
}
|
||||
}
|
||||
|
||||
export function divide(a, b) {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x / b, y: a.y / b };
|
||||
} else {
|
||||
return { x: a.x / b.x, y: a.y / b.y };
|
||||
}
|
||||
}
|
||||
|
||||
export function rotate(point, origin, angle) {
|
||||
const cos = Math.cos(toRadians(angle));
|
||||
const sin = Math.sin(toRadians(angle));
|
||||
const dif = subtract(point, origin);
|
||||
return {
|
||||
x: origin.x + cos * dif.x - sin * dif.y,
|
||||
y: origin.y + sin * dif.x + cos * dif.y,
|
||||
};
|
||||
}
|
||||
|
||||
export function rotateDirection(direction, angle) {
|
||||
return rotate(direction, { x: 0, y: 0 }, angle);
|
||||
}
|
||||
|
||||
export function min(a) {
|
||||
return a.x < a.y ? a.x : a.y;
|
||||
}
|
||||
|
||||
export function max(a) {
|
||||
return a.x > a.y ? a.x : a.y;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user