曲线艺术编程 coding curves 第十四章 其它曲线(Miscellaneous Curves)
第十四章 其它曲线(Miscellaneous Curves)
原作:Keith Peters https://www.bit-101.com/blog/2022/11/coding-curves/
译者:池中物王二狗(sheldon)
曲线艺术编程系列 第十四章
这是系列文章规划的最后一章。如果后面发现其它有趣的曲线类型可能加在这一章。我原计划清单里有几个主题没放出来,当然也不排除某天我改主意了。未来额外的内容也可能另起一章加到目录索引中。
在“最后”一篇, 我想我会讲一些随机曲线,这些曲线不值得单独开一章来讲。还有,我觉得把我从找到公式到编码的过程完整过一遍会很不错。
大麻曲线

Weisstein, Eric W "大麻曲线" 来源于网站 https://mathworld.wolfram.com/CannabisCurve.html
Wolfram Mathworld 是一个很好的发掘有趣公式的地方,顺便说一句,如果你想发掘更多 2d 曲线,那么在平面曲线(Plane Curve)这一章节可以深入找找。网站内容很全,还有其它曲线类型可探索。
为什么选择大麻曲线?。我只是觉得它很酷(译者注:本人在此申明我与赌毒不共戴天),仅仅用简单的相关地数学公式就可以画出如此复杂的东西。
下面是对应的数学公式:

好的,公式有点儿长,但它只是乘法,加法还有一些正弦和余弦计算。我们可以的。
它定义了一条极坐标曲线,这意味着相比于 x, y 的值,我们更关心角度与半径。我们有个函数 r(θ), θ 是希腊字母,theta。它通常代表角度。当然我们也能猜到 r 代表半径。所以我们需要一个函数传入角度得到对应的半径。
有了角度和半径,我们很容易计算出用于绘制线段的 x,y 点。组织代码后应该像下面在这样:
for (t = 0; t < 2 * PI; t += 0.01) {
radius = r(t)
x = cos(t) * radius
y = sin(t) * radius
lineTo(x, y)
}
stroke()
我们通过 t 计算得到半径,然后再通过半径和 t 计算得到下一个绘制线条的坐标点。
不过事实上来讲,r(θ) 除了在这个循环内不会在其它任何地方使用,我就直接硬编码了。
此处唯一额外要说明的就是需要传入参数 radius 用 radius 乘以公式。还需要用 x, y 让曲线居于中心点,所以我们也把它作为参数传递(xc 与 yc 代表 x 和 y 中点)。
(译者注:这里原作都在 r(t) 计算时用字母小 a 指代除公式之外的部分, 我觉得更难理解更麻烦,小 a 在英语中随处可见,又不在伪代码中明确标出,所以我决定去掉。直接用中文表达出作者原本的意图)
以下面代码作为起点:
function cannabis(xc, yc, radius) {
for (t = 0; t < 2 * PI; t += 0.01) {
r = radius * ... // that whole formula. we'll get to it.
x = cos(t) * r
y = sin(t) * r
lineTo(xc + x, yc + y)
}
closePath()
}
现在,我们在上面基础上进行编码。相当的简单,我们只需代入公式。分数部分我们使用 0.1 代替 1/10, 0.9 代替 9/10。开始吧!
function cannabis(xc, yc, radius) {
for (t = 0; t < 2 * PI; t += 0.01) {
r = radius * (1 + 0.9 * cos(8 * t)) * (1 + 0.1 * cos(24 * t)) * (0.9 + 0.1 * cos(200 * t)) * (1 + sin(t))
x = cos(t) * r
y = sin(t) * r
lineTo(xc + x, yc + y)
}
closePath()
}
现在,像下面代码这样看看:
canvas(600, 600)
cannabis(300, 300, 140)
stroke()
That gives me this image:
这会得到如下图:

Ah, 好的,有点儿东西。
首先,此公式使用笛卡尔坐标系,而我用的是上下相反的屏幕坐标系。所以我需要把 y 轴翻转。问题不大。
接着,中心点是所有“叶子”连接点。所以在翻转后,我可以将中心点设置在 canvas 靠近底部的位置。
最后,我猜 140 会是一个不错的半径值,它会将绘制出的图形限制在 600X600 大小的 canvas 内。事实上,我期望的是把图形限制在 canvas 大小的一半。但实际上大的叶子超出一部分也不影响。我们可以在代码中修复它,比如将半径乘以某些小数让大的叶子半径降下来。我就不做这部分限制了,我假装自己只会传合适的值,相关限制代码你自己可以搞定的。
function cannabis(xc, yc, radius) {
for (t = 0; t < 2 * PI; t += 0.01) {
r = radius * (1 + 0.9 * cos(8 * t)) * (1 + 0.1 * cos(24 * t)) * (0.9 + 0.1 * cos(200 * t)) * (1 + sin(t))
x = cos(t) * r
y = sin(t) * r
lineTo(xc + x, yc - y)
}
closePath()
}
我所做的只是将 lineTo 这一行用yc + y 代替了 yc - y 。
在调用函数时参数也调整了一下(经过试错后得出还不错的参数值)
canvas(600, 600)
cannabis(300, 520, 120)
stroke()

结果还阔以!
提醒一下。我经过仔细考虑调整了 canvas 的大小,这样 yc 参数值可以设置到 420。 你调不调的隨你。
当然,现在我很好奇这个公式到底做了些什么。 radius * 后面分 4 部分,圆括号内分别有 -- 三个 余弦 cos, 一个正弦 sin。第一个括号内直接写了数值(硬编码) 8 。
... (1 + 0.9 * cos(8 * t)) ...
自从设了值为 8 便有了 7 片可见的叶子,我猜它们之间有联系 - 它实际上有可能有 8 片叶子,只是最底部的那一片太小,我们看不到。 我将 8 调高到 12 ...

你看看!理论验证成功。 7 片可见叶子加上一片不可见的。
在第二部分数 24 的作用就不太容易看出来。
... (1 + 0.1 * cos(24 * t)) ...
如果把代码回调然后把数值 24 设为 0 ,叶子边缘会非常圆润。

调到 24 一倍至 48 会得到:

这结果有点儿像每片叶子上又生出了三片小叶子。让我们把值改回 24 然后改变乘数:
... (0.7 + 0.3 * cos(24 * t)) ...

还是看到三片小叶子,24 = 8 * 3 很合理。所以这部分使用非常小的乘数, 0.1, 来微调每片叶子 - 让它变的不那么圆润。酷。把代码调回原位后再往下看另一部分。
... (0.9 + 0.1 * cos(200 * t)) ...
数值 200 我猜是用来创建锯齿边的。如果我把它 改为 100 , 锯齿变就变少了。

但现在看起来块儿状化了。试着增加分辨率把 for 循环从 0.01 调至 0.005:

Mmmm... 丝滑。
反代码复原后再看最后一个 sin 的作用。
... (1 + sin(t))
我一开始猜它影响的是曲线的朝向。我想如果把这部分删掉,叶子可能会朝向一边。但我发现我猜错了。下面是我移掉这部分代码并将 yc 调回到 canvas 中心点 300 后的结果:

真是个小惊喜!在这个基础上我的点子可就多了,另外那消失的第 8 片叶子也找到了!
心形

Weisstein, Eric W. “Heart Curve.” From MathWorld–A Wolfram Web Resource. https://mathworld.wolfram.com/HeartCurve.html
再一次,还得靠 Mathworld。如你所见,没有一个单独的公式可以绘制心形曲线。此页展示了 8 种不同的绘制方法。个人来讲我喜欢倒数第二行的最后一个。
相比于上一次接触的极坐标公式(还有其中其它的例子), 此公式直接给出计算 x 和 y 值。当然还是得用 0 到 2*PI 循环出 t 值。
公式计算 y 坐标,有四个不同的计算部分。不太清楚四个计算合在一起的作用,但如果往下继续看,你会想着删减它们。我关心的还有两点,硬编码的数值太多还有就是没有直接改变心形大小的参数。但我肯定我们可以解决。
来吧,这是非常直接的公式,我们直接进入代码环节用代码写出来。
function heart(xc, yc) {
for (t = 0; t < 2 * PI; t += 0.01) {
x = 16 * pow(sin(t), 3)
y = 13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t)
lineTo(xc + x, yc + y)
}
closePath()
}
We can run this like so:
像下面这样调用:
canvas(600, 600)
heart(300, 300)
stroke()
And we’ll get:
得到结果:

基本正确,只是需要把它翻转过来,还有就是需要允许调整大小。现在大约宽度是 32 像素。这是硬编码值 16,乘以 2 倍。
翻转就很简单了再次将使用 yc - y
至于尺寸大小,先把硬编码的数值分别除以 16。
x = pow(sin(t), 3)
y = 0.8125 * cos(t) - 0.3125 * cos(2 * t) - 0.125 * cos(3 * t) - 0.0625 * cos(4 * t)
像这样处理后,我们得到的是 2 像素宽度的(1 * 2)心形(译都注:x 轴系数16/16归 1 了,原来 16 是 32 像素意味着输出的图像 1 就是 2 像素)。现在我们可以为它添加控制大小参数 size 了。
function heart(xc, yc, size) {
for (t = 0; t < 2 * PI; t += 0.01) {
x = size * pow(sin(t), 3)
y = size * (0.8125 * cos(t) - 0.3125 * cos(2 * t) - 0.125 * cos(3 * t) - 0.0625 * cos(4 * t))
lineTo(xc + x, yc - y)
}
closePath()
}
现在像下面这样调用:
canvas(600, 600)
heart(300, 300, 280)
stroke()
得到的结果:

不是很难。
我不打算再深入另外的心形的公式了,相信你自己可以探索,你只需改动其中的常数值看看会发生什么变化。你有更好的方式吗?完全不同的那种?
蛋(卵形)
几年前我才首次接触如何绘制蛋形。此篇中只展示结果,但没有写思考过程。这篇比较完整 https://www.bit-101.com/blog/2021/06/how-to-draw-an-egg/
公式是从这里找到的 http://www.mathematische-basteleien.de/eggcurves.htm
事实上这里有超多的绘制蛋形的公式。就像心形曲线一样,我好奇的是没有一个单独的绘制蛋形曲线的标准公式。
但我把目标锁定在了 “From the Oval to the Egg Shape” 这一章节。此处有一个通用的蛋形或椭圆形公式,y 轴半径在每个点上都是变化的。如果 x 偏右,则 y 值变大,如果 x 偏左则 y 值编小。很直观。
所以我们先从椭圆公式开始,椭圆公式在第三章中我们已经讲解过了。
function ellipse(x, y, rx, ry) {
res = 4.0 / max(rx, ry)
for (t = 0; t < 2 * PI; t += res) {
lineTo(x + cos(t) * rx, y + sin(t) * ry)
}
closePath()
}
公式很好很简洁,但我得把三角函数部分代码提出来方便对它进行平衡与缩放。为了简洁的解释我还把 res 变量去掉了直接硬编码为 0.01, 当然你可以选择保留它。
function egg(xc, yc, rx, ry) {
for (t = 0; t < 2 * PI; t += 0.01) {
x = cos(t)
y = sin(t)
lineTo(xc + x * rx, yc + y * ry)
}
closePath()
}
就是画了个椭圆,只是先确定改动代码有没有错误。
canvas(600, 600)
egg(300, 300, 280, 190)
stroke()

Yup,结果正是个椭圆。数值 280 和 190 是怎么来的?嗯,280 就是比 canvas 宽度一半还小一点点,rx。 ry 也是类似,不断试错后得到 190 这个看起来不错的值。
现在让我们把椭圆变成蛋形。那个网页中给了我们三个公式:
t1(x) = 1 + 0.2 * x
t2(x) = 1 / (1 - 0.2 * x)
t3(x) = e^(0.2 * x)
这些 t 函数是用来乘以 y 的。我就不再创建新函数了。就在 for 循环中直接乘。先从第一个 t 公式开始...
function egg(xc, yc, rx, ry) {
for (t = 0; t < 2 * PI; t += 0.01) {
x = cos(t)
y = sin(t)
y *= (1 + 0.2 * x)
lineTo(xc + x * rx, yc + y * ry)
}
closePath()
}

Woo! 得到了一个蛋!
现在我们可以调调公式内的参数了。先从 0.2 这个数值入手。把它设为 0.3 试试。

好的,它看起变的有点儿尖。改为 0.5?

更尖了。懂了哈。把值改回 0.1。

几乎与原椭圆别无二至。这说得通,如果值为 0, 那么这一行啥也没做,它就是个椭圆。让我们把它改回 0.2, 让它变成通常常见的蛋型,再把 ry 改成 220:

一个漂亮“肥”蛋。下面是 150:

我坚持我的数值 190 ,但小调一点也可能更好。多试试吧。让我们再试试其它公式。首先把 ry 调回 190。把公式换成:
y *= 1 / (1 - 0.2*x)
得到:

再试试第三个公式
y *= exp(0.2 * x)
还记得第五章提到过大部分数学库都会提供 exp 函数即 e^x ( e 的“参数”次幂)。这个公式就是调用了 exp 函数,结果如:

由于三个公式看起来都很像,所以我把它们用红绿蓝三种不同颜色都画了出来...

Yeah, 三个几乎相同。可能有几个像素的区别。让我们回头看原网站,它们讨论的是另一种不同的椭圆公式,并且让 y2 乘以 此公式的值:
另一种椭圆公式 x²/9+y²/4=1 变化至 x²/9+y²/4*t(x)=1
除数 9 和 4 依然是硬编码。如果对它们开方得到 3 和 2。而 280 的 2/3 刚好是 186(译者注:公式简化后可观察得到 ry 是 2/3 的 rx)。 所以之前我选择 ry 为 190 挺合理的!
无论如何,我们有了可以画出令人信服的蛋形公式,无论使用哪种算法。就到这儿吧。这就是我写文章的过程,当然我写代码也是类似的思考,你完整的了解了整个过程,去掉了一些源文中的细枝末节。但得到的结果依然相当不错!
(译者注:很多情况下你不需要知道公式的完整推导过程,人生苦短直接使用公式即可)
小结
如何从不同地方找到各种公式把它们转变成代码绘制出有趣的图形, 希望这会给你一些启发 - 如果你从未做过的话。
我把它们全部归档到了 coding curves 系列中。至少现在为止是这样。不过我想到了另一个系列,关注我不迷路!
博客园: http://cnblogs.com/willian/
github: https://github.com/willian12345/
曲线艺术编程 coding curves 第十四章 其它曲线(Miscellaneous Curves)的更多相关文章
- 《Linux命令行与shell脚本编程大全》 第十四章 学习笔记
第十四章:呈现数据 理解输入与输出 标准文件描述符 文件描述符 缩写 描述 0 STDIN 标准输入 1 STDOUT 标准输出 2 STDERR 标准错误 1.STDIN 代表标准输入.对于终端界面 ...
- 《Java并发编程实战》第十四章 构建自己定义的同步工具 读书笔记
一.状态依赖性的管理 有界缓存实现的基类 @ ThreadSafe public abstract class BaseBoundedBuffer<E> { @GuardeBy( &quo ...
- 《Java并发编程实战》第十四章 构建自己的同步工具定义 札记
一.状态依赖性的管理 有界缓存实现的基类 @ ThreadSafe public abstract class BaseBoundedBuffer<E> { @GuardeBy( &quo ...
- Python 编程快速上手 第十四章 处理 CSV 文件和 JSON 数据
前言 这一章分为两个部分,处理 CSV 格式的数据和处理 JSON 格式个数据. 处理 CSV 理解 csv csv 的每一行代表了电子表格中的每一行,每个逗号分开两个单元格csv 的内容全部为文本, ...
- java并发编程实战:第十四章----构建自定义的同步工具
一.状态依赖性管理 对于单线程程序,某个条件为假,那么这个条件将永远无法成真 在并发程序中,基于状态的条件可能会由于其他线程的操作而改变 可阻塞的状态依赖操作的结构 acquire lock on o ...
- 《Linux命令行与shell脚本编程大全》第十四章 处理用户输入
有时还会需要脚本能够与使用者交互.bash shell提供了一些不同的方法来从用户处获得数据, 包括命令行参数,命令行选项,以及直接从键盘读取输入的能力. 14.1 命令行参数 就是添加在命令后的数据 ...
- python#父与子的编程之旅#第十四章
1. 为BankAccount 建立一个类定义.它应该有一些属性,包括账户名(一个字符串).账号(一个字符串或整数)和余额(一个浮点数),另外还要有一些方法显示余额.存钱和取钱. class Bank ...
- 20190827 On Java8 第十四章 流式编程
第十四章 流式编程 流的一个核心好处是,它使得程序更加短小并且更易理解.当 Lambda 表达式和方法引用(method references)和流一起使用的时候会让人感觉自成一体.流使得 Java ...
- 《OpenCL异构并行编程实战》第十二至十四章
▶ 第十二章,在其他语言中使用 OpenCL ● JOCL(Java Building for OpenCL),PyOpenCL ● 一个 PyOpenCL 的例子代码,需要 pyopencl 包 i ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十四章:曲面细分阶段
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十四章:曲面细分阶段 代码工程地址: https://github. ...
随机推荐
- [架构]辨析: 高可用 | 集群 | 主从 | 负载均衡 | 反向代理 | 中间件 | 微服务 | 容器 | 云原生 | DevOps | ...
词汇集 灾备 冷备份 双机热备份 异地容灾备份 云备份 灾难演练 磁盘阵列(RAID) 故障切换 心跳监测 高可用 集群 主从复制(Master-Slave) 多集群横向扩容(master-clust ...
- ORA-12154: TNS:could not resolve the connect identifier specified--sys密码包含@符号
问题描述:在操作系统登录数据库时,由于忘记了sys密码,重新修改的sys密码包含@符号,登录时报错, ORA-12154: TNS:could not resolve the connect iden ...
- 即时通讯系统为什么选择GaussDB(for Redis)?
摘要:如果你需要一款稳定可靠的高性能企业级KV数据库,不妨试试GaussDB(for Redis). 每当网络上爆出热点新闻,混迹于各个社交媒体的小伙伴们全都开启了讨论模式.一条消息的产生是如何在群聊 ...
- ROS用hector创建地图
ROS用hector创建地图 连接小车 ssh clbrobot@clbrobot 激活树莓派 roslaunch clbrobot bringup.launch 打开hector_slam 重新开终 ...
- abp(net core)+easyui+efcore实现仓储管理系统——组织管理升级之下(六十二)
Abp(net core)+easyui+efcore实现仓储管理系统目录 abp(net core)+easyui+efcore实现仓储管理系统--ABP总体介绍(一) abp(net core)+ ...
- 以SQLserver为例的Dapper详细讲解
Dapper是一种轻量级的ORM(对象关系映射)工具,它提供了高效且易于使用的方式来执行数据库操作.Dapper是由Stack Overflow团队开发并维护的,它的主要目标是提供比EF更快.更直接的 ...
- 第138篇:了解HTTP协议(TCP/IP协议,DNS域名解析,浏览器缓存)
好家伙,发现自己的网络知识十分匮乏,赶紧补一下 这里先举个我生活中的例子 欸,作业不会写了,上网搜一下 用edge浏览器上bing必应搜一下(百度广告太多了,真不想用百度举例子) 假设这是我们 ...
- java跨越解决
1.配置文件解决跨域 使用Filter方式进行设置 @Slf4j @Component public class CorsFilter implements Filter { @Override pu ...
- WPF Window设置ResizeMode="NoResize"
WPF窗口设置属性ResizeMode="NoResize"时,回到桌面后,点击任意应用,都会将此窗口激活. 我们来看下详细操作: 1. WPF窗口设置属性ResizeMode 2 ...
- ADB-安装配置
一.只要下载ADB安装包即可 就这4个文件: 备注:如果下载放入到D盘去解压,打开dos窗口那么就要进入到D盘,然后再去执行adb命令,输入adb查看它是否安装成功 二.ADB命令简单使用 查看连接设 ...