- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
正弦波是自然界中最基础也最普遍的振荡形态——从钟摆、弹簧到声波、水波,都可以用sin()函数描述。本篇文章以 The Coding Train 的The Nature of Code课程第 3.6 节「Graphing Sine Wave」为主线,基于本仓库 learning/nature-of-code/3.6-graphing-sine 目录下的四个 p5.js 示例,系统讲解如何把正弦函数绘制成一条可见的波形曲线,并通过**周期(period)与相位(phase)**的变化让波形动起来。读完本文你将掌握:静态正弦图的逐点绘制方法、用角速度控制波形动画、用极坐标生成圆形波纹,以及如何把波形封装成可复用的 Wave 类并叠加多条波。
一、从「呼吸的圆」到「一条波」:为什么 sin() 能画出波形
在进入绘图之前,先回顾上一节 3.5 Simple Harmonic Motion 中的核心思想:简谐运动(Simple Harmonic Motion)的位移可以由sin(angle)描述。当角度随时间线性增长时,sin()的返回值在 -1 与 1 之间平滑往复,映射到屏幕坐标上就表现为来回振荡。
在 3.5 节的 main 示例 中,作者用这一思想画出了一个「呼吸的圆」:
let angle = 0; let angleV = 0; function draw() { background(0); translate(200, 200); fill(252, 238, 33); let r = map(sin(angle), -1, 1, 0, 200); // 将 sin 值映射为圆的半径 circle(0, 0, r * 2); angle += angleV; // 角度随时间增长 angleV += 0.0001; // 角速度也在增长 }这里只采样了「一个角度」对应的值。而波形图(sine wave graph)的本质,是同时取许多个角度,把每个角度对应的sin()值放在不同的 x 位置上——x 代表角度,y 代表该角度的正弦值,把这些点连起来就得到了一条波。
要完整描述一条波,需要三个参数(本仓库 3.7 节的 Wave 类 对它们有清晰的封装):
| 参数 | 含义 | 典型取值范围 |
|---|---|---|
| 振幅 amplitude | 波峰到平衡位置的高度,控制波形纵向拉伸 | 如 20~200 像素 |
| 周期 period | 完成一个完整波形所需的 x 跨度/时间 | 如 100~600 像素 |
| 相位 phase | 波形在 x 轴上的水平偏移,驱动动画 | 0~TWO_PI 之间递增 |
二、第一个静态正弦图:Sine Wave Graph Exercise
最基础的做法是一次性画出整条波,不做动画。对应仓库目录 sine-wave-graph-exercise(p5.js Web Editor 编号 EIbEYLTaZ)。
完整代码如下:
let r = 4; function setup() { createCanvas(600, 400); } function draw() { background(0); translate(300, 200); // 把原点移到画布中心 let total = floor(width / (r * 2)); // 能放下的点数量 noFill(); stroke(252, 238, 33); strokeWeight(8); beginShape(); for (let i = 0; i < total + 1; i++) { let angle = map(i, 0, total, 0, TWO_PI); // x 方向均匀分布角度 0~2π let y = map(sin(angle), -1, 1, -100, 100); // 正弦值映射到 y 轴 let x = map(i, 0, total + 1, -300, 300); // 等间距排列 x 坐标 vertex(x, y); // 记录一个顶点 } endShape(); // 连接所有顶点成一条曲线 }关键点拆解:
- 角度的分配:
map(i, 0, total, 0, TWO_PI)把索引i从 0 线性映射到2π,相当于让 x 轴每一列对应一个角度,整条波覆盖一个完整的正弦周期。 - 值的映射:
sin()输出范围是 [-1, 1],直接画会太小,因此用map(sin(angle), -1, 1, -100, 100)放大到 y 轴 ±100 像素。 - 绘制方式:
beginShape()+vertex(x, y)+endShape()是 p5.js 中连接离散点为连续曲线的标准手法;注释中的strokeWeight(8)让波形更醒目。
运行后你会看到一条静止的黄色正弦波横贯画布,这就是所有后续动画的基础形态。
三、让波动起来:Playing With Period(改变角速度)
静态图只是「一张照片」。真正的动画需要让每个点的角度随时间变化,于是波形就会像水波一样「流动」。仓库目录 playing-with-period(Web Editor 编号 SbRC-G0lU)演示了每个点拥有独立角速度的做法:
let angles = []; let angleV = []; let r = 16; function setup() { createCanvas(600, 400); let total = floor(width / (r * 2)); for (let i = 0; i < total; i++) { angles[i] = 0; // 每个点的初始角度 angleV[i] = 0.01 + i / 100; // 每个点各自的角速度,随 i 递增 } } function draw() { background(0); translate(300, 200); fill(252, 238, 33); stroke(252, 238, 33); for (let i = 0; i < angles.length; i++) { let y = map(sin(angles[i]), -1, 1, -200, 200); strokeWeight(4); let x = map(i, 0, angles.length, -300, 300); line(x, 0, x, y); // 从平衡线画一根竖直的"琴弦" circle(x, y, r * 2); // 在波的位置画圆点 angles[i] += angleV[i]; // 每个点按自己的速度累加角度 } }这一版与静态图的本质区别在于:
- 角度不再由 x 位置唯一决定,而是存进数组
angles[],在每帧draw()中累加更新,形成动画; - 角速度
angleV[i] = 0.01 + i / 100逐点递增:位置越靠右的点转得越快。这改变了波形各处的周期(period)——周期短的区域波形更「挤」,周期长的区域波形更「疏」,直观地展示了「改变周期会改变波形的疏密」这一知识点(对应视频章节Code! Let's try varying the period); - 每个 x 位置还画了一条从平衡线
y = 0到波形点的垂直线(line(x, 0, x, y)),把「位移」可视化出来,方便观察每根琴弦的伸缩。
四、等速流动的波:Graphing Wave(统一增量)
如果希望波以统一的速度整体平移(而不是疏密变化),需要让所有点的角速度相同。仓库目录 graphing-wave(Web Editor 编号 c_S9jiXz-)给出了答案:
let angles = []; let angleV = []; let r = 4; function setup() { createCanvas(600, 400); let total = floor(width / (r * 2)); for (let i = 0; i < total + 1; i++) { angles[i] = map(i, 0, total, 0, 2 * TWO_PI); // 初始相位覆盖两个完整周期 // angleV[i] = 0.01 + i / 100; // 注释掉的差异化角速度 } } function draw() { background(0); translate(300, 200); fill(252, 238, 33); stroke(252, 238, 33); beginShape(); for (let i = 0; i < angles.length; i++) { let y = map(sin(angles[i]), -1, 1, -200, 200); strokeWeight(4); let x = map(i, 0, angles.length, -300, 300); circle(x, y, r * 2); // 用圆点描出波形的每个点 angles[i] += 0.02; // 所有点统一增量 0.02,波形整体匀速平移 // angles[i] += angleV[i]; // 注释掉的差异化角速度 } endShape(); }两个值得注意的设计:
- 初始相位覆盖
2 * TWO_PI:每个点的起始角度横跨两个完整周期,因此画布上同时出现两条波峰,视觉上更丰富; - 统一增量
angles[i] += 0.02:所有点每帧增加相同的弧度,波形保持形状不变、整体匀速向左(或向右)流动。视频中作者先演示了差异化角速度的「变形」效果,再切回统一增量让波「丝滑」平移(对应章节Code! Whoa! Let's use a static increment value)。
五、进阶变形:Circular Wave Exercise(极坐标中的圆波)
正弦波不一定要画在直角坐标系里。仓库目录 circular-wave-exercise(Web Editor 编号 mOm2Is7ba)把正弦值叠加到圆的半径上,用极坐标生成一圈起伏的「圆形波」(水波纹效果):
let angle = 0; let r; let phase = 0; function setup() { createCanvas(windowWidth, windowHeight); r = width / 3; } function draw() { background(0); translate(width / 2, height / 2); stroke(255); strokeWeight(8); fill(50); // let increment = map(mouseX, 0, width, PI, 0.01); // 可以用鼠标控制精细度 let increment = TWO_PI / 32; // 沿圆周采样 32 个点 beginShape(); for (let a = 0; a < TWO_PI; a += increment) { let r1 = r + sin(a * 10 + phase) * 50; // 半径随角度正弦变化 let x = r1 * cos(a); // 极坐标 → 直角坐标 let y = r1 * sin(a); curveVertex(x, y); } endShape(CLOSE); // 闭合曲线 phase += 0.05; // 相位递增 → 波纹旋转 }实现要点:
- 极坐标转直角坐标:
x = r1 * cos(a)、y = r1 * sin(a)是极坐标绘图的标准公式,此前在 3.4 Polar Coordinates 一节已经系统介绍过; - 半径调制:
r1 = r + sin(a * 10 + phase) * 50表示圆周上每一点的半径都在基础值r附近波动,a * 10让波沿圆周出现 10 个起伏,phase则控制起伏的旋转位置; - 动画驱动:每帧
phase += 0.05,使波纹围绕圆心匀速旋转;endShape(CLOSE)把首尾闭合形成完整圆环。注释中还保留了用鼠标控制采样精细度的方案(map(mouseX, 0, width, PI, 0.01))。
六、从散点走向工程化:封装 Wave 类并叠加多条波
掌握单个正弦波的绘制后,下一步是把逻辑抽象成类,方便复用与叠加。这正是下一节 3.7 Additive Waves 做的事情,其核心是 wave.js 中仅有 14 行的 Wave 类:
class Wave { constructor(amp, period, phase) { this.amplitude = amp; // 振幅 this.period = period; // 周期 this.phase = phase; // 相位 } evaluate(x) { // 标准波形公式:y = sin(相位 + 2π·x / 周期) × 振幅 return sin(this.phase + (TWO_PI * x) / this.period) * this.amplitude; } update() { this.phase += 0.05; // 相位递增驱动动画 } }配套的 sketch.js 则演示了如何创建 5 条随机参数(振幅 20~80、周期 100~600、随机相位)的波,并在每个 x 位置把它们的evaluate(x)结果累加起来:
let waves = []; function setup() { createCanvas(600, 400); for (let i = 0; i < 5; i++) { waves[i] = new Wave(random(20, 80), random(100, 600), random(TWO_PI)); } } function draw() { background(0); for (let x = 0; x < width; x += 10) { let y = 0; for (let wave of waves) { y += wave.evaluate(x); // 多条波在同一位置的叠加 } noStroke(); ellipse(x, y + height / 2, 16); // 在叠加结果处画点 } for (let wave of waves) { wave.update(); // 每帧推进所有波的相位 } }这一小段代码把第 3.6 节的绘图技术升级成了模块化、可参数化的波形系统:evaluate(x)接收 x 坐标返回该点的位移,update()负责推进时间。多条波叠加后会产生复杂的干涉图形,这正是真实世界声波、潮汐、以及傅里叶级数(可参考仓库内 CC_125_Fourier_Series 等挑战)的视觉雏形。
七、拓展练习与学习路径
本视频在原文档 3.6-graphing-sine 中给出了明确的练习方向,结合本仓库代码可以继续探索:
- 改变起点角度(对应视频Code! Change the starting angles):修改
angles[i]的初始赋值(如map(i, 0, total, 0, TWO_PI)改为更小的范围),观察波形在 x 方向被「压缩」或「拉伸」; - 回到差异化角速度:取消
graphing-wave中angles[i] += angleV[i]一行的注释,对比统一增量与差异化角速度下波形形态的差异; - 制作自己的作品:在 sine-wave-graph-exercise 基础上把波形纵向偏移、旋转,或用多条不同周期/相位的波叠加出复杂图案——这正是 3.7 节 Additive Waves 的入口。
想继续深入,可以按课程顺序阅读 3.5 Simple Harmonic Motion(理解振幅与周期的来源)、3.4 Polar Coordinates(极坐标基础)、3.7 Additive Waves(波的叠加),最终把这些振荡思维应用到粒子系统(第 4 章)、自治代理(第 5 章)等更复杂的自然模拟中。
八、小结
本文完整复现了 The Nature of Code 3.6 节「Graphing Sine Wave」的四步进阶:静态绘波(sine-wave-graph-exercise)→ 差异化角速度改变周期(playing-with-period)→ 统一增量形成流动波(graphing-wave)→ 极坐标圆形波纹(circular-wave-exercise)。核心公式始终是y = sin(angle)配合map()做坐标映射,动画的本质则是让每个点的角度随帧递增。掌握这几个模式后,你便拥有了在 p5.js 中生成一切波状运动的基础工具——无论是水流、声波还是摆动,都不过是振幅、周期与相位这三个旋钮的组合游戏。
- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
相关推荐
The Nature of Code p5.js 示例项目教程
The Nature of Code p5.js 示例项目教程 1. 项目介绍 The Nature of Code p5.js 是一个开源项目,旨在将《The
示例工程文档/教程前端Nature of Code 5.18:用 p5.js 与 matter.js 搭建 2D 物理世界(Coding Train 仓库实战)
Nature of Code 5.18:用 p5.js 与 matter.js 搭建 2D 物理世界(Coding Train 仓库实战) 本篇文章以 Codi
教程文档示例工程p5.js 与 Processing 中的 2D Supershape(超级公式)绘制完全指南:从 Superformula 到实时动画
p5.js 与 Processing 中的 2D Supershape(超级公式)绘制完全指南:从 Superformula 到实时动画 导读 本篇技术指南以
教程文档示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考