Rust图像处理第18节-分形画布缩放 + 拖拽交互:图像缩放 + f32 性能优化

🦀 Rust + WASM 实战系列 第 18 篇 阅读时间:约 8 分钟 | 实战可运行

📌 写在前面

前两篇(16 曼德博、17 Julia)只能滑块调参数------没法"画布式"探索分形。

这一篇升级体验鼠标滚轮缩放(围绕光标)+ 拖拽平移 ,像用地图软件一样自由探索分形的细节。

同时首次实测性能优化f32 vs f64 真实数据对比。


🚀 TL;DR

功能 实现
滚轮缩放 围绕鼠标位置缩放(不是中心)
拖拽平移 鼠标按下拖动 → 平移复平面中心
拖动时降分辨率 渲染 1/3 分辨率,速度 ×9
f32 vs f64 一键切换,对比渲染时间
ini 复制代码
600×600 + iter=200 ≈ 7200 万次浮点运算
  ↓
f64 WASM:~150 ms
  ↓
f32 WASM:~75 ms (实测 ~2× 加速)
  ↓
拖动时降分辨率(200×200):~10 ms (快 15×)

📖 目录

  1. 滚轮缩放:围绕鼠标位置
  2. 拖拽平移:鼠标事件处理
  3. [拖动时降分辨率:性能 vs 视觉的取舍](#拖动时降分辨率:性能 vs 视觉的取舍 "#%E4%B8%89%E6%8B%96%E5%8A%A8%E6%97%B6%E9%99%8D%E5%88%86%E8%BE%A8%E7%8E%87%E6%80%A7%E8%83%BD-vs-%E8%A7%86%E8%A7%89%E7%9A%84%E5%8F%96%E8%88%8D")
  4. [f32 vs f64:实测对比](#f32 vs f64:实测对比 "#%E5%9B%9Bf32-vs-f64%E5%AE%9E%E6%B5%8B%E5%AF%B9%E6%AF%94")
  5. 关键代码
  6. 前端效果展示
  7. 踩坑提醒
  8. 第四部分收官

一、滚轮缩放:围绕鼠标位置

关键问题

普通缩放是"以图片中心为锚点"------但用户期望的是"以鼠标位置为锚点"(地图软件、Photoshop 都这么做)。

直觉 :滚轮向上滚(放大)时,鼠标指着的那个点应该保持不动

数学推导

缩放前
oldCx=centerX+ mouseX−w/2zoom \text{oldCx} = \text{centerX} + \frac{\text{mouseX} - w/2}{\text{zoom}} oldCx=centerX+zoommouseX−w/2

也就是说,鼠标位置的复平面坐标是 oldCx

缩放后zoom *= factor),我们希望同一个 mouseX 仍然对应 oldCx
oldCx=newCenterX+ mouseX−w/2newZoom \text{oldCx} = \text{newCenterX} + \frac{\text{mouseX} - w/2}{\text{newZoom}} oldCx=newCenterX+newZoommouseX−w/2

解出 newCenterX
newCenterX=oldCx− mouseX−w/2newZoom \text{newCenterX} = \text{oldCx} - \frac{\text{mouseX} - w/2}{\text{newZoom}} newCenterX=oldCx−newZoommouseX−w/2

代码

javascript 复制代码
function onWheel(e) {
  const rect = canvas.getBoundingClientRect()
  // 1. 鼠标在 canvas 上的像素坐标(0 ~ 600)
  const mx = (e.clientX - rect.left) * (W / rect.width)
  const my = (e.clientY - rect.top) * (H / rect.height)

  // 2. 算出缩放前鼠标处的复平面坐标
  const oldCx = centerX + (mx - W / 2) / zoom
  const oldCy = centerY + (my - H / 2) / zoom

  // 3. 更新 zoom(向上滚放大)
  const factor = e.deltaY < 0 ? 1.2 : 1 / 1.2
  zoom *= factor

  // 4. 调整 center,让鼠标位置保持不变
  centerX = oldCx - (mx - W / 2) / zoom
  centerY = oldCy - (my - H / 2) / zoom
}

4 行核心代码------完成了"以光标为中心缩放"。


二、拖拽平移:鼠标事件处理

3 个事件

javascript 复制代码
let isDragging = false
let lastX = 0, lastY = 0

canvas.onmousedown = (e) => {
  isDragging = true
  lastX = e.clientX
  lastY = e.clientY
  // 拖动时降分辨率
  renderScale = 0.33
}

canvas.onmousemove = (e) => {
  if (!isDragging) return
  const dxPx = e.clientX - lastX      // 像素位移
  const dyPx = e.clientY - lastY
  // 转成复平面位移
  centerX -= dxPx / zoom
  centerY -= dyPx / zoom
  lastX = e.clientX
  lastY = e.clientY
}

canvas.onmouseup = () => {
  isDragging = false
  // 松开后恢复全分辨率重渲
  renderScale = 1.0
  render()
}

方向问题

直觉:拖右 → 图片跟着往右 → 视野往左

数学上:复平面中心往反方向移动:

javascript 复制代码
centerX -= dxPx / zoom   // 注意是 -=

三、拖动时降分辨率:性能 vs 视觉的取舍

问题

600×600 画布每次渲染 7200 万次浮点运算,WASM f64 要 ~150ms。

拖动时每秒触发 30+ 次 mousemove 约等于触发间隔时间33.333ms,但每 150ms 才能完成一次渲染------严重掉帧

解决方案:降分辨率渲染

拖动时用 1/3 分辨率(200×200),渲染时间降到 ~10ms:

python 复制代码
600×600 像素:360,000 像素 × ~100 iter ≈ 3600 万次 → 150 ms
200×200 像素: 40,000 像素 × ~100 iter ≈   400 万次 →  10 ms  (快 15×)

视觉效果 :拖动时图稍微模糊,但流畅松手后再渲染全分辨率。

代码

javascript 复制代码
function render() {
  const W2 = Math.round(W * renderScale)
  const H2 = Math.round(H * renderScale)
  // 用 W2, H2 调 WASM
  
  if (renderScale === 1.0) {
    // 全分辨率:直接 putImageData
    ctx.putImageData(imageData, 0, 0)
  } else {
    // 低分辨率:先画到临时 canvas,再 drawImage 放大
    const tmp = document.createElement('canvas')
    tmp.width = W2
    tmp.height = H2
    tmp.getContext('2d').putImageData(imageData, 0, 0)
    ctx.imageSmoothingEnabled = true
    ctx.drawImage(tmp, 0, 0, W, H)  // 放大到 600×600
  }
}

imageSmoothingEnabled = true 让浏览器用双线性插值放大------比"像素块"好看得多。

性能对比

场景 分辨率 f64 耗时 f32 耗时
静态(不拖动) 600×600 ~150 ms ~75 ms
拖动中 200×200 ~10 ms ~5 ms
松手后 600×600 ~150 ms ~75 ms

拖动流畅 + 松手清晰------两全其美。


四、f32 vs f64:实测对比

理论分析

维度 f64 f32
字节数 8 4
有效数字 15~17 6~7
SIMD 吞吐量
Mandelbrot 够用? 绑绑有余 绑绑有余

为什么 f32 够用? Mandelbrot 迭代范围在 [-2, 2]完全用不到 17 位有效数字------f32 的 7 位绑绑有余。

实测数据(600×600, iter=200)

精度 平均耗时 加速比
f64 ~150 ms
f32 ~75 ms ~2×

结论 :f32 简单换一下类型,零成本加速 2×------这是性价比最高的优化。

实现方式

rust 复制代码
// f64 版本
let mut zr = 0.0;        // 默认 f64
let mut zi = 0.0;

// f32 版本(只是把类型改了)
let mut zr: f32 = 0.0;
let mut zi: f32 = 0.0;

几乎一模一样的代码------Rust 的类型推断让切换非常容易。

为什么 nalgebra / Rust 默认用 f64? f64 是 IEEE 754 标准,所有平台都保证精度一致 ;f32 在某些边缘情况下可能精度不够。但分形渲染不在那些边缘情况里


五、关键代码

Rust 端:f32 版本

rust 复制代码
#[wasm_bindgen]
pub fn mandelbrot_f32(
    width: u32, height: u32,
    center_x: f64, center_y: f64, zoom: f64,
    max_iter: u32, palette: u32,
) -> Vec<u8> {
    let w = width as usize;
    let h = height as usize;
    let mut pixels = vec![0u8; w * h * 4];

    let cx = center_x as f32;
    let cy = center_y as f32;
    let inv_zoom = (1.0 / zoom) as f32;

    for py in 0..h {
        for px in 0..w {
            let c_re = cx + (px as f32 - w as f32 / 2.0) * inv_zoom;
            let c_im = cy + (py as f32 - h as f32 / 2.0) * inv_zoom;

            let mut zr: f32 = 0.0;
            let mut zi: f32 = 0.0;
            let mut iter: u32 = 0;
            while iter < max_iter && (zr * zr + zi * zi) < 4.0 {
                let new_zr = zr * zr - zi * zi + c_re;
                let new_zi = 2.0 * zr * zi + c_im;
                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;
            pixels[idx]     = r;
            pixels[idx + 1] = g;
            pixels[idx + 2] = b;
            pixels[idx + 3] = 255;
        }
    }

    pixels
}

Vue 端:滚轮缩放 + 降分辨率

javascript 复制代码
function onWheel(e) {
  e.preventDefault()
  const rect = canvas.getBoundingClientRect()
  const mx = (e.clientX - rect.left) * (W / rect.width)
  const my = (e.clientY - rect.top) * (H / rect.height)

  // 鼠标位置对应的复平面坐标
  const oldCx = centerX + (mx - W / 2) / zoom
  const oldCy = centerY + (my - H / 2) / zoom

  // 缩放
  const factor = e.deltaY < 0 ? 1.2 : 1 / 1.2
  zoom *= factor

  // 调整中心
  centerX = oldCx - (mx - W / 2) / zoom
  centerY = oldCy - (my - H / 2) / zoom

  // 拖动时降分辨率
  renderScale = 0.33
  render()
  // 200ms 后恢复
  setTimeout(() => {
    if (!isDragging) {
      renderScale = 1.0
      render()
    }
  }, 200)
}

六、前端效果展示

打开页面后:

  1. 默认显示 Mandelbrot 全景,f64 精度
  2. 滚轮缩放:以鼠标为中心放大/缩小------海马谷可以放大到像素级细节
  3. 拖拽平移:按住鼠标拖动 → 平移复平面
  4. Mandelbrot ↔ Julia 切换:切换到 Julia 预设 c 常数
  5. f64 ↔ f32 切换观察渲染耗时变化------立刻看到 ~150ms → ~75ms
  6. 拖动时观察"降分辨率"标签:拖动期间分辨率降到 200×200,松手恢复 600×600

七、踩坑提醒

1. 滚轮事件要 preventDefault()

javascript 复制代码
function onWheel(e) {
  e.preventDefault()   // ← 否则页面会滚动!
  // ...
}

不阻止默认行为,整个页面会跟着滚轮上下滚动。

2. CSS 像素 vs canvas 像素要换算

javascript 复制代码
const rect = canvas.getBoundingClientRect()
const mx = (e.clientX - rect.left) * (W / rect.width)  // ← 关键换算

如果 canvas CSS 显示是 400×400 但内部是 600×600,鼠标坐标必须换算到 canvas 内部坐标系。

3. f32 在极端参数下可能"看起来不一样"

makefile 复制代码
f64:  0.123456789012345
f32:  0.12345679          ← 截断到 7 位

某些边缘 c 值(极深放大时)f32 可能产生 1-2 像素的色差------视觉上几乎察觉不到,但理论上存在。

4. 拖动时频繁重渲染会卡

javascript 复制代码
// ❌ 每次 mousemove 都渲染
canvas.onmousemove = (e) => {
  centerX.value -= dxPx / zoom.value
  // watch 触发 render() → 立即开始
}

// ✅ 用 renderScale + watch 自动降分辨率
canvas.onmousemove = (e) => {
  centerX.value -= dxPx / zoom.value
  // 监听器自动 render(),但因为 renderScale=0.33,每次只要 10ms
}

降分辨率是关键------不降分辨率的话,拖动期间会卡。

5. mouseup / mouseleave 都要处理

javascript 复制代码
canvas.onmouseup = onMouseUp       // 鼠标松开
canvas.onmouseleave = onMouseUp    // 鼠标离开 canvas

不监听 mouseleave,鼠标拖到 canvas 外面就"卡住"了。


八、第四部分收官

3 个分形任务全景

任务 算法 交互 性能优化
16 曼德博 滑块调参 介绍阶梯
17 Julia 拖 c 滑块实时变 ---
18 交互画布 滚轮 + 拖拽 + 双算法切换 f32 实测 ~2× + 降分辨率 ~15×

4 个认知升级

认知 例子
分形 = 复数迭代 + 逃逸时间 z = z² + c,超简单公式生成无限复杂度
复数 = 工程数学第二语言 电学、信号处理、量子力学、控制论都用
迭代算法 vs 确定性算法 分形是第一个"算到收敛"的算法
性能优化 = 性价比工程 f32 一行改类型 = 2× 加速;降分辨率 = 15×

性能优化回顾

diff 复制代码
原始:JS         →  1500 ms(卡顿)
+WASM f64        →   150 ms(流畅)    10×
+WASM f32        →    75 ms            2×  ← 实测
+降分辨率拖动   →    10 ms            7×
+Web Worker      →     3 ms(未来)   25×
+WebGPU          →   1 ms(未来)   75×

当前实现覆盖了前 3 行------后两行留作"进阶"。


🎁 写在最后

这一篇的真正主题不是分形------是**"前端性能优化"**:

技术 加速 实现难度 收益
滚轮缩放 + 拖拽 ∞(从 0 到能用 用户体验
降分辨率 15× 流畅交互
f32 极低 零成本加速

性价比排序:降分辨率 > f32 > 滚轮缩放 > Web Worker > WebGPU

教训 :性能优化先易后难------先做"低成本高回报"的(f32、降分辨率),再考虑复杂方案。

下部分预告(part 5:矩阵应用:从 RGB 到 PCA):RGB 矩阵变换 → PCA 图片压缩 → 线性回归去噪**------线性代数在图像处理中的实战。。


📦 项目地址pixel-math-wasm 🦀 Rust + WebAssembly 实战系列


🏷️ 标签#Rust #WebAssembly #分形 #交互 #性能优化 #f32 #降分辨率 #Canvas

相关推荐
doiito3 小时前
【开源项目】用 Rust 重构中文 TTS!kokoroi-rs 高性能语音合成引擎介绍
ai·rust
铅笔侠_小龙虾4 小时前
Rust 学习目录
开发语言·学习·rust
SmalBox9 小时前
【节点】[EvaluateRefractionData节点]原理解析与实际应用
unity3d·游戏开发·图形学
铅笔侠_小龙虾10 小时前
Rust 学习(4)-函数与注释
开发语言·学习·rust
前端H12 小时前
Biome & Rolldown:Rust 工具链接管前端基建
开发语言·前端·rust
独孤留白13 小时前
从C到Rust:struct Cell / RefCell 线程内局部可变性
rust
Kapaseker14 小时前
Rust 是如何处理异常的
rust·kotlin
铅笔侠_小龙虾15 小时前
Rust 学习(8)-切片类型
学习·rust·c#
程序员爱钓鱼19 小时前
Rust 函数与返回值详解:参数、表达式与返回类型
后端·rust