妙妙小豆子

胆子大一点,想开一点。长风破浪会有时,直挂云帆济沧海

三阶贝塞尔曲线演示 银牌收录

程序简介

贝塞尔曲线,这个曲线之前确实听过,也是看到这个视频才更了解了它。我把视频中的理解反应到程序里就是可以构造三阶贝塞尔曲线,定义表示,向量表示,画曲率圆,动画完整展示前三者的过程。
其中画曲率圆的编程过程最有趣,这个曲线在这里的本质是参数函数的表达形式,视频求曲率是用线性代数行列式求得的,我则是用高数曲率公式求导得的,还不得不求参数函数二阶导数,测试时又发现有时候构造的曲线其曲率会突然变号,观察发现曲线存在隐函数,变号就发生在两条函数相连,斜率为无穷的那些参数点,我用的公式不够表达,线代方法应该规避了,而我没再用高数隐函数求导解决这个问题。这个程序效果搭配参考视频更佳。

曲率的问题,可能还是要试试行列式才行。我现在的纠正曲率变号方式是,对首尾两点与中间两点构成直线的相对位置,进行分类讨论。曲率正常情况下,同侧有两种情况,无变号,或两次变号,异侧为一次变号。我先将曲率全翻为正,再根据分类讨论找零点。
就是这个找零点的过程有问题,我很抱歉这个程序还有不完善的地方。找零点我设置了一个阈值,这个阈值与分辨率有不清晰的关系。
我想说的是,现在这个程序可以展示大部分曲线,提供了一个展示的方式,但是!在特定位置有问题,对此,我必须告知。

第一次更新:
1. 字符串更新,不再使用 char
2. 参考资料加链接

第二次更新:
1. 图片更新,减小边框
2. 代码更新,现在兼容 vc2010,不兼容 vc6.0
vc2010:_stscanf_s,vc6.0:_stscanf
vc2010:_stprintf_s,vc6.0:_stprintf

程序执行效果

完整源代码

////////////////////////////////////////
// 程序:三阶贝塞尔曲线演示
// 作者:Gary
// 编译环境:Visual C++ 2010,EasyX_20211109
// 编写日期:2022-1-9

# include <math.h>
# include <graphics.h>
# include <string>

// 定义一个结构体,三阶贝塞尔曲线需要的其他点
struct Node
{
	double x, y;						// 相对原点的坐标
	double t, d;						// 与原点的方向,距离
	COLORREF color;						// 颜色
};

// 定义一个结构体,构造点
struct Node1
{
	double x[10][100], y[10][100];		// 画曲线
	double flag;						// 曲线细节
	double K[100], Km[100];				// 曲率
	double P[4][100];					// 向量配比
	double t[100], n[100];				// 切线方向,法线方向
};

// 定义一个结构体,按钮
struct Node2
{
	int posx1, posy1, posx2, posy2;		// 坐标
	int mod;							// 按钮状态
	double r;							// 半径
	LPTSTR text;						// 文本
};

// 定义一个类
class Gary
{
public:
	void carry ();						// 主进程
	void initialization ();				// 初始化
	void draw ();						// 绘制,也更新参数
	void check ();						// 参数更新
	void move ();						// 窗口主视角

private:
	double T, K;						// 分辨率与当前构造点位置
	double pi;							// 圆周率π
	int exit1, exit2, exit3;			// 进程控制
	Node P[6];							// 其他点
	Node1 F;							// 构造点
	Node2 box[11];						// 按钮
	HWND hOut;							// 画布
};

// 绘制函数
void Gary::draw ()
{
	int i, j, x, y;
	TCHAR s[20];
	cleardevice ();

	// 定义表示绘制
	if (box[0].mod == 1)
	{
		i = int (T);
		for (j = 4; j < 10; j++)
		{
			fillcircle (int (F.x[j][i] + 250), int (F.y[j][i] + 250), 3);
		}
		line (int (F.x[4][i] + 250), int (F.y[4][i] + 250), int (F.x[5][i] + 250), int (F.y[5][i] + 250));
		line (int (F.x[5][i] + 250), int (F.y[5][i] + 250), int (F.x[6][i] + 250), int (F.y[6][i] + 250));
		line (int (F.x[7][i] + 250), int (F.y[7][i] + 250), int (F.x[8][i] + 250), int (F.y[8][i] + 250));
	}

	// 向量表示绘制
	if (box[1].mod == 1)
	{
		i = int (T);
		x = 250; y = 250;
		for (j = 0; j < 4; j++)
		{
			line (x, y, int (x + F.P[j][i] * cos (P[j].t) * P[j].d), int (y + F.P[j][i] * sin (P[j].t) * P[j].d));
			x = int (x + F.P[j][i] * cos (P[j].t) * P[j].d);
			y = int (y + F.P[j][i] * sin (P[j].t) * P[j].d);
		}
	}

	// 曲率圆绘制
	if (box[2].mod == 1)
	{
		i = int (T);
		if (F.K[i] == 0)
		{
			line (int (F.x[0][i] + 250 + 500 * cos (F.t[i])), int (F.y[0][i] + 250 + 500 * sin (F.t[i])), int (F.x[0][i] + 250 + 500 * cos (F.t[i] + pi)), int (F.y[0][i] + 250 + 500 * cos (F.t[i] + pi)));
		}
		else
		{
			fillcircle (int (F.x[0][i] + 250 + 1.0 / F.K[i] * cos (F.n[i])), int (F.y[0][i] + 250 + 1.0 / F.K[i] * sin (F.n[i])), int (1.0 / F.K[i]));
		}
	}

	// 界面绘制,坐标轴绘制,参数绘制
	fillrectangle (500, 0, 800, 510);
	line (0, 0, 0, 500);
	line (0, 0, 500, 0);
	line (0, 510, 700, 510);
	line (700, 0, 700, 510);
	line (520, 230, 690, 230); line (690 - 5, 230 + 5, 690, 230); line (690 - 5, 230 - 5, 690, 230);
	outtextxy (520, 230 + 10, _T ("0")); outtextxy (605, 230 + 10, _T ("t")); outtextxy (690, 230 + 10, _T ("1"));
	line (520, 20, 520, 230); line (520, 20, 520 - 5, 20 + 5); line (520, 20, 520 + 5, 20 + 5);
	line (520, 375, 690, 375); line (690 - 5, 375 + 5, 690, 375); line (690 - 5, 375 - 5, 690, 375);
	outtextxy (520, 480 + 10, _T ("0")); outtextxy (605, 480 + 10, _T ("t")); outtextxy (690, 480 + 10, _T ("1"));
	line (520, 270, 520, 480); line (520, 270, 520 - 5, 270 + 5); line (520, 270, 520 + 5, 270 + 5);

	// 第五个动点的参数更新
	i = int (T);
	P[4].x = i * 160.0 / F.flag + 520; P[4].y = 50;

	// 第五个动点和构造点的绘制
	fillcircle (int (P[4].x), int (P[4].y), 5);
	fillcircle (int (F.x[0][i] + 250), int (F.y[0][i] + 250), 3);
	line (int (P[4].x), 50, int (P[4].x), 230);

	// 前四个动点和四个向量点绘制
	for (j = 0; j < 4; j++)
	{
		fillcircle (int (P[j].x + 250), int (P[j].y + 250), 5);
		fillcircle (int (P[4].x), int (-F.P[j][i] * 160.0 + 230), 3);
	}

	// 曲率点绘制
	fillcircle (int (i * 160.0 / F.flag + 520), int (-F.K[i] * 105 / K + 375), 3);

	// 参考线绘制
	switch (box[7].mod)
	{
	case 1:
	{
		for (j = 0; j < 3; j++)
		{
			line (int (250 + P[j].x), int (250 + P[j].y), int (250 + P[j + 1].x), int (250 + P[j + 1].y));
		}
		break;
	}
	default: break;
	}

	for (i = 0; i < F.flag - 1; i++)
	{
		// 贝塞尔曲线绘制
		line (int (F.x[0][i] + 250), int (F.y[0][i] + 250), int (F.x[0][i + 1] + 250), int (F.y[0][i + 1] + 250));

		// 系数曲线绘制
		for (j = 0; j < 4; j++)
		{
			line (int (i * 160.0 / F.flag + 520), int (-F.P[j][i] * 160.0 + 230), int ((i + 1) * 160.0 / F.flag + 520), int (-F.P[j][i + 1] * 160.0 + 230));
		}

		// 曲率曲线绘制
		if (F.K[i] != 0) { line (int (i * 160.0 / F.flag + 520), int (-F.K[i] * 105 / K + 375), int ((i + 1) * 160.0 / F.flag + 520), int (-F.K[i + 1] * 105.0 / K + 375)); }

	}

	// 按钮绘制
	for (i = 0; i < 10; i++)
	{
		if (box[i].mod == 1) { settextcolor (LIGHTRED); }
		fillrectangle (box[i].posx1, box[i].posy1, box[i].posx2, box[i].posy2);
		outtextxy (box[i].posx1 + 2, box[i].posy1 + 5, box[i].text);
		settextcolor (BLACK);
	}

	// 参数绘制
	_stprintf_s (s, _T ("%0.0f"), F.flag);		outtextxy (750, 215, s);
	_stprintf_s (s, _T ("%0.0f"), T);			outtextxy (750, 265, s);

	// 原点绘制
	fillcircle (250, 250, 3);
	FlushBatchDraw ();
}

// 参数更新函数
void Gary::check ()
{
	int i, j, k;
	double t, t1, t2;

	// 四个动点的参数更新
	for (i = 0; i < 4; i++)
	{
		// 方向
		if (P[i].x == 0)
		{
			if (P[i].y > 0) { P[i].t = pi / 2; }
			else { P[i].t = -pi / 2; }
		}
		else
		{
			P[i].t = atan (P[i].y / P[i].x);
			if (P[i].x > 0) { P[i].t = P[i].t; }
			else { P[i].t = P[i].t + pi; }
		}
		P[i].d = sqrt (pow (P[i].x, 2) + pow (P[i].y, 2));
	}

	// 构造点的参数更新
	K = 0;
	for (t = 0, i = 0; t < 1; t += 1.0 / F.flag, i++)
	{
		// 原值
		F.x[0][i] = P[0].x * (-t * t * t + 3 * t * t - 3 * t + 1) + P[1].x * (3 * t * t * t - 6 * t * t + 3 * t) + P[2].x * (-3 * t * t * t + 3 * t * t) + P[3].x * (t * t * t);
		F.y[0][i] = P[0].y * (-t * t * t + 3 * t * t - 3 * t + 1) + P[1].y * (3 * t * t * t - 6 * t * t + 3 * t) + P[2].y * (-3 * t * t * t + 3 * t * t) + P[3].y * (t * t * t);

		F.x[4][i] = (1 - t) * P[0].x + t * P[1].x;
		F.y[4][i] = (1 - t) * P[0].y + t * P[1].y;

		F.x[5][i] = (1 - t) * P[1].x + t * P[2].x;
		F.y[5][i] = (1 - t) * P[1].y + t * P[2].y;

		F.x[6][i] = (1 - t) * P[2].x + t * P[3].x;
		F.y[6][i] = (1 - t) * P[2].y + t * P[3].y;

		F.x[7][i] = (1 - t) * F.x[4][i] + t * F.x[5][i];
		F.y[7][i] = (1 - t) * F.y[4][i] + t * F.y[5][i];

		F.x[8][i] = (1 - t) * F.x[5][i] + t * F.x[6][i];
		F.y[8][i] = (1 - t) * F.y[5][i] + t * F.y[6][i];

		F.x[9][i] = (1 - t) * F.x[7][i] + t * F.x[8][i];
		F.y[9][i] = (1 - t) * F.y[7][i] + t * F.y[8][i];

		// 一阶导
		F.x[1][i] = P[0].x * (-3 * t * t + 6 * t - 3) + P[1].x * (9 * t * t - 12 * t + 3) + P[2].x * (-9 * t * t + 6 * t) + P[3].x * (3 * t * t);
		F.y[1][i] = P[0].y * (-3 * t * t + 6 * t - 3) + P[1].y * (9 * t * t - 12 * t + 3) + P[2].y * (-9 * t * t + 6 * t) + P[3].y * (3 * t * t);

		// 二阶导
		F.x[2][i] = P[0].x * (-6 * t + 6) + P[1].x * (18 * t - 12) + P[2].x * (-18 * t + 6) + P[3].x * (6 * t);
		F.y[2][i] = P[0].y * (-6 * t + 6) + P[1].y * (18 * t - 12) + P[2].y * (-18 * t + 6) + P[3].y * (6 * t);

		// 向量配比
		F.P[0][i] = (-t * t * t + 3.0 * t * t - 3.0 * t + 1.0);
		F.P[1][i] = (3.0 * t * t * t - 6.0 * t * t + 3.0 * t);
		F.P[2][i] = (-3.0 * t * t * t + 3.0 * t * t);
		F.P[3][i] = (t * t * t);

		// 曲率
		if (F.x[1][i] == 0) { F.K[i] = 0; }
		else
		{
			F.K[i] = (F.y[2][i] * F.x[1][i] - F.y[1][i] * F.x[2][i]) / pow (F.x[1][i], 3) / sqrt (pow ((1.0 + pow (F.y[1][i] / F.x[1][i], 2)), 3));
		}
		if (F.K[i] > K) { K = F.K[i]; }
		else if (-F.K[i] > K) { K = -F.K[i]; }

		// 方向
		if (F.x[1][i] == 0)
		{
			if (F.y[1][i] > 0) { F.t[i] = pi / 2; }
			else { F.t[i] = -pi / 2; }
		}
		else
		{
			F.t[i] = atan (F.y[1][i] / F.x[1][i]);
			if (F.x[1][i] > 0) { F.t[i] = F.t[i]; }
			else { F.t[i] = F.t[i] + pi; }
		}
		F.n[i] = F.t[i] + pi / 2.0;
	}

	// 曲率纠正
	// 整体转正
	for (i = 0; i < F.flag; i++)
	{
		if (F.K[i] < 0)
		{
			F.K[i] = -F.K[i];
		}
	}

	// P 1 与 P 2 点的法线向量
	P[5].y = P[1].x - P[2].x;
	P[5].x = P[2].y - P[1].y;

	// 判断 P 0 与 P 3 的相对位置
	t = (P[5].x * (P[0].x - P[1].x) + P[5].y * (P[0].y - P[1].y)) * (P[5].x * (P[3].x - P[1].x) + P[5].y * (P[3].y - P[1].y));

	// 同号为同边,异号为异边
	if (t > 0)
	{
		t1 = 0.001; t2 = 0.001;
		j = 0; k = int (F.flag);
		for (i = 1; i < F.flag / 2.0; i++)
		{
			if (F.K[i] == 0)
			{
				j = i;
			}
			if (F.K[int (F.flag - i)] == 0)
			{
				k = int (F.flag) - i;
			}
		}
		for (i = int (F.flag * 0.2); i < F.flag / 2.0; i++)
		{
			if (fabs (F.K[i]) < t1)
			{
				j = i;
				t1 = fabs (F.K[i]);
			}
			if (fabs (F.K[int (F.flag - i)]) < t2)
			{
				k = int (F.flag) - i;
				t2 = fabs (F.K[int (F.flag - i)]);
			}
		}
		if (abs (j - k) > 5)
		{
			F.K[k] = 0;
			F.K[j] = 0;
			for (i = 0; i < j; i++)
			{
				F.K[i] = -F.K[i];
			}
			for (i = k + 1; i < F.flag; i++)
			{
				F.K[i] = -F.K[i];
			}
		}
	}
	else
	{
		j = 1;
		for (i = 2; i < F.flag - 1; i++)
		{
			if (F.K[i] == 0 || (F.K[i] < F.K[i + 1] && F.K[i] < F.K[i - 1]))
			{
				j = i;
			}
		}
		for (i = 0; i < j; i++)
		{
			F.K[i] = -F.K[i];
		}
	}
}

// 初始化函数
void Gary::initialization ()
{
	// 参数初始化
	int i;
	T = 1;
	pi = acos (-1.0);
	P[0].x = -100; P[0].y = 100;
	P[1].x = -100; P[1].y = -100;
	P[2].x = 100; P[2].y = -100;
	P[3].x = 100; P[3].y = 100;
	F.flag = 100;

	// 按钮初始化
	for (i = 0; i < 10; i++)
	{
		box[i].posx1 = 710;
		box[i].posy1 = 10 + 50 * i;
		box[i].posx2 = 790;
		box[i].posy2 = 40 + 50 * i;
		box[i].mod = 0;
	}
	box[7].mod = 1;

	box[0].text = _T ("定义表示");
	box[1].text = _T ("向量表示");
	box[2].text = _T ("曲率表示");
	box[3].text = _T ("动画演示");
	box[4].text = _T ("分率:");
	box[5].text = _T ("定位:");
	box[6].text = _T ("退出");
	box[7].text = _T ("参考线");
	box[8].text = _T ("重置");
	box[9].text = _T ("曲率纠正");

	// 画布初始化
	setbkcolor (WHITE);
	cleardevice ();
	setfillcolor (WHITE);
	settextcolor (BLACK);
	setlinecolor (BLACK);

	check ();
	draw ();
}

// 窗口主视角函数,获取用户操作
void Gary::move ()
{
	// 鼠标
	ExMessage m;
	TCHAR ss[20];
	int i;
	exit2 = 0;
	while (exit2 == 0)
	{
		if (peekmessage (&m, EM_MOUSE | EM_KEY))
		{
			// 左键单击判断
			if (m.message == WM_LBUTTONDOWN)
			{
				// 判断是否点击了按钮
				for (i = 0; i < 10; i++)
				{
					if (m.x > box[i].posx1 && m.y > box[i].posy1 && m.x < box[i].posx2 && m.y < box[i].posy2)
					{
						break;
					}
				}

				switch (i)
				{
					// 定义表示状态按钮
				case 0:
				{
					box[0].mod = box[0].mod != 1 ? 1 : 0;
					draw ();
					break;
				}

				// 向量表示状态按钮
				case 1:
				{
					box[1].mod = box[1].mod != 1 ? 1 : 0;
					draw ();
					break;
				}

				// 曲率表示状态按钮
				case 2:
				{
					box[2].mod = box[2].mod != 1 ? 1 : 0;
					draw ();
					break;
				}

				// 动画演示按钮
				case 3:
				{
					for (i = 2; i < F.flag - 2; i++)
					{
						T = i;
						draw ();
						Sleep (1);
					}
					break;
				}

				// 分辨率显示按钮
				case 4:
				{
					InputBox (ss, 10, _T ("输入分辨率(2~100)"));
					_stscanf_s (ss, _T ("%d"), &i);
					if (i >= 2 && i <= 100)
					{
						F.flag = int (i);
					}
					else
					{
						MessageBox (hOut, _T ("输入错误,不在范围内"), _T ("来自小豆子的提醒"), MB_OK);
					}
					check ();
					draw ();
					break;
				}

				// 构造点位置显示按钮
				case 5:
				{
					InputBox (ss, 10, _T ("定位(小于分辨率)"));
					_stscanf_s (ss, _T ("%d"), &i);
					if (i >= 1 && i < F.flag)
					{
						T = int (i);
					}
					else
					{
						MessageBox (hOut, _T ("输入错误,不在范围内"), _T ("来自小豆子的提醒"), MB_OK);
					}
					draw ();
					break;
				}

				// 退出按钮
				case 6:
				{
					exit2 = 1; exit1 = 1;
					break;
				}

				// 参考线显示状态按钮
				case 7:
				{
					box[7].mod = box[7].mod != 1 ? 1 : 0;
					draw ();
					break;
				}

				// 重置按钮
				case 8:
				{
					exit2 = 1;
					break;
				}

				// 曲率纠正取反按钮
				case 9:
				{
					for (i = 0; i < F.flag; i++)
					{
						F.K[i] = -F.K[i];
					}
					draw ();
					break;
				}
				default:break;
				}

				// 判断是否点击前四个可动点
				if (m.x > 0 && m.y > 0 && m.x < 500 && m.y < 500)
				{
					for (i = 0; i < 4; i++)
					{
						// 重新构造贝塞尔曲线
						if (sqrt (pow (P[i].x + 250 - m.x, 2) + pow (P[i].y + 250 - m.y, 2)) < 5)
						{
							exit3 = 0;
							while (exit3 == 0)
							{
								if (peekmessage (&m, EM_MOUSE | EM_KEY))
								{
									if (m.message == WM_MOUSEMOVE)
									{
										if (m.x > 0 && m.y > 0 && m.x < 500 && m.y < 500)
										{
											P[i].x = m.x - 250;
											P[i].y = m.y - 250;
										}
										check ();
										draw ();
									}
									// 左键单击固定点
									else if (m.message == WM_LBUTTONDOWN)
									{
										exit3 = 1;
										draw ();
									}
								}
							}
							break;
						}
					}
				}

				// 判断是否点击第五个可动点
				else if (sqrt (pow (P[4].x - m.x, 2) + pow (P[4].y - m.y, 2)) < 5)
				{
					// 重新设置构造点
					exit3 = 0;
					while (exit3 == 0)
					{
						if (peekmessage (&m, EM_MOUSE | EM_KEY))
						{
							if (m.message == WM_MOUSEMOVE)
							{
								if (m.x > 520 && m.x < 680)
								{
									T = (double (m.x) - 520.0) / 160.0 * F.flag;
									draw ();
								}
							}

							// 左键单击固定点
							else if (m.message == WM_LBUTTONDOWN)
							{
								exit3 = 1;
								draw ();
							}
						}
					}
				}
			}
		}
	}
}

// 主进程
void Gary::carry ()
{
	// 窗口定义
	initgraph (801, 511);
	SetWindowText (hOut, _T ("三阶贝塞尔曲线演示"));

	// 进程控制
	exit1 = 0;
	BeginBatchDraw ();
	while (exit1 == 0)
	{
		initialization ();
		move ();
	}
	EndBatchDraw ();
	closegraph ();
}

// 主函数
int main (void)
{
	Gary G;
	G.carry ();
	return 0;
}

参考资料

添加评论