数学科普
自绘Mandelbrot集合(三)
作者:安迁
六、Java程序代码
本节给出Java程序代码(JavaScript版本见下节)。目前这个程序运行后只能绘出一个全红的800*600的png格式图像,并存在c:盘的temp文件夹下,名为”image.png”。如果你的c:盘下还没有temp文件夹,就请先创建一下。
package mandelbrot;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class Mandelbrot {
// (1)可调参数
double ESCAPERADIUS = 4.0;
int MAXITERNUMBER = 1000;
// -0.743030 + 0.126433i @ 0.016110 /0.75
double OX = -0.743030;
double OY = 0.126433;
double WIDTH = 0.016110;
double RATIO = 0.75;
int IMAGEWIDTH = 800;
// (2)计算所得参数
int IMAGEHEIGHT = (int) (IMAGEWIDTH * RATIO);
double PIXELSIZE = WIDTH / IMAGEWIDTH;
double COFFSET = IMAGEWIDTH % 2 == 0 ? (IMAGEWIDTH / 2) - 0.5 : (IMAGEWIDTH / 2);
double ROFFSET = IMAGEHEIGHT % 2 == 0 ? (IMAGEHEIGHT / 2) - 0.5 : (IMAGEHEIGHT / 2);
// (3)图象缓存
BufferedImage image = new BufferedImage(IMAGEWIDTH, IMAGEHEIGHT, BufferedImage.TYPE_INT_RGB);
// (4)程序入口
public static void main(String[] args) {
new Mandelbrot().draw();
}
// (5)主程序
void draw() {
for (int row = 0; row < IMAGEHEIGHT; row++) {
for (int col = 0; col < IMAGEWIDTH; col++) {
int color = calcColor(col, row);
drawColor(col, row, color);
}
}
saveImage();
}
// (6)计算颜色
int calcColor(int col, int row) {
double cx = (col - COFFSET) * PIXELSIZE + OX;
double cy = (row - ROFFSET) * PIXELSIZE + OY;
double d = iter(cx, cy);
return getColor(d);
}
// (7)迭代计算
double iter(double cx, double cy) {
return 0;
}
// (8)调色盘
int getColor(double d) {
return 0xFF0000;
}
// (9)在图像像素(col, row)处画上颜色rgb
void drawColor(int col, int row, int rgb) {
image.setRGB(col, IMAGEHEIGHT - row - 1, rgb);
}
// (10)保存图像
void saveImage() {
try {
ImageIO.write(image, "png", new File("c:\\temp\\image.png"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
七、JavaScript程序代码
要使用HTML内嵌JavaScript程序的朋友则请将下列代码拷贝粘贴到纯文本编辑器中(比如Windows下的Notepad),并存为html文件,可将其命名为“mandelbrot.html”,然后用网络浏览器打开。你应该能看到显示有一个红色的长方形的页面。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Mandelbrot</title>
<script>
// (4)程序入口
window.onload = function() {
// (1)可调参数
var ESCAPERADIUS = 4.0;
var MAXITERNUMBER = 1000;
// -0.743030 + 0.126433i @ 0.016110 /0.75
var OX = -0.743030;
var OY = 0.126433;
var WIDTH = 0.016110;
var RATIO = 0.75;
var IMAGEWIDTH = 800;
// (2)计算所得参数
var IMAGEHEIGHT = IMAGEWIDTH * RATIO;
var PIXELSIZE = WIDTH / IMAGEWIDTH;
var COFFSET = IMAGEWIDTH % 2 == 0 ? (IMAGEWIDTH / 2) - 0.5 : (IMAGEWIDTH / 2);
var ROFFSET = IMAGEHEIGHT % 2 == 0 ? (IMAGEHEIGHT / 2) - 0.5 : (IMAGEHEIGHT / 2);
// (3)图象缓存
var canvas = document.getElementById("image");
var context = canvas.getContext("2d");
canvas.width = IMAGEWIDTH;
canvas.height = IMAGEHEIGHT;
var imagedata = context.createImageData(IMAGEWIDTH, IMAGEHEIGHT);
// (5)主程序
for (row = 0; row < IMAGEHEIGHT; row++) {
for (col = 0; col < IMAGEWIDTH; col++) {
var color = calcColor(col, row);
drawColor(col, row, color);
}
}
saveImage();
// (6)计算颜色
function calcColor(col, row) {
var cx = (col - COFFSET) * PIXELSIZE + OX;
var cy = (row - ROFFSET) * PIXELSIZE + OY;
var d = iter(cx, cy);
return getColor(d);
}
// (7)迭代计算
function iter(cx, cy) {
return 0;
}
// (8)调色盘
function getColor(d) {
return 0xFF0000;
}
// (9)在图像像素(col, row)处画上颜色rgb
function drawColor(col, row, rgb) {
var pindex = ((IMAGEHEIGHT - row - 1) * IMAGEWIDTH + col) * 4;
imagedata.data[pindex] = (rgb>>16) & 0xFF;
imagedata.data[pindex+1] = (rgb>>8) & 0xFF;
imagedata.data[pindex+2] = rgb & 0xFF;
imagedata.data[pindex+3] = 255;
}
// (10)保存图像
function saveImage() {
context.putImageData(imagedata, 0, 0);
}
};
</script>
</head>
<body>
<canvas id="image" width="0" height="0"></canvas>
</body>
</html>
八、语言相关部分
首先我们解决掉和所使用的计算机语言密切相关,所以在不同语言之间写法很不同的部分。这些部分和Mandelbrot集合的绘制算法其实没什么关系,属于相应语言中的特定知识,我在此只稍微提一下,不作详细解释。
首先是特定的声明,包括Java程序中“// (1)可调参数”前面的那些行,以及HTML文件中不包含在<script>
和</script>
之间段落内(即JavaScript程序部分)的那些行,大多属于“如果懂这门语言就必定懂为什么这么写”的内容,不多做解释。除了HTML中的这句
<canvas id="image" width="0" height="0"></canvas>
这是可以看作是创建了一块画布挂在网页上,画布的标识号“id”为“image”。我们将要画出来的图像,就是要显示在这块画布上。
然后是两个版本也都有的“(4)程序入口”部分,程序开始运行时就从这里进入,注意到JavaScript版本的入口在JavaScript程序部分的最上方,而Java版本则是从main()
函数开始。
和具体绘图相关的则是第(3)、(9)、(10)部分。
“(3)图象缓存”部分都是定义了一个图像缓存。两种语言的绘图方式都必须先在缓存中进行,绘完以后才一次性地用“(10)保存图像”中的saveImage()
函数表现出来。Java版本的saveImage()
函数中的语句
ImageIO.write(image, "png", new File("c:\\temp\\image.png"));
是将图像保存在“c:\temp”文件夹下的image.png文件中,如果把上面语句中的两个“png”都改成“jpg”,那么存成的文件就是jpeg格式的。大家可以按自己喜好决定。而JavaScript版本的saveImage()
是将图像缓存内容表现在刚才说到的标识号为“image”的画布上,从而让我们在浏览器上看到画出的图像。
“(9)在图像像素(col, row)处画上颜色rgb”则就是在第四节中提到的点彩画法,呼叫drawColor(col, row, rgb)
函数就能在图像缓存的第col列第row行那个像素上点上颜色代码为rgb的颜色。关于以整数表示的RGB颜色代码,我们在谈论调色盘时会作较具体的介绍。特别值得提一句的是,如果仔细看程序代码的话,在两个版本中,当我们宣称是在第row行画点时,我们其实都是在图像缓存的第IMAGEHEIGHT - row - 1行画点。
image.setRGB(col, IMAGEHEIGHT - row - 1, rgb);
这是因为在数学习惯上,Y轴的方向朝上,越往上纵坐标越大;而在计算机绘图中,Y轴的方向通常则朝下。所以如果我们真的是在图像缓存的第row行画点的话,画出来的Mandelbrot集合图像会是上下颠倒的。
上述两个版本的特定声明部分,程序入口的第(4)部分以及和具体绘图相关的第(3)、(9)、(10)部分一直到本文结束都不会再变动,我也不再加解释。
九、绘图算法部分
剩下的(1)、(2)、(5)、(6)、(7)、(8)部分则是和Mandelbrot集合的绘制具体相关部分,如果对比两个版本,可以发现它们很相似。只是由于两种语言的对数据类型的处理方式不同,在Java语言中必须显式地说明每个变量,每个函数的参数以及返回值的数据类型(int
,double
等),而在JavaScript语言中则只须作var
和function
声明。相信有一定的计算机编程经验,但没有学过这两种语言的读者也容易看懂。
在“ (1)可调参数”部分定义了7个参数,以Java版本为例:
// (1)可调参数
var ESCAPERADIUS = 4.0;
var MAXITERNUMBER = 1000;
// -0.743030 + 0.126433i @ 0.016110 /0.75
double OX = -0.743030;
double OY = 0.126433;
double WIDTH = 0.016110;
double RATIO = 0.75;
var IMAGEWIDTH = 800;
前两个参数先略过,接下去4个参数定义了一个区域坐标,而最后的IMAGEWIDTH则定义了要绘制的图像的宽度即横向像素数。读者可以在任何时候尝试改变这些参数,尤其是试用本文许多插图下的区域坐标定义,来验证是否能绘出和插图一致的区域的图像。
“ (2)计算所得参数”中则计算了一些通过在(1)中的参数所计算得到的参数,比如所绘图像高度,每个像素所代表的在复平面上的正方形的边长等等。COFFSET和ROFFSET被用在下面要介绍的“(6)计算颜色”函数中,它们的计算公式也许对不熟悉这样写法的读者来说有点奇怪:
double COFFSET = IMAGEWIDTH % 2 == 0 ? (IMAGEWIDTH / 2) - 0.5 : (IMAGEWIDTH / 2);
可其实这无非是在说,如果IMAGEWIDTH是偶数(IMAGEWIDTH % 2
等于0的话),那么COFFSET的值就是(IMAGEWIDTH / 2) - 0.5
;如果IMAGEWIDTH是奇数,那么COFFSET的值就是IMAGEWIDTH / 2
,但因为这是整数除法,要去掉余数,所以结果其实和(IMAGEWIDTH-1) / 2
相同。
“(5)主程序”部分就是第四节中介绍的点彩绘图原理。两个针对行和列的循环遍历图片上的每个像素,循环体则是两句无比简单的呼叫:
int color = calcColor(col, row);
drawColor(col, row, color);
第一句用我们将介绍的(6)中的calcColor()
函数来计算每个像素的颜色,再用前面介绍的(9)drawColor()
方法在图像缓存的对应像素上画出计算出来的颜色。两个循环执行后,所有像素都绘制完毕,最后呼叫(10)saveImage()
来表现图像。
上面提到的(1)、(2)、(5)部分,除了会变动可调参数的数值外,一直到本文结束也不再改变。
本文中我们将要重大修改的则是剩下的(6)、(7)、(8)部分,当然也是关键。不过目前看起来它们也简单得很。仍以Java版本为例。
// (6)计算颜色
int calcColor(int col, int row) {
double cx = (col - COFFSET) * PIXELSIZE + OX;
double cy = (row - ROFFSET) * PIXELSIZE + OY;
double d = iter(cx, cy);
return getColor(d);
}
首先计算了两个值cx和cy,它们其实就是第col列第row行那个像素所代表的复平面上的正方形的中心点的坐标,计算中用到了前面计算的参数COFFSET、ROFFSET和PIXELSIZE。“有WIDTH长度的线段,其中心点坐标为OX。线段被均匀分成IMAGEWIDTH段,试问第col段的中心点cx是多少?”这是个非常简单的数学问题,我不作详细推导了,只需注意“第n段”的n是从0开始算的,最左边的是第0段。
计算出像素中心点在复平面上的坐标后,我们将用这个坐标代表的复数进行Mandelbrot集合定义中的迭代运算,即呼叫(7)中的iter()
函数,然后调用(8)中的调色盘函数来取得对应的颜色。也就是说,这个像素就被它的中心点代表了。
不过目前(7)和(8)中没什么具体的东西,无论cx和cy是什么,最终(6)总是返回十六进制数FF0000,这是红色的颜色代码,所以绘出的图是一片通红。
枯燥的准备工作终于做完,下面我们将逐步填充和修改(6)、(7)、(8)部分,以达到我们绘制Mandelbrot集合图像的目的。注意到上面两个版本的程序的长度都大约是80行,为达到最终程序只有150行的目标,我们只能再填充70行程序。