使用HTML5生成曼德勃罗集分形
HTML5 提供了一个很酷的新元素——Canvas。这是 SVG 的替代品。您可以使用 JavaScript 在其上绘图,因此我们也可以绘制分形。

引言
HTML5 提供了一个很酷的新元素——Canvas。这是 SVG 的替代品。您可以使用 JavaScript 在其上绘图。那么理论上,我们也可以绘制分形吗?是的,我们可以。本文将介绍如何使用 HTML5 Canvas 元素绘制曼德尔布罗集。大多数浏览器都支持 HTML5,包括 Internet Explorer 9,但 IE8 及更低版本不支持 Canvas。
为了绘图,我们必须获取 Canvas 的上下文:canvas.getContext('2d')。“2d”参数是上下文的名称。大多数浏览器仅支持 2D 上下文,但 Chrome 有实验性的 WebGL 支持 3D 上下文。接下来,使用上下文元素,我们可以绘制并获取或设置图像数据。在此曼德尔布罗集解决方案中,我们将使用获取和设置图像数据,因为我们将逐像素绘制,这比绘制微矩形更快。
背景
我从这个参考资料中学习了 2D 上下文接口。
脚本结构
脚本分为两部分:公式和查看器。公式是用于计算单个像素的独立函数。它们的输入是坐标(来自我的库的复数),输出是介于 0 和 1 之间的数字(迭代次数除以最大迭代次数)。查看器部分为 Canvas 中的所有坐标运行指定的公式,然后用特殊的调色板表示它们。查看器还处理缩放。得益于这种结构,它是可扩展的。我们可以轻松添加新公式,例如朱利亚集。
曼德尔布罗集公式
曼德尔布罗集需要复数。我用 JavaScript 为这些数字编写了一个简单的类。您可以从文章顶部下载它。所以将复数类添加到页面中。
<script type="text/javascript" src="complex.js"></script>
现在我们可以定义曼德尔布罗集函数了。
function mandelbrot(xy) {
}
xy 将是一个复数。此函数将是一个公式。生成它的算法是迭代的。我们将在每次迭代中重新计算复数 z。它的初始值是 xy。循环将一直进行,直到 z 的绝对值小于逃逸半径。逃逸半径通常等于 4。在曼德尔布罗集中心,迭代次数将是无限的。因此,我们必须定义最大迭代次数以避免无限循环。让我们在函数中定义 z,并用 i 来计数迭代次数。
var z = new Complex(xy.x, xy.y);
现在我们需要定义最大迭代次数、逃逸半径以及用于重新计算 z 的其他数字。默认偏移量和大小也将被声明。它们将在函数之后。
// Code of function ends.
mandelbrot.maxIter = 32; // Maximal iterations
// Power of fractal. For normal Mandelbrot it will be (2,0).
mandelbrot.power = new Complex(2.0, 0.0);
mandelbrot.bailout = 4.0; // Bailout value.
mandelbrot.offset = new Complex(-3.0, -2.0); // Default offset of fractal.
mandelbrot.size = new Complex(0.25, 0.25); // Default size of fractal.
现在我们可以定义公式的 while 循环了。
while (i < mandelbrot.maxIter && z.abs() <= mandelbrot.bailout) {
    z = z.pow(mandelbrot.power).add(xy); // Recalculating z.
    i++; // Iterations + 1.
}
现在我们可以计算迭代次数了。函数的返回值必须在 0 到 1 之间。1 是最大返回值,最大迭代次数是 maxIter。所以返回值将是 i 除以最大迭代次数。我还应用了一个平滑算法。
if (i < mandelbrot.maxIter) {
    // Smoothing algorithm.
    i -= Math.log(Math.log(z.abs())) / Math.log(mandelbrot.power.abs());
    return i / mandelbrot.maxIter;
}
else
    return 0.0;
带有其值的函数应该如下所示:
function mandelbrot(xy) {
    var z = new Complex(xy.x, xy.y);
    var i = 0;
    while (i < mandelbrot.maxIter && z.abs() <= mandelbrot.bailout) {
        z = z.pow(mandelbrot.power).add(xy);
        i++;
    }
    if (i < mandelbrot.maxIter) {
        i -= Math.log(Math.log(z.abs())) / Math.log(mandelbrot.power.abs());
        return i / mandelbrot.maxIter;
    }
    else
        return 0.0;
}
mandelbrot.maxIter = 32;
mandelbrot.power = new Complex(2.0, 0.0);
mandelbrot.bailout = 4.0;
mandelbrot.offset = new Complex(-3.0, -2.0);
mandelbrot.size = new Complex(0.25, 0.25);
查看器
首先,让我们创建页面的主体。
<h1>Mandelbrot</h1>
<div id="di" style="position: relative;">
    <canvas id="cv" width="580" height="580" 
            style="border-style: solid; border-width: 1px;"
            onmousedown="canvas_onmousedown(event);">
        Your browser doesn't support canvas.
    </canvas>
</div>
Div 保留供将来使用。在 div 中,您可以看到一个名为 cv 的 Canvas。现在让我们定义 generateFractal 函数。
function generateFractal(formula, resetSize) {
}
formula 将是像我们的曼德尔布罗集公式一样的函数。我们将把公式函数——而不是它的值——传递给 generateFractal。如果 resetSize 为 true,则偏移量和大小变量将设置为分形的默认值。现在,在页面加载时添加曼德尔布罗集生成。
onload="generateFractal(mandelbrot);"
定义以下全局变量:
var lastFormula; 	// Here we will store actual formula.
var tim; 		// In the future we will store here timeout for generating
         		// lines - it simulates asynchronous thread.
var offset = new Complex(-3.0, -2.0); 	// Offset of fractal view.
var size = new Complex(0.25, 0.25); 		// Scale of fractal view.
当第一行计算完成时,我们将设置超时(1 毫秒)来计算第二行。这 1 毫秒用于刷新 Canvas。tim 将是当前超时的 ID。首先,我们必须停止之前的计算(如果尚未完成)。然后我们必须将新公式设置为 lastFormula。
clearTimeout(tim);
lastFormula = formula;
然后我们必须(可选地)重置偏移量和大小。
if (resetSize) {
    offset = new Complex(formula.offset.x, formula.offset.y);
    size = new Complex(formula.size.x, formula.size.y);
} 
然后我们必须获取 Canvas 及其宽度和高度。
var w = cv.width;
var h = cv.height;
现在我们必须获取 Canvas 的上下文和图像数据。也定义 y。它将是实际渲染的行数。
var g = cv.getContext("2d"); // Image context.
var img = g.getImageData(0, 0, w, h); // Image data.
var pix = img.data; // Table of canvas image data.
现在在 generateFractal 中声明 drawLine 函数。
function drawLine() {
}
现在,如果 y < h(Canvas 的高度大于 0)
if (y < h)
tim = setTimeout(drawLine, 1);
嵌套函数 drawLine 将计算分形的一条线,将分形的渲染部分设置到 Canvas,然后为其设置下一个线条的超时。让我们编写这段代码。但首先,我们需要调色板和用于绘制像素的函数。我将不解释调色板的工作原理。以下是从公式返回值获取颜色的函数:
function getColor(i) {
    var k = 1.0 / 3.0;
    var k2 = 2.0 / 3.0;
    var cr = 0.0;
    var cg = 0.0;
    var cb = 0.0;
    if (i >= k2) {
        cr = i - k2;
        cg = (k - 1) - cr;
    }
    else if (i >= k) {
        cg = i - k;
        cb = (k - 1) - cg;
    }
    else {
        cb = i;
    }
    var r = parseInt(cr * 3 * 255);
    var g = parseInt(cg * 3 * 255);
    var b = parseInt(cb * 3 * 255);
    return [r, g, b];
}
将其放入 generateFractal 函数中。现在让我们定义绘制像素的函数。它必须能够访问 pix——像素表。所以将其放在生成分形函数内部。
function drawPixel(x, y, i) {
    var c = getColor(i);
    var off = 4 * (y * w + x);
    pix[off] = c[0];
    pix[off + 1] = c[1];
    pix[off + 2] = c[2];
    pix[off + 3] = 255;
}
第一行通过公式的返回值获取颜色。第二行,我们计算要绘制的像素的起始位置。每个像素在数组中分配 4 个字节(R、G、B、A)。在最后 4 行中,它设置像素的字节。最后一个字节是 255,因为图像不是半透明的(这个通道是 alpha)。现在您可以编写 drawLine 函数了。
首先,定义循环:
for (var x = 0; x < w; x++) {
    var c = formula(new Complex(x / w / size.x + 
                    offset.x, y / h / size.y + offset.y));
    drawPixel(x, y, c);
}
在变量 c 中,我们计算了公式的值。它将缩放和平移后的位置传递给公式。接下来,它将指定的像素绘制到 Canvas。计算完一行后,我们必须更新 Canvas。
g.putImageData(img, 0, 0);
现在,我们必须为计算下一行设置超时(如果下一行的编号小于 Canvas 的高度)。
if (++y < h)
tim = setTimeout(drawLine, 1);
基本查看器已准备就绪。
朱利亚集公式
朱利亚集公式与曼德尔布罗集非常相似。有一个改动:在循环中不添加像素,而是添加种子。种子是一个点。它可以是随机的,也可以是从曼德尔布罗集中选择的。
function julia(xy) {
    var z = new Complex(xy.x, xy.y);
    var i = 0;
    while (i < julia.maxIter && z.abs() <= julia.bailout) {
        z = z.pow(julia.power).add(julia.seed);
        i++;
    }
    if (i < julia.maxIter) {
        i -= Math.log(Math.log(z.abs())) / Math.log(julia.power.abs());
        return i / julia.maxIter;
    }
    else
        return 0.0;
}
julia.maxIter = 32;
julia.power = new Complex(2.0, 0.0);
julia.bailout = 4.0;
julia.offset = new Complex(-2.0, -2.0);
julia.size = new Complex(0.25, 0.25);
julia.seed = new Complex(0.0, 0.0);
演示中的其他功能
我还实现了分形缩放。它处理 Canvas 和主体的鼠标事件。当您按下鼠标按钮时,它会保存第一个位置;当您按住按钮时,它会保存第二个位置。它会计算分形的新偏移量和比例。此外,当您移动鼠标时,它会显示一个预览——一个红色矩形(具有绝对定位的 div)。
在演示中,还有切换功能:您可以选择曼德尔布罗集上的一个点,然后生成朱利亚集分形。
为了保存分形,我获取 Canvas 的数据 URL 并将其设置为一个图像。然后用户可以将图像保存到磁盘。数据 URL 是带有图像数据的十六进制路径。
您可以在源代码中查看这些函数的详细信息。
结论
HTML5 Canvas 是一个很棒的元素,可以使用 JavaScript 动态绘图。我们也可以用它来绘制分形。
历史
- 2011-12-22 - 添加了朱利亚集




