曲线艺术编程 coding curves 第三章 弧,圆,椭圆(ARCS, CIRCLES, ELLIPSES)

第三章 弧,圆,椭圆(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

曲线艺术编程系列第三章

这一篇中我们将关注如何绘制圆弧,圆和椭圆。(结束前再聊聊正切相关的)

很可能你使用的编程平台已内建了一些这样的 api。举个例子,虽然 HTML Canvas api 没有直接画圆形和椭圆的函数,但它有一个 arc 函数,可以用它来间接实现。如何自己动手实现这些功能很有用。某时某刻总会用到的。

首先,我们先聚焦于圆弧与圆。可以说圆弧是圆的一部分,也可以说圆是圆弧延展 360 度而成。从哪个方向开始探索都可以,但对我更钟意从圆开始再进入圆弧部分。

统一度量衡

我使用的绘图 api 内 y 轴与标准笛卡尔坐标系是相反的。负的向上,正的下向。

有别于数学和科学中使用的笛卡尔坐标系, 这在图形绘制 api 中很常见。它与 Processing, HTML Canvas, Cairographics, .net graphics 以及其它很多图形库一样。

有些 api 确实使用笛卡尔坐标系,当角度值为正向增长时代表逆时针方向。

但有些如 pygame, 是混用,y 是为正时是向下,但正向角度旋转时却是逆时针的。

这会影响角度的测量。0 度指向东方。在笛卡尔坐标系中,正向角度转动是逆时针,负向角度转动移动是顺时针。在 Y 轴反转的系统中,正好相反,在这章中我不会再强调这里的差别。我们这里会重新实现一些已内建的绘图 api 函数。

在你学完圆弧这一章节后,如果你想在笛卡尔坐标系下创建圆弧,圆和椭圆的函数也会很简单。

定义

圆的定义一般来说会像这样:“与给定的一个中心点等距的一堆点的集合” ,但当你真的想画一个圆的时候,发现这并没什么卵用。我并不需要一堆无限的点。我们仅需要足够多的点用短线串起来形成圆。

你也见过“圆方程”类似 x2+y2=r^2(译者注:这里是x平方+y平方= r 平方,可表示圆周上任意一点)。 当你尝试想画圆,这好好像也没啥用。

然后你得到了下面这样的参数方程

x = a + r * cos(t)
y = b + r * sin(t)

这里 a 和 b 是圆的中心点, r 是半径, t 是范围参数变量从 0 到 2 * PI。

这里才开始有点儿用。我们可以定义一个圆心点和半径然后跑一个循环,从 0 至 2 * PI 从而得到一堆点,然后用线将这些点连接起来。

还是得提醒你,这里展示的是伪代码。关于伪代码的问题请参考第一章内的说明内容。

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 来开头。我信你自己能搞定。这很简单。cx, cy 和 radius 就是上面公式中的 a, b 和 r 。 t 是用于循环的弧度。

注意,最后需要用 closePath 闭合一下。大多数绘图 api 都有这一特性。它会将最后一点与路径最起始点相连,将圆闭合起来。可能在你的平台上有一丢丢不一样,但大概应该如下图所示:

image

有一个问题,循环中 0.01 是我猜的一个大概值。如果你定的太大,比如 0.2 ,那么相当于你大踏步绕圆跳一圈,得到的结果将会是下面这样很粗糙的圆,看起来可不怎么润:

image

但你如果将增长值设的太小,那么系统将会做太多无用的绘制。圆周越大你就需要更多的线段让它看起来丝滑,半径越小需要的线段就越少。如果你用 0.01 这个常量用于递增,你将在每个圆上绘制 628 条线段。这在小圆上面可就太浪费了。

我上下而求索,找到了一个可用的方案,大至是 4.0 / radius。 在半径 5 至 200 范围内,比直接使用 0.01 这个递增值绘制时的线断少了一半,但看起来依然不错。

可能因系统而异,你自己尝试一下不同值看看。

封装成函数

有了这些, 我们可以把绘制圆封装成一个函数:

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 方法移在函数内移徐掉了,这样你可以用函数创建圆,可以选择描边或填充,或两者都用。如果你愿意,你可以进一步封装 strokeCircle 函数和 fillCircle 函数。下面是函数使用演示

width = 600
height = 600
canvas(width, height)
 
circle(width / 2, height / 2, 200)
stroke()

圆弧

已经完成圆这部分了,现在我们可以在此基础上创建 arc 函数。你的编程语言可能有现成的 api ,但这不重要,我们再实现一遍。很简单和圆函数一样,但我们用 start 代表开始位置 0 和 end 代替结束位置的 2 * PI, 我们让调用者决定开始与结束位置。

不再解释了因为过于简单我直接抛出伪代码吧

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

你看简单吧,仅仅是将硬编码的开始与结束角度替换成参数传入的形式。当然,我移除了 closePath() 调用,取而代之的是最终 lineTo,这样更精确一点。

像下面一样使用它:

width = 600
height = 600
canvas(width, height)
 
arc(width / 2, height / 2, 250, 0.5, 3.5)
stroke()

结果会是:

image

有一丢丢问题。如果我将输入的开始与结束对调呢?

arc(width / 2, height / 2, 250, 3.5, 0.5)

它会立即结束循环,因为 3.5 已经大于 0.5了。啥也不会画出来。 我想要的是开始在 3.5 度,又绕回到 0.5 度像下面这样:

image

一种方式是我们只要保证结束的度数大于开始度数。我们可以判断如果结束度数小于开始度数,那么直接加上 2*PI,直到结束度数大于开始度数。

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

现在应该可以正常展示成上面期望的那样了。

还有一件事儿,我们总是假定用户绘制是顺时针的。我们应该让用户自己决定。

幸运地是这很容易实现,我们只需要传另一个参数 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)
}

如果你足够幸运,你使用的编程语言支持像这样变量交换:

start, end = end, start

如果不能直接像上面这样交替,那就只能用老方法了:

temp = start
start = end
end = temp

这样调用

arc(width / 2, height / 2, 250, 3.5, 0.5, false)
stroke()

会给你期望的圆弧了:

image

下面这段代码

arc(width / 2, height / 2, 250, 3.5, 0.5, true)
stroke()

则给你这样的圆弧:

image

两者都是开始于 3.5 度结束到 0.5,一条正的,一条按反的方式画。

正如开始时在圆形处提到过,这里我选择了正向角度顺时针为默认,这与笛卡尔坐标系不同。现在你知道在不同方向上如何绘制圆弧,你可以选择你一个喜欢的作为默认方向。

现在我们有了一个强大的圆弧函数,我们其实可以用它替换原来圆函数内的一些重复代码,像下面这样

function circle(x, y, r) {
  arc(x, y, r, 0, 2 * PI, true)
}

从 0- 2*PI 可不就是一个圆么。

片段与扇区

这里还有几个可以创建的函数如果你觉得它们有用的话。将圆弧首尾相连起(一条弦)形成的一个圆弧片。我们可以这样实现它,在绘制圆弧完毕时直接调用 closePath 函数,你使用的编程语言中肯定也有类似 closePath 的函数。

function segment(x, y, r, start, end, anticlockwise) {
  arc(x, y, r, start, end, anticlockwise)
  closePath()
}

这个圆弧片,就是从 2.5 度至 4.5 度

image

一个扇形就是将圆弧用线段从中心点连接起来,我们可以调用 lineTo 至中心点,然后再 closePath

function sector(x, y, r, start, end, anticlockwise) {
  arc(x, y, r, start, end, anticlockwise)
  lineTo(x, y)
  closePath()
}

用上面圆弧片一样的参数画的扇形:

image

现在你可以自己画饼图了。

多边形

在介绍椭圆之前,我想先奖励个正多边型。 这倒不是我认为多边形是曲线,但数学上来讲它可能真的是。 无论如何,反正来都来了,把它学了吧。

最开始我们讨论过分辨率,我们看到过低分辨率的圆边上看起来是一段一段的。你能看到圆是由单独一条条线段组成的。我们可以把这个 bug 点转化成一个可用的特征。如果我们将分辨率降低到足够低直到只有6个片段组成一个圆,我们就得到了一个六边形, 5 条线段就是 五边形,4 条就是方形,3 条就是三角形。 我们仅需要特别处理我们希望有多少条边,除以 2*PI 当作分辨率就可以成形了。

实现如下:

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

像下面这样调用:

polygon(300, 300, 250, 5)
stroke()

得到一个五边形:

image

也许你想为这个多边形初始化一个特别的角度,你可以这样做

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

现在你可以这样使用

polygon(300, 300, 250, 5, 0.5)
stroke()

得到了一个转了一点角度的多边形

image

试试传入不同的边数。

一个有趣的效果是创建一系列大小不同的多边形,每个多边形相应旋转一丢丢的角度:

angle = 0
for (r = 5; r <= 255; r += 10) {
  polygon(300, 300, r, 5, angle)
  stroke()
  angle += 0.05
}

效果如下:

image

也许有一点点离题,但你看图形里突然形成了5条新的曲线,能接受,能接受。

椭圆

本文最后一部分,椭圆。

好的让我们来看看椭圆在维基百科中的定义...

环绕两个焦点的平面曲线,对于曲线上的所有点,到焦点的两个距离之和为常数
https://en.wikipedia.org/wiki/Ellipse

Em... 完全搞不懂,太数学化了。再看看这条解释...

椭圆是圆锥截面的封闭类型:沿圆锥与平面相交的平面曲线
https://en.wikipedia.org/wiki/Ellipse

还是一样不好理解,好吧,继续...

一个椭圆也可以用一个焦点和椭圆外一条叫做准线的线来定义:对于椭圆上的所有点,到焦点的距离和到准线的距离之比是一个常数。
https://en.wikipedia.org/wiki/Ellipse

好吧,还是不好懂,但就如之前那样,最终我们可以找到可用的参数方程,和之前的圆参数方程差不多

x = a + rx * cos(t)
y = b + ry * sin(t)

这里,除了用 a 和 b 表示圆心点之外,还有 rx 和 ry 最简单就是把它们想象成 “radius x” 和“radius y”, 尽管这些名字可能让数学家鄙视。但对于一个未旋转的椭圆 rx 就是等于一半的椭圆宽,ry 等于一半的椭圆高。

所以我们可以编写下面这样一个函数

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

值得提醒的一点,是分辨率值, 我将 4.0 除以 rx 和 ry 中的最大值。你可以想想有没有更好的,但这对于我来说足够用了。现在你可以像下面这样调用它

ellipse(300, 300, 250, 150)
stroke()

And get:

得到

image

小奖励

有时候我写了就停不下来。接下来的这部分与创建曲线关系不大…,或者说也有一定相关。你看完后再看想想是不是有关系吧。 比起在圆周(或圆弧,多边形,椭圆)上每个点之间用线段相连,我们可以在这些点上画一些其它的形状。我们将增加曲线点之间的间隔以拥有足够空间容纳其它形状,不至于挤在一起,不然看起来太乱。事实上,多边形函数正好适用在这里。它可以让我们画一个由多个圆组成的圆形环。对于代码我就不解释了,你应该可以理解。

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

image

总结

有想过要不要写这一部分,跑题已经跑的够远的了,这一篇也足够的长了。

到目前为止都很基础,但希望足够有趣。从这章之后,我们将慢慢接触一点复杂的东西希望内容更加的有趣。

本章 Javascript 源码 https://github.com/willian12345/coding-curves/tree/main/examples/ch03


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

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