
Julia 朱利亚分形:拖动常数 c 看图实时变化
🦀 Rust + WASM 实战系列 第 17 篇 阅读时间:约 6 分钟 | 实战可运行
📌 写在前面
上一篇讲了 Mandelbrot------c 在复平面上变化 (每个像素一个 c),z₀ = 0 固定。
Julia 是 Mandelbrot 的孪生兄弟 ------迭代公式完全一样 :z_{n+1} = z_n² + c,但参数角色互换:
Mandelbrot:每个像素一个 c,z0=0 固定Julia:c 固定为某个常数,z0 在复平面上变化
算法代码几乎一样(只交换两个参数的循环范围 )。真正的价值不是"换参数"------而是实时拖动 c 看图变化。
🚀 TL;DR
zn+1=zn2+c
- 跟 Mandelbrot 公式完全一样
- 区别 :Julia 的 c 是固定的常数(前端滑块实时调)
- 结果 :拖动 c 时整个图案会戏剧性变化------这是 Mandelbrot 做不到的
📖 目录
- [Julia 的真正乐趣](#Julia 的真正乐趣 "#%E4%B8%80julia-%E7%9A%84%E7%9C%9F%E6%AD%A3%E4%B9%90%E8%B6%A3")
- 为什么公式一样但效果完全不同
- [算法:和 Mandelbrot 的关键 2 行差异](#算法:和 Mandelbrot 的关键 2 行差异 "#%E4%B8%89%E7%AE%97%E6%B3%95%E5%92%8C-mandelbrot-%E7%9A%84%E5%85%B3%E9%94%AE-2-%E8%A1%8C%E5%B7%AE%E5%BC%82")
- 关键代码
- [经典 Julia 常数(5 个必看)](#经典 Julia 常数(5 个必看) "#%E4%BA%94%E7%BB%8F%E5%85%B8-julia-%E5%B8%B8%E6%95%B05-%E4%B8%AA%E5%BF%85%E7%9C%8B")
- 前端效果展示
- 踩坑提醒
- 下篇预告:性能优化
一、Julia 的真正乐趣
Mandelbrot 没法"调 c"------因为 c 已经是图的 x 轴(整个集合就是 c 的可视化)。
Julia 可以拖动 c 滑块实时看图变化------这是 Mandelbrot 做不到的。
拖动 c 实部 → 图案的"骨架"在水平方向拉伸拖动 c 虚部 → 图案的"细节密度"在垂直方向变化拖动到某些区域 → 图案突然从"无限细节"变成"全黑 / 全白"(c 落在 Mandelbrot 集合内)
直觉:Mandelbrot 像是"一张固定的地图",Julia 像是"一个可以变形的地图"。
二、为什么公式一样但效果完全不同?
zn+1=zn2+c
Mandelbrot 和 Julia 完全相同的迭代规则 ,只是初始化不同:
| 算法 | c 来源 | z0 来源 |
|---|---|---|
| Mandelbrot | 每个像素一个 c(从像素映射) | 永远 0 |
| Julia | 固定常数(滑块控制) | 每个像素一个 z0(从像素映射) |
为什么效果天差地别?
- Mandelbrot 把 c 当"主角"------所以整个集合就是 c 的所有可能性的可视化
- Julia 把 c 当"背景设定"------所以每个 c 决定一种"物理规则",整个复平面在这个规则下演化
类比:Mandelbrot 像是"一张地图",Julia 像是"一个地图 + 一组物理参数"。
三、算法:和 Mandelbrot 的关键 2 行差异
公式回顾(详见 本系列上一节)
迭代公式 z=z2+c 拆成实部/虚部(task 16 §二 有详细推导):
Re(z2+c)Im(z2+c)=zr2−zi2+cre=2⋅zr⋅zi+cim
逃逸判断: zr2+zi2>4。
Mandelbrot vs Julia:参数角色对比
| 维度 | Mandelbrot | Julia |
|---|---|---|
| c 来自 | 每个像素(从像素映射) | 固定常数(滑块控制) |
| z0 来自 | 永远 0 | 每个像素(从像素映射) |
| 物理意义 | 看每个 c "命运如何" | 看每个 z0 在固定 c 规则下"命运如何" |
Julia 的核心循环
rust
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 里像素→ z0),其他完全一样。
一个具体的 Julia 计算
取 c=−0.8+0.156i(海马谷),画布中心点 z0=0(注意 Julia 从像素位置出发,所以画布中心对应 z0=0):
| 步骤 | zr | zi | zr2−zi2 | 2⋅zr⋅zi | new zr | new zi |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | −0.8 | 0.156 |
| 1 | −0.8 | 0.156 | 0.616 | −0.250 | −0.184 | −0.094 |
| 2 | −0.184 | −0.094 | 0.025 | 0.035 | −0.775 | 0.191 |
| 3 | −0.775 | 0.191 | 0.564 | −0.296 | −0.236 | −0.105 |
| 4 | −0.236 | −0.105 | 0.045 | 0.050 | −0.755 | 0.206 |
| ... | ... | ... | ... | ... | ... | ... |
每步的 zr2+zi2:
| 步骤 | zr2+zi2 | 逃逸? |
|---|---|---|
| 1 | 0.664 | 否 |
| 2 | 0.043 | 否 |
| 3 | 0.315 | 否 |
| 4 | 0.066 | 否 |
| ... | (持续震荡) | ... |
可以看到 zr2+zi2 始终 < 4,这个 z0 点最终在集合内 ------涂黑色。
要点 :Julia 的迭代过程和 Mandelbrot 完全一样 ,只是 c 和 z0 的角色互换了。
四、关键代码
rust
#[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 提到循环外
rust
// ❌ 错(虽然对,但每次循环多一次读)
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 值会"几乎全黑"
ini
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 #复数 #迭代 #逃逸时间 #算法 #算法