6.程序化的形状(Procedural Shapes) #Programming Design System

​ 到目前为止,我们自定义形状的代码还是非常繁琐。我们通过逐行输入顶点函数来手动创建形状,用这种方法碰到更加复杂的形状就会爆炸,根据我们的书名,这种方法不太适合。现在我们知道了beginShape()的用法,接下来我们要用for循环和sin()cos()函数以程序化的方式绘制自定义形状。

正弦和余弦

多年来,我已经看到了许多同学面对sincos函数苦苦挣扎。原因也很容易理解:这些词看起来相当吓人和抽象,尤其是当你自认为不擅长数学的时候。这既不幸,又没必要。不幸的是,这两个函数是大多是程序设计的基本组成部分,并且对它们充分理解能够解决你许多视觉上的问题。没有必要是因为它们并不难学。即使你对本章内容一点也不了解,也可以通过记住两行相同的代码开始。

正弦和余弦能让我们在圆周上找到任意的位置。它们通过将角度转换为单位圆的x坐标和y坐标来实现。然后再把这些值乘以实际圆得半径来按比例放大。三角函数按照咱们高中学的知识sin就是对边比邻边,cos就是斜边比邻边。

在P5中,这些函数称为sin()cos()。它们接受一个以弧度制表示的参数,并返回一个在-1到1 之间的值。下面的两行代码演示了如何获取这些值并将它们乘以实际圆的半径。记住这两行代码,它们非常重要。

1
2
var x = cos(RADIANS) * RADIUS;
var y = sin(RADIANS) * RADIUS;

为了说明这点,请看下面的示例。我们用相同的代码在大圆圆周的330°的位置绘制小圆。

1
2
3
4
5
6
7
8
9
10
translate(width/2, height/2);

noFill();
var radius = width * 0.3;
ellipse(0, 0, radius*2, radius*2);

fill(30);
var x = cos(radians(330)) * radius;
var y = sin(radians(330)) * radius;
ellipse(x, y, 20, 20);

sin()cos()函数为那些围绕着一个中心点并且没有重复轮廓的图形(正多边形)提供了一种绘制的思路。

for循环

尽管整本书都在使用for循环,但还是要在这里介绍一下for循环的基本功能。for循环允许我们通过递增或者递减的方式改变一个通常为i变量,知道变量不满足表达式,循环才会停止。在下面的例子中,我们初始化i的值为0,只要我们变量的值小于10,就会进行迭代,并且每次迭代之后变量将递增1。最终这个循环会执行十次,变量会从0递增到9,在画布上绘制十个矩形。

1
2
3
for(var i = 0; i < 10; i++) {
rect(0, 0, 100, 100);
}

不幸的是,所有这些矩形的位置和大小相同,rect()一次又一次的接受同样的参数,所有矩形都重叠在一起。i在这个时候就能发挥用处了,每次循环的时候i都会发生变化,通过i就能使矩形产生变化。下面的示例用i将十个矩形放置在x轴上,每个矩形相聚一个像素。

尽管你现在可能还不能马上理解,但这在绘制图形时是一项重要的技能。因为i在每次迭代的时候都会递增,所以可以用作画布上图形分布的标量。例如,如果我们想让矩形彼此相邻放置,则可以把大于矩形宽度的值乘以i

1
2
3
for(var i = 0; i < 10; i++) {
rect(i * 105, 0, 100, 100);
}

我们可以用相同的方法绘制自定义形状。我们在循环在beginShape()endShape()函数之间添加顶点。在下面的示例中,我们用这个方法在画布中间添加十个随机顶点。

1
2
3
4
5
6
7
8
translate(width/2, height/2);
beginShape();
for(var i = 0; i < 10; i++) {
var x = random(-100, 100);
var y = random(-100, 100);
vertex(x, y);
}
endShape();

结合上面说的方法

我们把上面随机放置的顶点改为沿着圆周按顺序的顶点。这个时候我们之前记住的两行代码就派上了用处。但我们这次传递给sin()cos()的角度是把圆周分为i份得来的。最终我们得到了一个正十边形。

1
2
3
4
5
6
7
8
translate(width/2, height/2);
beginShape();
for(var i = 0; i < 10; i++) {
var x = cos(radians(i * 36)) * 100;
var y = sin(radians(i * 36)) * 100;
vertex(x, y);
}
endShape();

通过更矮迭代次数和顶点之间的角度,可以绘制所有基本形状。下面的代码在顶部添加了一些变量,来根据顶点数自动计算间距。更改numVertices变量,将会出现另一个形状。

1
2
3
4
5
6
7
8
9
10
var numVertices = 3; // or 4 or 30
var spacing = 360 / numVertices;
translate(width/2, height/2);
beginShape();
for(var i = 0; i < 10; i++) {
var x = cos(radians(i * spacing)) * 100;
var y = sin(radians(i * spacing)) * 100;
vertex(x, y);
}
endShape();

你可能会说:“太好了,我们发明了新的基本形状函数”。其实我们用这个方法可以绘制更加复杂的形状。我们来看几个例子,它们都用相同的sin()cos()公式来绘制不同类型的形状。我们将从下面的波浪形圆圈开始,每个顶点的半径都是随机的,看起来就像是手工绘制的。

1
2
3
4
5
6
7
8
9
10
11
12
translate(width/2, height/2);

beginShape();
for(var i = 0; i < 100; i++) {
更改每个顶点的半径

var radius = 100 + random(5);
var x = cos(radians(i * 3.6)) * radius;
var y = sin(radians(i * 3.6)) * radius;
vertex(x, y);
}
endShape();

通过在每个顶点的高低半径交替,可以创建出下面的星形。使用不同的参数,或用rotate()函数改变星形的方向,我们可以很容易的改变星形的样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
translate(width/2, height/2);

//将初始半径设置为100

var radius = 100;

beginShape();
for(var i = 0; i < 10; i++) {

//在cos / sin公式中使用半径

var x = cos(radians(i * 36)) * radius;
var y = sin(radians(i * 36)) * radius;
vertex(x, y);

//更改下一个顶点的半径

if(radius == 100) {
radius = 50;
} else {
radius = 100;
}
}
endShape();

这是用 quadraticVertex()函数创建的花朵, quadraticVertex()所有的控制点和顶点的位置都是通过sin()cos()生成。使用贝塞尔曲线时,请记住以vertex()函数调用开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//自动计算间距
var numVertices = 7;
var spacing = 360 / numVertices;

beginShape();
//循环一圈,用曲线闭合形状。
for(var i = 0; i < numVertices+1; i++) {

//找到顶点的位置
var angle = i * spacing;
var x = cos(radians(angle)) * 100;
var y = sin(radians(angle)) * 100;

if(i == 0) {
//如果循环是第一次运行,创建简单顶点。
vertex(x, y);
}
else {
//否则,创建一个二次Bézier顶点,其控制点位于两点之间的中间,并且半径较大。
var cAngle = angle - spacing/2;
var cX = cos(radians(cAngle)) * 180;
var cY = sin(radians(cAngle)) * 180;
quadraticVertex(cX, cY, x, y);
}
}
endShape();

您会发现自己只需要使用一种循环功能。如下创建两个形状:第一个使用sin(),第二个使用cos()(如下面的代码所示)。

1
2
3
4
5
6
7
8
9
10
11
12
strokeWeight(20);
strokeCap(SQUARE);
translate((width/2) - 200, height/2);
beginShape();
for(var i = 0; i < 200; i++) {
//x轴上2个像素间距。
var x = i * 2;
//y轴上的200像素高波形。
var y = cos(i * radians(2)) * 100;
vertex(x, y);
}
endShape();

在设计过程中,可以使用正弦和余弦来创建一系列不同的形状。在约瑟夫·穆勒·布罗克曼(JosefMüller-Brockmann)的设计中,一系列指数增长的圆弧围绕着画布的左下角旋转。

约瑟夫·米勒-布罗克曼的贝多芬海报

Sediment Mars是莎拉·哈拉切(Sarah Hallacher)和亚历山德拉·维拉米尔(Alessandra Villaamil)创作的一系列海报。通过sin()cos()函数,生成一个椭圆形,然后通过添加随机值来让他失真。

莎拉·哈拉切(Sarah Hallacher)和亚历山德拉·维拉米尔(Alessandra Villaamil)的Sediment Mars

Generative Play项目是Adria Navarro的卡片游戏,它使用程序绘图来创建各种各样的生成角色。这些角色的身体是使用sin()cos()创建的。

Adria Navarro的生成游戏

本章介绍了一种与传统设计过程有本质不同的设计方法。我们没有一个一个去绘制形状,而是编写算法来替我们完成这些任务。使用循环来绘制图形是一种强大的方式,因为它能让程序员用更少的代码做更多的事情,从而减轻了手工绘制每个对象的痛苦。这同时也是程序设计中最难的部分,因为设计人员需要花更多的时间将系统提炼成代码,而且他们不能像传统设计工具那样轻易的操纵单个形状。美国计算机科学家唐纳德·努斯(Donald Knuth)称这是从设计到元设计的过渡:

“元设计(Meta-design)比设计困难得多;解释如何画比画东西难得多。[…]然而,一旦我们成功地解释了如何以足够通用的方式绘制事物,那么在不同情况下,相同的解释将适用于所有形状。因此花时间制定描述如何绘制是值得的。”

Donald Knuth (1986), The Metafont Book

这也是本书的主要论点,当设计师学会了系统的思考设计过程,并且用软件实现这些系统时,他们能做出以前无法完成的工作。

作业

尝试使用本章介绍的技术绘制所有基本形状。然后继续生成其他类型的形状。你可以random()用来操纵形状轮廓吗?可以使用贝塞尔曲线代替简单顶点吗?

你可以把作业发在下面评论区中。

本文译自Programming Design Systems

知识共享许可协议

本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。