@@ -390,10 +390,61 @@ const drawImagePlaceholder = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_FREEDRAW_PRESSURE = 0.5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws freedraw points as individual rounded capsule segments.
|
* Draws a single tapered capsule (variable-width filled stroke segment) from
|
||||||
* Each segment [points[i-1] → points[i]] is an independent stroke with
|
* (x0,y0) with radius r0 to (x1,y1) with radius r1. The shape is a filled
|
||||||
* round lineCap so adjacent caps overlap, producing paint-splat splotches.
|
* path consisting of a back semicircle at the start, a straight side on each
|
||||||
|
* side, and a front semicircle at the end, so that adjacent segments sharing
|
||||||
|
* a point use the same radius and produce a seamlessly continuous stroke.
|
||||||
|
*/
|
||||||
|
const drawTaperedCapsule = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
x0: number,
|
||||||
|
y0: number,
|
||||||
|
r0: number,
|
||||||
|
x1: number,
|
||||||
|
y1: number,
|
||||||
|
r1: number,
|
||||||
|
) => {
|
||||||
|
const dx = x1 - x0;
|
||||||
|
const dy = y1 - y0;
|
||||||
|
const len = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (len < 0.5) {
|
||||||
|
// Degenerate segment — draw a filled circle at the larger radius
|
||||||
|
const r = Math.max(r0, r1);
|
||||||
|
context.beginPath();
|
||||||
|
context.arc((x0 + x1) / 2, (y0 + y1) / 2, r, 0, Math.PI * 2);
|
||||||
|
context.fill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const angle = Math.atan2(dy, dx);
|
||||||
|
const px = -dy / len; // perpendicular unit x = -sin(angle)
|
||||||
|
const py = dx / len; // perpendicular unit y = cos(angle)
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
// Back semicircle at P0: clockwise from (P0 + perp*r0) through (back of P0) to (P0 - perp*r0)
|
||||||
|
context.arc(x0, y0, r0, angle + Math.PI / 2, angle - Math.PI / 2, false);
|
||||||
|
// Neg-perp side: P0 - perp*r0 → P1 - perp*r1 (arc endpoint is already P0 - perp*r0)
|
||||||
|
context.lineTo(x1 - px * r1, y1 - py * r1);
|
||||||
|
// Front semicircle at P1: clockwise from (P1 - perp*r1) through (front of P1) to (P1 + perp*r1)
|
||||||
|
context.arc(x1, y1, r1, angle - Math.PI / 2, angle + Math.PI / 2, false);
|
||||||
|
// Perp side: P1 + perp*r1 → P0 + perp*r0
|
||||||
|
context.lineTo(x0 + px * r0, y0 + py * r0);
|
||||||
|
context.closePath();
|
||||||
|
context.fill();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws freedraw points as individual pressure-aware tapered capsule segments.
|
||||||
|
* Each segment [points[i-1] → points[i]] is drawn as a filled tapered capsule
|
||||||
|
* whose start radius is derived from pressures[i-1] and end radius from
|
||||||
|
* pressures[i]. Because adjacent segments share the same radius at their
|
||||||
|
* common junction point, the result is a continuous stroke with naturally
|
||||||
|
* varying pen pressure.
|
||||||
*
|
*
|
||||||
* @param fromIndex Draw segments starting from this index (inclusive).
|
* @param fromIndex Draw segments starting from this index (inclusive).
|
||||||
* Segments are from max(fromIndex,1) to points.length-1.
|
* Segments are from max(fromIndex,1) to points.length-1.
|
||||||
@@ -405,33 +456,50 @@ const drawFreeDrawSegments = (
|
|||||||
renderConfig: StaticCanvasRenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
fromIndex: number,
|
fromIndex: number,
|
||||||
) => {
|
) => {
|
||||||
const { points } = element;
|
const { points, pressures } = element;
|
||||||
const strokeColor =
|
const strokeColor =
|
||||||
renderConfig.theme === THEME.DARK
|
renderConfig.theme === THEME.DARK
|
||||||
? applyDarkModeFilter(element.strokeColor)
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
: element.strokeColor;
|
: element.strokeColor;
|
||||||
|
|
||||||
context.strokeStyle = strokeColor;
|
|
||||||
context.fillStyle = strokeColor;
|
context.fillStyle = strokeColor;
|
||||||
context.lineCap = "round";
|
|
||||||
context.lineJoin = "round";
|
const baseRadius = (element.strokeWidth * 4.25) / 2;
|
||||||
context.lineWidth = element.strokeWidth * 4.25;
|
|
||||||
|
const getPressure = (i: number): number =>
|
||||||
|
pressures.length > i ? pressures[i] : DEFAULT_FREEDRAW_PRESSURE;
|
||||||
|
|
||||||
if (fromIndex === 0 && points.length === 1) {
|
if (fromIndex === 0 && points.length === 1) {
|
||||||
// Single-point stroke → filled circle (dot)
|
// Single-point stroke → filled circle (dot)
|
||||||
const r = (element.strokeWidth * 4.25) / 2;
|
const r = baseRadius * getPressure(0) * 2;
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.arc(points[0][0], points[0][1], r, 0, Math.PI * 2);
|
context.arc(points[0][0], points[0][1], r, 0, Math.PI * 2);
|
||||||
context.fill();
|
context.fill();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FREEDRAW_DEBUG_COLORS = [
|
||||||
|
"#e03131",
|
||||||
|
"#f76707",
|
||||||
|
"#2f9e44",
|
||||||
|
"#1971c2",
|
||||||
|
"#7048e8",
|
||||||
|
"#e64980",
|
||||||
|
];
|
||||||
const start = Math.max(fromIndex, 1);
|
const start = Math.max(fromIndex, 1);
|
||||||
for (let i = start; i < points.length; i++) {
|
for (let i = start; i < points.length; i++) {
|
||||||
context.beginPath();
|
context.fillStyle = FREEDRAW_DEBUG_COLORS[i % FREEDRAW_DEBUG_COLORS.length];
|
||||||
context.moveTo(points[i - 1][0], points[i - 1][1]);
|
const r0 = baseRadius * getPressure(i - 1) * 2;
|
||||||
context.lineTo(points[i][0], points[i][1]);
|
const r1 = baseRadius * getPressure(i) * 2;
|
||||||
context.stroke();
|
drawTaperedCapsule(
|
||||||
|
context,
|
||||||
|
points[i - 1][0],
|
||||||
|
points[i - 1][1],
|
||||||
|
r0,
|
||||||
|
points[i][0],
|
||||||
|
points[i][1],
|
||||||
|
r1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user