使用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 - 添加了朱利亚集