☰
Circle Morphing(一):用极坐标与线性插值实现圆到三角形的形状变形
2026/10/12 4:41:14 网站建设 项目流程
  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

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

本篇技术指南围绕 Coding Train「编码挑战」系列第 81.1 期展开,讲解如何实现艺术家与教育家 Golan Levin 提出的Circle Morphing(圆形变形)挑战的一种解法:在一个圆的圆周路径与一个三角形的路径之间,通过逐点配对与线性插值(lerp),让形状在二维平面中连续、平滑地来回变形。读完本文,你将掌握「用相同数量的采样点描述两种几何形状,再按索引逐点插值实现形状补间」这一可复用到任意 2D 形状动画的通用方法,并能在 p5.js 与 Processing 两套环境中直接运行仓库内提供的完整实现。

挑战背景:什么是 Circle Morphing

Circle Morphing 由 Golan Levin 提出,主题是插值(interpolation)——在两个形状之间找到中间形态。Golan Levin 在 Coding Train 客座系列中先讲解了模运算(The Modulo Operator with Golan Levin),随后发布了 Circle Morphing Challenge 视频,其中给出了历史背景并提出了本次挑战。本仓库的关联文档 081.1-circle-morphing-part-1 记录了 Coding Train 对该挑战的第一次解答,配套源码位于 CodingChallenges/CC_081.1_Circle_Morphing_Part_1(p5.js 与 Processing 两个版本)。

本期的核心任务是:让一个圆平滑地变形成三角形,再变回圆。因为圆与三角形是两种截然不同的几何形状,变形不能靠旋转或缩放完成,而必须重新思考「如何描述一个形状的轮廓」。

核心技术准备:极坐标与线性插值

实现本次挑战需要两个数学工具:

1. 极坐标 ↔ 笛卡尔坐标转换

圆形天然适合用极坐标描述:给定半径r与角度a,圆周上的点可由三角函数换算为笛卡尔坐标:

x = r * cos(a) y = r * sin(a)

p5.js 源码中的polarToCartesian函数正是这一公式的直接实现(见 P5/sketch.js):

function polarToCartesian(r, angle) { return createVector(r * cos(angle), r * sin(angle)); }

注意 p5.js 中通过angleMode(DEGREES)将角度单位设为角度制;而 Processing 版本由于内置三角函数默认使用弧度,需额外用radians(angle)转换(见 CC_081_1_Circle_Morphing_Part_1.pde)。相关极坐标基础可参考配套教程 9.20 Polar Coordinates。

2. 线性插值(lerp)

lerp(a, b, amt)返回a与b之间按比例amt(0~1)混合的值:amt=0时结果为a,amt=1时结果为b。两个端点相同,中间插值便连成一条直线——这正是把圆弧上的点「拉」成三角形边的关键。p5.js 中既可用全局lerp(x1, x2, amt)插值单个数,也可用p5.Vector.lerp(v1, v2, amt)插值整个向量。

核心算法思想:路径点配对

要让圆平滑地变形成三角形,最直接的做法是:用相同数量的点分别采样圆与三角形两条轮廓路径,使第i个圆点与第i个三角形点一一对应,再对每一对点按同一比例amt插值。由于两条路径点数完全一致且顺序相同,插值后得到的点集依然是一条首尾相连的闭合轮廓,从而形成连续的中间形状。

p5.js 实现中用两个数组存放两条路径(P5/sketch.js):

let cirPath = []; // 圆的路径点 let triPath = []; // 三角形的路径点 let spacing = 10; // 角度采样步长(度) let theta = 0; // 用于驱动变形的动画角度

spacing决定采样密度:步长越小,路径点越多,轮廓越平滑。

第一步:生成圆形路径

圆的采样非常直观:从 0° 到 360°,每隔spacing度取一个点(源码 P5/sketch.js):

for (let a = startA; a < 360; a += spacing) { let cv = polarToCartesian(radius, a); cirPath.push(cv); ... }

这里半径radius = 100,画布400 × 400,绘制时会通过translate(width / 2, height / 2)将坐标系原点移到画布中心,因此路径点围绕中心分布。由于是闭合形状,绘制时使用endShape(CLOSE)自动连接首尾。

第二步:生成三角形路径——三段 120° 圆弧内插值

三角形路径是本期算法的精髓。思路是:把三角形看成由三条边组成,每条边对应圆周上的一段 120° 圆弧。对于每一段圆弧,取圆弧的起点start与终点end(都在圆周上),然后对该段圆弧内的每个采样点,用amt = (a % 120) / 120作为比例,在起点与终点之间做线性插值,得到一个位于三角形边上的点。

p5.js 实现(P5/sketch.js):

let radius = 100; let startA = 0; let endA = 120; let start = polarToCartesian(radius, startA); let end = polarToCartesian(radius, endA); for (let a = startA; a < 360; a += spacing) { let cv = polarToCartesian(radius, a); cirPath.push(cv); let amt = (a % 120) / (endA - startA); // 当前点在本段圆弧内的比例 0~1 let tv = p5.Vector.lerp(start, end, amt); // 在边的两端点之间插值 triPath.push(tv); // 每走完 120°,切换到下一条边 if ((a + spacing) % 120 === 0) { startA = startA + 120; endA = endA + 120; start = polarToCartesian(radius, startA); end = polarToCartesian(radius, endA); } }

逐段拆解:

  • a % 120:把全局角度映射到当前 120° 圆弧段内的相对位置,模运算在此处起到「段内定位」的作用,正是 Golan Levin 模运算教程的应用场景。
  • amt = (a % 120) / (endA - startA):归一化到 0~1,作为插值比例。由于endA - startA恒等于 120,等价于除以 120。
  • p5.Vector.lerp(start, end, amt):在圆弧两端点之间插值。amt=0时取起点,amt=1时取终点,中间值在连接两点的直线(即三角形边)上滑动。
  • 段切换:当(a + spacing) % 120 === 0时,把起点终点各加 120°,进入下一条边。三轮循环后覆盖 0°~360°,三条边首尾相连闭合。

这样,triPath中的点数与cirPath完全相同(都是360 / spacing = 36个点),且每个三角形点都与一个圆点一一对应——这是后续逐点插值的前提。

第三步:动态变形绘制

变形动画在draw()中完成。每一帧用正弦波生成随时间往返的插值比例amt,让形状在圆与三角形之间来回变化(P5/sketch.js):

function draw() { background(220); translate(width / 2, height / 2); rotate(30); stroke(0); strokeWeight(4); noFill(); let amt = (sin(theta) + 1) / 2; // 在 0~1 之间往复 theta += 5; beginShape(); for (let i = 0; i < cirPath.length; i++) { let cv = cirPath[i]; let tv = triPath[i]; let x = lerp(cv.x, tv.x, amt); // 逐点插值 let y = lerp(cv.y, tv.y, amt); vertex(x, y); } endShape(CLOSE); }

关键细节:

  • amt = (sin(theta) + 1) / 2:sin(theta)的值域为 [-1, 1],加 1 后为 [0, 2],除以 2 归一化到 [0, 1]。每帧theta += 5使比例随时间平滑往复,形状便在圆与三角形之间来回「呼吸」。若把amt固定为 0 或 1,则分别得到纯圆或纯三角形。
  • 逐点插值:对每对(cv, tv)分别插值x与y,等价于对位置向量整体做lerp。
  • rotate(30):将整体旋转 30°,让三角形呈现端正的视觉姿态;Processing 版对应rotate(radians(30))。
  • noFill()+strokeWeight(4):只描边不填充,便于观察轮廓变形过程。

源码中还保留了被注释的调试代码:单独绘制cirPath、单独绘制triPath,以及用ellipse标出三角形路径点,取消注释即可直观查看两条路径的采样分布,便于理解配对关系。

参数与变量一览

变量/常量值(p5.js)作用
spacing10(度)圆周采样步长,决定路径点数与轮廓平滑度
radius100圆的半径,也是三角形顶点的外接圆半径
startA/endA0 / 120(度)当前三角形边对应的圆弧起止角度
amt(构造期)(a % 120) / 120三角形边内的插值比例
amt(绘制期)(sin(theta) + 1) / 2全局变形比例,0 为圆,1 为三角形
theta每帧 +5驱动变形的动画角度

调整spacing可以观察采样密度对轮廓精度的影响(越小越平滑但点更多);调整radius可缩放整体大小;theta的增量则控制变形速度。

Processing 版本移植要点

仓库同时提供了 Processing 版实现 CC_081_1_Circle_Morphing_Part_1.pde,与 p5.js 版逻辑完全一致,仅有几处语言差异:

  • 用ArrayList<PVector>替代 JavaScript 数组;
  • 三角函数默认弧度制,polarToCartesian与rotate处需用radians()显式转换;
  • PVector.lerp(start, end, amt)为静态方法,参数顺序与 p5.js 相同;
  • lerp(cv.x, tv.x, amt)的全局函数签名与 p5.js 一致。

运行方式

  • p5.js 版:直接用浏览器打开 P5/index.html。页面通过 CDN 引入 p5.js(1.4.1)并加载同目录下的sketch.js,无需本地构建。
  • Processing 版:用 Processing IDE 打开 CC_081_1_Circle_Morphing_Part_1.pde 直接运行。

延伸阅读:Part 2 的另一种解法

本期采用「逐点插值」方案;第 2 期 081.2-circle-morphing-part-2 则实现另一种思路:不重新生成三角形路径,而是在圆形路径上逐步删除可移动的点,只保留三个 120° 间隔的固定点,让圆逐渐「塌缩」成三角形。其源码见 CodingChallenges/CC_081.2_Circle_Morphing_Part_2/P5/sketch.js(Processing 版为 CC_081_2_Circle_Morphing_Part_2.pde)。对比两期实现,可以深入体会「点配对插值」与「点删除重排」两种形状变形范式的差异。

总而言之,本期的价值不止于一个演示:任何两个由等量有序采样点描述的闭合轮廓,都可以用逐点lerp实现平滑变形。这一模式可直接推广到星形、多边形、不规则有机体等任意 2D 形状之间的补间动画。

  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

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

相关推荐

上一篇:战绩查询与自动 B/P:Seraphine 上手指南
下一篇:LeetDown 3步降级iPhone 5与iPad 4

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

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

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

立即咨询