曲線藝術編程 coding curves 第十四章 其它曲線(Miscellaneous Curves)

第十四章 其它曲線(Miscellaneous Curves)

原作:Keith Peters https://www.bit-101.com/blog/2022/11/coding-curves/

譯者:池中物王二狗(sheldon)

blog: http://cnblogs.com/willian/

源碼:github: https://github.com/willian12345/coding-curves

曲線藝術編程系列 第十四章

這是系列文章規劃的最後一章。如果後面發現其它有趣的曲線類型可能加在這一章。我原計劃清單裏有幾個主題沒放出來,當然也不排除某天我改主意了。未來額外的內容也可能另起一章加到目錄索引中。

在“最後”一篇, 我想我會講一些隨機曲線,這些曲線不值得單獨開一章來講。還有,我覺得把我從找到公式到編碼的過程完整過一遍會很不錯。

大麻曲線

image

Weisstein, Eric W "大麻曲線" 來源於網站 https://mathworld.wolfram.com/CannabisCurve.html

Wolfram Mathworld 是一個很好的發掘有趣公式的地方,順便說一句,如果你想發掘更多 2d 曲線,那麼在平面曲線(Plane Curve)這一章節可以深入找找。網站內容很全,還有其它曲線類型可探索。

爲什麼選擇大麻曲線?。我只是覺得它很酷(譯者注:本人在此申明我與賭毒不共戴天),僅僅用簡單的相關地數學公式就可以畫出如此複雜的東西。

下面是對應的數學公式:

image

好的,公式有點兒長,但它只是乘法,加法還有一些正弦和餘弦計算。我們可以的。

它定義了一條極座標曲線,這意味着相比於 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:

這會得到如下圖:

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()

image

結果還闊以!

提醒一下。我經過仔細考慮調整了 canvas 的大小,這樣 yc 參數值可以設置到 420。 你調不調的隨你🙂。

當然,現在我很好奇這個公式到底做了些什麼。 radius * 後面分 4 部分,圓括號內分別有 -- 三個 餘弦 cos, 一個正弦 sin。第一個括號內直接寫了數值(硬編碼) 8 。

... (1 + 0.9 * cos(8 * t)) ...

自從設了值爲 8 便有了 7 片可見的葉子,我猜它們之間有聯繫 - 它實際上有可能有 8 片葉子,只是最底部的那一片太小,我們看不到。 我將 8 調高到 12 ...

image

你看看!理論驗證成功。 7 片可見葉子加上一片不可見的。

在第二部分數 24 的作用就不太容易看出來。

... (1 + 0.1 * cos(24 * t)) ...

如果把代碼回調然後把數值 24 設爲 0 ,葉子邊緣會非常圓潤。

image

調到 24 一倍至 48 會得到:

image

這結果有點兒像每片葉子上又生出了三片小葉子。讓我們把值改回 24 然後改變乘數:

... (0.7 + 0.3 * cos(24 * t)) ...

image

還是看到三片小葉子,24 = 8 * 3 很合理。所以這部分使用非常小的乘數, 0.1, 來微調每片葉子 - 讓它變的不那麼圓潤。酷。把代碼調回原位後再往下看另一部分。

... (0.9 + 0.1 * cos(200 * t)) ...

數值 200 我猜是用來創建鋸齒邊的。如果我把它 改爲 100 , 鋸齒變就變少了。

image

但現在看起來塊兒狀化了。試着增加分辨率把 for 循環從 0.01 調至 0.005:

image

Mmmm... 絲滑。

反代碼復原後再看最後一個 sin 的作用。

... (1 + sin(t))

我一開始猜它影響的是曲線的朝向。我想如果把這部分刪掉,葉子可能會朝向一邊。但我發現我猜錯了。下面是我移掉這部分代碼並將 yc 調回到 canvas 中心點 300 後的結果:

image

真是個小驚喜!在這個基礎上我的點子可就多了,另外那消失的第 8 片葉子也找到了!

心形

image

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:

得到結果:
image

基本正確,只是需要把它翻轉過來,還有就是需要允許調整大小。現在大約寬度是 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()

得到的結果:

image

不是很難。

我不打算再深入另外的心形的公式了,相信你自己可以探索,你只需改動其中的常數值看看會發生什麼變化。你有更好的方式嗎?完全不同的那種?

蛋(卵形)

幾年前我才首次接觸如何繪製蛋形。此篇中只展示結果,但沒有寫思考過程。這篇比較完整 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()

image

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()
}

image

Woo! 得到了一個蛋!

現在我們可以調調公式內的參數了。先從 0.2 這個數值入手。把它設爲 0.3 試試。

image

好的,它看起變的有點兒尖。改爲 0.5?

image

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

image

幾乎與原橢圓別無二至。這說得通,如果值爲 0, 那麼這一行啥也沒做,它就是個橢圓。讓我們把它改回 0.2, 讓它變成通常常見的蛋型,再把 ry 改成 220:

image

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

image

我堅持我的數值 190 ,但小調一點也可能更好。多試試吧。讓我們再試試其它公式。首先把 ry 調回 190。把公式換成:

y *= 1 / (1 - 0.2*x)

得到:

image

再試試第三個公式

y *= exp(0.2 * x)

還記得第五章提到過大部分數學庫都會提供 exp 函數即 e^x ( e 的“參數”次冪)。這個公式就是調用了 exp 函數,結果如:

image

由於三個公式看起來都很像,所以我把它們用紅綠藍三種不同顏色都畫了出來...

image

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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章