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. 最后这个练习貌似很有意思。但学到这里我已经很想死了,血槽还有血的慢慢玩吧。。

Be Sociable, Share!

4 thoughts on “Processing:数学(下)”

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

    Reply

    ww Reply:

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

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *