按钮的创建就不在这里过多赘述了,前面的文章已经反复展示了,这里直接上代码和结果。
一、有效边表法
用vector元素构建NET表,定义结构体EdgeNode用来存放边的最小y坐标、当前x坐标、△x和边的最大y坐标。填充方法是void polygon_fill(vector<vector<int>>& polygon),主函数写在void CMFCApplication2View::On32777()中。通过绘图函数polygon点的坐标定义位置不同,展现出陀螺、棍子和沙漏的绘制,棍子上的绳子用的是math中的arc方法画曲线。我的想法是陀螺是两个等腰三角形底边拼在一起,而沙漏恰恰相反,是两个等腰三角形顶点拼在一起,棍子就是画一个宽很窄的矩形。这些图案画完后,再用写好的填充算法进行填充即可。
算法思路
多边形扫描转换是将多边形的顶点表示转换为点阵表示,从多边形给定的边界出发,求出位于其内部的各个像素,并将帧缓存内各对应元素设置为相应的颜色。因此我们需要首先确定多边形的范围,进而确定扫描线的范围。步骤如下:
1‘初始化并建立NET表。
2‘遍历每一条扫描线,对于每一个多边形点,寻找与其构成边的两点,如果寻找到的点在此点的上方(即y0小于y1),则将此边加入到ET[i]中(i对应的y0的坐标)。这样每次加入的边都是向上,不会重复。
3‘将AET设为空。
4‘执行以下步骤直至NET和AET都为空:
a. 更新:如ET中的y非空,则将其中所有边取出并插入AET中。
b. 填充:对AEL中的边两两配对,每对边中x坐标按规则取整,获得有效的填充区段,再填充。
c. 排序:如果有新边插入AET,则对AET中各边排序。
d. 删除:从扫描线中删除已经填充的区段。
代码:
using namespace std;
struct EdgeNode {
int ymin; // 边的最小y坐标
double x; // 当前x坐标
double dx; // △x
int ymax; // 边的最大y坐标
};
void polygon_fill(vector<vector<int>>& polygon) {//i*2矩阵
int y_min = INT_MAX;
int y_max = INT_MIN;
// 找到多边形的最小和最大y坐标
for (const auto& point : polygon) {
y_min = min(y_min, point[1]);
y_max = max(y_max, point[1]);
}
vector<vector<EdgeNode>> NET(y_max + 1);
// 构造NET
for (int i = 0; i < polygon.size(); i++) {
int next_idx = (i + 1) % polygon.size();
int x1 = polygon[i][0];
int y1 = polygon[i][1];
int x2 = polygon[next_idx][0];
int y2 = polygon[next_idx][1];
int ymin = min(y1, y2);
int ymax = max(y1, y2);
double dx = (double)(x2 - x1) / (y2 - y1);// △x
if (ymax != ymin) {
if (ymin == y1) {
NET[ymin].push_back({ ymin, (double)(x1), dx, ymax });
}
else if (ymin == y2) {
NET[ymin].push_back({ ymin, (double)(x2), dx, ymax });
}
}
}
vector<EdgeNode> AET;
//int ynow = y_min; // 当前扫描线号
for (int i = y_min; i <= y_max; i++) { // 对每一条扫描线
for (auto& edge : NET[i]) { // 把ymin=i的边放入AET
if (edge.ymin == i) {
AET.push_back(edge);
}
}
}
for (int i = y_min; i <= y_max; i++) { // 对每一条扫描线
// 使用插入排序法对AET按x坐标递增顺序排序
for (int j = 1; j < AET.size(); j++) {
int k = j - 1;
EdgeNode key = AET[j];
while (k >= 0 && AET[k].x > key.x) {
AET[k + 1] = AET[k];
k--;
}
AET[k + 1] = key;
}
// 若允许多边形的边自交,用冒泡排序法对AET重新排序
for (int j = 0; j < AET.size() - 1; j++) {
for (int k = 0; k < AET.size() - j - 1; k++) {
if (AET[k].x > AET[k + 1].x) {
swap(AET[k], AET[k + 1]);
}
}
}
// 遍历AET,找到配对的交点并填色
for (int j = 0; j < AET.size(); j += 2) {
int x_start = (int)(ceil(AET[j].x));
int x_end = (int)(ceil(AET[j + 1].x));
COLORREF rgb = RGB(0 + rand() % 255, 0 + rand() % 255, 0 + rand() % 254);
for (int x = x_start; x < x_end; x++) {
putpixel(x, i, rgb);
}
}
// 遍历AET,更新x值,删除ymax=i的边
vector<EdgeNode> new_AET;
for (const auto& edge : AET) {
if (edge.ymax > i) {
EdgeNode new_edge = edge;
new_edge.x += new_edge.dx;
new_AET.push_back(new_edge);
}
}
AET = new_AET;
}
}
//主函数
void CMFCApplication2View::On32777()
{
// TODO: 在此添加命令处理程序代码
// 有效边表法(AET)绘制填充多边形
initgraph(800, 480);
vector<vector<int>> polygon = { {560,150}, {630,270 },{560,270},{630,150} };//漏斗形
polygon_fill(polygon);
polygon = { {110,220}, {190, 220},{150,270}};//陀螺
polygon_fill(polygon);
polygon = { {110,220}, {190,220},{150,170} };
polygon_fill(polygon);
arc(300,100,150,180,80,false);//绳子
polygon = { {300,139}, {420,139},{420,140 }, {300,140}};//棍子
polygon_fill(polygon);
outtextxy(200, 320, "陀螺");//文字输出
outtextxy (580 ,320 , "沙漏");//文字输出
//system("pause");
Sleep(3500);
closegraph();
}
效果:
二、种子填充法
我在此用的四连通种子填充法之边界填充算法。边界填充算法强调边界的存在,只要是边界内的点无论是什么颜色,都替换成指定的颜色。因此判断边界内的点是否已填充,按照右左下上的顺序进行四次递归进行填充,选择这个顺序可以显示出一种从右往左拉上窗帘的视觉效果。我的填充方法名是BoundarySeedFill,主函数是OnBoundaryseedfill,建立对话框与用户进行互动。种子点坐标和填充区域由用户通过键盘进行输入,用Dlg函数获取值,再使用CDC类的Rectangle()函数画矩形,进行填充。
算法思路
1‘确定种子点:选择一个位于要填充的区域内的像素作为种子点。2‘确定填充区域:根据特定的规则,确定要填充的区域。这通常可以通过判断像素之间的颜色或属性是否相似来完成。3‘填充颜色:从种子点开始,将新的颜色或属性填充到相邻的像素中。这个过程可以按照一定的方向或顺序进行,例如从上到下、从左到右等。4‘判断停止条件:在填充过程中,需要判断何时停止填充。这通常可以通过判断像素之间的颜色或属性是否仍然相似来完成。如果相邻的像素不再满足填充条件,则停止填充。5‘继续填充:重复步骤3和4,直到所有满足填充条件的像素都被填充为新的颜色或属性。
代码:
//种子填充法之边界填充算法(四连通)
//边界填充算法强调边界的存在,只要是边界内的点无论是什么颜色,都替换成指定的颜色。
//注释了左边着色的递归,产生窗户拉窗帘的效果
void CMFCApplication2View::BoundarySeedFill(CDC* pDC, int x, int y, COLORREF colorBoundary, COLORREF colorNew)
{
COLORREF colorCurrent = pDC->GetPixel(x,y);
if (colorCurrent != colorBoundary && colorCurrent != colorNew)
{
pDC->SetPixel(x, y, colorNew);
//Sleep(10);
BoundarySeedFill(pDC, x + 1, y, colorBoundary, colorNew);
BoundarySeedFill(pDC, x, y + 1, colorBoundary, colorNew);//右左下上
//BoundarySeedFill(pDC, x - 1, y, colorBoundary, colorNew);
BoundarySeedFill(pDC, x, y - 1, colorBoundary, colorNew);//右左下上
}
}
void CMFCApplication2View::OnBoundaryseedfill()
{
//TODO: 在此添加命令处理程序代码
boundarySeedDlg dlg;
if (dlg.DoModal() == IDOK) {//输入框显示时
//给全局变量赋值,获取对话框声明的变量
bx1 = dlg.b_BoundarySeedX11;
bx2 = dlg.b_BoundarySeedXX2;
by1 = dlg.b_BoundarySeedY1;
by2 = dlg.b_BoundarySeedYY2;
bzx =dlg.b_BoundarySeedZX;
bzy = dlg.b_BoundarySeedZY;
}
CDC* pDC = GetDC();//画矩形
pDC->Rectangle(bx1, by1, bx2, by2);
//pDC->Rectangle(200,200,300,300);
BoundarySeedFill(pDC,bzx, bzy, BLACK, GREEN);
//BoundarySeedFill(pDC,201, 230, BLACK, GREEN);
}
效果:
随便输入坐标点,比如(200,200),(300,300),(201,230),填充后如下图: