克拉德尼图形:调一下频率,沙子自己排成对称花纹

一块金属板,撒一层细沙,用弓子在边上拉一声音------沙子会从某些地方跑开,最后堆到几条线上,排成对称的花纹。这就是克拉德尼图形(Chladni figures),两百多年前 Ernst Chladni 用它把「声音的振动模态」变成了能看见的东西。

照片和公式网上都有,但静态的照片回答不了两件事:沙子为什么偏偏堆在那几条线上?为什么音调一换,花纹就整个变了样子?所以我把这件事做成了一个网页:拖动频率滑杆,模态 n、m 跟着推进,4200 颗沙粒被反复推挤,最后自己落到节线上排出花纹;右下角点一下就能跑 16 条自检,把公式算出来的数字验一遍。

几条关键信息先摆出来:

  • 源码:Demo Park 公开仓库 → atomgit.com/deli007/dem...,index.html 可直接下载
  • 生成物:单个 index.html,纯前端、零依赖、不联网、不上传,全部计算在本地完成
  • 本地运行:双击文件即可;或 python -m http.server 8000
  • 本地验证:页面自检 16/16 通过;模态 (2,3) 里 98.6% 的沙粒最终落进 |A| < 0.10 的节线窄带

它不是严谨的声学仿真。板上的振动场用理想驻波公式代替,沙子用的是「沿 −∇|A|² 下滑」的简化受力,模态编号也只是把 45 种组合排成一条队,不对应任何一块真实金属板的固有频率。能拖、能看、数字能验,但不等于实验室里那块板。

为什么值得看:沙子其实是被「筛」到那几条线上的

板在振动的时候,不同位置晃得不一样:有的地方上下抖得最凶(波腹),有的地方几乎不动(节线)。沙粒躺在晃动的板面上,会被一次次弹起来、甩向振幅更小的方向;一旦挪到振幅几乎为零的地方,板面不再给它能量,它就趴在那里不动了。几百次推挤下来,沙子自动排成沿着节线的窄带------花纹不是画上去的,是被「筛」出来的。

页面上板的振幅用克拉德尼给正方形薄板的经典形式:

text 复制代码
A(x, y) = cos(nπx)·cos(mπy) − cos(mπx)·cos(nπy)      // x、y 归一化到 0~1

A = 0 的曲线就是节线。n、m 是两个整数,决定节线的疏密:n + m 越大,节线越多、花纹越碎。

这个页面比一张照片多出来的,是三个实现上的决定:

  1. 受力用 −∇|A|²,而不是「朝着 A = 0 挪」。梯度指向振幅增长最快的方向,取负号就是往振动最小的方向滑;而且越靠近节线,|A| 越小、|∇A| 越大,粒子被挤进窄带的速度越快。同样的种子跑 720 步,模态 (2,3) 有 98.6% 的沙粒落进 |A| < 0.10 的窄带,(4,5) 是 99.7%,(6,7) 能跑到 100%。
  2. 切换模态时 n、m 用指数缓动逼近(k = 1 − exp(−2.8·dt)),所以拖滑杆看到的是花纹连续变形、沙子跟着流动,而不是一帧跳过去。
  3. 粒子之间加了软排斥(作用半径 0.010、强度 0.0009)和一点点随机扰动(0.00024)。少了这两样,4200 颗沙子会叠成一根死线,看不出堆积的厚度感。

也说几处真实的边界:

  • 模态表里只有 n < m 的 45 种组合(n、m 都取 1~10)。n = m 时公式恒等于 0,整块板处处是节线,压根没有花纹,所以直接排除了。
  • 画面上的对称性不是真实金属板那种四方对称。因为 A(x, y) 与 A(y, x) 只差一个符号,|A| 关于主对角线严格对称(采样残差为 0),所以花纹看起来是「斜着对称」,而不是照片里那种四个方向都对称的克拉德尼图。
  • 板是方的,最大边长 780 px(min(92vw, 78vh, 780px)),窗口再宽它就停在 780 px 不再变大。

任务描述

需求是直接粘进码道 Web 输入框的,原文如下:

text 复制代码
做一个单文件的「克拉德尼图形」交互演示网页(index.html,中文界面,无外部依赖)。

核心功能:
- 一块正方形金属板,用 canvas 渲染 4000 颗以上细沙粒子。
- 板上有一个驻波振动场,振幅用克拉德尼公式 A(x,y) = cos(nπx)·cos(mπy) - cos(mπx)·cos(nπy) 计算(x、y 归一化到 0~1),n、m 是模态编号。
- 沙子被振动推离波腹、向波节线(A≈0 的线)聚集,最终排出对称的克拉德尼花纹。
- 用滑杆连续调节频率,频率映射到 n、m 组合(频率增大时模态沿对角线推进),面板实时显示当前 n、m、频率数值。
- 提供几个预设按钮(不同 n、m 组合),一键切到该模态。
- 「重新撒沙」按钮:把粒子重新随机撒满整块板。

视觉要求:
- 深色板面 + 米色细沙,沙粒有明暗和轻微堆积效果,节点线对称清晰。
- 频率切换时粒子平滑迁移,不要瞬移。
- 页面自适应,窗口大时板子跟着放大。

自检与校验:
- 页面右下角一个「自检」区域,运行 runSelfTests(),用断言检查关键函数:A(x,y) 在中心/角落的取值、切换模态后节点线数量、粒子坐标始终在 0~1 范围内等,显示「通过 X / 失败 Y」。
- 频率输入框做校验:非数字或超出范围时给明确中文提示,并保留上一次的有效值。

完成后打开预览,让我直接看到运行效果。

这轮码道实际做了什么

只提了一个单文件需求,交回来的是一个 22 KB 的 index.html,打开就能跑。它自己补的东西比需求里写的多:

  • 振动场 :chladniA(x, y, n, m) 直接实现上面那条公式,另外解析求了一遍梯度 chladniGrad 给受力用,省掉数值差分。
  • 模态表:按 n + m 升序、n 升序排出 45 个模态;频率滑杆 120 ~ 1500 Hz 线性映射到这张表,120 Hz 对应 (1,2),1500 Hz 对应 (9,10)。
  • 沙子 :4200 颗,位置和速度存在 Float32Array 里;每帧先建一张 110×110 的空间分桶网格,只在 3×3 的邻桶里算软排斥,避开了 4200² 的两两比较。
  • 渲染:两块 canvas------一块画沙,一块预计算 150×150 的节线参考线(可勾选叠加),互不干扰。
  • 控制:频率滑杆 + 可手敲的数字输入框、6 个预设按钮(1-2 / 1-3 / 2-3 / 2-4 / 3-4 / 3-5)、重新撒沙、叠加节线参考线。
  • 自检卡 :右下角固定面板,runSelfTests() 里 16 条断言,从「中心点 A = 0」「四角都是节点」「交换 n、m 后 |A| 不变」一直覆盖到「粒子不出界」「非法输入被拒绝」。

验证与踩坑

生成完我没有只看截图,而是把 index.html 拉到本地实际跑了一遍。

  • 自检 16/16 通过 (页面点出来的原文是「通过 16 / 失败 0」)。挑几条能对数的:
    • A(0.5, 0.5, n=5, m=7) = 0、A(0.37, 0.21, 2, 3) = 0.5059、A(0.37, 0.21, 3, 2) = −0.5059------交换 n、m 只翻符号;
    • A(0.3, 0.4, 2, 3) = −0.5194,离节线越远振幅越大;
    • 节线附近 |∇A| = 13.17,波腹附近只有 4.39------这是「沙子会被挤进窄带」的数学依据;
    • 模拟收敛那条实测 98.6%,比断言要求的 75% 高出一截。
  • 模态和频率对得上号:(1,2) 在 120 Hz;(3,4) = 370.9 Hz;(4,5) = 590.5 Hz;(6,7) = 1123.6 Hz;(9,10) 在 1500 Hz。
  • 节线确实随模态变多:固定截线 y = 0.37 数振幅符号变化次数,(1,2) 是 1 次、(2,3) 是 2 次、(4,5) 是 4 次;换成 y = 0.30 数 (6,7),是 6 次。
  • 失败路径 :频率框里敲 abc → 红字 "abc" 不是有效数字。,输入框回退到上一次有效值(当时是 1124);敲 99999 或 2000 → 频率需在 120 ~ 1500 Hz 之间。;清空 → 请输入一个数字。。三种情况页面都不崩,沙子和花纹停在上一帧。
  • 一个真实的小瑕疵:最后一条自检的文案写的是「公式对所有模态数值稳定(46 个模态采样无 NaN)」,但模态表实际只有 45 个(从 1~10 里取两个不同的数,C(10,2) = 45)。断言本身没错------它确实对全部模态采样过、结果都是有限值------只是那句文案里的数字多算了一个。不影响结论,但既然发现了就写出来。

准备环境:进入码道 Web

浏览器打开码道 Web 版:devcloud.cn-north-4.huaweicloud.com/chat?source...,登录后就能在对话窗口输入需求,不需要装软件。

码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。

本地复现

生成物只有一个 index.html,复现没有依赖:

  1. 从 Demo Park 本案例目录下载 index.html。
  2. 双击打开,页面默认停在 120 Hz、模态 (1,2)。
  3. 想用静态服务也行:python -m http.server 8000,然后访问 http://localhost:8000。
  4. 点「运行自检」,应该看到「通过 16 / 失败 0」。沙子收敛要几秒,等花纹稳定下来再截屏更清楚。

使用码道体会

这轮下来能带走的,主要是几条和模型打交道的习惯:

  • 把验收方式写进需求。结尾那句「完成后打开预览,让我直接看到运行效果」,省掉的是「你先给我看下效果」这一轮来回。
  • 数值断言要连着参考值一起给。需求里点明了自检要覆盖「中心/角落取值、交换 n、m 后是否对称、粒子是否越界」,它就把 16 条断言真的做进了页面;只写「加个自检」,多半只能得到一句打印。
  • 把「丝滑」翻译成可实现的句式 。需求里写「切换模态时粒子平滑迁移,不要瞬移」,落到代码里就是 k = 1 − exp(−2.8·dt) 这一行指数缓动------描述效果比描述实现更好使。
  • 性能约束要提前说。4200 颗粒子如果写成两两比较,一帧就是 1764 万次距离计算;需求里点明「粒子要多」,它自己补了空间分桶------这类结构问题早说清楚,才不会交付一个 5 帧的幻灯片。
  • 失败路径单独写一段。非法输入给什么提示、保不保留上一次的值,需求里写「给明确中文提示,并保留上一次的有效值」,它照做了;不写,默认往往是什么都不发生。
  • Agent 说完成不算验收。16/16 是页面里点出来的数字,不是回复里那句话;滑杆我自己拖过一遍,非法输入也自己敲过一遍,才敢写上面那一段。

亲手点三下

页面打开就能验,三下够了:

  1. 看尺子 :点「运行自检」,确认是「通过 16 / 失败 0」。顺手核一条------A(0.37, 0.21, 2, 3) 和 A(0.37, 0.21, 3, 2) 必须互为相反数,说明交换 n、m 只翻转符号、花纹不变。
  2. 换模态:点预设「3-4」,看沙子从上一张花纹慢慢挪到新花纹(不是闪一下);再把滑杆推到最右,模态走到 (9,10),节线明显变密。
  3. 看失败路径 :频率框里敲 abc 或 2000,必须看到红字提示、输入框回退到上一次的有效值,页面不白屏、不报错。

直接下载试玩

总结

把「声音能让沙子排出花纹」做成一个能拖的网页之后,最关键的一行不是渲染,而是受力那行 fx = −2·A·∂A/∂x:它把「沙子该往哪走」交给了场的梯度,所以节线一变、沙子自己就跟着挪,不需要为每种模态单独写动画。想接着玩,可以试试加一个「真实频率标度」开关(让模态随频率按平方根关系走,而不是线性排队)、给沙粒加拖尾,或者把方板换成圆板------圆板的节线是同心圆加直径,画出来是另一种花纹。

相关推荐
光影少年1 小时前
TurboModule原理
前端·前端框架·turbopack
亿元程序员1 小时前
自从有了 AI,我就再也不想拼 UI 了……
前端
数据狐(Datafox)1 小时前
淘宝商品详情API实战:多语言代购商城自动同步数据完整方案
开发语言·前端·数据库·爬虫·json
hasty2 小时前
HTML 已经转义,为何仍有 XSS?Sharp srcdoc 漏洞中的第二次解析
前端·html·xss
涛涛ing2 小时前
WebMCP正式进入Origin Trial:AI Agent不再“装人”,前端交互范式正在被重写
前端
y = xⁿ2 小时前
关于Agent智能体开发
开发语言·前端·javascript
梅孔立2 小时前
Codex 完整教程(Node.js安装 + npm安装Codex + DeepSeek配置 + 快捷键 + 设置)
前端·npm·node.js
事圆则缓2 小时前
Flutter 状态管理框架对比(一):先看地图,再选工具
前端·javascript·flutter
中杯可乐多加冰2 小时前
解决方案:CEB 文件 Web 端在线预览方案,Linux部署ceb文件在线预览工具
linux·运维·前端·ceb