第十四章 其它曲線(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/