drawing-app / src /app /element-utils.js
qtitpc's picture
Upload 14 files
73e552c verified
Raw
History Blame Contribute Delete
7.55 kB
import rough from "roughjs/bundled/rough.esm";
import getStroke from "perfect-freehand";
const generator = rough.generator();
export const createElement = (id, x1, y1, x2, y2, type, pencilSize = 3) => {
switch (type) {
case "line":
case "rectangle":
const roughElement =
type === "line"
? generator.line(x1, y1, x2, y2)
: generator.rectangle(x1, y1, x2 - x1, y2 - y1);
return { id, x1, y1, x2, y2, type, roughElement };
case "pencil":
return { id, type, points: [{ x: x1, y: y1, isErased: false }], size: pencilSize };
case "text":
return { id, type, x1, y1, x2, y2, text: "" };
case "capture":
return { id, type, x1, y1, x2, y2 };
default:
throw new Error(`Type not recognised: ${type}`);
}
};
export const isPointNearEraser = (x, y, eraserX, eraserY, eraserSize) => {
const distance = Math.sqrt(Math.pow(x - eraserX, 2) + Math.pow(y - eraserY, 2));
return distance <= eraserSize;
};
const nearPoint = (x, y, x1, y1, name) => {
return Math.abs(x - x1) < 5 && Math.abs(y - y1) < 5 ? name : null;
};
const onLine = (x1, y1, x2, y2, x, y, maxDistance = 1) => {
const a = { x: x1, y: y1 };
const b = { x: x2, y: y2 };
const c = { x, y };
const offset = distance(a, b) - (distance(a, c) + distance(b, c));
return Math.abs(offset) < maxDistance ? "inside" : null;
};
const positionWithinElement = (x, y, element) => {
const { type, x1, x2, y1, y2 } = element;
switch (type) {
case "line":
const on = onLine(x1, y1, x2, y2, x, y);
const start = nearPoint(x, y, x1, y1, "start");
const end = nearPoint(x, y, x2, y2, "end");
return start || end || on;
case "rectangle":
const topLeft = nearPoint(x, y, x1, y1, "tl");
const topRight = nearPoint(x, y, x2, y1, "tr");
const bottomLeft = nearPoint(x, y, x1, y2, "bl");
const bottomRight = nearPoint(x, y, x2, y2, "br");
const inside = x >= x1 && x <= x2 && y >= y1 && y <= y2 ? "inside" : null;
return topLeft || topRight || bottomLeft || bottomRight || inside;
case "pencil":
const betweenAnyPoint = element.points.some((point, index) => {
const nextPoint = element.points[index + 1];
if (!nextPoint) return false;
return onLine(point.x, point.y, nextPoint.x, nextPoint.y, x, y, 5) != null;
});
return betweenAnyPoint ? "inside" : null;
case "text":
return x >= x1 && x <= x2 && y >= y1 && y <= y2 ? "inside" : null;
default:
throw new Error(`Type not recognised: ${type}`);
}
};
export const distance = (a, b) => Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
export const getElementAtPosition = (x, y, elements) => {
return elements
.map(element => ({ ...element, position: positionWithinElement(x, y, element) }))
.find(element => element.position !== null);
};
export const adjustElementCoordinates = element => {
const { type, x1, y1, x2, y2 } = element;
if (type === "rectangle") {
const minX = Math.min(x1, x2);
const maxX = Math.max(x1, x2);
const minY = Math.min(y1, y2);
const maxY = Math.max(y1, y2);
return { x1: minX, y1: minY, x2: maxX, y2: maxY };
} else {
if (x1 < x2 || (x1 === x2 && y1 < y2)) {
return { x1, y1, x2, y2 };
} else {
return { x1: x2, y1: y2, x2: x1, y2: y1 };
}
}
};
export const cursorForPosition = position => {
switch (position) {
case "tl":
case "br":
case "start":
case "end":
return "nwse-resize";
case "tr":
case "bl":
return "nesw-resize";
default:
return "move";
}
};
export const resizedCoordinates = (clientX, clientY, position, coordinates) => {
const { x1, y1, x2, y2 } = coordinates;
switch (position) {
case "tl":
case "start":
return { x1: clientX, y1: clientY, x2, y2 };
case "tr":
return { x1, y1: clientY, x2: clientX, y2 };
case "bl":
return { x1: clientX, y1, x2, y2: clientY };
case "br":
case "end":
return { x1, y1, x2: clientX, y2: clientY };
default:
return null;
}
};
export const getSvgPathFromStroke = stroke => {
if (!stroke.length) return "";
const d = stroke.reduce(
(acc, [x0, y0], i, arr) => {
const [x1, y1] = arr[(i + 1) % arr.length];
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
return acc;
},
["M", ...stroke[0], "Q"]
);
d.push("Z");
return d.join(" ");
};
export const drawElement = (roughCanvas, context, element) => {
switch (element.type) {
case "line":
case "rectangle":
roughCanvas.draw(element.roughElement);
break;
case "pencil":
const nonErasedSegments = [];
let currentSegment = [];
element.points.forEach((point, index) => {
if (!point.isErased) {
currentSegment.push([point.x, point.y]);
} else if (currentSegment.length > 0) {
nonErasedSegments.push([...currentSegment]);
currentSegment = [];
}
});
if (currentSegment.length > 0) {
nonErasedSegments.push(currentSegment);
}
nonErasedSegments.forEach(segment => {
if (segment.length > 1) {
const stroke = getSvgPathFromStroke(getStroke(segment, {
size: element.size || 3,
thinning: 0.5,
smoothing: 0.5,
streamline: 0.5,
}));
context.fillStyle = '#000';
context.fill(new Path2D(stroke));
}
});
break;
case "text":
context.textBaseline = "top";
context.font = "24px sans-serif";
context.fillText(element.text, element.x1, element.y1);
break;
case "capture":
context.save();
context.strokeStyle = "#000000";
context.setLineDash([5, 5]);
context.lineWidth = 1;
context.strokeRect(
element.x1,
element.y1,
element.x2 - element.x1,
element.y2 - element.y1
);
context.restore();
break;
default:
throw new Error(`Type not recognised: ${element.type}`);
}
};
export const adjustmentRequired = type => ["line", "rectangle"].includes(type);
export const calculateBoundsForElements = (elementsList) => {
if (elementsList.length === 0) return null;
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
elementsList.forEach(element => {
if (element.type === "text") {
minX = Math.min(minX, element.x1);
minY = Math.min(minY, element.y1);
maxX = Math.max(maxX, element.x2);
maxY = Math.max(maxY, element.y2);
} else if (element.type === "pencil") {
element.points.forEach(point => {
minX = Math.min(minX, point.x);
minY = Math.min(minY, point.y);
maxX = Math.max(maxX, point.x);
maxY = Math.max(maxY, point.y);
});
} else if (element.type === "line" || element.type === "rectangle") {
minX = Math.min(minX, element.x1, element.x2);
minY = Math.min(minY, element.y1, element.y2);
maxX = Math.max(maxX, element.x1, element.x2);
maxY = Math.max(maxY, element.y2, element.y2);
}
});
const padding = 20;
return {
x1: minX - padding,
y1: minY - padding,
x2: maxX + padding,
y2: maxY + padding
};
};