☰
单文件HTML打造可验证渲染的Techno音画机器
2026/9/25 15:06:51 网站建设 项目流程

单个 HTML 文件里塞进一个 techno 机器,听起来像演示项目,实际上它把两件事压缩到很小的体积里:一是用代码实时生成音乐节奏,二是让每一次视觉渲染结果可以被验证。这里的关键是 verifiable renders。很多网页动画看起来类似,但同一段代码在不同时间、不同机器上跑出来的画面很难对账;如果换成“给定种子、给定帧号,画面就应当可复现”的设计思路,那么这个 HTML 文件就从一个一次性 demo 变成了一个可测试、可对比、可给别人复现的运行单元。下面的完整实现会围绕这个思路,从零写一个单文件 techno machine:Web Audio 承担声音,Canvas 承担画面,确定性随机数承担可复现性,最后附完整代码、验证方法和排错清单。

如果你平时用 HTML、CSS、JavaScript 做页面,但还没有把 Web Audio API 和 Canvas 组合起来做过“生成式小作品”,这篇文章非常适合当入门练习。读完以后,你会得到一个可以直接双击打开的音画 demo,并且能解释清楚“为什么两次渲染结果可以保持一致”。

1. 先拆解“单文件 techno machine”和“可验证渲染”

1.1 techno machine 在这个项目里指什么

这里没有一份完整的产品规格说明书,但从标题可以明确技术对象:在单个 HTML 文件中实现一台 techno 机器。techno machine 可以理解为一个小型音乐生成器,它循环播放 techno 风格的节奏,同时用视觉动画把节拍画出来。

技术栈并不神秘:

  • Web Audio API 负责实时合成鼓点、踩镲和低音贝斯。
  • Canvas 2D API 负责绘制画面,用圆环、粒子、旋转线条等元素表现节奏。
  • 一段确定性随机数算法代替Math.random(),保证同一个种子下每次生成的画面可复现。

真实项目里,这种单文件 demo 很常见。比如在技术社区用 Show HN 形式分享作品时,单个 HTML 文件是最容易传播的形态:不需要安装依赖,不需要构建,别人把文件保存下来就能看。

1.2 为什么坚持用单个 HTML 文件

把 CSS、HTML、JavaScript 全部内联在一起,看起来像“偷懒”,实际上有几个具体好处:

  1. 零依赖、零构建。文件不依赖 npm 包、CDN 或网络请求,保存到本地双击就能运行。
  2. 分享成本低。一个.html文件就是完整项目,发文件、放进压缩包、托管到任意静态页面都可以。
  3. 代码可审计。所有逻辑都在同一个文件里,别人能直接看到你在用什么算法、有没有联网、有没有隐藏请求。
  4. 版本对比方便。你可以把某一天的版本复制成techno-20250101.html,过段时间再对比行为差异。

代价也很明确:单文件不适合大型项目。当代码超过一千行,或者需要多人协作时,模块化是必须的。单文件更适合“作品级 demo”或“教学示例”,这也是这个项目的合理定位。

1.3 verifiable renders 到底验证什么

verifiable renders 翻译过来是“可验证渲染”,核心含义是:相同输入条件下,渲染结果应当可复现、可对比、可审计。

具体到本项目,输入条件包括:

  • seed 字符串,用来派生调色板、粒子位置等随机参数。
  • BPM 值,决定步进速度和音符间隔。
  • frame 编号,决定当前画面处于动画时间轴的第几帧。
  • 固定画布尺寸和固定逻辑帧率。

输出则是一张确定的 Canvas 画面。你可以对画面计算一个哈希值,比如a1b2c3d4e5f67890,把这个值和 frame 编号一起展示在页面上。两个浏览器如果得到相同的哈希,说明它们在相同输入下渲染结果一致;如果不一致,说明存在渲染差异。

这种方式比“肉眼看着差不多”更有说服力,也方便做回归测试:以后改了代码,如果相同 seed、相同 frame 的哈希变了,你知道画面行为发生了变化。

1.4 适用读者和前置知识

这篇文章适合下面几类读者:

  • 会写基础 HTML/CSS/JavaScript,想动手做一个生成式音画 demo。
  • 了解 Canvas 基本绘图,但没做过音频调度。
  • 对 Web Audio API 感兴趣,想要一个最小的合成器示例。
  • 想把“动画可复现”这个设计思路用到自己的项目里。

前置知识不需要太多。你能读懂基本的 JavaScript 函数、事件监听和requestAnimationFrame就够了。Web Audio 和确定性随机数的细节,文章会逐步解释。

2. 搭建单文件骨架:页面、样式、画布三件事

2.1 页面结构:一个 canvas 加一组控制项

单文件项目也要有清晰的页面结构。本项目只做四件事:

  • 一个输入框,用来填写 seed。
  • 一个数字输入框,用来调整 BPM。
  • 一组按钮,控制开始、停止、重置、捕获哈希。
  • 一个 Canvas 画布,用来渲染画面。
  • 一小块文字区域,用来显示当前帧号和像素哈希。

HTML 结构看起来是这样:

<div class="controls"> <label>Seed <input id="seed" type="text" value="default-seed"></label> <label>BPM <input id="bpm" type="number" min="80" max="160" value="124"></label> <button id="startBtn">Start</button> <button id="stopBtn">Stop</button> <button id="resetBtn">Reset</button> <button id="captureBtn">Capture Hash</button> </div> <div id="meta">ready</div> <canvas id="viz" width="960" height="540"></canvas>

注意这里把控制项放在 Canvas 上方,文字显示放在中间,这样在任何屏幕宽度下都不容易被遮挡。

2.2 固定画布尺寸,别让 CSS 随意改变内部像素

Canvas 有一点很关键:<canvas width="960" height="540">是画布的“内部像素尺寸”,CSS 里的width: 100%只影响它显示出来的大小,不会改变内部绘图分辨率。

本项目选择固定960x540作为内部尺寸,有三个原因:

  1. 可复现性好。内部像素固定,getImageData拿到的像素矩阵大小固定,哈希计算稳定。
  2. 显示自适应。CSS 可以把画布缩放到手机屏幕,不影响内部逻辑。
  3. 性能可控。960x540 大约 200 万像素,在普通电脑上 Canvas 2D 绘制足够流畅。

在样式中设置:

canvas { width: 100%; max-width: 960px; height: auto; background: #000; }

这样画布会按比例缩放,但在内部始终以 960x540 的坐标系统绘图。

2.3 脚本组织方式:先定模块,再写代码

单文件不代表可以把代码乱写。为了后面好解释,JavaScript 按模块分成几个区域:

  • 工具函数:字符串哈希、确定性随机数生成器、种子组合。
  • 音频模块:创建 AudioContext、合成 kick/hat/bass、调度音符。
  • 视觉模块:逻辑帧推进、调色板生成、Canvas 绘制。
  • 验证模块:从 Canvas 像素计算哈希。
  • 事件绑定:按钮点击、键盘空格、seed 变化。
  • 初始化:生成种子、画第一帧、启动渲染循环。

这个顺序也是文章后面的讲解顺序。

2.4 最小骨架代码

先给一个最小骨架,完整代码在第 6 章:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>Techno Machine</title> <style> /* 页面样式 */ </style> </head> <body> <!-- 控制区和 canvas --> <script> "use strict"; // 分区编写 JavaScript </script> </body> </html>

这个骨架的核心意义是:让所有代码都在一个文件里,同时保持逻辑清晰。“内联”不等于“混乱”,只要把功能边界分开,单文件也可以有良好的可读性。

3. 用 Web Audio API 写一个最小 techno 音序器

3.1 AudioContext 的创建与恢复

Web Audio API 的核心对象是AudioContext。它管理一个音频图,所有声音节点都连接在这个图上。现代浏览器为了用户体验,通常不允许页面加载后立刻自动播放声音,必须由用户手势触发。

所以项目里不在一开始创建 AudioContext,而是在点击 Start 按钮时创建:

function createAudioGraph() { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); masterGain = audioCtx.createGain(); masterGain.gain.value = 0.7; masterGain.connect(audioCtx.destination); }

创建之后还要调用audioCtx.resume()。因为某些浏览器在页面启动阶段创建 AudioContext,它的状态会停留在suspended,需要用户手势配合恢复。

这里有一个常被忽略的细节:resume()返回一个 Promise。如果你在异步回调里继续调度音符,要确保 Promise 已经 resolve。

3.2 三种基础音色:Kick、Hi-hat、Bass

techno 的基础节奏通常由 Kick、Hi-hat、Bass 组成。用 Web Audio 合成这些音色并不复杂。

Kick 使用一个正弦波振荡器,频率从 160Hz 快速下滑到 46Hz,同时音量快速衰减。这样听起来就是“嘭”一声短促的低鼓:

function playKick(time) { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = "sine"; osc.frequency.setValueAtTime(160, time); osc.frequency.exponentialRampToValueAtTime(46, time + 0.11); gain.gain.setValueAtTime(0.9, time); gain.gain.exponentialRampToValueAtTime(0.001, time + 0.32); osc.connect(gain); gain.connect(masterGain); osc.start(time); osc.stop(time + 0.35); }

频率从高到低快速下滑,是 Kick 音色的关键。如果不加频率变化,只有音量衰减,听起来更像一个闷响,缺少打击感。

Hi-hat 则使用一段噪声缓冲。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询