Yuzhe's Blog

yuzhes

用傅里叶画字母

用傅里叶画字母

我花了几天时间做了一个交互 demo:用一组旋转的圆(本轮,Epicycles)画出名字里的字母。这篇文章记录数学原理、踩过的坑和解决方案。

傅里叶本轮是什么?

对一组二维采样点做离散傅里叶变换(DFT),得到一组旋转向量。把它们首尾相连,每个向量以自己的频率和幅度旋转,最末端的轨迹就能描出任意封闭曲线。

给定 N 个复数采样点 zk=xk+iykz_k = x_k + i y_k

Xn=k=0N1zke2πikn/NX_n = \sum_{k=0}^{N-1} z_k \cdot e^{-2\pi i \cdot kn / N}

每个 XnX_n 编码了一个本轮的幅度相位频率。渲染时,t[0,1]t \in [0,1] 时刻的笔位置是:

z(t)=nXne2πintz(t) = \sum_{n} X_n \cdot e^{2\pi i \cdot n \cdot t}

在 canvas 里:每一帧把所有旋转向量加起来,画出向量链,记录轨迹。

采样字母路径

第一个问题:怎么得到一组有序的 2D 点来描出字母轮廓?

第一次尝试:把字母渲染到离屏 canvas,读像素,用 Moore 邻域轮廓追踪排序边缘像素。理论可行——但对细笔画和尖角,追踪器会在 1 像素宽的笔尖处进入 2 步死循环,永远不终止。

第二次尝试:极角排序——收集所有边缘像素,按与质心的角度排序。听起来很优雅,但对非凸形状立刻崩溃。字母 Y 有三条臂,极角扫描会把每条臂看成两个区间,画出星形而不是 Y。

真正可行的方案:SVG path.getPointAtLength()

每个字母都可以用 SVG 路径或折点定义,然后:

const N = 512
const total = path.getTotalLength()
const pts = Array.from({ length: N }, (_, i) => {
  const p = path.getPointAtLength(i / N * total)
  return { x: p.x, y: p.y }
})

等间距采样,顺序天然正确,不需要追踪、排序或处理拓扑。DFT 直接正常工作。

U 字母的问题

字母 U 顶部是开口的。但 DFT 需要封闭路径——z(t)z(t)t=0t=0 循环回 t=0t=0,起点和终点必须相同。

最朴素的封闭方式:连接两个顶角,画出一条横线。结果是方括号,不是 U。

方案一:从左上角出发,原路返回。

路径:(左上)→ 左边向下 → 曲线 → 右边向上 → (右上)→ 原路返回 → (左上)

闭合是零长度,没有横盖。但动画从左上角开始,第一笔就是往下画。每个循环结束时笔在左上角,下一帧又立刻往下——在每次循环的起始处出现一条明显的从顶角到底部的竖线。

方案二:从底部中心出发。

路径:

  1. (底部中心)→ 向上描左臂 → (左上)→ 原路返回 → (底部中心)
  2. (底部中心)→ 向上描右臂 → (右上)→ 原路返回 → (底部中心)

起点 = 终点 = 曲线最深处。循环”缝合处”藏在曲线中间,视觉上完全不可见。每条臂独立地去-返。

U: [
  {x:.5,  y:1},
  {x:.35, y:.97}, {x:.2, y:.87}, {x:.15, y:.68}, {x:.15, y:0},  // 左臂向上
  {x:.15, y:.68}, {x:.2, y:.87}, {x:.35, y:.97},                 // 原路返回
  {x:.5,  y:1},
  {x:.65, y:.97}, {x:.8, y:.87}, {x:.85, y:.68}, {x:.85, y:0},  // 右臂向上
  {x:.85, y:.68}, {x:.8, y:.87}, {x:.65, y:.97},                 // 原路返回
  {x:.5,  y:1},                                                   // 零闭合
]

干净的 U,没有任何多余笔画。

Math.min 的坑

在对数千个点做归一化时:

const minX = Math.min(...points.map(p => p.x)) // 💥 RangeError

JavaScript 把展开的数组变成函数参数。超过几千个元素后,调用栈溢出。用 reduce:

const minX = points.reduce((m, p) => Math.min(m, p.x), Infinity)

谐波数量选择

用多少个本轮?太少了字母的棱角会丢失,太多了每个采样噪声都被忠实还原。

对 512 个采样点的字母路径,80–100 个谐波正好——棱角清晰,不放大噪声。按幅度排序渲染(大圆先,小圆后)也让视觉更清爽:先看到整体结构,再是细节。

在线 Demo

点这里看 Demo——五个字母同时动画,各自不同颜色。

约 200 行原生 JS,不依赖任何库,纯 canvas。DFT 就是一个对采样点的双重循环。


下一篇:用 DLA(扩散限制聚集)晶体生长算法,以同样的字母为晶核,让粒子随机游走直到碰触晶体——另一种涌现。