Brotplot 2.0: 创建新程序






1.60/5 (2投票s)
使用 HTML5 的 canvas 和 JavaScript 创建一个 Mandelbrot 绘图器。
截图库
引言
2002 年,在阅读了 James Gleick 的混沌:一门新的科学(维京企鹅公司,1987 年)中的这个脚注后,我创建了我的第一个 Mandelbrot 绘图器。
Mandelbrot 集程序只需要几个基本的部分。 主要引擎是一个指令循环,它获取其起始复数并对其应用算术规则。 对于 Mandelbrot 集,规则是:Z→Z2+C,其中Z从零开始,C是对应于正在测试的点的复数。[...]为了跳出这个循环,程序需要监视运行的总数。 如果总数趋于无穷大,越来越远离平面中心,则原始点不属于该集合,并且如果运行的总数在其真实或虚数部分中变得大于 2 或小于 -2,则它肯定会趋于无穷大——程序可以继续。 但是,如果程序重复计算多次而不大于 2,则该点是该集合的一部分。[...]程序必须针对网格上的数千个点重复此过程,比例可以调整以获得更大的放大倍数。 并且程序必须显示其结果。(第 231 页) |
这看起来足够简单,我想我可以编写这样的程序; 我很兴奋。 我决定使用 C++ 编写一个绘图器,一个小时后,我正在看分形。 快进 10 年,我又来了。 我一直在寻找一个项目,我可以用它来学习 HTML5 和 jQuery,并且在阅读了一些关于 canvas 标签如何工作的信息后,我意识到一个新的 Mandelbrot 绘图器会很好地服务于这个目的。
在本文中,我旨在涵盖该程序的详细信息。 我不打算解释 HTML5 或 jQuery UI 的基础知识,当然也不打算涵盖高级复数分析或动力学。 也就是说,不需要这样的专业知识来理解该程序的设计和功能。 您只需要具备一些 HTML5、JavaScript 和一些大学水平的代数知识。
数学:没有那么复杂
Mandelbrot 集是一个复数集,它具有分形边界,其图像以其美丽和无限细节而闻名。 复数集是由复数组成的集合。 复数是任何形式为a+bi的数,其中a和b是实数,i是定义为i=√-1的虚数单位。 随后,a被称为实部,bi被称为虚部。 复数变量通常以粗体大写字母书写(例如,Z,C)以区别于普通变量。 迭代函数Z→Z2+C如上面的脚注中所述,重复应用于每个复数点C。 如果结果复数的实部或虚部曾经大于 2,那么我们就知道原始点C不属于该集合。 为了绘制该集合,这些复数的实部沿 x 轴绘制,虚部沿 y 轴绘制。 为了执行计算,必须将函数分成实部和虚部,如下所示
Z→Z2+C
(a+bi)→(a+bi)2+(a0+b0i)
(a+bi)→(a+bi)(a+bi)+(a0+b0i)
(a+bi)→(a2+2abi+bi2)+(a0+b0i)
(a+bi)→a2+2abi-b2+a0+b0i
(a+bi)→(a2-b2+a0)+(2ab+b0)i
an = an-12-bn-12+a0
bn = 2an-1bn-1+b0
对于an和bn的得到的递归方程给出了基于点的前一个值和点本身来计算点的第n次迭代的显式函数。
绘图器:从数学到代码
程序的主要组件是绘图器;其他一切的存在只是为了支持它及其功能。 绘图器的核心是 canvas 元素。 首先调整 canvas 的大小,以便为 UI 的所有元素留出空间,然后强制为 4:3 的比例。(注意:除了作者的偏好之外,没有其他原因)
width = $(document).innerWidth()-$("#ERightPanel").width()-50;
height = $(document).innerHeight()-$("#EMainDisplay").position().top-157; // top+gallery = 157
if(width*(3/4)
In addition to the canvas, the plotter also uses three JavaScript arrays: two value arrays—aiX and aiY—for the real and imaginary components and an active flag array. All of the arrays are sized the same as the canvas, so there is a one-to-one correspondence between indexes of the various arrays. All points begin with a 0 value in both value arrays and are flagged as true in the active array.
for(var i=0;i
The real and imaginary components of the value for each point are computed separately according to the functions described above and stored in their respective value arrays. Since the component functions rely on the component values of the original point, it is not possible to simply traverse the arrays in a linear fashion—the (x, y) coordinates of the current point (a0, b0) must be known. To calculate the needed components, some simple residue arithmetic using the current array index, i, is used.
a0 = i%width
b0 = Math.Floor(i/width)
Translation of the plot is achieved by altering the original point C by adding an offset term like this:
a0 = (i%width-xOff)
b0 = (Math.Floor(i/width)-yOff)
Magnification of the plot is achieved in a similar manner by multiplying by a scaling factor like this (where fMag ≤ 1):
a0 = (i%width-xOff)*fMag
b0 = (Math.Floor(i/width)-yOff)*fMag
For each index traversed in the array, the plot moves an fMag'th of a point offset from the origin. The final computation of the plot, incorporating the above into the recurrence equations, looks like this:
var x = aiX[i];
var y = aiY[i];
aiX[i] = x*x-y*y+(i%width-xOff)*fMag
aiY[i] = 2*x*y+(Math.Floor(i/width)-yOff)*fMag
Armed with this computation, the plotter can traverse the value arrays, applying the arithmetical rule to each point as described in the footnote. After initializing the display, a JavaScript interval is set to call the frame() function and update the display.
t = setInterval(frame, 1);
function frame()
{
if(!colorMan.bActive)
return;
var p;
if(beginColorCycle)
colorMan.inc(colorFunc);
var r = colorMan.comp(0);
var g = colorMan.comp(1);
var b = colorMan.comp(2);
for(var i=0; i= 2 || aiY[i] >= 2)
{
afActive[i] = false;
beginColorCycle = true;
continue;
}
if(!beginColorCycle)
continue;
p = i*4;
surface.data[p] = r;
surface.data[p+1] = g;
surface.data[p+2] = b;
}
context.putImageData(surface, 0, 0);
}
If the value of either component is greater than or equal to 2, the point is flagged as false in the active array. The plotter takes an early-out approach, skipping the point immediately if it is not active. If, after its value is updated, the point is still active, its color is then incremented. The plotter does this once for each point in the plot and then repeats the process.
Colorizing of the pixels is handled by an object delBrot, named such since it controls the color gradient of the plot. This approach allows a great deal of flexibility in handling multiple colorizing schemes since the color data is maintained internally. Because it is the color manager, the object instance is named colorMan and, when combined with the object name, this of course results in a colorMandelBrot.
UI: controlling the chaos
The user interface of this program relies heavily upon jQuery UI. All of the jQuery UI widgets used in the program are virtually unaltered versions of the demos at jqueryui.com [link]. In typical jQuery fashion, they are all initialized in the $(document).ready() handler.
The primary component is an animated accordion [link]. Its purpose is to hide information until requested. The specific directions pertaining to the various sections of the program are hidden until that section is activated. This prevents the user from being overwhelmed by a screen full of text. This also allows for very thorough instructions to be written without worry as to the total amount of text. When the accordion's state changes, the handler checks which section is active and either shows or hides pieces of the UI as they gain or lose focus.
$("#EControls").accordion
({
//fillSpace: true,
autoHeight: false,
change: function(event, ui)
{
var active = $("#EControls").accordion("option", "active");
if(active == 3)
{
if(!$("#EImageGallery").is(":visible"))
$("#EImageGallery").show("drop", null, 500);
}
else
if($("#EImageGallery").is(":visible"))
$("#EImageGallery").hide("drop", null, 500);
if(active == 1)
{
if(!$("#ERightPanelLower").is(":visible"))
{
loadCoord();
$("#ERightPanelLower").show("drop", {direction: "right"}, 500);
}
}
else
if($("#ERightPanelLower").is(":visible"))
$("#ERightPanelLower").hide("drop", {direction: "right"}, 500);
}
});
Another important component is the animated slider. This component is used for the saved coordinate picker as well as the image gallery. Here, the interface uses motion to indicate relevance (i.e. "Hey, that thing's moving—it must be related to the thing I just clicked."). This is important because several of the UI elements are placed outside the main accordion component to reduce clutter and keep the accordion’s size consistent. This means that related elements are displayed in different areas of the screen, and so there is a risk of the user not realizing that they are meant to be used together. Animating the separated components when they become relevant should help the user make that association.
The plot can be controlled via the mouse by following the instructions in the first section of the accordion. The Plot controls section contains additional controls over the translation and zoom of the plot. These controls are all implemented as adjustments to the plot parameters. Plot translation is acheived by adjusting the plot offset by the vector <center - clicked point>.
$("#EMainDisplay").mousedown(function(e)
{
var left = e.pageX-$("#EMainDisplay").offset().left;
var top = e.pageY-$("#EMainDisplay").offset().top;
switch(e.which)
{
case 1:
xOff += xCenter-left;
yOff += yCenter-top;
break;
case 3:
var f = $("#EFactor").val();
if(e.shiftKey)
f=1/f;
fMag /= f;
xOff = xCenter-(left-xOff)*f;
yOff = yCenter-(top-yOff)*f;
break;
}
initDisplay();
});
Zoom is controlled in a similar manner, by multiplying or dividing the plot scaling factor by the zoom factor.
The dynamic range option in the Render controls section is enabled by default and increases the visible detail of the plot. At higher magnifications, it can take many iterations before pixels begin to get flagged out of set. Without dynamic range enabled, the plot would immediately begin to increment the color values of pixels, wasting the available colors on an essentially blank image. Dynamic range supresses colorization until the first pixel is flagged out of set.
$("#EDRange").is(":checked") ? beginColorCycle = false : beginColorCycle = true;
function frame()
{
...
if(aiX[i] >= 2 || aiY[i] >= 2)
{
afActive[i] = false;
beginColorCycle = true;
continue;
}
if(!beginColorCycle)
continue;
...
}
All of the colorization schemes are handled by delBrot. The default colorization mode increments a single color component from 0-255 before moving to the next color, starting with red, then green, and then blue. Once all the color components are at maximum value (pure white), delBrot disables itself.
function(mode)
{
switch(mode)
{
case 0: //default, single-pass
case 1: //color cycling
if(!this.bActive)
return;
if(aiComponents[0]<255)
aiComponents[0]++;
else
{
if(aiComponents[1]<255)
aiComponents[1]++;
else
{
if(aiComponents[2]<255)
aiComponents[2]++
else
{
if(mode==0)
this.bActive = false;
else
{
aiComponents[0]=0;
aiComponents[1]=0;
aiComponents[2]=0;
}
}
}
}
break;
case 2: //color oscillation
if(!this.bActive)
return;
if(bForward)
if(aiComponents[0]<255)
aiComponents[0]++;
else
{
if(aiComponents[1]<255)
aiComponents[1]++;
else
{
if(aiComponents[2]<255)
aiComponents[2]++
else
{
bForward = false;
}
}
}
else
if(aiComponents[2]>0)
aiComponents[2]--;
else
{
if(aiComponents[1]>0)
aiComponents[1]--;
else
{
if(aiComponents[0]>0)
aiComponents[0]--;
else
bForward = true;
}
}
break;
}
};
The color cycling mode functions the same as the default mode except that it resets the current color to black upon reaching the end. Color oscillation increments the individual color channels in the same manner as the other modes except that once it's reached the end it begins to decrement the color, causing the active pixels to oscillate between black and white.
HTML5's localStorage object allows for plot settings as well as images to be saved locally. Two master entries are used—plots and shots—to store the names of the entries containing the actual plot and image data. Since web storage only works with text, image data is first converted to base-64 encoded text using the canvas's toDataURL() method. Unfortunately, web storage is typically very limited and uncompressed image data consumes a lot of space, so local storage of image data is usually limited to 3–6 pictures.
Closing
I have been very pleased with the way this project turned out. My intention was to learn HTML5 and jQuery, and this project allowed me to do just that. I personally find it extra rewarding that, in this version, I was able to implement some features that I never got around to implementing in my previous version, such as improved color controls and the ability to take screenshots. Of course, as with the previous version, this time I am again leaving behind some unfinished ideas (to be implemented perhaps in another 10 years or so). There are, for example, a handful of bugs of which I'm aware; I have some ideas for user-defined color schemes; and I'd really like to add some parallelism using web workers. As it is, even without parallel processing, this version is not much slower than my old C++ version (and the enhanced image quality and UI features more than make up for the difference). If I write another version, the next one will likely be in assembly. I'd be curious to do a side-by-side speed test and, also, I'd like to write a version that allows for arbitrarily small real numbers (limited by computer memory, of course). The current version can reach a magnification factor of only 1016 due to loss of precision, so I'd like to create a version in assembly that can handle even tinier numbers to see how far down the rabbit hole really goes. For the time being, however, I think I'm going to call this one “done”—again.
Thanks for reading, and I hope you enjoy using the program as much as I enjoyed making it!