news 2026/10/14 15:03:17

p5.js 正弦波绘制与动画:The Nature of Code 3.6 Graphing Sine Wave 实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
p5.js 正弦波绘制与动画:The Nature of Code 3.6 Graphing Sine Wave 实战指南
  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载

正弦波是自然界中最基础也最普遍的振荡形态——从钟摆、弹簧到声波、水波,都可以用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(); }

两个值得注意的设计:

  1. 初始相位覆盖2 * TWO_PI:每个点的起始角度横跨两个完整周期,因此画布上同时出现两条波峰,视觉上更丰富;
  2. 统一增量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 中给出了明确的练习方向,结合本仓库代码可以继续探索:

  1. 改变起点角度(对应视频Code! Change the starting angles):修改angles[i]的初始赋值(如map(i, 0, total, 0, TWO_PI)改为更小的范围),观察波形在 x 方向被「压缩」或「拉伸」;
  2. 回到差异化角速度:取消graphing-wave中angles[i] += angleV[i]一行的注释,对比统一增量与差异化角速度下波形形态的差异;
  3. 制作自己的作品:在 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)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 15:02:31

插件系统原理与故障排查:从接口约定到插件开发实践

1. 从一次加载失败说起&#xff1a;插件是怎么工作的我先说一个最近的经历。有次在配置一个持续交付流水线&#xff0c;启动服务后给我甩了一个failed to load plugins web boot: 2 entries did not activate的报错&#xff0c;附带两个插件名称。当时第一反应是"插件没生…

作者头像 李华
网站建设 2026/10/14 15:02:26

UE C++组件非构造函数创建全攻略:动态挂载与踩坑指南

做UE C的朋友应该都听过一条“铁律”&#xff1a;组件必须在构造函数里用CreateDefaultSubobject创建。我一开始也是这么背的&#xff0c;直到第一次在运行时给角色动态挂武器、给程序化生成的建筑添门加灯&#xff0c;才意识到这条铁律还有后半句——构造函数只是创建组件最常…

作者头像 李华
网站建设 2026/10/14 15:02:14

智能桌面时钟设计

** 智能桌面时钟设计** 一直想做个智能桌面小摆件&#xff0c;能显示时间&#xff0c;能看天气&#xff0c;能更换背景&#xff0c;现在做好了&#xff0c;贴出来分享一下&#xff0c;记录一下历程。 最先想到的是屏幕必须是圆的&#xff0c;平板和手机已经把方…

作者头像 李华
网站建设 2026/10/14 15:01:44

Neo4j+RAG+Agent工程落地实战:多模态知识库与抗投毒架构

1. 项目概述&#xff1a;这不是一份“新闻简报”&#xff0c;而是一份面向工程落地的Agent/LLM技术实践切片“Agent / LLM 技术精选日报 2026-09-28&#xff08;知乎版&#xff09;”这个标题&#xff0c;乍看像一份资讯汇编&#xff0c;但实际它承载的是当前AI工程化最前沿、…

作者头像 李华
网站建设 2026/10/14 15:01:27

数学规划模型实战指南:从线性规划到生产排产优化

我做运筹优化相关工作这些年&#xff0c;被问得最多的一句话是&#xff1a;你们天天说数学规划模型&#xff0c;是不是就是列几个方程扔给电脑算&#xff1f;每次我都得解释——列方程只是最后一步&#xff0c;真正难的&#xff0c;是把一个具体的业务问题翻译成人话&#xff0…

作者头像 李华
网站建设 2026/10/14 15:00:23

编程尚未被解决:从表达、复杂度到验证的工程实践思考

1. 为什么“编程尚未被解决”不是一句丧气话第一次看到“编程尚未被解决”这个说法&#xff0c;很多人下意识会觉得这是一句吐槽&#xff0c;甚至是一句丧气话。毕竟我们每天写代码、跑测试、上线部署&#xff0c;日子照样过&#xff0c;需求照样交付&#xff0c;怎么就“尚未被…

作者头像 李华