65.9K
CodeProject 正在变化。 阅读更多。
Home

使用HTML5生成曼德勃罗集分形

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (26投票s)

2011年11月17日

CPOL

6分钟阅读

viewsIcon

71323

downloadIcon

1601

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 中,您可以看到一个名为 cvCanvas。现在让我们定义 generateFractal 函数。

function generateFractal(formula, resetSize) {
}

formula 将是像我们的曼德尔布罗集公式一样的函数。我们将把公式函数——而不是它的值——传递给 generateFractal。如果 resetSizetrue,则偏移量和大小变量将设置为分形的默认值。现在,在页面加载时添加曼德尔布罗集生成。

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 毫秒用于刷新 Canvastim 将是当前超时的 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 < hCanvas 的高度大于 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 - 添加了朱利亚集
© . All rights reserved.