improve smoothing

This commit is contained in:
dwelle
2026-04-03 19:23:26 +02:00
parent e48043aef5
commit 90c4770a5b
2 changed files with 77 additions and 6 deletions
+74 -4
View File
@@ -1187,6 +1187,7 @@ const getFreeDrawSvgPath = (element: ExcalidrawFreeDrawElement) => {
if (element.strokeShape === "fixed") {
return getSvgPathFromFixedFreeDrawPoints(
getRenderableFixedFreeDrawPoints(element),
element.strokeWidth,
) as SVGPathString;
}
@@ -1257,6 +1258,10 @@ const getSvgPathFromPoints = (
const FIXED_FREEDRAW_MIN_SMOOTH_ALIGNMENT = 0.6;
const FIXED_FREEDRAW_MIN_SMOOTH_SEGMENT_LENGTH = 0.2;
const FIXED_FREEDRAW_MIN_CORNER_ROUNDING = 0.75;
const FIXED_FREEDRAW_MAX_CORNER_ROUNDING = 6;
const FIXED_FREEDRAW_CORNER_ROUNDING_FACTOR = 0.35;
const FIXED_FREEDRAW_CORNER_ROUNDING_WIDTH_FACTOR = 1.5;
const shouldSmoothFixedFreeDrawPoint = (
previousPoint: readonly number[],
@@ -1284,8 +1289,60 @@ const shouldSmoothFixedFreeDrawPoint = (
return alignment >= FIXED_FREEDRAW_MIN_SMOOTH_ALIGNMENT;
};
const getFixedFreeDrawRoundedCorner = (
previousPoint: readonly number[],
currentPoint: readonly number[],
nextPoint: readonly number[],
strokeWidth: number,
) => {
const previousDeltaX = currentPoint[0] - previousPoint[0];
const previousDeltaY = currentPoint[1] - previousPoint[1];
const nextDeltaX = nextPoint[0] - currentPoint[0];
const nextDeltaY = nextPoint[1] - currentPoint[1];
const previousSegmentLength = Math.hypot(previousDeltaX, previousDeltaY);
const nextSegmentLength = Math.hypot(nextDeltaX, nextDeltaY);
if (!previousSegmentLength || !nextSegmentLength) {
return null;
}
const alignment =
(previousDeltaX * nextDeltaX + previousDeltaY * nextDeltaY) /
(previousSegmentLength * nextSegmentLength);
if (alignment >= FIXED_FREEDRAW_MIN_SMOOTH_ALIGNMENT) {
return null;
}
const cornerRounding = Math.min(
Math.max(
strokeWidth * FIXED_FREEDRAW_CORNER_ROUNDING_WIDTH_FACTOR,
FIXED_FREEDRAW_MIN_CORNER_ROUNDING,
),
previousSegmentLength * FIXED_FREEDRAW_CORNER_ROUNDING_FACTOR,
nextSegmentLength * FIXED_FREEDRAW_CORNER_ROUNDING_FACTOR,
FIXED_FREEDRAW_MAX_CORNER_ROUNDING,
);
if (!cornerRounding) {
return null;
}
return {
entryPoint: [
currentPoint[0] - (previousDeltaX / previousSegmentLength) * cornerRounding,
currentPoint[1] - (previousDeltaY / previousSegmentLength) * cornerRounding,
] as const,
exitPoint: [
currentPoint[0] + (nextDeltaX / nextSegmentLength) * cornerRounding,
currentPoint[1] + (nextDeltaY / nextSegmentLength) * cornerRounding,
] as const,
};
};
const getSvgPathFromFixedFreeDrawPoints = (
points: ReadonlyArray<readonly number[]>,
strokeWidth: number,
): string => {
const len = points.length;
@@ -1303,14 +1360,27 @@ const getSvgPathFromFixedFreeDrawPoints = (
const previousPoint = points[index - 1];
const currentPoint = points[index];
const nextPoint = points[index + 1];
path += shouldSmoothFixedFreeDrawPoint(
const shouldSmooth = shouldSmoothFixedFreeDrawPoint(
previousPoint,
currentPoint,
nextPoint,
)
);
const roundedCorner = shouldSmooth
? null
: getFixedFreeDrawRoundedCorner(
previousPoint,
currentPoint,
nextPoint,
strokeWidth,
);
path += shouldSmooth
? `Q${roundPoint(currentPoint)}${averagePoint(currentPoint, nextPoint)}`
: `L${roundPoint(currentPoint)}`;
: roundedCorner
? `L${roundPoint(roundedCorner.entryPoint)}Q${roundPoint(
currentPoint,
)}${roundPoint(roundedCorner.exitPoint)}`
: `L${roundPoint(currentPoint)}`;
}
return `${path}L${roundPoint(points[len - 1])}`;
+3 -2
View File
@@ -30,16 +30,17 @@ describe("freedraw stroke shape", () => {
expect(vi.mocked(getStroke)).not.toHaveBeenCalled();
});
it("renders fixed stroke corners as straight segments", () => {
it("rounds fixed stroke corners without flattening them", () => {
const element = API.createElement({
type: "freedraw",
strokeShape: "fixed",
strokeWidth: 1,
points: [pointFrom(0, 0), pointFrom(10, 0), pointFrom(10, 10)],
});
const [path] = ShapeCache.generateElementShape(element, null);
expect(path).toBe("M0,0 L10,0 L10,10 ");
expect(path).toBe("M0,0 L8.5,0 Q10,0 10,1.5 L10,10 ");
});
it("smooths dense fixed stroke points without dropping them", () => {