第三章 弧,圓,橢圓(TRIG 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
曲線藝術(shù)編程系列第三章
這一篇中我們將關(guān)注如何繪制圓弧,圓和橢圓。(結(jié)束前再聊聊正切相關(guān)的)
很可能你使用的編程平臺(tái)已內(nèi)建了一些這樣的 api。舉個(gè)例子,雖然 HTML Canvas api 沒有直接畫圓形和橢圓的函數(shù),但它有一個(gè) arc 函數(shù),可以用它來(lái)間接實(shí)現(xiàn)。如何自己動(dòng)手實(shí)現(xiàn)這些功能很有用。某時(shí)某刻總會(huì)用到的。
首先,我們先聚焦于圓弧與圓??梢哉f圓弧是圓的一部分,也可以說圓是圓弧延展 360 度而成。從哪個(gè)方向開始探索都可以,但對(duì)我更鐘意從圓開始再進(jìn)入圓弧部分。
統(tǒng)一度量衡
我使用的繪圖 api 內(nèi) y 軸與標(biāo)準(zhǔn)笛卡爾坐標(biāo)系是相反的。負(fù)的向上,正的下向。
有別于數(shù)學(xué)和科學(xué)中使用的笛卡爾坐標(biāo)系, 這在圖形繪制 api 中很常見。它與 Processing, HTML Canvas, Cairographics, .net graphics 以及其它很多圖形庫(kù)一樣。
有些 api 確實(shí)使用笛卡爾坐標(biāo)系,當(dāng)角度值為正向增長(zhǎng)時(shí)代表逆時(shí)針方向。
但有些如 pygame, 是混用,y 是為正時(shí)是向下,但正向角度旋轉(zhuǎn)時(shí)卻是逆時(shí)針的。
這會(huì)影響角度的測(cè)量。0 度指向東方。在笛卡爾坐標(biāo)系中,正向角度轉(zhuǎn)動(dòng)是逆時(shí)針,負(fù)向角度轉(zhuǎn)動(dòng)移動(dòng)是順時(shí)針。在 Y 軸反轉(zhuǎn)的系統(tǒng)中,正好相反,在這章中我不會(huì)再?gòu)?qiáng)調(diào)這里的差別。我們這里會(huì)重新實(shí)現(xiàn)一些已內(nèi)建的繪圖 api 函數(shù)。
在你學(xué)完圓弧這一章節(jié)后,如果你想在笛卡爾坐標(biāo)系下創(chuàng)建圓弧,圓和橢圓的函數(shù)也會(huì)很簡(jiǎn)單。
圓
定義
圓的定義一般來(lái)說會(huì)像這樣:“與給定的一個(gè)中心點(diǎn)等距的一堆點(diǎn)的集合” ,但當(dāng)你真的想畫一個(gè)圓的時(shí)候,發(fā)現(xiàn)這并沒什么卵用。我并不需要一堆無(wú)限的點(diǎn)。我們僅需要足夠多的點(diǎn)用短線串起來(lái)形成圓。
你也見過“圓方程”類似 x2+y2=r^2(譯者注:這里是x平方+y平方= r 平方,可表示圓周上任意一點(diǎn))。 當(dāng)你嘗試想畫圓,這好好像也沒啥用。
然后你得到了下面這樣的參數(shù)方程
x = a + r * cos(t)
y = b + r * sin(t)
這里 a 和 b 是圓的中心點(diǎn), r 是半徑, t 是范圍參數(shù)變量從 0 到 2 * PI。
這里才開始有點(diǎn)兒用。我們可以定義一個(gè)圓心點(diǎn)和半徑然后跑一個(gè)循環(huán),從 0 至 2 * PI 從而得到一堆點(diǎn),然后用線將這些點(diǎn)連接起來(lái)。
還是得提醒你,這里展示的是偽代碼。關(guān)于偽代碼的問題請(qǐng)參考第一章內(nèi)的說明內(nèi)容。
width = 600
height = 600
canvas(width, height)
cx = width / 2
cy = height / 2
radius = 250
for (t = 0; t < PI * 2; t += 0.01) {
lineTo(cx + cos(t) * radius, cy + sin(t) * radius)
}
closePath()
stroke()
取決于你所使用的繪圖 api, 在你使用 lineTo 之前,你很可能需要用 moveTo 來(lái)開頭。我信你自己能搞定。這很簡(jiǎn)單。cx, cy 和 radius 就是上面公式中的 a, b 和 r 。 t 是用于循環(huán)的弧度。
注意,最后需要用 closePath 閉合一下。大多數(shù)繪圖 api 都有這一特性。它會(huì)將最后一點(diǎn)與路徑最起始點(diǎn)相連,將圓閉合起來(lái)??赡茉谀愕钠脚_(tái)上有一丟丟不一樣,但大概應(yīng)該如下圖所示:
有一個(gè)問題,循環(huán)中 0.01 是我猜的一個(gè)大概值。如果你定的太大,比如 0.2 ,那么相當(dāng)于你大踏步繞圓跳一圈,得到的結(jié)果將會(huì)是下面這樣很粗糙的圓,看起來(lái)可不怎么潤(rùn):
但你如果將增長(zhǎng)值設(shè)的太小,那么系統(tǒng)將會(huì)做太多無(wú)用的繪制。圓周越大你就需要更多的線段讓它看起來(lái)絲滑,半徑越小需要的線段就越少。如果你用 0.01 這個(gè)常量用于遞增,你將在每個(gè)圓上繪制 628 條線段。這在小圓上面可就太浪費(fèi)了。
我上下而求索,找到了一個(gè)可用的方案,大至是 4.0 / radius。 在半徑 5 至 200 范圍內(nèi),比直接使用 0.01 這個(gè)遞增值繪制時(shí)的線斷少了一半,但看起來(lái)依然不錯(cuò)。
可能因系統(tǒng)而異,你自己嘗試一下不同值看看。
封裝成函數(shù)
有了這些, 我們可以把繪制圓封裝成一個(gè)函數(shù):
function circle(x, y, r) {
res = 4 / r
for (t = 0; t < PI * 2; t += res) {
lineTo(x + cos(t) * r, y + sin(t) *r)
}
closePath()
}
注意:我將 stroke 方法移在函數(shù)內(nèi)移徐掉了,這樣你可以用函數(shù)創(chuàng)建圓,可以選擇描邊或填充,或兩者都用。如果你愿意,你可以進(jìn)一步封裝 strokeCircle 函數(shù)和 fillCircle 函數(shù)。下面是函數(shù)使用演示
width = 600
height = 600
canvas(width, height)
circle(width / 2, height / 2, 200)
stroke()
圓弧
已經(jīng)完成圓這部分了,現(xiàn)在我們可以在此基礎(chǔ)上創(chuàng)建 arc 函數(shù)。你的編程語(yǔ)言可能有現(xiàn)成的 api ,但這不重要,我們?cè)賹?shí)現(xiàn)一遍。很簡(jiǎn)單和圓函數(shù)一樣,但我們用 start 代表開始位置 0 和 end 代替結(jié)束位置的 2 * PI, 我們讓調(diào)用者決定開始與結(jié)束位置。
不再解釋了因?yàn)檫^于簡(jiǎn)單我直接拋出偽代碼吧
function arc(x, y, r, start, end) {
res = 4 / r
for (t = start; t < end; t += res) {
lineTo(x + cos(t) * r, y + sin(t) *r)
}
lineTo(x + cos(end) * r, y + sin(end) *r)
}
你看簡(jiǎn)單吧,僅僅是將硬編碼的開始與結(jié)束角度替換成參數(shù)傳入的形式。當(dāng)然,我移除了 closePath() 調(diào)用,取而代之的是最終 lineTo,這樣更精確一點(diǎn)。
像下面一樣使用它:
width = 600
height = 600
canvas(width, height)
arc(width / 2, height / 2, 250, 0.5, 3.5)
stroke()
結(jié)果會(huì)是:
有一丟丟問題。如果我將輸入的開始與結(jié)束對(duì)調(diào)呢?
arc(width / 2, height / 2, 250, 3.5, 0.5)
它會(huì)立即結(jié)束循環(huán),因?yàn)?3.5 已經(jīng)大于 0.5了。啥也不會(huì)畫出來(lái)。 我想要的是開始在 3.5 度,又繞回到 0.5 度像下面這樣:
一種方式是我們只要保證結(jié)束的度數(shù)大于開始度數(shù)。我們可以判斷如果結(jié)束度數(shù)小于開始度數(shù),那么直接加上 2*PI,直到結(jié)束度數(shù)大于開始度數(shù)。
function arc(x, y, r, start, end) {
while (end < start) {
end += 2 * PI
}
res = 4 / r
for (t = start; t < end; t += res) {
lineTo(x + cos(t) * r, y + sin(t) *r)
}
lineTo(x + cos(e) * r, y + sin(e) *r)
}
現(xiàn)在應(yīng)該可以正常展示成上面期望的那樣了。
還有一件事兒,我們總是假定用戶繪制是順時(shí)針的。我們應(yīng)該讓用戶自己決定。
幸運(yùn)地是這很容易實(shí)現(xiàn),我們只需要傳另一個(gè)參數(shù) anticlockwise,如果值為 true,我們只需要交替 start 與 end 就可以了。
function arc(x, y, r, start, end, anticlockwise) {
if (anticlockwise) {
start, end = end, start
}
while (end < start) {
end += 2 * PI
}
res = 4 / r
for (t = start; t < end; t += res) {
lineTo(x + cos(t) * r, y + sin(t) *r)
}
lineTo(x + cos(e) * r, y + sin(e) *r)
}
如果你足夠幸運(yùn),你使用的編程語(yǔ)言支持像這樣變量交換:
start, end = end, start
如果不能直接像上面這樣交替,那就只能用老方法了:
temp = start
start = end
end = temp
這樣調(diào)用
arc(width / 2, height / 2, 250, 3.5, 0.5, false)
stroke()
會(huì)給你期望的圓弧了:
下面這段代碼
arc(width / 2, height / 2, 250, 3.5, 0.5, true)
stroke()
則給你這樣的圓弧:
兩者都是開始于 3.5 度結(jié)束到 0.5,一條正的,一條按反的方式畫。
正如開始時(shí)在圓形處提到過,這里我選擇了正向角度順時(shí)針為默認(rèn),這與笛卡爾坐標(biāo)系不同。現(xiàn)在你知道在不同方向上如何繪制圓弧,你可以選擇你一個(gè)喜歡的作為默認(rèn)方向。
現(xiàn)在我們有了一個(gè)強(qiáng)大的圓弧函數(shù),我們其實(shí)可以用它替換原來(lái)圓函數(shù)內(nèi)的一些重復(fù)代碼,像下面這樣
function circle(x, y, r) {
arc(x, y, r, 0, 2 * PI, true)
}
從 0- 2*PI 可不就是一個(gè)圓么。
片段與扇區(qū)
這里還有幾個(gè)可以創(chuàng)建的函數(shù)如果你覺得它們有用的話。將圓弧首尾相連起(一條弦)形成的一個(gè)圓弧片。我們可以這樣實(shí)現(xiàn)它,在繪制圓弧完畢時(shí)直接調(diào)用 closePath 函數(shù),你使用的編程語(yǔ)言中肯定也有類似 closePath 的函數(shù)。
function segment(x, y, r, start, end, anticlockwise) {
arc(x, y, r, start, end, anticlockwise)
closePath()
}
這個(gè)圓弧片,就是從 2.5 度至 4.5 度
一個(gè)扇形就是將圓弧用線段從中心點(diǎn)連接起來(lái),我們可以調(diào)用 lineTo 至中心點(diǎn),然后再 closePath
function sector(x, y, r, start, end, anticlockwise) {
arc(x, y, r, start, end, anticlockwise)
lineTo(x, y)
closePath()
}
用上面圓弧片一樣的參數(shù)畫的扇形:
現(xiàn)在你可以自己畫餅圖了。
多邊形
在介紹橢圓之前,我想先獎(jiǎng)勵(lì)個(gè)正多邊型。 這倒不是我認(rèn)為多邊形是曲線,但數(shù)學(xué)上來(lái)講它可能真的是。 無(wú)論如何,反正來(lái)都來(lái)了,把它學(xué)了吧。
最開始我們討論過分辨率,我們看到過低分辨率的圓邊上看起來(lái)是一段一段的。你能看到圓是由單獨(dú)一條條線段組成的。我們可以把這個(gè) bug 點(diǎn)轉(zhuǎn)化成一個(gè)可用的特征。如果我們將分辨率降低到足夠低直到只有6個(gè)片段組成一個(gè)圓,我們就得到了一個(gè)六邊形, 5 條線段就是 五邊形,4 條就是方形,3 條就是三角形。 我們僅需要特別處理我們希望有多少條邊,除以 2*PI 當(dāng)作分辨率就可以成形了。
實(shí)現(xiàn)如下:
function polygon(x, y, radius, sides) {
res = PI * 2 / sides
for (i = 0; i < PI * 2; i+= res) {
lineTo(x + cos(i) * radius, y + sin(i) * radius)
}
closePath()
}
像下面這樣調(diào)用:
polygon(300, 300, 250, 5)
stroke()
得到一個(gè)五邊形:
也許你想為這個(gè)多邊形初始化一個(gè)特別的角度,你可以這樣做
function polygon(x, y, radius, sides, rotation) {
res = PI * 2 / sides
for (i = 0; i < PI * 2; i+= res) {
lineTo(x + cos(i + rotation) * radius, y + sin(i + rotation) * radius)
}
closePath()
}
現(xiàn)在你可以這樣使用
polygon(300, 300, 250, 5, 0.5)
stroke()
得到了一個(gè)轉(zhuǎn)了一點(diǎn)角度的多邊形
試試傳入不同的邊數(shù)。
一個(gè)有趣的效果是創(chuàng)建一系列大小不同的多邊形,每個(gè)多邊形相應(yīng)旋轉(zhuǎn)一丟丟的角度:
angle = 0
for (r = 5; r <= 255; r += 10) {
polygon(300, 300, r, 5, angle)
stroke()
angle += 0.05
}
效果如下:
也許有一點(diǎn)點(diǎn)離題,但你看圖形里突然形成了5條新的曲線,能接受,能接受。
橢圓
本文最后一部分,橢圓。
好的讓我們來(lái)看看橢圓在維基百科中的定義...
環(huán)繞兩個(gè)焦點(diǎn)的平面曲線,對(duì)于曲線上的所有點(diǎn),到焦點(diǎn)的兩個(gè)距離之和為常數(shù)
https://en.wikipedia.org/wiki/Ellipse
Em... 完全搞不懂,太數(shù)學(xué)化了。再看看這條解釋...
橢圓是圓錐截面的封閉類型:沿圓錐與平面相交的平面曲線
https://en.wikipedia.org/wiki/Ellipse
還是一樣不好理解,好吧,繼續(xù)...
一個(gè)橢圓也可以用一個(gè)焦點(diǎn)和橢圓外一條叫做準(zhǔn)線的線來(lái)定義:對(duì)于橢圓上的所有點(diǎn),到焦點(diǎn)的距離和到準(zhǔn)線的距離之比是一個(gè)常數(shù)。
https://en.wikipedia.org/wiki/Ellipse
好吧,還是不好懂,但就如之前那樣,最終我們可以找到可用的參數(shù)方程,和之前的圓參數(shù)方程差不多
x = a + rx * cos(t)
y = b + ry * sin(t)
這里,除了用 a 和 b 表示圓心點(diǎn)之外,還有 rx 和 ry 最簡(jiǎn)單就是把它們想象成 “radius x” 和“radius y”, 盡管這些名字可能讓數(shù)學(xué)家鄙視。但對(duì)于一個(gè)未旋轉(zhuǎn)的橢圓 rx 就是等于一半的橢圓寬,ry 等于一半的橢圓高。
所以我們可以編寫下面這樣一個(gè)函數(shù)
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()
}
值得提醒的一點(diǎn),是分辨率值, 我將 4.0 除以 rx 和 ry 中的最大值。你可以想想有沒有更好的,但這對(duì)于我來(lái)說足夠用了。現(xiàn)在你可以像下面這樣調(diào)用它
ellipse(300, 300, 250, 150)
stroke()
And get:
得到
小獎(jiǎng)勵(lì)
有時(shí)候我寫了就停不下來(lái)。接下來(lái)的這部分與創(chuàng)建曲線關(guān)系不大…,或者說也有一定相關(guān)。你看完后再看想想是不是有關(guān)系吧。 比起在圓周(或圓弧,多邊形,橢圓)上每個(gè)點(diǎn)之間用線段相連,我們可以在這些點(diǎn)上畫一些其它的形狀。我們將增加曲線點(diǎn)之間的間隔以擁有足夠空間容納其它形狀,不至于擠在一起,不然看起來(lái)太亂。事實(shí)上,多邊形函數(shù)正好適用在這里。它可以讓我們畫一個(gè)由多個(gè)圓組成的圓形環(huán)。對(duì)于代碼我就不解釋了,你應(yīng)該可以理解。
width = 600
height = 600
canvas(width, height)
cx = width / 2
cy = height / 2
res = PI * 2 / 20 // to draw 20 circles
for (t = 0; t < PI * 2; t += res) {
x = cx + cos(t) * 200
y = cy + sin(t) * 200
circle(x, y, 20)
stroke()
}
總結(jié)
有想過要不要寫這一部分,跑題已經(jīng)跑的夠遠(yuǎn)的了,這一篇也足夠的長(zhǎng)了。
到目前為止都很基礎(chǔ),但希望足夠有趣。從這章之后,我們將慢慢接觸一點(diǎn)復(fù)雜的東西希望內(nèi)容更加的有趣。
本章 Javascript 源碼 https://github.com/willian12345/coding-curves/tree/main/examples/ch03文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-468708.html
博客園: http://cnblogs.com/willian/
github: https://github.com/willian12345/文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-468708.html
到了這里,關(guān)于曲線藝術(shù)編程 coding curves 第三章 弧,圓,橢圓(ARCS, CIRCLES, ELLIPSES)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!