专栏导航
下一篇:无
本节前言
对于本节所讲解的知识,有可能,你会需要时不时地参考本专栏的其它文章。真的遇到了需要参考之前的文章的知识点,请你自行查阅。
我呢,也会提到一部分的参考课节。但是呢,你不应该依赖于我的主动提及。最好呢,你自己能够多去了解和查看本专栏目录。
关于贝塞尔曲线,最初,我是在学习 3ds max 的时候,接触到了它。后来呢,我在学习 PhotoShop 软件的时候,又一次接触了它。没想到,后来,在学习 Windows API 编程的时候,再一次学习了它。
在 PhotoShop 里面,用钢笔工具绘制出来的曲线,便是贝塞尔样条曲线。
没有学过 PhotoShop 的话,那么,你依然可以跟着佩措尔德的教材,来学习这个东西。
本节内容,来自于对佩措尔德教材的大段抄录。
一. 本节内容
英文单词 "spline"(曲尺,又称为样条曲线)最初指的是一块可以弯曲的物体,可以是木头、橡皮或者金属条,它可以用来在纸张上绘制曲线。例如,如果有一些离散的点,并且想在它们之间画曲线,无论是内插拟合还是外插拟合,都应当首先将这些点描绘在图纸上。接着,固定一个曲尺在这些点上,使用铅笔沿着曲尺绕着这些点弯曲的方向绘制曲线。
当然,现在样条曲线已经变成了一个个数学公式。有很多种不同的样条曲线,但是贝塞尔样条曲线是计算机图形程序设计中用得最广的。它是后来才加到操作系统的图形工具库中,并且它出自一个几乎不可能的来源:20 世纪 60 年代,法国雷诺汽车公司由手工设计车体(使用粘土模型)转变到使用计算机辅助设计。这就需要用到一些数学工具,而皮埃尔·贝济埃(Pierre Bezier)提出了一套数学公式,事实证明这些数学公式起到了很大的作用。
从那时起,二维的贝塞尔样条曲线被认为是在计算机图形学中最有用的曲线之一(仅次于直线和椭圆)。在 PostScript 中,贝塞尔样条曲线用于所有的曲线(甚至椭圆也由贝塞尔样条曲线来近似)。PostScript 字体的字符轮廓的定义也使用贝塞尔样条曲线定义。(TrueType 则使用另外一种相对简单快速的样条曲线公式。)
一条二维贝塞尔样条曲线使用四个点来定义:两个端点和两个控点。两个端点表示曲线的起点和终点。控点就好像磁铁一样,把曲线从两个端点间的直线处吸弯。对此,下述程序所示的 Bezier 交互式程序便是一个好的例证。
/*---------------------------------------
BEZIER.C -- Bezier Splines Demo
(c) Charles Petzold, 1998
水饺编程改编,2026
---------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("Bezier");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;
if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("注册窗口类失败!"),
szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(szAppName, TEXT("Bezier Splines"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
void DrawBezier(HDC hdc, POINT apt[], BOOL flag_eraser)
{
HPEN hPen, hOldPen;
COLORREF color;
hPen = hOldPen = NULL;
PolyBezier(hdc, apt, 4);
if (TRUE == flag_eraser)
{
color = RGB(255, 0, 0);
hPen = CreatePen(PS_DASH, 1, color);
hOldPen = (HPEN)SelectObject(hdc, hPen);
}
MoveToEx(hdc, apt[0].x, apt[0].y, NULL);
LineTo(hdc, apt[1].x, apt[1].y);
MoveToEx(hdc, apt[2].x, apt[2].y, NULL);
LineTo(hdc, apt[3].x, apt[3].y);
if (TRUE == flag_eraser)
{
SelectObject(hdc, hOldPen);
DeleteObject(hPen);
}
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static POINT apt[4];
HDC hdc;
int cxClient, cyClient;
PAINTSTRUCT ps;
switch (message)
{
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
apt[0].x = cxClient / 4;
apt[0].y = cyClient / 2;
apt[1].x = cxClient / 2;
apt[1].y = cyClient / 4;
apt[2].x = cxClient / 2;
apt[2].y = 3 * cyClient / 4;
apt[3].x = 3 * cxClient / 4;
apt[3].y = cyClient / 2;
return 0;
case WM_LBUTTONDOWN:
case WM_RBUTTONDOWN:
case WM_MOUSEMOVE:
if (wParam & MK_LBUTTON || wParam & MK_RBUTTON)
{
hdc = GetDC(hwnd);
SelectObject(hdc, GetStockObject(WHITE_PEN));
DrawBezier(hdc, apt, FALSE);
if (wParam & MK_LBUTTON)
{
apt[1].x = LOWORD(lParam);
apt[1].y = HIWORD(lParam);
}
if (wParam & MK_RBUTTON)
{
apt[2].x = LOWORD(lParam);
apt[2].y = HIWORD(lParam);
}
SelectObject(hdc, GetStockObject(BLACK_PEN));
DrawBezier(hdc, apt, TRUE);
ReleaseDC(hwnd, hdc);
}
return 0;
case WM_PAINT:
InvalidateRect(hwnd, NULL, TRUE);
hdc = BeginPaint(hwnd, &ps);
DrawBezier(hdc, apt, TRUE);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
对于上述代码块中的代码,你暂时不需要理解。因为它涉及了以后的章节才会讲到的鼠标消息的处理方法。此时,你只需要能够将其编译出来,并且在运行过程中了解这个程序的使用方法和运行效果就可以了。
程序编译和运行,这种纯软件操作的问题,如果你忘了,可以参考下述文章。
参考课节:使用 VS2010 编写 C语言程序
参考课节:用 VS2019 编写C语言程序
本节程序,主要的代码逻辑,都是佩措尔德原教材的代码逻辑。但是呢,我也改动了一部分代码逻辑。
程序运行的结果如下图所示。
图1
图1 所示,为默认的运行结果。
注意,在图1 中,黑色实线的部分,才是贝塞尔曲线。红色虚线的部分,它并不是贝塞尔曲线,而是辅助线。
程序运行起来以后,你可以通过鼠标左键单击和左键拖动的操作,来改变第一个控点的位置,从而影响整体的贝塞尔曲线的形状。你也可以通过鼠标右键的单击和右键拖动,来改变第二个控点的位置,从而影响整体的贝塞尔曲线的形状。
我相信,你很快地,就能够理解大致的操作方式。
这个程序,我当初在学习的时候,我觉得,它是一个很好玩的程序。
在我这里,我在运行了程序以后,没有改变第一个控点的位置,而是选择去改变第二个控点的位置。形成的图形如下所示。
图2
除了绘制贝塞尔样条曲线本身,程序还从第一个控点向左边的第一个端点(也就是起点)画直线,并从第二个控点向右边的端点画直线。由控点向端点绘制的直线,属于是辅助线。我将这个辅助线用虚线来绘制。
用虚线来画线条,同样是涉及了在以后的章节内容中才会讲到的知识。默认地,在客户区中绘制线条,采用的是实线。我们也可以用虚线来绘制线条。想要用虚线来画线,我们需要用到的,是画笔知识。画笔知识,本章的后面,我们就会讲到。此刻,你有个数就行。程序中有关于画笔的使用,你暂时应该是还看不懂涉及画笔的代码。
所以,再次强调,本节程序,你暂时不需要阅读其中的代码,更不需要掌握其中的代码逻辑。暂时地,你只需要能够将本节程序编译出来,运行它,掌握使用方法,这就够了。
至于说,想要很好地理解本节程序代码,你需要学习完本章后面会讲到的画笔知识,也需要学习完以后的章节中会讲到的鼠标消息知识,你才能看懂本节程序代码。
结束语
本节,就先到这里了。下一节,我们接着讲贝塞尔样条曲线。
专栏导航
下一篇:无