用桌面应用程序绘制分形和函数

效果演示

运行程序,默认绘制出如图样式的朱莉亚集合
zhuliya
按下键盘数字“2”可绘制曼德博集
mandebo
按数字键盘上的“3”到“6”可展示朱利亚集合的不同种样式 如下图所示
按下3效果:
c3
按下4效果:
c4
按下5效果:
c5
按下6效果:
c6
按下数字键盘上的“7”到“9”以及“0”,进入函数的绘制模块。屏幕上会分别展示如下几何图形的绘制。
按下7效果:
7
按下8效果:
8
按下9效果:
9
按下0效果:
10
(除正弦函数外)
在函数的绘制模块中,按键盘上的WASD可以进行图像的移动。
特殊情况:按下“0”后(即绘制正弦函数的情况),再按AD不能进行图像的平移,按AD只能改变图像的振幅大小。而WS可以对图像进行上下平移。
你可以进入如下链接来观看展示效果。
录制链接,提取码:qect

理论原理

完整代码见:完整源码,提取码:t0gc

按键部分的核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
case WM_KEYDOWN:
switch (wParam) {
case '1':
oper = '1';
NUMx = 0.02;
NUMy = 0.01;
InvalidateRect(hWnd, NULL, TRUE);
break;
case '2':
oper = '2';
InvalidateRect(hWnd, NULL, TRUE);
break;
case '3':
oper = '1';
NUMx = 0.2;
NUMy = 0.2;
InvalidateRect(hWnd, NULL, TRUE);
break;
case '4':
oper = '1';
NUMx = 0.5;
NUMy = 0.2;
InvalidateRect(hWnd, NULL, TRUE);
break;
case '5':
oper = '1';
NUMx = 0.7;
NUMy = 0.2;
InvalidateRect(hWnd, NULL, TRUE);
break;
case '6':
oper = '1';
NUMx = 0.2;
NUMy = 0.7;
InvalidateRect(hWnd, NULL, TRUE);
break;
case '7':
oper = '7';
InvalidateRect(hWnd, NULL, TRUE);
break;
case '8':
oper = '8';
InvalidateRect(hWnd, NULL, TRUE);
break;
case '9':
oper = '9';
InvalidateRect(hWnd, NULL, TRUE);
break;
case '0':
oper = '0';
InvalidateRect(hWnd, NULL, TRUE);
break;
case 'W':
step = step - 10;
InvalidateRect(hWnd, NULL, TRUE);
break;
case 'A':

amplitude = amplitude - 10;
InvalidateRect(hWnd, NULL, TRUE);
break;
case 'S':
step = step + 10;
InvalidateRect(hWnd, NULL, TRUE);
break;
case 'D':
amplitude = amplitude + 10;
InvalidateRect(hWnd, NULL, TRUE);
break;
}
break;

此处的WM_KEYDOWN触发按键事件,是用来结合WM_PAINT屏幕绘制事件中的switch(oper)的不同事件(case)来达到不同的绘制效果。(除三角函数外)而WASD是通过改变像素点的横纵坐标加的值(step和amplitude)来使函数移动的。三角函数的情况中,amplitude改变的振幅大小,step使其上下移动(即改变像素点纵坐标的值)。这是最基础也最容易实现的部分。
以下是我对所展示的分形的数学意义的理解。
mandeboji
zhuliyaji
理解数学意义后,就可以写出绘制的代码了。更进一步的转换流程如下图。
曼德博集:
manzhuan
朱莉亚集(为了省事,朱利亚集合这里,我将Z0的横纵坐标分别以Cx,Cy表示。其中,定值复数的横纵坐标分别用NUMy、NUMy来表示,因为用变量表示能方便后续调试。按下数字按键的3到6时候,改变的其实是NUMx NUMy的值,这样就不用重复写绘制朱利亚集合的代码了。):
zhuzhuan

朱利亚集合核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
while (Iteration < IterationMax && ((Zx2 + Zy2) < ER2))
{
if (Iteration == 0)
{

Zy = 2 * Cx*Cy + NUMy;
Zx = Cx * Cx - Cy * Cy + NUMx;
Zx2 = Zx * Zx;
Zy2 = Zy * Zy;
Iteration++;
}
if (Iteration >= 1)
{
Zx = Zx * Zx - Zy * Zy + NUMx;
Zy = 2 * Zx*Zy + NUMy;
Zx2 = Zx * Zx;
Zy2 = Zy * Zy;
Iteration++;
}

}

函数的数学意义:
玫瑰花函数:x=a* sin(nθ)* cos(θ), y=asin(nθ) sin(θ);对于方程式ρ=5* sin(3θ)、ρ=5 sin(2θ)、ρ=5 sin(3*θ/2),分别对应的是三叶、四叶和六叶玫瑰线;
阿基米德螺旋线:x=(a+pθ)cos(θ),y=(a+pθ)sin(θ);
圆:x^2+y^2=c^2;即x=cos(θ),y=sin(θ);
正弦函数:y=sinx;

函数部分的核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
switch (oper) {
case '1':
zhuliyajihe:
{int iX, iY;
double Cx, Cy;
double PixelWidth = (CxMax - CxMin) / iXmax;
double PixelHeight;
PixelHeight = (CyMax - CyMin) / iYmax;
COLORREF color;
double Zx, Zy;
double Zx2, Zy2;
double a = 0; double b = 0;
int Iteration;
double ER2;
ER2 = EscapeRadius * EscapeRadius;
for (iY = 0; iY < iYmax; iY++)
{
Cy = CyMin + iY * PixelHeight;
if (fabs(Cy) < PixelHeight / 2) Cy = 0.0;
for (iX = 0; iX < iXmax; iX++)
{
Cx = CxMin + iX * PixelHeight;
Zx = Zy = Zy2 = Zx2 = 0.0;
Iteration = 0;
while (Iteration < IterationMax && ((Zx2 + Zy2) < ER2))
{
if (Iteration == 0)
{

Zy = 2 * Cx*Cy + NUMy;
Zx = Cx * Cx - Cy * Cy + NUMx;
Zx2 = Zx * Zx;
Zy2 = Zy * Zy;
Iteration++;
}
if (Iteration >= 1)
{
Zx = Zx * Zx - Zy * Zy + NUMx;
Zy = 2 * Zx*Zy + NUMy;
Zx2 = Zx * Zx;
Zy2 = Zy * Zy;
Iteration++;
}

}
if (Iteration == IterationMax)
color = RGB(0, 0, 0);//前景色黑色
else
color = RGB(255, 255, 255);//背景色白色

SetPixel(hdc, iX, iY, color);
//
}
}
}
break;
mandeboji:
case '2':
{int iX, iY;//像素位置
double Cx, Cy;//数学公式中逻辑坐标
double PixelWidth = (CxMax - CxMin) / iXmax;
double PixelHeight;
PixelHeight = (CyMax - CyMin) / iYmax;
COLORREF color;

double Zx, Zy;
double Zx2, Zy2;
double a = 0; double b = 0;
int Iteration;//记录递归次数
double ER2;
ER2 = EscapeRadius * EscapeRadius;
for (iY = 0; iY < iYmax; iY++)
{
Cy = CyMin + iY * PixelHeight;
if (fabs(Cy) < PixelHeight / 2) Cy = 0.0;
for (iX = 0; iX < iXmax; iX++)
{
Cx = CxMin + iX * PixelHeight;
Zx = Zy = Zy2 = Zx2 = 0.0;
Iteration = 0;
while (Iteration < IterationMax && ((Zx2 + Zy2) < ER2))
{

Zy = 2 * Zx*Zy + Cy;
Zx = Zx2 - Zy2 + Cx;
Zx2 = Zx * Zx;
Zy2 = Zy * Zy;
Iteration++;
}
if (Iteration == IterationMax)
color = RGB(0, 0, 0);//前景色黑色
else
color = RGB(255, 255, 255);//背景色白色

SetPixel(hdc, iX, iY, color);
//
}
}
}
break;
disanzhe:
case '0':
{int iX, iY;//像素位置
double PixelWidth = (CxMax - CxMin) / iXmax;
double PixelHeight;
PixelHeight = (CyMax - CyMin) / iYmax;
COLORREF color;
for (iX = 0; iX < iXmax; iX++)
{
iY = sin(iX*3.1415926 / 180)*amplitude+step;
color = RGB(0, 0, 0);//前景色黑色
SetPixel(hdc, iX, iY, color);


}
}//c3
break;
meiguihua:
case '7':
{
int iX, iY;
int iXII;
double PixelWidth = (CxMax - CxMin) / iXmax;
double PixelHeight;
PixelHeight = (CyMax - CyMin) / iYmax;
COLORREF color;
for (iX = 0; iX < 800; iX++)
{
float ix = iX * 3.1415926 / 180;
iXII = 50 * sin(3*ix/2)* cos(ix)+ amplitude;
iY = 50 * sin(3*ix/2)* sin(ix)+step;
color = RGB(0, 0, 0);//前景色黑色
SetPixel(hdc, iXII, iY, color);


}

}
break;//c4
yuan:
case '9':
{

int iX, iY;
int iXII;
double PixelWidth = (CxMax - CxMin) / iXmax;
double PixelHeight;
PixelHeight = (CyMax - CyMin) / iYmax;
COLORREF color;
for (iX = 0; iX < 800; iX++)
{
float ix = iX * 3.1415926 / 180;
iXII = 50*cos(ix) + amplitude;
iY = 50*sin(ix) + step;
color = RGB(0, 0, 0);//前景色黑色
SetPixel(hdc, iXII, iY, color);


}
}
break;//c0
luoxuanxian:
case '8':
{
{

int iX, iY;
int iXII;
double PixelWidth = (CxMax - CxMin) / iXmax;
double PixelHeight;
PixelHeight = (CyMax - CyMin) / iYmax;
COLORREF color;
for (iX = 0; iX < 800; iX++)
{
float ix = iX * 3.1415926 / 180;
iXII = (5+5*ix) * cos(ix) + amplitude;
iY = (5+5*ix) * sin(ix) + step;
color = RGB(0, 0, 0);//前景色黑色
SetPixel(hdc, iXII, iY, color);


}
}
}
break;//c8
}

因为圆、螺旋线、玫瑰花并不是狭义上一个x对应一个y的函数,在平面坐标中需要用参数方程表示,因而我引入了一个新的临时变量iXII。最终绘制在屏幕上的像素点横坐标其实是iXII的值,而不是iX的值。
ix这个临时变量则是我用来进行角度和弧度转换的。平面坐标上的点对应的实数值应该是弧度,而不是角度。计算机默认运算时调用的则是角度,所以要进行换算。

后记

分形部分:绘制曼德博集和茱莉亚集

函数部分:绘制圆、螺旋线、玫瑰花、正弦函数

后续我会更新程序设计分类的更多内容,比如如何用C编写生命游戏等。