曲线艺术编程 coding curves 第十章 螺旋曲线(SPIRALS)
原作:Keith Peters https://www.bit-101.com/blog/2022/11/coding-curves/
译者:池中物王二狗(sheldon)
第十章 螺旋(SPIRALS)
曲线艺术编程系列第 10 章
来聊聊螺旋线。
螺旋非常像圆,它是一组点到定圆中心点的距离关系。与圆不同的是,圆的点与中心点距离固定不变,而螺旋上的点与中心点距离是变化的。给定点与中心点的距离通常是由一个基于两点之间的角度计算得到。所以你需要有函数传入角度返回半径。然后你可以通过半径与角度找到那个角度上对应的点。有很多不同的螺旋公式,会得到不同形态的螺旋。让我们从最基础的一个开始。
注:圆有时候也被称为“退化”的旋转。“退化”这词在这里没有贬义,只是说圆遵循螺旋的“规则”但又不是通常我们想象中的螺旋。就像一个三角形其中某一边的长度为 0。所有三角形数学计算通常都能正常工作,但在我们眼中它却成了一条线。
阿基米德螺旋
如你所见,每个圆的半径都会定量增加。下面是这个螺旋线的公式:
r = a * t
此处,t 是弧度 a 是某个常量,上图中 a 设为 5。 两者相乘的结果即是这个 t 角度上的半径值。如果将 6 增加到 10,我们会得到如下 :
现在如你看到的这样,常量 a 决定了螺旋内每个圆之间的间距。在这个例子中,螺旋都旋到 canvas 边界外了。
当我们绘制螺旋时,我们首先需要决定绘制多少个圈。要绕多少圈?如果 t 从 0 到 2 * PI,我们得到的是一个圈:
你可以看到这个螺旋有一个圈,螺旋曲线从中心开始向外扩展。三个圈表示 t 从 0 到 2 * PI * 3。
有了上面这些,我们可以将它们合在一起创建一个螺旋线的调式器(playground),像这样:
width = 800
height = 800
canvas(width, height)
translate(width / 2, height / 2)
cycles = 10
res = 0.01
for (t = res; t < 2 * PI * cycles; t += res) {
r = archimedean(5, t)
x = cos(t) * r
y = sin(t) * r
lineTo(x, y)
}
stroke()
function archimedean(a, t) {
r = a * t
return r
}
比如我们希望有 10 个圈, 循环结束点就是 2 * PI * cycles。调用 archimedean 螺旋函数,传入 5 作为它的常量, t 作为弧度。它会返回给我们一个半径值,我们可以用它和弧度计算出对应点的 x, y 坐标并用线连接。
我使用的绘图 api , 角度正向增加是顺时针方向。所以这个螺旋从中心顺时针绕出来。这可能与你正在使用的编程平台内表现有所不同。这取决于你使用的绘图 api 如何处理角度方向。为了翻转旋转方向,有几种方式:
A. 用 scale 翻转 canvas
scale(1, -1)
B. 改变代码创建点的方式,使其中一个轴为负 例如:
x = cos(t) * r
y = -sin(t) * r
C. 将循环的方向变反
for (t = -res; t > -2 * PI * cycles; t -= res) {
任意一种方式都可以让你的螺旋方向改变。
还有最后一件事...
代码中值得注意的一点是循环起始点是 res 而不是 0
for (t = res; t < 2 * PI * cycles; t += res) {
想知道为我为啥这么做,我们需要从下一种螺旋线寻找答案。
双曲线螺旋 Hyperbolic Spiral
这个螺旋与第一个螺旋看起来非常不同。上个螺旋内每个圈是等距的。你可能会说每个圈之间的距离都增长了,还是等等先别下结论。
下面这个函数是这个螺旋准备的:
function hyperbolic(a, t) {
r = a / t
return r
}
也没有特别大的不同。我们用除代替了乘。上图我传入了 1000 。如果我将值降到 10, 我们会得到一个很小的螺旋线像这样:
这就很明显了 for 循环以 res 开始而不是 0, 如果是 0,那么就会出现除数为 0 的这种情况,这会导致某些问题。也许会崩溃,或者半径为 NaN (not a number) 值, 这没什么用。所以我们将起始值设为非 0。
另一个值得注意的事是螺旋线的旋转方向。上图我画了 20 个圈。如果降为 5(a 变回 1000),将得到:
你现在可能看到随着 t 值增长螺旋变的很大但,但增长的速度慢小了。当有 100 个圈,螺旋中心位置开始有些堵了。
就如我之前所说,咋一看每个圈半径都在增长,但你现在可以观察到实际上是开始时很大,但是圈与圈之间的半径值逐渐变小了。
费马螺线
这种螺旋线很漂亮。这是公式:
function fermat(a, t) {
r = a * pow(t, 0.5)
return r
}
这里我们将 a 乘以 t 的 0.5 平方。假定你的编程语言内置了 pow 函数计算第一个参数的第二个参数次幂。根据你使用的编程语言,它可能是这样:
r = a * (t^0.5)
or
或这样:
r = a * (t**0.5)
上一张图 a 设为了20 绘制了 20 圈。 绘制 10 圈 结果如下:
可以看到螺旋线是从内向外绘制的。 一开始圈与圈之间的间距比较大,但越往外面圈的间距越来越小,还超出边界了
圈数改回 20 个圈 a 设为 40:
我们能看到间距有所增加。
100 个圈 a 设为 15。
最终,每圈之间已很难有间距。而我们得到了一些有趣的摩尔纹图案
连锁螺线 Lituus Spiral
它看起来像双曲螺线,但公式却更接近于费马螺线:
function fermat(a, t) {
r = a * pow(t, -0.5)
return r
}
我们公需要将指数中的 0.5 改为 -0.5。上图螺旋线绘制了 20 个圈 a 的值为 500。 下面是 10 个圈
你可以看到这是另一种向内绘制的螺旋线。 改回 20 圈, a 设为 50,得到:
词 “Lituus” 原意是弯曲的杖,弯曲的魔法棒或角。上图很好的解析了它名字的由来。
将 a 升到 1000, 可以得到:
可以这样讲,a 值越低,螺旋吞噬进中心越快。
还有很多类型的螺旋线,来,继续!
对数螺旋 Logarithmic Spiral
它有点像反费马螺旋。一开始间隔挺小,越往外越大。公式:
function logarithmic(a, k, t) {
r = a * exp(k * t)
return r
}
公式看起来比之前遇到过的要复杂一点。t 前有两个参数。我们先从 exp 函数开始学习。
我们实际上在谐振波图(Harmonographs) 这一章见过它. 回顾一下,有一个数学常数 e ,也称欧拉数。它的值大概是 2.71828。 当我们在与对数打交道时,常将 e 乘方。所以很多数学程序库会直接提供一个函数, 常被命名为 exp。 举个具体的例子,在 javascript 中,有常用 e, Math.E。为了计算 e 的 2 次幂,就得像下面这样做:
Math.pow(math.E, 2)
但其实已经有一个叫 exp 的函数了,你可以直接用:
Math.exp(2)
代码中的公式也得改成,
r = a * exp(k * t)
我们用 a 乘以 e 的 k * t 次幂。上面的图,我得将 a 设成 0.5 且 将 k 设为 0.05。
如果将 a 变为 1,我们将得到一个相当大的螺旋线,虽然它们看起来很像。
a 设为 0.24 后:
还是挺像的,但更小一点儿了。
将 a 重置为 0.5 并且将 k 升到 0.1
你可以看到扩张的更快了。将 k 设为 0.04 它对结果的影响远超我的预期
黄金螺旋(Golden spiral)
这类螺旋增长率叫“黄金比例”,大概接近 1.618。它的公式是:
function golden(t) {
r = pow(PHI, 2 * t / PI)
return r
}
首先,这个函数除了 t 之外没有其它参数。 PHI 黄金比例值是硬编码在函数内的。很多数学程序库都有黄金比例这个内建常量。如果你没有,将这近似的设置为:
PHI = 1.61803
或者你想更精确一点,你可以像下面这样写得到精确值:
PHI = (1 + sqrt(5) ) / 2
然后放在某个地方按需引用即可。
你很可能已经见过这种螺旋图了:
By Romain – Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=114415511
这实际是一个斐波那契螺旋。方块大小是下两个方块之和且每个方块内的曲线是一个角为中心 90 度生成弧线。它不是精确的黄金螺旋,但很接近了。
还有更多
仔细阅读这个螺旋线列表,也许你能找到其它更有趣的
https://en.wikipedia.org/wiki/List_of_spirals
还有:
https://mathworld.wolfram.com/topics/Spirals.html
螺旋角(Spirangles)
在写这一章的时候我偶然发现了这个词 (Spirangles)。本质上它是由直线线段组成的螺旋。通过改变每个线段的角度,你可以将它们组成不同的形状。
在已有的基础上实现它非常容易。上面中有个例子使用的是 archimedean 函数,a 为 3,并且 cycles 为 20。 窍门是减低分辨率 res 上图的效果我是将 res 设为了发下值:
res = PI * 2 / 3
Now on each step of the for loop, t will increase by one-third of a circle. Here are some others, dividing by 4 and 5.
现在,循环中每一步,t 会增加 1/3 个圆。 以下是其它, 除 4 和 除 5 的效果:
窍门你已经知道了。你可能想在其它方法上试试。其中一些通过这样修改后变的让人相当满意。
葵花螺旋(Sunflowers)
若是没有讨论葵花螺旋,那么螺旋这一篇就不算完整。试试用葵花,斐波那契与螺旋的组合搜索,你能得到巨量的阅读材料与漂亮图片。我的意思是,除了绘制葵花螺旋这类有趣的螺旋线外,其它的螺旋我就让你自己去探索了。黄金比例在这个图中是天生的,图是这样:
你可以看到不止一条螺旋,有许多螺旋以不同角度进进出出。我常用下面的代码实现(还是以伪代码展示):
width = 800
height = 800
canvas(width, height)
translate(width / 2, height / 2)
count = 1000
for (i = 0; i < count; i++) {
percent = i / count
size = 14.0 * percent
r = 380.0 * percent
t = i * PI * 2 * PHI
x = cos(t) * r
y = sin(t) * r
circle(x, y, size)
fill()
}
我们用变量 count 代表想要绘制多少“葵花籽”。这里 count 设为了 1000.
然后我们循环 用 i / count 得到一个百分比值。
变量 size 是每颗“种子”的半径。当 i 越接近 count 时,percent 的值会越接近 1,因此 size 的值也会越接近最大值 14。
同样 r 用于表示种子分布的半径。它最高值是 380, 仅比 canvas 宽度的一半小一点点。
我们用 t = i * PI * 2 * PHI
计算出 t , 它就是神奇的葵花斐波那契公式。说真的,你想知道更多,那就认真看一下。有了角度和半径,我们就可以得到 x 和 y 坐标,然后根据种子自己的大小绘制种子了。
到目前为止我想说的螺旋线就这么多了。下回见!
本章 Javascript 源码 https://github.com/willian12345/coding-curves/blob/main/examples/ch10
博客园: http://cnblogs.com/willian/
github: https://github.com/willian12345/