绘制平滑的三次贝塞尔曲线

August 21, 2021
project

基础知识

阅读之前你需要知道的知识包括

  • canvas的坐标系
  • 直角坐标中两点的中点公式
  • 直角坐标中两点距离公式
  • 基础的三角函数
  • 投影基础知识
  • canvas绘制贝塞尔曲线

面临的问题

1. 选择二次贝塞尔曲线 or 三次贝塞尔曲线

2. 贝塞尔曲线控制点计算

问题分析

问题1:

由于二次贝塞尔曲线绘制后,将只有一处弯曲,在多节点连接时,呈现效果很差。并且在45°,135°,225°,315°时,需要做特殊的处理,否侧得到的曲线的弧度过大。

问题2:

在确定使用三次贝塞尔曲线后,需要通过计算得出曲线绘制时的两个控制点C1,C2。然后通过CanvasRenderingContext2D.bezierCurveTo进行绘制。

由于我们需要两个控制点,所以,我们将会把起点SP(start point)和终点EP(end point)间的连线S-E均分为4份。得到如下点: $$ \begin{align*} Split_{m} = (\frac{(X_{SP}+X_{EP})}2,\frac{(Y_{SP}+Y_{EP})}2)\ \end{align*} $$ 得到S-E的公式L(x)为 $$ L(x) = \frac{X_{Split_{m}}}{Y_{Slit_{m}}}x $$ 根据L(x)可知S-E的斜率满足 $$ \tan \theta = \frac{X_{Split_{m}}}{Y_{Slit_{m}}} $$ 然后将$Split_{m}$作为坐标系的原点,建立直角坐标系,得到 $$ \begin{align*} len = \sqrt{(X_{Split_{m}}-X_{SP})^{2}+(Y_{Split_{m}}-Y_{SP})^{2}}\ \ \theta = \arctan \frac{X_{Split_{m}}}{Y_{Slit_{m}}}\ \ Y_{offset} = len·\cos \theta \ \ C1=(X_{Split_{m}},Y_{Split_{m}}-len)\ C2=(X_{Split_{m}},Y_{Split_{m}}+len) \end{align*} $$

代码部分

typescript
/**
 * @param props 
 * @typeof props {
		start: number[];
		end: number[];
		canvas: CanvasRenderingContext2D;
	}
 */
export const drawLine = (props: Common.LineProps) => {
	const { start, end, canvas: ctx, color } = props;

	const getMidCoord = (c1: number, c2: number) => {
		if (c1 === c2) {
			return c1;
		}
		return (c1 + c2) / 2;
	};

	const [x1, y1] = start;
	const [x2, y2] = end;
	const [midX, midY] = [getMidCoord(x1, x2), getMidCoord(y1, y2)];
	const drawMirror = (y1: number, y2: number) => {
		if (y1 > y2) {
			return ctx.bezierCurveTo(control2[0], control2[1], control1[0], control1[1], end[0], end[1]);
		} else {
			return ctx.bezierCurveTo(control1[0], control1[1], control2[0], control2[1], end[0], end[1]);
		}
	};
	const degCos = Math.cos(Math.atan((x1 - midX) / (y1 - midY)));

	const lineLen = Math.sqrt(Math.pow(y1 - midY, 2) + Math.pow(x1 - midX, 2)) * 2;

	const control1 = [midX, midY - degCos * (lineLen / 2)];
	const control2 = [midX, midY + degCos * (lineLen / 2)];

	ctx.beginPath();
	ctx.moveTo(start[0], start[1]);
	drawMirror(y1, y2);
	ctx.lineWidth = 2;
	ctx.strokeStyle = color ? color : "#000";
	ctx.stroke();
	ctx.closePath();
};