Julia 朱利亚分形:拖动常数 c 看图实时变化
🦀 Rust + WASM 实战系列 第 17 篇 阅读时间:约 6 分钟 | 实战可运行
📌 写在前面
上一篇讲了 Mandelbrot——c 在复平面上变化(每个像素一个 c),z₀ = 0 固定。
Julia 是 Mandelbrot 的孪生兄弟——迭代公式完全一样:z_{n+1} = z_n² + c,但参数角色互换:
算法代码几乎一样(只交换两个参数的循环范围)。真正的价值不是"换参数"——而是实时拖动 c 看图变化。
🚀 TL;DR
- 跟 Mandelbrot 公式完全一样
- 区别:Julia 的 c 是固定的常数(前端滑块实时调)
- 结果:拖动 c 时整个图案会戏剧性变化——这是 Mandelbrot 做不到的
📖 目录
一、Julia 的真正乐趣
Mandelbrot 没法"调 c"——因为 c 已经是图的 x 轴(整个集合就是 c 的可视化)。
Julia 可以拖动 c 滑块实时看图变化——这是 Mandelbrot 做不到的。
拖动 c 实部 → 图案的"骨架"在水平方向拉伸拖动 c 虚部 → 图案的"细节密度"在垂直方向变化拖动到某些区域 → 图案突然从"无限细节"变成"全黑 / 全白"(c 落在 Mandelbrot 集合内)
直觉:Mandelbrot 像是"一张固定的地图",Julia 像是"一个可以变形的地图"。
二、为什么公式一样但效果完全不同?
Mandelbrot 和 Julia 完全相同的迭代规则,只是初始化不同:
| 算法 | 来源 | 来源 |
|---|---|---|
| Mandelbrot | 每个像素一个 c(从像素映射) | 永远 0 |
| Julia | 固定常数(滑块控制) | 每个像素一个 (从像素映射) |
为什么效果天差地别?
- Mandelbrot 把 c 当"主角"——所以整个集合就是 c 的所有可能性的可视化
- Julia 把 c 当"背景设定"——所以每个 c 决定一种"物理规则",整个复平面在这个规则下演化
类比:Mandelbrot 像是"一张地图",Julia 像是"一个地图 + 一组物理参数"。
三、算法:和 Mandelbrot 的关键 2 行差异
公式回顾(详见 本系列上一节)
迭代公式 拆成实部/虚部(task 16 §二 有详细推导):
逃逸判断:。
Mandelbrot vs Julia:参数角色对比
| 维度 | Mandelbrot | Julia |
|---|---|---|
| 来自 | 每个像素(从像素映射) | 固定常数(滑块控制) |
| 来自 | 永远 0 | 每个像素(从像素映射) |
| 物理意义 | 看每个 "命运如何" | 看每个 在固定 规则下"命运如何" |
Julia 的核心循环
for each pixel (px, py):
// ① 像素 → z₀(不再是 c!)
z_re = center_x + (px - w/2) / zoom
z_im = center_y + (py - h/2) / zoom
// ② c 是循环外的固定常数
for iter:
if zr² + zi² > 4: 停止 // 逃逸
new_zr = zr² - zi² + c_re // 实部
new_zi = 2*zr*zi + c_im // 虚部
zr, zi = new_zr, new_zi
就这 2 行差异(Mandelbrot 里像素→c,Julia 里像素→),其他完全一样。
一个具体的 Julia 计算
取 (海马谷),画布中心点 (注意 Julia 从像素位置出发,所以画布中心对应 ):
| 步骤 | new | new | ||||
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | ||
| 1 | ||||||
| 2 | ||||||
| 3 | ||||||
| 4 | ||||||
| ... | ... | ... | ... | ... | ... | ... |
每步的 :
| 步骤 | 逃逸? | |
|---|---|---|
| 1 | 否 | |
| 2 | 否 | |
| 3 | 否 | |
| 4 | 否 | |
| ... | (持续震荡) | ... |
可以看到 始终 < 4,这个 点最终在集合内——涂黑色。
要点:Julia 的迭代过程和 Mandelbrot 完全一样,只是 和 的角色互换了。
四、关键代码
#[wasm_bindgen]
pub fn julia(
width: u32,
height: u32,
c_re: f64,
c_im: f64,
zoom: f64,
center_x: f64,
center_y: f64,
max_iter: u32,
palette: u32,
) -> Vec<u8> {
let w = width as usize;
let h = height as usize;
let mut out = vec![0u8; w * h * 4];
let inv_zoom = 1.0 / zoom;
let cr = c_re; // ← c 提出来,不进内循环
let ci = c_im;
for py in 0..h {
for px in 0..w {
// 像素 → 复数(这是 z₀,不是 c)
let zr0 = center_x + (px as f64 - w as f64 / 2.0) * inv_zoom;
let zi0 = center_y + (py as f64 - h as f64 / 2.0) * inv_zoom;
// 迭代 z = z² + c(c 固定!)
let mut zr = zr0;
let mut zi = zi0;
let mut iter: u32 = 0;
while iter < max_iter && (zr * zr + zi * zi) < 4.0 {
let new_zr = zr * zr - zi * zi + cr;
let new_zi = 2.0 * zr * zi + ci;
zr = new_zr;
zi = new_zi;
iter += 1;
}
let (r, g, b) = color_from_iter(iter, max_iter, palette);
let idx = (py * w + px) * 4;
out[idx] = r;
out[idx + 1] = g;
out[idx + 2] = b;
out[idx + 3] = 255;
}
}
out
}
调色板函数和 Mandelbrot 共用同样的公式(fire / ocean / forest / grayscale)——项目里调色板代码完全相同,只是函数被 julia.rs 和 mandelbrot.rs 各自定义一份。
未来重构:调色板可以抽到独立模块(如
fractal/palette.rs),让两个分形共用——这是架构优化的机会,留给读者自行尝试。
五、经典 Julia 常数(5 个必看)
打开前端页面,依次点 5 个预设按钮看效果:
| 常数 | 名称 | 视觉特点 |
|---|---|---|
c = -0.8 + 0.156i | 🐴海马谷 | 细丝状结构,密集旋转 |
c = -0.4 + 0.6i | 🌸连分数 | 颗粒状粉尘,分散细节 |
c = 0.285 + 0.01i | ⭐星形 | 对称的树枝状,从中心向外 |
c = -0.835 - 0.2321i | 🔥火焰 | 螺旋 + 火焰般的流动 |
c = -0.4 - 0.59i | 🌑全黑 | 几乎所有点逃逸 → 接近纯黑 |
关键观察
拖动 c 时会看到:
- 某些 c 值 → 整张图几乎全黑(所有点都逃逸)
- 某些 c 值 → 整张图几乎全彩(所有点都不逃逸)
- 过渡区域 → 图案剧烈变化,几个像素的 c 变化就能改天换地
为什么? 因为
c落在 Mandelbrot 集合内部时 Julia 集是连通的(不会全黑),落在外部时可能不连通(容易全黑)。
六、前端效果展示
打开页面后:
- 默认显示 🐴 海马谷(c = -0.8 + 0.156i,调色板火焰)
- 拖动 c 实部 / 虚部滑块——实时看图变化(这是 Mandelbrot 做不到的)
- 点击 5 个预设按钮:海马谷 / 连分数 / 星形 / 火焰 / 全黑
- 切换调色板:火焰 / 海洋 / 绿色 / 灰度
- 缩放 / 平移:和 Mandelbrot 一样
- 重置按钮:回到默认
七、踩坑提醒
1. c_re 和 c_im 提到循环外
// ❌ 错(虽然对,但每次循环多一次读)
let new_zr = zr * zr - zi * zi + c_re;
// ✅ 对(提前 let cr = c_re,循环内用 cr)
let cr = c_re;
let new_zr = zr * zr - zi * zi + cr;
虽然编译器通常会优化掉重复读,但显式提到循环外是好习惯——性能 + 可读性都更好。
2. Julia 在某些 c 值会"几乎全黑"
c = -0.4 - 0.59i → 大部分点快速逃逸 → 整张图全黑
不是 bug——是 Julia 集合本身不连通("尘埃"型)导致视觉上很暗。
调高迭代次数 + 换调色板能稍微缓解,但根本上 c 决定图案。
3. 拖动 c 时性能
WASM 串行 f64 在 600×600 画布上每次拖动都要重算 360,000 像素——拖动滑块时可能卡。
下一篇(任务 18)会用 Web Worker 多线程 + 增量渲染解决。本篇先用单线程跑通,性能优化留给下一篇。
八、下篇预告:性能优化
任务 18:交互缩放 + 拖拽 + 性能深度优化。
前一篇(task 16)讲的"性能优化阶梯"只是预告——本篇会真正实现:
- Web Worker 多线程:开 4 个线程并行渲染(Mandelbrot / Julia 各 2 个),加速 4×
- 增量渲染:拖动时只重算变化的区域,不是每次全图重算
- f32 vs f64:用
f32替代f64加速 2× - 平滑着色初探:消除"色带",让 Julia 的细节更平滑
核心目标:把 Mandelbrot / Julia 从"150 ms" 降到 "16 ms 以下"——实现 60 FPS 实时拖动。
🎁 写在最后
这一篇最大的认知升级:
Julia 不是"换参数"——是"换物理规则"。
拖动 c 时,每个像素都在新规则下重新演化——这就是 Julia 的"动力学"魅力。
| 任务 | 套路 | 关注点 |
|---|---|---|
| 16 曼德博 | 迭代 | c 变化(每个像素一个 c) |
| 17 Julia | 迭代 | c 固定(滑块控制 + z₀ 变化) |
变的是参数角色,不是算法。掌握这一点,你就能在**"任何 c 值"** 下看到 Julia 的"个性"——和真实世界一样,每个常数有不同"性格"。
你已经走过了 16 篇,现在掌握了两大经典分形。下一篇学性能优化——把这两个分形真正玩起来(60 FPS 拖动)。
📦 项目地址:pixel-math-wasm 🦀 Rust + WebAssembly 实战系列
🏷️ 标签:#Rust #WebAssembly #分形 #Julia #复数 #迭代 #逃逸时间 #算法 #算法