曲線藝術編程 coding curves 第七章 拋物線(Parabolas)

拋物線 Parabolas

原作: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

曲線藝術編程系列第7章

我承認這一章腦暴時,再三考慮過是否要將拋物線包含進來。此篇覆蓋的拋物線比起之前三章我們弄出來的複雜曲線相比非常的簡單且基礎的。但當我真的開始後才發現它也非常的酷,並且擁有很多有趣的特性。事實上,我一開始還打算將雙曲線也包含在這一章, 但當我深入拋物線後,決定雙曲線還是另找其它時間講吧。

先確認一下我們講的拋物線是像下面這樣的:

image

學習拋物線之前你首先得知道它是圓錐截面的一種。由於我們僅在這裏介紹二維曲線,它的三維形式有點不相關。你可以自行在維基百科上查詢相關信息:

https://en.wikipedia.org/wiki/Parabola

自從我們講了圓和橢圓,本章與下一章我將填坑所有相關的圓錐曲線。雖然這從未是目標,但我向來是摟草打兔子,能講的就都講了吧。

上面的第一張圖,注意拋物線是 y 軸對稱的,頂點(最高或最低點,取決於拋物線的開口方向)就是最接近 x 軸的點。

像大多數幾何圖形一樣,有很多種公式可以描述它們。下面是一個相當簡單易用的一個:

y = a * x * x

在這個式子中,a 參數是控制拋物線開口寬度的 - 無論開口是向上還是向下。相當簡單。 先畫個看看,但首先做些準備工作寫一些通用函數。

準備工作

就如最上面那張圖,我們希望原始位置位於 canvas 中心點以便完整顯示拋物線。所以我們需要將 canvas 原點平移到那裏。

當然,這也是很好的機會用於對比你使用的語言平臺繪圖 api 與 普通笛卡爾座標系是否上下顛倒。換句話說, y 軸的值越大越向下,反之越向上。也就是說 y 座標翻轉過來就是普通笛卡爾。

最後,要是座標軸能看到就更好了。我們可以簡單的寫個函數將軸畫出來。

爲了平移到 canvas 中心。你可能需要使用像下面這樣的方法:

translate(width / 2, height / 2)

我假定你的繪圖 api 內建了 translate 方法。好像老是解釋這一點顯的我有點囉嗦, 所以我假定你的繪圖 api 常用方法都有內建支持了。

function center() {
  translate(width / 2, height / 2)
}

就像之前一樣,這裏用的是僞代碼,無論你想在何種目標對象上繪製,都可以像這樣調用這些方法。Processing 就是像這樣寫的 (譯都注:Processing 圖形庫 processing.org) ,但其它系統中這些方法很有可能是在 canvas 對象下。它更可能像下面這樣調用:

canvas.translate(canvas.width / 2, canvas.height / 2)

我肯定可以搞得定。

爲了翻轉 y 軸,你可以這樣調用 scale:

scale(1, -1)

這能讓 x 軸不變 y 軸翻轉。好了,好了,在這個系列文章中有時候翻轉有時候又不翻轉。此係列文指在實踐練習繪圖課程,而非嚴密的數學教程。所以你應該知道啥時候翻轉 canvas 啥時候不翻轉了吧。

最後, 還要有一個函數用於繪製座標軸。像下面這樣:

function drawAxes() {
  lineWidth = 0.25
  moveTo(-width, 0)
  lineTo(width, 0)
  moveTo(0, -height)
  lineTo(0, height)
  stroke()
  lineWidth = 1
} 

此函數實現了橫豎兩條線且超過了 canvas 的邊界,但問題不大。 線的寬度同樣設置的很細,這僅僅是用來輔助的線條, 繪製完成後線寬重新設爲 1。 在你的實際代碼中有可能需要調用類似 pushing 和 popping 或 saving 和 restoring 這樣的 api 用於canvas 的上下文狀態管理, 以便於恢復到調用此函數之前的狀態,因爲它可能調用前的上下文狀態線寬不是 1。

Ok, 現在可以設置 canvas 了:

width = 800
height = 800
canvas(width, height)
 
center()
scale(1, -1)
drawAxes()

下圖就是這一番操作後的結果,這是一個好的開始:

image

你可能想將以上的代碼整合到 setup 函數內。這取決於你自己。

繪製拋物線

在上面代碼的基礎上,我們可以用一個循環將 x 從左至右貫穿 canvas。我們將 a 設爲很小的一個值比如 0.003 因爲我們操作的是幾百像素值。我選這個值是因爲它讓繪出的結果圖在我們可視的 canvas 範圍內

a = 0.003
for (x = -width / 2; x <= width / 2; x++) {
  y = a * x * x
  lineTo(x, y)
}
stroke()

這就是我們繪製的結果了:

image

如果將 a 值大,比如 0.3 , 我們會得到非常窄的拋物線:

image

如果變的更小,如 0.0003 , 我們會得到一個非常寬的拋物線:

image

技術上講,你不應該將 a 值設爲 0。如果你這麼幹了,那麼你會得到一條直線。如果 a 變爲負值,拋物線開口將會反向。這是 -0.003 :

image

這就是拋物線的全部了。

Oh, 等等。還有一些事情需要交待!

焦點與準線

拋物線還有一個熟知的知識點叫焦點。焦點的定義如下:

x = 0
y = 1 / (4 * a)

讓我們把這個點畫出來:

a = 0.003
for (x = -width / 2; x <= width / 2; x++) {
  y = a * x * x
  lineTo(x, y)
}
stroke()
 
// draw focus
focusX = 0
focusY = 1/(4 * a)
circle(focusX, focusY, 4)
fill()

image

還有另一個屬性是準線。這是一條水平線用於表示 y 值:

y = -1 / (4 * a)

我們可以把它畫出來:

a = 0.003
for (x = -width / 2; x <= width / 2; x++) {
  y = a * x * x
  lineTo(x, y)
}
stroke()
 
// 繪製焦點
focusX = 0
focusY = 1/(4 * a)
circle(focusX, focusY, 4)
fill()
 
// 繪製準線
directrixY = -1/(4 * a)
moveTo(-width / 2, directrixY)
lineTo(width / 2, directrixY)
stroke()

image

很明顯,頂點距離焦點與準線的距離相等。因爲 y 值的公式相等只是符號相反。

但這裏有個有趣的事實 - 等距適用於拋物線上的任意一點(譯者注:拋物線上的任意一到到焦點和焦點垂直到準線的距離相等)!我們能根據上面的代碼展示出來...

lineWidth = 0.5
for (x = -width / 2; x <= width/2; x += 40) {
    // find a point on the parabola
    // 在拋物線上找到那個點
    y = a * x * x
    circle(x, y, 4)
    // draw a vertical line from the directrix to that point
    // then from the that point to the focus
    // 畫一條線垂直於準線於那個點
    // 再將那個點連接至焦點
    moveTo(x, directrixY)
    lineTo(x, y)
    lineTo(0, focusY)
}

我們簡單的從拋物線上採樣了一些點。畫出點,然後再從準線上畫一條垂線到這個點,然後再連接到焦點。從點出發的兩條線長度相等。可能沒啥直接的實際用途,但它看起來還是巧妙的!

image

切線

另一個你能做的事情是找到拋物線上的任意點的切線。這個代表曲線在那個點上的斜率。切線點 x0, y0 公式:

y = 2 * a * x0 * x - a * x0 * x0

這裏也許有一點點亂,因爲我們即有 x 又有 x0, 再次提示 x0 是拋物線上的某個點,而 x 是定義切線點中的一個點。公式給出了x 對應的 y 。 讓我們開始對曲線上的單個點進行編碼吧。先畫一個拋物線, 然後選擇一個 x0, y0 點然後用公式找到另兩個點用於繪製出切線。

// 拋物線
a = 0.003
for (x = -width / 2; x <= width / 2; x++) {
  y = a * x * x
  lineTo(x, y)
}
stroke()
 
// 找任意一個拋物線上的點
x = -80
y = a * x * x
circle(x, y, 4)
fill()
 
// 找一個遠離 canvas 左邊的點
x1 = -width / 2
y1 = 2 * a * x0 * x1 - a * x0 * x0
 

// 找一個遠離 canvas 右邊的點
x2 = width / 2
y2 = 2 * a * x0 * x2 - a * x0 * x0
 
// 畫線
moveTo(x1, y1)
lineTo(x2, y2)
stroke()

我就就得到了如下的點與線:

image

現在我們可以像上面那樣畫出多條拋物線上面的切線。

lineWidth = 0.5
for (x0 = -width / 2; x0 <= width/2; x0 += 40) {
    // find a point on the parabola
    y0 = a * x0 * x0
    circle(x0, y0, 4)
    fill()
 
    // find a point on the far left of the canvas
    x1 = -width / 2
    y1 = 2 * a * x0 * x1 - a * x0 * x0
 
    // and one on the far right
    x2 = width / 2
    y2 = 2 * a * x0 * x2 - a * x0 * x0
 
    // and draw a line
    moveTo(x1, y1)
    lineTo(x2, y2)
    stroke()
}

代碼很相似只是把它放到了循環內,然後結果是:

image

你可以將原始的拋物線條去掉並將間隔縮緊,然後你就可以得到一些僞線條藝術了

拋物面反射鏡

另一個拋物線特性是射線打到拋物線後最後都會聚集到單一的一個點上。利用這個原理它一般被應用於天線和射電望遠鏡,將收到的信號聚集於接受器,各種各樣的太陽能設備會將太陽光彙集於一點(這個點會異常的燙)。毫無疑問,這個匯集點就是拋物面的焦點。

image

https://en.wikipedia.org/wiki/Parabolic_antenna#/media/File:Erdfunkstelle_Raisting_2.jpg

事實上,我記得在我小時候,我的繼父有一個小的太陽能打火機,像這樣:

image

雖然比你日常使用的東西要新奇,但它確實能正常點火工作。

如果你懂了相關的數學,你可以找到射線射到拋物線上的點,找到這個點對應的切線。然後找到這個點的法線(垂直於切線的向量)並反射射入法線點的射線。如果你都做對了,你就會發現反射的射線都彙集到了焦點。我就不和你一起練習了,讓我們繪製一些射線看看正不正常。我們僅僅畫了一些從 canvas 頂部垂直向下交於拋物線的直線,然後再將其連接到焦點。

a = 0.003
 
// 在這個位置假裝畫了個拋物線 
// 繪製焦點
focusX = 0
focusY = 1/(4 * a)
circle(focusX, focusY, 4)
fill()
 
lineWidth = 0.5
for (x = -width / 2; x <= width/2; x += 40) {
    // 找到一個拋物線上的點
    y = a * x * x
     
    moveTo(x, -height / 2)
    lineTo(x, y)
    lineTo(focusX, focusY)
    stroke()
}

image

你可以試着找一條任意的線垂直到拋物線然後連接到焦點。你會發現它從曲線上按它正確的角度反射了。再次我就套用公式直接畫出來了,如果可以的話你可以從物理角度上算算應該會得到一樣的結果。

注意:這隻能在正確配置垂直於 y 軸的射線時纔會正常運行。如果射線從其它角度射入那麼它就不會聚集於拋物面的焦點了。這就是爲啥太陽能點菸器必須要擺到正常的角度才能獲取足夠的熱度用於點菸, 衛星接受器也一樣要調整到正常角度信號才能足夠好。

另一個公式

當然,拋物線不會總是居中 y 軸對稱且頂點靠近 x 軸的這一種形態。以上我們接觸的拋物線樣子是因爲我們使用的是最簡單的公式。下面是另一種:

	
y = a * x * x + b * x + c

我們在此處添加了一些額外的參數。最後添加的 c 很明顯是用於直接影響曲線頂點在 y 軸的位置。參數 b 就有一點點複雜了,讓我們編碼運行看看它到底是幹什麼用的。先把它寫成一個拋物線函數:

function parabola(a, b, c, x0, x1) {
    for (x = x0; x <= x1; x++) {
        y = a*x*x + b*x + c
        lineTo(x, y)
    }
}

你也許想對它進行改進,但在此處能用就行。我們只是從 x0 循環至 x1, 找到每個 x 上的 y 點,然後全連接起來。

parabola(0.01, 0, 0, -width/2, width/2)

結果就是之前我們已經實現過的一條拋物線:

image

下面兩個是給 c 分別賦於 -200 與 +200 的結果。

parabola(0.01, 0, -200, -width/2, width/2)
parabola(0.01, 0, 200, -width/2, width/2)

image

沒啥大驚喜,現在把 c 改回 0, 然後把 b 變成正數。

parabola(0.01, 3, 0, -width/2, width/2)

image

b 爲負數:

parabola(0.01, -3, 0, -width/2, width/2)

image

a 爲正數,b 用於控制拋物線位置左、右移動。那如果將 a 變成 負數會如何?

parabola(-0.01, -3, 0, -width/2, width/2)
parabola(-0.01, 3, 0, -width/2, width/2)

image

毫無意外。它們向上移動了。

注意,到目前爲止我們所用到的公式,我們將 x 與 y 交換將得到拋物線開口向左或向右的形狀。

image

在個例子的簡單公式:

x = a * y * y

豪華進階版:

x = a * y * y + b * y + c

總結

好了, 這就是全部我要講的拋物線內容。我不知道你的編程過程中是否有繪製拋物線的需求,如果有,那麼你現在已經整裝待發了!

本章 Javascript 源碼 https://github.com/willian12345/coding-curves/blob/main/examples/ch07


博客園: http://cnblogs.com/willian/
github: https://github.com/willian12345/

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