曲线艺术编程 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/

posted @ 2023-06-10 01:49  池中物王二狗  阅读(222)  评论(0编辑  收藏  举报
转载入注明博客园 王二狗Sheldon Email: willian12345@126.com https://github.com/willian12345