6

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

 1 year ago
source link: https://www.cnblogs.com/willian/p/17452767.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

曲线艺术编程系列第三章

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

很可能已经内建了一些这样的绘制功能。比如,虽然 HTML Canvas api 没有直接画圆的函数, 但它有一个 arc 和 ellipse 椭圆的方法间接实现。

如何手动实现这些功能很有用。在某时,某地,某平台上总会用到的。

首先,我们先聚焦于圆弧与圆。可以说圆弧是圆的一部分,也可以说圆是圆弧延展 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/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK