Processing:数学(下)

14. 我们直觉上习惯以度数来考虑角。但Processing却要求我们用弧度来制作一个角。

弧度
弧度

角度、弧度转换公式:
弧度 = 2 * PI * (角度/360)
幸运的是,若我们习惯了以角度来考虑角而又必须以弧度写代码,Processing为我们提供了radians()函数自动将角度值转化为弧度值。另外,常数PI和TWO_PI也可以现成使用(分别等于180 ° 和 360 ° )。以下代码将使形状旋转60 ° 。
float angle = radians(60);
rotate(angle);

15. 复习一下,数学常数PI (或 π )是圆周与其直径的比率(围绕圆周的距离)。它的值约等于3.14159。

16. Sohcahtoa,貌似无意义而且奇怪的词,确实巨大部分计算机图形工作的基础。当你要计算一个角,决定点间距,除了圆形或弧形等等等等的时候,你会发现三角学的重要性。sohcahtoa是记忆三角学基础,正弦、余弦和正切的助记符。
◎ soh : sine = 对边/斜边
◎ cah : cosine = 邻边/斜边
◎ toa : tangent = 对边/邻边

Sohcahtoa
Sohcahtoa

17. 我们要在Processing内绘制图形,需要给出其x,y坐标值,这种坐标称为笛卡尔坐标。另外一种有用的坐标称为极坐标,以空间内围绕原点旋转(以角度计)的以及距离原点的半径定义的一个点。我们可以将其作为p5内一个函数的引数。三角函数公式允许我们将这些坐标转换为笛卡尔,然后被用于绘制形状。

笛卡尔和极坐标转换
笛卡尔和极坐标转换

sine(theta) = y/r → y = r * sine(theta)
cosine(theta) = x/r → x = r * cosine(theta)

18. 比如,如果r为75,角度为45° (或 PI/4 弧度) ,我们可以如下计算x和y:
float r = 75;
float theta = PI / 4; // 我们同样可以说: float theta = radians(45);
float x = r * cos(theta);
float y = r * sin(theta);

圆形轨迹
圆形轨迹

19。 这样的转换对我们来说是非常有用的,例如说,你如何用笛卡尔坐标使一个形状延圆形轨迹运动?那将非常困难。不过使用极坐标也很简单,只需增加角度即可!
// 极坐标 (r, theta)被转换为笛卡尔坐标 (x,y)并在ellipse()函数应用
float r = 75;
float theta = 0;

void setup() {
size(200,200);
background(255);
smooth();
}

void draw() {

// 极坐标到笛卡尔坐标的转换
float x = r * cos(theta);
float y = r * sin(theta);

// 在x,y绘制圆形
noStroke();
fill(0);
ellipse(x + width/2, y + height/2, 16, 16); // 为窗口中央进行调整

// 增加角度
theta += 0.01;
}

20. Daniel出题,来,我们一起来画一盘七彩蚊香吧。。注意,只需在上例代码基础上修改一行、增加一行即可。我做出来了,你呢?

七彩蚊香
七彩蚊香
钟摆
钟摆

21. 正弦曲线是平滑的,并且总在-1和1间变化。这种行为类型称为振动,在两点间的周期运动,例如钟摆。在p5中,我们可以通过将正弦函数的值赋给一个对象的位置来模拟这种振动。下例为一个摇动的钟摆的代码:
float theta = 0.0;

void setup() {
size(200,200);
smooth();
}

void draw() {
background(255);

// sin()函数的输出在-1到1间平滑振动.
// 通过加1,我们得到0~2间的振动.
// 通过乘以100(width/2),我们得到0~200的值,这可以用于x位置。
float x = (sin(theta) + 1) * width/2;

// 对于每次循环,增加theta。
theta += 0.05;

// 用正弦值绘制圆形
fill(0);
stroke(0);
line(width/2,0,x,height/2);
ellipse(x,height/2,16,16);
}

22. Daniel出题:将以上功能封装入Oscillator对象,获取一个Oscillators数组,每个都围绕x和y轴以不同的比率运动。哦,和你一样,我还不大会用类,因此最后在这里看一下答案。看完还是很有帮助的,一遍遍的看这些类似代码,差不多也能记个七八成了吧。。

正弦路径球球
正弦路径球球

23. 同样的,我们可以在正弦函数的路径上绘制一序列形状以实现有趣的效果。
float theta = 0.0;

void setup() {
size(200,200);
smooth();
}

void draw() {
background(255);

// 增加theta
theta += 0.02;
noStroke();
fill(0);
float x = theta;

// 一个for循环被用来绘制依附正弦波路径的所有点 (使其放大到窗口像素尺寸).
for (int i = 0; i <= 20; i++) { // 基于正弦函数计算y float y = sin(x)*height/2; // 绘制一个圆 ellipse(i*10,y + height/2,16,16); // 顺x轴移动 x += 0.2; } }

24. 1975年,Benoit Mandelbrot创造了分形(fractal)体系用以形容自然界中那些自相似的形状。产生这些形状的一个程序被称为递归或循环(recursion)。

25. 我们知道在draw()内,一个函数可以呼叫另一个函数。但它们可以呼叫它们本身吗?draw()可以呼叫draw()吗?实际上,它可以(虽然这会造成死循环的惨烈局面..)。函数呼叫自身便是递归。在数学中,最普遍的例子便是阶乘。n个数的阶乘:
n! = n * n – 1 * . . . . * 3 * 2 * 1
0! = 1

在p5内用for循环写:
int factorial(int n) {
int f = 1;
for (int i = 0; i < n; i++ ){ f = f * (i + 1); } return f; }
再进一步观察阶乘:
4! = 4 * 3 * 2 * 1
3! = 3 * 2 * 1

因此. . . 4! = 4 * 3!
再推:
n! = n * ( n – 1)!
1! = 1

26. 阶乘的定义包括阶乘吗?!这有点像说“疲倦”被定义为“当你疲倦时的感受”。这种在函数内自参照的概念被称为递归。我们可以利用递归写一个呼叫自身阶乘的函数。
int factorial(int n) {
if (n == 1) {
return 1;
} else {
return n * factorial(n-1);
}
}

27. 下图为fractorial(4)时的情况

fractorial(4)
fractorial(4)

28. 同样的原理可以被用来绘制有趣的图形。看看接下来的递归函数。
void drawCircle(int x, int y, float radius) {
ellipse(x, y, radius, radius);
if(radius > 2) {
radius *= 0.75f;
drawCircle(x, y, radius);
}
}
drawCircle()干了什么?它绘制一个圆,然后以相同的参数(微微调整)呼叫自身。结果就是一系列在自身内绘制的圆形。注意以上函数仅在半径大于2时递归呼叫自身。这是一个关键点。所有递归函数必须拥有一个推出的条件(exit condition)!像forwhile循环一样,如果没有停止的条件,则很可能会出现死循环最后使程序崩溃。

分形一
分形一

29. 让我们试着将drawCircle()搞得更复杂一点点。绘制一个圆形,并在其左右分别绘制两个大小为其一半的圆,如此反复:
void setup() {
size(200,200);
smooth();
}

void draw() {
background(255);
stroke(0);
noFill();
drawCircle(width/2,height/2,100);
}

void drawCircle(float x, float y, float radius) {
ellipse(x, y, radius, radius);
if(radius > 2) {
// drawCircle()呼叫自己两次,制造一个分支效果
// 对每个圆来说,两个更小的圆分别在其一左一右绘制.
drawCircle(x + radius/2, y, radius/2);
drawCircle(x – radius/2, y, radius/2);
}
}

分形二
分形二

30. 同样的,我们可以基于上例分别再在每个圆的一上一下各增加一个圆,效果很美:
void drawCircle(float x, float y, float radius) {
ellipse(x, y, radius, radius);
if(radius > 8) {
drawCircle(x + radius/2, y, radius/2);
drawCircle(x – radius/2, y, radius/2);
drawCircle(x, y + radius/2, radius/2);
drawCircle(x, y – radius/2, radius/2);
}
}

31. 这道练习搞得我很纠结(哦,看来注释真的很重要,hey,Daniel,别说填你留给我的空了,光看懂你答案的代码都很难啊- -),还是属于对象没学好…:
void setup() {
size(400,200);
smooth();
}

void draw() {
background(255);
stroke(0);
branch(width/2,height,100);
}

void branch(float x, float y, float h) {
line(x,y,x-h,y-h);
line(x,y,x+h,y-h);
if (h > 2) {
branch(x-h,y-h,h/2);
branch(x+h,y-h,h/2);
}
}
恩,经过半天一夜的思考和草稿,我终于弄明白这题了。。还有想不明白的可以问我。

分形三
分形三

32. 之前我们学习的数组是一维的,比如:
int[] myArray = { 0,1,2,3};
其实它也可以多维,比如二维的数组看起来就像这样:
int[][] myArray = { { 0,1,2,3},{3,2,1,0},{3,5,6,1},{3,8,3,4} } ;
可以理解为,数组的数组。而三维数组便可理解为数组的数组的数组。

33. 出于我们的目的,我们最好将二维数组看做一个矩阵,写做:
int[][] myArray = { {0, 1, 2, 3},
{ 3, 2, 1, 0},
{ 3, 5, 6, 1},
{ 3, 8, 3, 4} };

34. 我们可以利用这种数据结构编码一个图片的信息。例如,一个不同颜色的网格可由如下代码实现(每个值代表一个颜色):
int[][] myArray = { { 236, 189, 189, 0},
{ 236, 80, 189, 189},
{ 236, 0, 189, 80},
{ 236, 189, 189, 80} };

35. 顺序阅遍一维数组,我们用for循环:
int[] myArray = new int[10];
for (int i = 0; i < myArray.length; i++ ) { myArray[i] = 0; }
而对于二维数组,如果想要关照每一个元素,我们必须要使用两个嵌套的循环。这给了我们一个对于矩阵内每一行每一列的计数器变量。
int cols = 10;
int rows = 10;
int[][] myArray = new int[cols][rows];

// 两个嵌套的循环使得我们可以访问二维数组中的每一点。使每个列i,方位每个行j。
for (int i = 0; i < cols; i++) { for (int j = 0; j < rows; j++) { myArray[i][j] = 0; } }

灰点点
灰点点

36. 一个例子:
// 建立维数
size(200,200);
int cols = width;
int rows = height;

// 申明2D数组
int[][] myArray = new int[cols][rows];

// 初始化2D数组值
for (int i = 0; i < cols; i ++ ) { for (int j = 0; j < rows; j ++ ) { myArray[i][j] = int(random(255)); } } // 画点 for (int i = 0; i < cols; i ++ ) { for (int j = 0; j < rows; j ++ ) { stroke(myArray[i][j]); point(i,j); } }

正弦闪格
正弦闪格

37. 一个二维数组同样可以用来存储对象,这尤其适用于那些包括某种网格或板子的程序。下例显示的正是将一堆Cell对象存储于一个二维数组。每个cell的亮度由一个正弦函数引发由0~255的振动。
// 对象的2D数组
Cell[][] grid;

// 网格的行数和列数
int cols = 10;
int rows = 10;

void setup() {
size(200,200);
grid = new Cell[cols][rows];

// 计数器变量i和j同样是行和列的数目
// 在本例中, 它们被用于格子对象构造器的引数
for (int i = 0; i < cols; i ++ ) { for (int j = 0; j < rows; j ++ ) { // Initialize each object grid[i][j] = new Cell(i*20,j*20,20,20,i + j); } } } void draw() { background(0); for (int i = 0; i < cols; i ++ ) { for (int j = 0; j < rows; j ++ ) { // 振动并显示每个对象 grid[i][j].oscillate(); grid[i][j].display(); } } } // 一个Cell对象 class Cell { // 一个cell对象通过变量x,y,w,h获取其在网格内的位置和大小 float x,y; // x,y位置 float w,h; // 宽、高 float angle; // 振动亮度的角度 // Cell构造器 Cell(float tempX, float tempY, float tempW, float tempH, float tempAngle) { x = tempX; y = tempY; w = tempW; h = tempH; angle = tempAngle; } // 振动意味着增加角度 void oscillate() { angle += 0.02; } void display() { stroke(255); // 用正弦波计算颜色 fill(127 + 127*sin(angle)); rect(x,y,w,h); } }

38. 最后这个练习貌似很有意思。但学到这里我已经很想死了,血槽还有血的慢慢玩吧。。

《Processing:数学(下)》上有4条评论

  1. 请问那个七彩蚊香加的是哪一行?

    Reply

    ww Reply:

    无论用“李李”还是“eva”作为id,都可以显示你的急于求成,静下心来,好好想想,然后再提问

    Reply

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注