大家是否看过下面的视频:
它是著名的曼德博集合(Mandelbrot set),是一种在复平面上组成分形的点的集合,是数学家Benoit B. Mandelbrot(本华.曼德博)提出的分形理论中最著名的分形。
这个集合的奇妙之处在于将曼德博集合无限放大都能够有精妙的细节在内,而这瑰丽的图案仅仅由一个简单的公式生成。因此有人认为曼德博集合是"人类有史以来做出的最奇异、最瑰丽的几何图形",曾被称为"上帝的指纹"。
今天,我们将分享什么是分形理论,如何用MoonBit来绘制出曼德博分形,用MoonBit发现数学之美。
什么是分形理论
首先,让我们来了解一下什么是分形理论。
分形理论是由曼德博在1975年创造的,它源于拉丁语"fractus",意思是"破碎的"或"断裂的"。分形理论的数学基础是分形几何学,这一理论的最基本特点是用分数维度的视角和数学方法描述和研究客观事物。
也正是因为如此,它跳脱出了我们常规世界的维度,可以更加具体和真实地描述复杂系统,得以窥见客观事物的复杂和多样性。
由于分形具有"无限复杂性",你可能认为分形很难制作,但这是一个非常简单的过程。要做一个分形,只需要一遍又一遍地重复同样的过程。用数学术语来说,数学分形是一个迭代(递归的一种形式)的方程。
最著名的分形是曼德博集合,它来自于复数集合c。数学家Adrien Douady(阿德里安·杜阿迪)定义了下面的函数:
为致敬曼德博,将其命名为曼德博集合。当从z=0迭代时,不会向无穷大发散。从本质上来说,这是一个迭代公式,式子当中的变量都是复数。所以当你按照这个式子进行代入计算的时候,局部的图形都能和整体表现出相似的地方,并且这种相似往往是集中在细微之处的,要仔细观察才能发现。
如何用 MoonBit 绘制曼德博集合
接下来,我们将分享如何用MoonBit绘制曼德博集合。
为了确定我们要绘制的图形区域,必须先介绍区域坐标这个概念。一个复平面的点由一个复数来表示(d=x+yi)。加上 width 和 height 来确定复平面上一个长方形区域。
假设一个图像宽为w个像素,高为h个像素,我们需要计算w*h个像素的颜色,并且将它们画上。
我们使用 MooBit 来完成颜色的计算部分,然后将计算完的颜色传给 js,用 js canvas 来画图。
颜色的计算
JavaScript
pub func calc_color(col : Int, row : Int, ox : Float64, oy : Float64,
width : Float64) -> Int {
let pixel_size = width / image_width
let cx = (float_of_int(col) - coffset) * pixel_size + ox
let cy = (float_of_int(row) - roffset) * pixel_size + oy
var r = 0
var g = 0
var b = 0
var i = -1
while i <= 1 {
var j = -1
while j <= 1 {
let d = iter(
cx + float_of_int(i) * pixel_size / 3.0,
cy + float_of_int(j) * pixel_size / 3.0,
)
let c = get_color(d)
r = r + c.asr(16).land(0xFF)
g = g + c.asr(8).land(0xFF)
b = b + c.land(0xFF)
j = j + 1
}
i = i + 1
}
r = r / 9
g = g / 9
b = b / 9
return r.lsl(16).lor(g.lsl(8)).lor(b)
}
这里用于计算 row 行和 col 列那个像素所代表的复平面上的正方形的中心点的坐标。
JavaScript
let pixel_size = width / image_width
let cx = (float_of_int(col) - coffset) * pixel_size + ox
let cy = (float_of_int(row) - roffset) * pixel_size + oy
我们知道对一个复数c,它属于Mandelbrot集合,当且仅当下面这个递归定义得到的无穷复数列一直处于复平面上以原点为中心以2为半径的圆内:; 如果我们把写成这样实部和虚部分开的形式,同样也把 c 写成的形式(和分别为c的实部和虚部),那么上面的递归定义其实就是,;复数属于Mandelbrot集合,当且仅当对所有的自然数n,。calc_color接下来调用iter来计算 x_n 和 y_n。这个函数返回首次出逃逸半径时的迭代次数,如果迭代了 max_iter_number 次没有逃逸就返回-1.0。
JavaScript
pub func iter(cx : Float64, cy : Float64) -> Float64 {
var x = 0.0
var y = 0.0
var newx = 0.0
var newy = 0.0
var smodz = 0.0
var i = 0
while i < max_iter_number {
newx = x * x - y * y + cx
newy = 2.0 * x * y + cy
x = newx
y = newy
i = i + 1
smodz = x * x + y * y
if smodz >= escape_radius {
return float_of_int(i) + 1.0 - log(log(smodz) * 0.5) / log(2.0)
}
}
return -1.0
}
接着我们需要根据返回的迭代次数来选择相应的颜色。我们首先需要的是一个调色盘,这就是interpolation的作用,interpolation用于生成色彩梯度。
JavaScript
func interpolation(f : Float64, c0 : Int, c1 : Int) -> Int {
let r0 = c0.asr(16).land(0xFF)
let g0 = c0.asr(8).land(0xFF)
let b0 = c0.land(0xFF)
let r1 = c1.asr(16).land(0xFF)
let g1 = c1.asr(8).land(0xFF)
let b1 = c1.land(0xFF)
let r = floor((1.0 - f) * float_of_int(r0) + f * float_of_int(r1) + 0.5)
let g = floor((1.0 - f) * float_of_int(g0) + f * float_of_int(g1) + 0.5)
let b = floor((1.0 - f) * float_of_int(b0) + f * float_of_int(b1) + 0.5)
return r.lsl(16).lor(g.lsl(8).lor(b))
}
get_color 先将迭代次数进行一些转换然后传入 interpolation 来得到相应的颜色。
JavaScript
pub func get_color(d : Float64) -> Int {
if d >= 0.0 {
var k = 0.021 * (d - 1.0 + log(log(128.0)) / log(2.0))
k = log(1.0 + k) - 29.0 / 400.0
k = k - float_of_int(floor(k))
k = k * 400.0
if k < 63.0 {
return interpolation(k / 63.0, 0x000764, 0x206BCB)
} else if k < 167.0 {
return interpolation((k - 63.0) / (167.0 - 63.0), 0x206BCB, 0xEDFFFF)
} else if k < 256.0 {
return interpolation((k - 167.0) / (256.0 - 167.0), 0xEDFFFF, 0xFFAA00)
} else if k < 342.0 {
return interpolation((k - 256.0) / (342.0 - 256.0), 0xFFAA00, 0x310230)
} else {
return interpolation((k - 342.0) / (400.0 - 342.0), 0x310230, 0x000764)
}
} else {
return 0x000000
}
}
颜色的计算到此就完成了。
使用 canvas 绘制
创建一个 canvas:
HTML
<html>
<body>
<canvas id="canvas"></canvas>
</body>
在 js 代码中获取 canvas,设置 canvas 的大小:
JavaScript
let canvas = document.getElementById("canvas");
var IMAGEWIDTH = 800;
var IMAGEHEIGHT = 600;
canvas.width = IMAGEWIDTH;
canvas.height = IMAGEHEIGHT;
创建一个 ImageData 来保存计算好的像素的颜色:
JavaScript
var imagedata = context.createImageData(IMAGEWIDTH, IMAGEHEIGHT);
接着导入 MoonBit 代码:
JavaScript
WebAssembly.instantiateStreaming(fetch("target/mandelbrot.wasm"), spectest).then(
(obj) => {
obj.instance.exports._start();
const calcColor = obj.instance.exports["mandelbrot/lib::calc_color"];
const drawColor = obj.instance.exports["mandelbrot/lib::draw_color"];
//...
绘制图像:
JavaScript
function saveImage() {
context.putImageData(imagedata, 0, 0);
}
function generateImage() {
for (row = 0; row < IMAGEHEIGHT; row++) {
for (col = 0; col < IMAGEWIDTH; col++) {
let x = +ox.value;
let y = +oy.value;
let w = +width.value;
var color = calcColor(col, row, x, y, w);
drawColor(imagedata, col, row, color);
}
}
saveImage();
}
这就是具体实现的效果图:
Mandelbrot 的绘制涉及很多数学推导,这篇博客没有详细的解释,可以参考:eigolomoh.bitbucket.io/math/draw_m...
完整的代码:github.com/moonbitlang...
MoonBit 编程实践
欢迎来到「MoonBit编程实践」!本栏目将为你提供如何使用MooBit强大功能,轻松实现各种工业应用与创意项目。我们将与你分享实用的示例代码、项目构建步骤以及技术见解,无论你是编程新手还是经验丰富的开发者,都可以轻松玩转MoonBit。
我们也期待你积极地分享你的编程实践!让我们一起开启MoonBit编程之旅🎉