一个心形线引发的故事光看这个标题你可能以为只是数学课本里的一道例题或者某个程序员炫技画出来的浪漫图案。但真把这条曲线从公式推导到屏幕渲染完整走一遍你会发现它牵扯的东西远比想象中多极坐标与直角坐标的转换、参数方程的构造、隐函数绘制的数值稳定性、不同绘图库对采样密度的敏感度甚至还能延伸到工程上怎么把一条数学曲线变成可复用的图形资源。我前后用不同方式实现过五六遍心形线每次都能踩出新坑这次索性把整个链路拆开讲透。这篇内容适合三类人看一是想搞懂心形线表达式到底怎么来的数学爱好者二是需要用代码画出精确心形曲线的开发者三是做数据可视化或图形生成时被隐函数绘制坑过的从业者。不管你是刚接触参数方程的新手还是已经能熟练调库的老手下面这些推导细节和实操记录应该都能对上你的某些疑问。核心关键词心形线、笛卡尔心形函数、极坐标方程、参数方程、隐函数绘制会自然穿插在各章节里不刻意堆砌。1. 心形线到底是什么从几何定义到方程选型1.1 几何定义先行的理解方式很多人一上来就背公式结果换个坐标系就懵。我的建议是先抓住几何定义心形线是一个圆在另一个同样半径的圆上纯滚动时滚动圆上某一定点划出的轨迹。这个定义叫外摆线的一种特例两个圆半径相等时轨迹恰好收成一个心形。理解这一点很关键因为后面所有方程形式本质上都是对这个滚动过程的代数描述只是选的坐标系不同。从几何定义出发你能直观感受到几个性质曲线关于滚动方向所在直线对称有一个尖点也就是心尖尖点处切线方向发生突变。这些性质在写代码绘制时会直接影响采样策略——尖点附近如果采样太稀画出来就是个圆钝的缺口完全不像心。我第一次用均匀采样画的时候心尖直接糊成一团后来才明白问题出在参数化方式上。1.2 三种主流表达式的取舍逻辑心形线常见的表达式有三类选哪个取决于你的使用场景不是越复杂越好。表达式类型具体形式适用场景主要坑点极坐标方程r a(1 - cosθ) 或 r a(1 cosθ)快速绘图、教学演示尖点处θ采样需加密参数方程x a(2cos t - cos 2t), y a(2sin t - sin 2t)需要逐点坐标、动画轨迹参数范围要覆盖完整周期隐函数方程(x² y² - ax)² a²(x² y²)数学推导、隐函数绘图库数值求解易在尖点失稳极坐标形式最简洁r a(1 - cosθ) 在 θ 从 0 到 2π 时画出的心尖朝左改成 1 cosθ 心尖朝右加负号或换 sin 就能旋转方向。参数方程的好处是直接给出笛卡尔坐标不用做三角函数转换画动画时逐点推进很自然。隐函数形式在数学上最对称美但实际用绘图库画的时候最麻烦因为它是关于 x 和 y 的隐式关系多数绘图工具需要网格采样加等值线提取尖点附近梯度变化剧烈分辨率不够就断线。我个人的选型原则很简单要快就用极坐标要控制每个点就用参数方程要做数学推导或验证恒等式才碰隐函数形式。下面各章节的实操也按这个优先级展开。1.3 为什么标题里强调笛卡尔心形函数严格说心形线不是函数意义上的 y f(x)因为一个 x 对应多个 y它是一条曲线而非单值函数。标题里用笛卡尔心形函数表达式其实是民间叫法指的是笛卡尔坐标系下描述这条曲线的方程。真正在笛卡尔坐标系里写成显式函数是不行的只能写成隐函数或者拆成上下两支。这一点在给别人讲的时候要提前说清楚否则新手会一直纠结为什么解不出 y。我见过有人硬把参数方程消参想得到 y f(x)结果得到带正负根号的分段表达式画出来还得手动拼接上下半支反而比直接用参数方程麻烦。所以别被函数两个字带偏理解成笛卡尔坐标系下的曲线方程就对了。2. 极坐标方程推导一步步把公式推出来2.1 从滚动圆到极坐标关系推导极坐标方程 r a(1 - cosθ) 的过程我习惯用滚动圆的几何关系来推比纯代数消元直观得多。设固定圆半径为 a圆心在原点滚动圆半径也是 a初始时滚动圆上的定点 P 位于固定圆最右侧即 (2a, 0) 位置。滚动圆沿固定圆逆时针滚动角度 φ 后滚动圆圆心转到了角度 φ 的位置圆心坐标为 (a cosφ, a sinφ)。关键在于定点 P 相对滚动圆心的位置。滚动过程中滚动圆自身也转了 φ 角度因为纯滚动圆心走过的弧长等于滚动圆转过的弧长半径相等所以转角相等所以 P 相对圆心的方向是从初始的 0 度方向转过了 φ但要注意方向是反向的。经过几何关系整理P 的坐标可以写成x a cosφ a cos(φ - π) 的修正形式更稳妥的推导是直接用外摆线通式。两个半径相等的圆外摆线参数方程为 x a(2cos t - cos 2t), y a(2sin t - sin 2t)其中 t 是滚动圆圆心转过的角度。把这个参数方程转成极坐标令 r² x² y²展开计算r² a²[(2cos t - cos 2t)² (2sin t - sin 2t)²] a²[4cos²t - 4cos t cos 2t cos²2t 4sin²t - 4sin t sin 2t sin²2t] a²[4 - 4(cos t cos 2t sin t sin 2t) 1] a²[5 - 4cos(t)]这里用到了 cos t cos 2t sin t sin 2t cos(2t - t) cos t。所以 r a√(5 - 4cos t)这看起来和 r a(1 - cosθ) 不一样原因是参数 t 和极角 θ 不是同一个量。要得到简洁的极坐标形式需要换元。令 θ 为极角经过关系推导可得 r 2a(1 - cosθ) 形式的变体具体系数取决于你选的基准半径定义。很多教材直接给 r a(1 - cosθ)这里的 a 是心形线的特征尺度和滚动圆半径差一个倍数使用时按需缩放即可。提示不同教材对 a 的定义不一致有的 a 是滚动圆半径有的 a 是心形线最宽处的一半。抄公式前先确认 a 的几何含义否则画出来的大小对不上。2.2 极坐标绘图的采样策略拿到 r a(1 - cosθ) 后最直接的画法就是 θ 从 0 到 2π 均匀取点算出 r 再转成 (x, y) (r cosθ, r sinθ)。但均匀采样在尖点附近会出问题。尖点出现在 θ 0 处对应 1 - cosθ 0r 0这个位置曲线收缩到原点两侧曲线几乎重合。如果 θ 步长是 0.1 弧度尖点附近相邻两点距离很小但方向突变画出来会看到一个小折角或者缺口。我的处理办法是对 θ 做非均匀采样在 0 和 2π 附近加密。具体做法可以用 θ 2π * (u - sin(u)/(2π)) 这类变换把均匀的 u 映射成两端密的 θ或者更简单粗暴地在 [0, 0.3] 和 [2π-0.3, 2π] 区间用更小的步长。实测下来总采样点数从 200 提到 800 并配合两端加密心尖就能画得很锐利。import numpy as np import matplotlib.pyplot as plt a 1.0 # 非均匀采样两端加密 u np.linspace(0, 2*np.pi, 2000) theta u - 0.15*np.sin(u) # 轻微非均匀尖点附近更密 r a * (1 - np.cos(theta)) x r * np.cos(theta) y r * np.sin(theta) plt.figure(figsize(6, 6)) plt.plot(x, y, colorcrimson, linewidth2) plt.axis(equal) plt.axis(off) plt.show()这段代码里theta u - 0.15*np.sin(u)是一个经验性的非均匀映射sin 项让 u 接近 0 和 2π 时 θ 变化变慢等效于加密。系数 0.15 是我试出来的太大反而让中间段采样过稀。你可以根据实际图形微调。2.3 方向与缩放的快速调整极坐标形式的好处是调整方向特别方便。r a(1 - cosθ) 心尖朝左改成 r a(1 cosθ) 心尖朝右。把 cosθ 换成 sinθ心尖就朝上或朝下。想要倾斜 45 度用 cos(θ - π/4) 即可。缩放直接改 aa 翻倍图形整体放大一倍形状不变。这里有个容易忽略的点a 为负数时图形会翻转。r a(1 - cosθ) 中如果 a 取负r 在大部分 θ 下为负极坐标里负半径表示反向延长画出来是同一个心形但旋转了 180 度。所以别用负 a 来做镜像直接改角度偏移更可控。3. 参数方程实现逐点可控的绘制方案3.1 参数方程的形式与周期确认参数方程 x a(2cos t - cos 2t), y a(2sin t - sin 2t) 里t 从 0 到 2π 走一圈正好画完整个心形。这里 t 的周期是 2π 而不是 4π因为 cos 2t 和 sin 2t 的周期是 π但 2cos t 的周期是 2π整体最小正周期是 2π。确认周期很重要我第一次写的时候想当然用了 4π结果图形重叠画了两遍虽然看起来一样但采样点浪费了一倍。参数方程的优势在于每个 t 直接对应一个 (x, y)做动画时可以让 t 随时间增长笔尖沿着曲线走视觉效果很自然。做轨迹动画时我通常让 t 从 0 线性增长到 2π每帧画到当前 t 为止的折线配合一点缓动函数让起笔和收笔慢一些。3.2 参数方程与极坐标的互相验证写完参数方程版本后我习惯拿它和极坐标版本叠在一起对比确认两者画的是同一条曲线。做法是把参数方程的 (x, y) 转成极坐标 r √(x²y²), θ atan2(y, x)然后和 a(1 - cosθ) 比较。注意这里有个尺度差异参数方程里 a 的含义和极坐标里 a 的含义可能差一个系数直接比 r 值会对不上但归一化后形状应该完全重合。t np.linspace(0, 2*np.pi, 1000) a 1.0 x_param a*(2*np.cos(t) - np.cos(2*t)) y_param a*(2*np.sin(t) - np.sin(2*t)) # 转极坐标验证形状 r_param np.sqrt(x_param**2 y_param**2) theta_param np.arctan2(y_param, x_param) # 归一化后与 1-cos 形式对比形状比例 r_norm r_param / r_param.max() r_ref (1 - np.cos(theta_param)) / 2.0 print(np.max(np.abs(r_norm - r_ref))) # 应接近 0实测这个差值在 1e-3 量级主要来自采样点不完全对齐形状验证通过。这种交叉验证在实现新曲线时很值得做能快速发现公式抄错或参数范围写错的问题。3.3 用参数方程做描边动画描边动画是心形线最出圈的用法之一。核心思路是维护一个进度变量 p 从 0 到 1每帧取 t 从 0 到 2πp 的曲线段画出来再在末端画一个小圆点当笔尖。为了让动画不显得机械我给 p 加了一个缓入缓出p 0.5 - 0.5*cos(π * progress)progress 是线性时间。这样起笔和收笔都慢中间快观感更舒服。注意动画帧率低的时候如果每帧都重画整条曲线性能会随进度增长而下降。更好的做法是只画新增的线段或者用固定采样点数按进度截取。我一般预采样 2000 个点每帧按进度取前 N 个点绘制2000 点的折线在现代设备上毫无压力。参数方程还有个隐藏好处尖点位置明确。t 0 时 x a(2 - 1) a, y 0这是心尖对应的点取决于方向。知道尖点参数值后做局部放大或者特殊标记就很方便不用像极坐标那样去数值搜索 r 最小的位置。4. 隐函数形式与数值绘制的坑4.1 隐函数方程的结构解读隐函数形式 (x² y² - ax)² a²(x² y²) 看起来吓人拆开看其实有规律。左边是 (x² y² - ax)²右边是 a²(x² y²)。令 ρ² x² y²方程变成 (ρ² - ax)² a²ρ²开方得 ρ² - ax ±aρ。取正号整理得 ρ² - aρ - ax 0这是关于 ρ 的二次方程解出来再转极坐标就能回到 r a(1 cosθ) 的形式。所以隐函数形式和极坐标形式是等价的只是表达方式不同。理解这个等价关系后你就明白隐函数形式并不是另一种曲线而是同一条曲线的另一种写法。它的价值在于某些数学推导里对称性更好看比如证明曲线围成面积时用隐函数形式做对称性分析更顺手。但绘图时它是最不友好的形式。4.2 网格采样绘制的分辨率陷阱用等值线方式画隐函数本质是在网格上计算 F(x, y) (x²y²-ax)² - a²(x²y²) 的值然后找 F 0 的等值线。问题在于尖点附近 F 的梯度变化极大网格分辨率不够时等值线提取算法会在尖点处断开或者画出一个圆角。我做过对比实验网格从 200×200 提到 500×500尖点从明显圆钝变成基本锐利提到 1000×1000 后改善不明显但内存和计算时间翻倍。所以 500×500 左右是个性价比不错的档位。另外网格范围要留余量心形线最宽处约 2a最长处约 2a范围取 [-1.5a, 2.5a] 比较稳妥太紧会裁掉边缘。import numpy as np import matplotlib.pyplot as plt a 1.0 n 600 x np.linspace(-1.5*a, 2.5*a, n) y np.linspace(-2*a, 2*a, n) X, Y np.meshgrid(x, y) F (X**2 Y**2 - a*X)**2 - a**2*(X**2 Y**2) plt.figure(figsize(6, 6)) plt.contour(X, Y, F, levels[0], colorscrimson, linewidths2) plt.axis(equal) plt.axis(off) plt.show()这段代码能画出完整心形但你会注意到尖点处线条略粗或者有轻微毛刺这是等值线算法在梯度剧烈处的正常表现。要更平滑可以后处理做一次曲线平滑或者干脆改用参数方程绘制。4.3 隐函数绘制的适用边界说句实在话纯为了画一个心形隐函数形式是最不推荐的路子。它的计算成本高、尖点质量差、参数调整不直观。那什么时候用它一是教学场景要展示同一个曲线的不同表达二是某些数学软件或论文里需要隐函数形式做符号推导三是你想验证自己实现的等值线算法在极端梯度下的表现。除此之外参数方程和极坐标都能更快更好地完成任务。我踩过的一个坑是拿隐函数形式去做高分辨率导出结果 2000×2000 网格算了好几分钟还爆内存。后来改成参数方程直接生成矢量路径导出又快又清晰。这个教训就是绘图目的决定表达形式别为了公式好看牺牲实用性。5. 常见问题与排查实录5.1 图形变形与方向错乱的排查表实际动手时遇到的问题五花八门我整理了一张速查表覆盖大部分新手会撞上的情况。现象可能原因排查与解决心形被压扁或拉长坐标轴未等比例设置 axis equal 或手动锁定宽高比心尖处有缺口采样密度不足尖点附近加密采样或提高总点数图形只画了一半参数范围不足确认 θ 或 t 覆盖完整 2π心形方向不对公式符号或角度偏移换 1±cosθ 或加角度偏移调整隐函数绘制断线网格分辨率不够提高网格数到 500 以上图形位置偏移坐标范围设置不当调整绘图区间留足余量这张表里最容易被忽略的是第一行。很多人画完发现心形胖了或者瘦了以为是公式错了其实是绘图窗口不是正方形x 和 y 单位长度不一致导致视觉变形。加一行等比例设置立刻恢复正常。5.2 采样点数与文件体积的平衡做网页或移动端展示时采样点数直接影响文件体积和渲染性能。我做过一组实测200 点的心形在手机屏幕上已经看不出棱角500 点在任何常规尺寸下都足够平滑2000 点只在超大屏或者需要局部放大时才必要。如果是 SVG 路径点数直接决定路径字符串长度200 点的路径大约几 KB2000 点就上几十 KB 了。我的经验值是静态展示用 300 到 500 点动画用 1000 到 2000 点预采样再按进度截取。别盲目堆点数超过视觉分辨率的采样纯属浪费。判断标准很简单把图形放大到实际显示尺寸的两倍如果还看不出折线感点数就够了。5.3 公式抄错的高频位置心形线公式在传播过程中被抄错的地方很集中我见过最多的三类错误一是参数方程里 cos 2t 的系数写成 2cos 2t 或者漏掉负号二是极坐标里 1 - cosθ 的括号位置搞错写成 1 - cos θ 但实际想表达的是 (1 - cos)θ 之类三是隐函数里 a² 和 a 的幂次混淆。这些错误画出来的图形往往看起来像心但哪里不对劲比如心尖偏移或者左右不对称。排查办法是拿标准形式做数值比对取几个特殊角度比如 θ 0, π/2, π手算 r 值再和代码输出对比。θ 0 时 r 应为 0尖点θ π 时 r 应为 2a最宽处这两个点对上了基本就没大问题。6. 从一条曲线到可复用图形资源6.1 导出为矢量路径的实操把心形线做成可复用资源最实用的形式是 SVG 路径。参数方程采样后得到点列转成 SVG 的 path 的 M 和 L 指令即可。想要曲线更平滑可以用二次或三次贝塞尔拟合但对心形线这种光滑曲线500 点折线在视觉上已经等价于曲线没必要上贝塞尔增加复杂度。def heart_to_svg_path(a1.0, n500): t np.linspace(0, 2*np.pi, n) x a*(2*np.cos(t) - np.cos(2*t)) y a*(2*np.sin(t) - np.sin(2*t)) # 翻转 y 轴适配 SVG 坐标系 pts [f{xi:.3f},{-yi:.3f} for xi, yi in zip(x, y)] return M L .join(pts) Z导出时注意 SVG 的 y 轴向下数学坐标的 y 轴向上所以要取负。这个细节不处理的话心形会上下颠倒尖点跑到上面去。我第一次导出没注意做出来的图标全是倒心排查了半天才发现是坐标系方向问题。6.2 参数化调整的实用技巧做成资源后最常被要求的就是能不能改胖一点能不能歪一点。对应到公式上改胖瘦调 a 没用a 只是整体缩放真正改形状需要引入额外参数。一个简单办法是对 x 或 y 方向单独缩放比如 x 乘 1.2 让心形变宽但这会破坏曲线的数学纯粹性。如果只是做图标这种变形完全可以接受如果要求严格的心形线就只能整体缩放不能单轴拉伸。旋转的话在参数方程里给 t 加偏移即可或者在导出后对点列做旋转矩阵变换。我一般选后者因为旋转矩阵对所有点统一处理不用改公式复用性更好。6.3 性能与精度的最终取舍走到资源化这一步核心矛盾就是精度和体积。我的最终方案是默认 400 点提供 200 点精简版和 1000 点高清版两档。400 点覆盖 95% 的使用场景200 点用于小尺寸图标1000 点用于需要放大的展示。这个分档策略来自实际项目里的反馈——大多数场景根本用不到超高精度给多了反而增加加载负担。另外提醒一句如果心形要参与碰撞检测或者面积计算这类几何运算别用采样点列近似直接用解析形式算。采样点做面积计算会有系统性偏差点数越多越接近真值但永远差一点而解析积分能给出精确结果。这个区分在图形编程里很关键画图和算图是两套逻辑。我在实际使用中最大的体会是心形线这种看起来简单的曲线真正画好、用好、复用好的关键不在公式本身而在采样策略和坐标系处理这些工程细节上。公式五分钟就能查到但尖点怎么加密、导出怎么翻轴、点数怎么定这些只能靠一遍遍试出来。上面这些参数和代码都是我反复调过的你可以直接拿去改但建议至少自己手推一遍极坐标到参数方程的转换推过之后对曲线的理解会完全不一样。
阅读完成 · 觉得有帮助?