三阶贝塞尔曲线演示
2022-1-9 ~ 2022-2-25
(0)
程序简介
贝塞尔曲线,这个曲线之前确实听过,也是看到这个视频才更了解了它。我把视频中的理解反应到程序里就是可以构造三阶贝塞尔曲线,定义表示,向量表示,画曲率圆,动画完整展示前三者的过程。
其中画曲率圆的编程过程最有趣,这个曲线在这里的本质是参数函数的表达形式,视频求曲率是用线性代数行列式求得的,我则是用高数曲率公式求导得的,还不得不求参数函数二阶导数,测试时又发现有时候构造的曲线其曲率会突然变号,观察发现曲线存在隐函数,变号就发生在两条函数相连,斜率为无穷的那些参数点,我用的公式不够表达,线代方法应该规避了,而我没再用高数隐函数求导解决这个问题。这个程序效果搭配参考视频更佳。
曲率的问题,可能还是要试试行列式才行。我现在的纠正曲率变号方式是,对首尾两点与中间两点构成直线的相对位置,进行分类讨论。曲率正常情况下,同侧有两种情况,无变号,或两次变号,异侧为一次变号。我先将曲率全翻为正,再根据分类讨论找零点。
就是这个找零点的过程有问题,我很抱歉这个程序还有不完善的地方。找零点我设置了一个阈值,这个阈值与分辨率有不清晰的关系。
我想说的是,现在这个程序可以展示大部分曲线,提供了一个展示的方式,但是!在特定位置有问题,对此,我必须告知。
第一次更新:
- 字符串更新,不再使用 char
- 参考资料加链接
第二次更新:
- 图片更新,减小边框
- 代码更新,现在兼容 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;
}
添加评论
取消回复