Processing:平移和旋转

1. z轴指任意一点的深度。这貌似很抽象,这么理解吧,x轴和y轴分别代表萤幕的宽和高,那么z轴就位于你萤幕的背后(负值)或之前(正值)。

2. 用二维的方式也可以做出三维的效果,比如下例展示一个由小变大的矩形,看起来就像它从远处向我们走来:
float r = 8;

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

void draw() {
background(255);

// 在萤幕中央显示一个矩形
stroke(0);
fill(175);
rectMode(CENTER);
rect(width/2,height/2,r,r);

// 增大矩形
r++ ;

// 反复开启矩形的动态
if (r > width) {
r = 0;
}
}

3. 幸运的是,Processing懂得透视,并选取适宜的二维像素来创建三维效果。我们应当认识到,在我们进入3D像素坐标的同时,一部分的控制必须交由p5的渲染器来完成。你不在能像控制2D图形那样精确的控制它们的像素位置,因为XY位置将被调整用来计算3D透视。

4. 下图为笛卡尔3D系统的坐标图示:

3-D coordinate
3-D coordinate

5. 注意,这么写是错误的:
rect(x, y, z, w, h);

6. 在p5的世界为形状使用3D坐标,我们必须学习一个新的函数,叫做translate()。函数translate()移动原点—(0,0)—相对其之前状态。我们知道当一个sketch开始运行,原点位于萤幕左上角。如果我们写translate(50,50),结果将如下图所示:

translate(50,50);
translate(50,50);

另外,每当运行一次draw(),原点都会将其重置回萤幕左上角。

7. 看这个例子:
void setup() {
size(200,200);
smooth();
}

void draw() {
background(255);
stroke(0);
fill(175);

// 抓取鼠标位置, constrain到萤幕尺寸
int mx = constrain(mouseX,0,width);
int my = constrain(mouseY,0,height);

// 平移到鼠标位置
translate(mx,my);
ellipse(0,0,8,8);

// 向右平移100像素
translate(100,0);
ellipse(0,0,8,8);

// 向下平移100像素
translate(0,100);
ellipse(0,0,8,8);

// 向左平移100像素
translate(-100,0);
ellipse(0,0,8,8);
}
translate()第一个引数控制左右平移,第二个则控制上下平移。translate()的“相对之前状态”在这个例子里体现的比较明显,相对的不永远是ellipse(0,0,8,8)这个球;,而是translate()之后的那个“新球”

8. translate()可以接受第三个引数作为z轴值。
// 沿z轴平移
translate(0,0,50);
rectMode(CENTER);
rect(100,100,8,8);
以上代码沿z轴平移50像素,然后在位置(100,100)绘制一个矩形。这在技术上是正确的,但当我们使用translate()时,一个好习惯是将位置(x,y)指定为平移的一部分,如下:
// 当使用translate()时,矩形的位置时(0,0),因为translate()将带我们到矩形的位置
translate(100,100,50);
rectMode(CENTER);
rect(0,0,8,8);
最后,我们可以用它照做一开始那个矩形变大的例子:
// 一个Z(深度)坐标的变量
float z = 0;

void setup() {
// 当时用 (x,y,z) 坐标时, 我们必须告诉p5我们需要一个3D sketch.
// 我们为此为size()函数增加第三个引数, P3D (或 OPENGL)
size(200,200,P3D);
}

void draw() {
background(255);
stroke(0);
fill(175);

// 在一个形状显示前平移一个点
translate(width/2,height/2,z);
rectMode(CENTER);
rect(0,0,8,8);

// 增大z
z++ ;

// 重启矩形
if (z > 200) {
z = 0;
}
}

14-3
14-3

9. 这个题又把我打败了,,折腾好久才搞出来,不过终于算是对各种形状同时translate()的情况有点心得了(再次注意,它的平移是基于上一个点的位置,这是理解这个函数至关重要的一点):
size(200,200);
background(255);
stroke(0);
fill(0,100);
rectMode(CENTER);
translate(width/2,height/2);
rect(0,0,100,100);
translate(50,-50);
rect(0,0,100,100);
translate(-100,100);
line(0,0,-50,50);

10. translate()函数对相对一个给出的中点绘制的一系列形状很有用。
void display() {
// 画Zoog的身体
fill(150);
rect(x, y,w/6,h*2);

// 画Zoog的头
fill(255);
ellipse( x,y-h/2 ,w,h);
}
使用translate()可以将代码简化为:
void display() {
// 将原点(0,0)移至(x,y)
translate(x,y);
// 画Zoog的身体
fill(150);
rect(0,0,w/6,h*2);
// 画Zoog的头
fill(255);
ellipse(0,-h/2,w,h);
}

11. 观察之前的例子,你会发现我们赋予了size()函数第三个引数,这代表绘制模式。默认(不写)为JAVA2D,是的,顾名思义,这是一个绘制2D图像的模式。要绘制3D图形,有以下两个方式:
◎ P3D——Processing开发者创造的一个3D渲染器。有一点需要注意,P3D不支持平滑效果(通过smooth()函数实现)。
◎ OPENGL——借助硬件加速的3D渲染器。如果你的机器有支持OPENGL的显卡装载,你便可以使用这个模式。因为在渲染速度上的优势,它在在高分辨率窗口展示大量形状时被推荐使用。而且经过对比,我发觉OPENGL模式的确比P3D模式的显像更平滑。

12. 要使用OPENGL模式,你还需实现导入OPENGL库。Sketch -> Import Library -> OpenGL 或 手写 import processing.opengl.*; 均可完成此操作。

13. 除了之前教过的绘制矩形、圆形、点、线等,我们可以利用
beginShape(), endShape(), 和vertex()函数创建我们自己想要的形状。
矩形:
rect(50,50,100,100);
等同于:
beginShape();
vertex(50,50);
vertex(150,50);
vertex(150,150);
vertex(50,150);
endShape(CLOSE);
是的,在这个例子里显得比原先更加复杂,但是这种方法在绘制不规则图形或多边形的时候是必备的。

14. beginShape()表示我们将开始用数个顶点(vertex)绘制我们的自定义形状,vertex()当然是用来标注我们多边形内各个顶点的位置,endShape()表明我们已完成了顶点添加,其引数CLOSE表明这个形状应当被关闭,即,最后一个顶点将与第一个顶点相连。参阅Processing站点参考获取更多关于这三个函数的调用方法。

15. 你可以使用引数限制beginShape()绘制的图像,比如LINES,TRIANGLES等等。

16. 另外,你可以用curveVertex()替代vertex(),这将用曲线替代直线。注意,使用curveVertex()时,第一个顶点和最后一个顶点将不会被显示。这是因为,这两点要被用来定义弯曲率。

17. 让我们用beginShape() , endShape() , 和vertex()画一个立体的金字塔,共五面,四面三角形,底座一个矩形。

虽然麻烦,但我建议你在草稿纸上画出上述诸点,以帮助你理解它们的空间位置关系
虽然麻烦,但我建议你在草稿纸上画出上述诸点,以帮助你理解它们的空间位置关系

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

void draw() {
background(255);

// 金字塔的顶点相对于一个中点绘制
// 因此,我们呼叫translate()来将金字塔放在窗口适宜位置.
// 一个稍好一点的选择也许是将位移放入drawPyramid()函数并将x,y,z传递为引数
translate(100,100,0);
drawPyramid(150);
}

// 这个函数在中点周围的弹性距离内设置金字塔的顶点,
// 这个弹性距离取决于传递给它的引数的值.
void drawPyramid(int t) {
stroke(0);

// 这个金字塔有四面,每面被绘制为一个单独的三角形
// 每面有三个顶点,形成一个三角形的形状
// 参数” t ” 决定了三角形的大小。

beginShape(TRIANGLES);
fill(255,150); // 注意每个多边形都可以具有自己的颜色.
vertex(-t,-t,-t);
vertex( t,-t,-t);
vertex( 0, 0, t);

fill(150,150);
vertex( t,-t,-t);
vertex( t, t,-t);
vertex( 0, 0, t);

fill(255,150);
vertex( t, t,-t);
vertex(-t, t,-t);
vertex( 0, 0, t);

fill(150,150);
vertex(-t, t,-t);
vertex(-t,-t,-t);
vertex( 0, 0, t);

endShape();
}

18. 是的,上例结果你拿给其他任何人看,以那个视角人人都不会觉得那是一个立体的金字塔,为了解决这个问题,我们需要将它转动一下。然后在后边的学习中,我们要模拟一个日地月旋转系统。先记下三条规则:
◎ 形状在p5内被rotate()函数转动。
rotate()函数使用一个引数,以弧度来衡量的角度
rotate()函数以顺时针方向转动形状。

14-5
14-5

19. 如果我们想要将一个矩形旋转45°,这么写:
rotate(radians(45));
rectMode(CENTER);
rect(width/2,height/2,100,100);

结果将是错误的。为什么?这里我们要记住一个非常重要的特性,p5内的旋转是——形状永远围绕原点旋转。上例内的原点在哪里?左上角!原点没有经过位移,因此矩形并不围绕它的中心旋转,而是围绕左上角旋转。

20. 当然,也许有一天你会做那种需要将图形围绕左上角旋转的项目,但是在那一天到来之前,你将始终需要在旋转前将原点移到合适的位置,然后再显示矩形。是的,
translate()将在此刻拯救你。
translate(width/2,height/2);
rotate(radians(45));
rectMode(CENTER);
rect(0,0,100,100);
哦!是的!我们想做的正是这样!
我们还可以将上述代码扩展一下,用mouseX(当然也可以是mouseY)的值来计算一个旋转的角度出来。
void setup() {
size(200,200);
}

void draw() {
background(255);
stroke(0);
fill(175);

// 将原点位移到窗口中心
translate(width/2,height/2);

// 基于鼠标X轴的位置和窗口宽度,角的范围从0到PI
float theta = PI*mouseX / width;

// 依角theta的值旋转
rotate(theta);

// 用CENTER模式显示矩形
rectMode(CENTER);
rect(0,0,100,100);
}

以直线中心为中心顺时针旋转
以直线中心为中心顺时针旋转

21. Daniel出题,画右边这样的图,让图形一起围绕直线中心旋转,恩,终于顺利做出来了。。最大体会还是:要深刻领会translate()精神啊。
float theta = 0.0;

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

void draw() {
background(255);
theta += 0.01;

// 将原点位移到中心
translate(width/2,height/2);

rotate(theta);

drawLine(80);
drawCircle(80);

// 基于第一个圆的位置继续位移第二个圆
translate (160, 0);
drawCircle(80);
}

void drawLine (int t) {
stroke(0);
fill(175);

beginShape (LINES);
vertex (-t, 0);
vertex (t, 0);
endShape();
}

void drawCircle (int t) {
stroke(0);
fill(175);

ellipse(-t, 0, 10, 10);
}

22. 以上二维旋转属于围绕Z轴旋转,另外为围绕X和Y轴旋转的情况,p5还提供给我们rotateX()rotateY()两个函数支持,然后函数rotateZ()同样存在并等同于rotate()
绕Z轴旋转
绕X轴旋转
绕Y轴旋转

23. 是的,我们当然可以将各种旋转结合运用:
void setup() {
size(200,200,P3D);
}

void draw() {
background(255);
stroke(0);
fill(175);
translate(width/2,height/2);
rotateX(PI*mouseY/height);
rotateY(PI*mouseX/width);
rectMode(CENTER);
rect(0,0,100,100);
}

24. OK,现在回到金字塔的例子,一起见证3D的效果吧。本例还包含另一个基于大金字塔偏移的小金字塔。注意,它仍然和大金字塔围绕同一个原点旋转(因为rotateX()rotateY()先于第二个translate()被呼叫,哦哦哦,原来p5是这么个由上而下的代码执行顺序呀)
float theta = 0.0;

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

void draw() {
background(255);
theta += 0.01;

translate(100,100,0);
rotateX(theta);
rotateY(theta);
drawPyramid(50);

// translate the scene again
translate(50,50,20);
// call the pyramid drawing function
drawPyramid(10);
}

void drawPyramid(int t) {
stroke(0);

// this pyramid has 4 sides, each drawn as a separate triangle
// each side has 3 vertices, making up a triangle shape
// the parameter ” t ” determines the size of the pyramid
beginShape(TRIANGLES);

fill(150,0,0,127);
vertex(-t,-t,-t);
vertex( t,-t,-t);
vertex( 0, 0, t);

fill(0,150,0,127);
vertex( t,-t,-t);
vertex( t, t,-t);
vertex( 0, 0, t);

fill(0,0,150,127);
vertex( t, t,-t);
vertex(-t, t,-t);
vertex( 0, 0, t);

fill(150,0,150,127);
vertex(-t, t,-t);
vertex(-t,-t,-t);
vertex( 0, 0, t);

endShape();
}

25. 这本书的网站下有个留言,基于这个例子做出了很特殊的效果,见这个页面的评论部分。

26. 在 translate()和rotate()之外,还有一个函数叫做scale() ,它用于放大或缩小萤幕上对象的大小。和rotate()一样,比例效果也是基于原位置。引数:1.0即100%。例如:scale(0.5) 以原尺寸一半的大小绘制对象;scale(3.0)将对像大小增加到300%。下例展示了一个用scale()实现的矩形增大实验:
float r = 0.0;
void setup() {
size(200,200);
}
void draw() {
background(0);
// 移位到窗口正中
translate(width/2,height/2);
// 基于r重比例形状大小
scale(r);

stroke(255);
fill(100);
rectMode(CENTER);
rect(0,0,10,10);

r += 0.02;
}
注意上例中,scale()在使矩形变大的同时使得边框伴随变粗。scale()同样支持二个或三个引数(分别代表x,y,z)。

27. 矩阵(Matrix)动态保存当前状态并随时可供以后使用。这最终将使得我们在不影响他物的情况下自由移动或旋转我们的形状。你可以呼叫printMatrix()查看任意时刻的矩阵。

28. 一个最好的证明这个概念的例子,创造两个以不同速度和方向围绕各自中心旋转的矩形。在进行中,我们将发现错误,然后引入pushMatrix()和popMatrix()函数的介绍。先在左上角画一个绕Y轴转的矩形,再在右下角画一个 绕Y轴转的矩形,是的,独处的时候,二者都没有问题,但是一旦放到一块:
float theta1 = 0;
float theta2 = 0;

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

void draw() {
background(255);
stroke(0);
fill(175);
rectMode(CENTER);

translate(50,50);
rotateZ(theta1);
rect(0,0,60,60);

theta1 + = 0.02;

translate(100,100);
rotateY(theta2);
rect(0,0,60,60);

theta2 + = 0.02;
}
试运行一下,问题出现,右下角的矩形也开始围绕左上角的矩形旋转(而不是独自在右下角绕Y轴旋转)。要记住,所有位移和旋装都是基于前一个坐标系统的状态。我们需要一个能够还原之前状态的矩阵系统以使得各个形状可以独立运动。

29. 保存和还原旋转/位移状态可以被pushMatrix()popMatrix() 完成。push=保存,pop=还原。

30. 为使各个矩形独立旋转,我们可以写下如下算法:
1)保存当前变形矩阵。这是我们的起点,从(0,0)并且没有旋转开始。
2)移动并旋转第一个矩形。
3)显示第一个矩形。
4)从第一步还原矩阵,使其不受第二、三步的影响。
5)移动并旋转第二个矩形。
6)显示第二个矩形。
用以上思路重写上例:
float theta1 = 0;
float theta2 = 0;
void setup() {
size(200,200,P3D);
}

void draw() {
background(255);
stroke(0);
fill(175);
rectMode(CENTER);
pushMatrix();

translate(50,50);
rotateZ(theta1);
rect(0,0,60,60);
popMatrix();

pushMatrix();
translate(150,150);
rotateY(theta2);
rect(0,0,60,60);
popMatrix();
theta1 += 0.02;
theta2 += 0.02;
}
尽管技术上并不需要,在第二个矩形前后加pushMatrix()popMatrix()是一个好习惯(比如如果我们需要添加更多的形状),同时也更容易将各个部分当作不同的个体来对待。实际上,这个例子真的应当以面向对象的方式来做,然后是的每个对象呼叫自己各自的pushMatrix(), translate(), rotate()和popMatrix()

31. 接下来我们如法炮制用数组做一堆旋转的矩形:
// Rotater对象数组
Rotater[] rotaters;

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

rotaters = new Rotater[20];

// 随机决定Rotaters各属性
for (int i = 0; i < rotaters.length; i++ ) { rotaters[i] = new Rotater(random(width),random(height),random(-0.1,0.1),random(48)); } } void draw() { background(255); // All Rotaters spin and are displayed for (int i = 0; i < rotaters.length; i++ ) { rotaters[i].spin(); rotaters[i].display(); } } // 一个Rotater类 class Rotater { float x,y; // x,y位置 float theta; // 旋转角度 float speed; // 旋转速度 float w; // 矩形大小 Rotater(float tempX, float tempY, float tempSpeed, float tempW) { x = tempX; y = tempY; // 角度总是初始化为0 theta = 0; speed = tempSpeed; w = tempW; } // 增大角度 void spin() { theta += speed; } // 显示矩形 void display() { rectMode(CENTER); stroke(0); fill(0,100); // pushMatrix()和popMatrix()在类的display()方法中被呼叫. // 这样,每个Rotater对象都将在各自的位移和旋转上被渲染出来! pushMatrix(); translate(x,y); rotate(theta); rect(0,0,w,w); popMatrix(); } }

32. 嵌套使用pushMatrix()和popMatrix()会产生有趣的效果。在计算机科学领域push(推)和pop(送)常被理解为“堆叠(stack)”。stack是什么呢?设想一位老师在夜晚阅卷,将卷子整理为一叠,那么她放在桌面上的第一张试卷将被她最后阅读,而最后一张放上的试卷将被她最先阅读。在对列里这恰恰是反过来的。不像排队买电影票,排在第一位的总是第一个买到票,而排在最后的总是最后买到票。push意味着将一些东西放入堆叠中的过程,pop取出一些东西。因此,在程序中,pushMatrix()popMatrix()的数量永远是想等的。以旋转的矩形为基础,我们可以看到嵌套pushMatrix()popMatrix()是如何的有用,下例为一个行星系统的模拟:
// 围绕太阳及行星旋转的角度
float theta = 0;

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

void draw() {
background(255);
stroke(0);

// 位移到窗口正中绘制太阳
translate(width/2,height/2);
fill(255,200,50);
ellipse(0,0,20,20);

// 地球围绕太阳旋转
pushMatrix();
rotate(theta);
translate(50,0);
fill(50,200,255);
ellipse(0,0,10,10);

// 月亮#1围绕地球传
// 在绘制月亮#1前呼叫以保持其位移状态
//
这样,我们便能在绘制月亮#2前pop并返回地球。
// 所有月亮都围绕地球旋转 (地球自身同时围绕太阳转).
pushMatrix();
rotate(-theta*4); // 角度为负则逆时针旋转
translate(15,0);
fill(50,255,200);
ellipse(0,0,6,6);
popMatrix();

// 月亮#2同样围绕地球转
pushMatrix();
rotate(theta*2);
translate(25,0);
fill(50,255,200);
ellipse(0,0,6,6);
popMatrix();

popMatrix();

theta += 0.01;
}
我试了一下,将最后一个popMatrix();移到地球之后运行,则所有球都围绕着太阳旋转,这给我们一个启示,在嵌套pushMatrix()popMatrix()中,无论有多少组pushMatrix()和popMatrix(),它们的translate()都是基于首先出现在pushMatrix()popMatrix()里边的那个对象的位置。你可以基于这一点,改写代码,使得在其他旋转都不改变的情况下月亮#1(将其改为其他颜色并将旋转角度加大将能更好的看到效果)围绕月亮#2旋转。是的,又是一个容易让人头晕混淆的概念,但是你必须弄清楚这些基础性的东西。

令人晕眩的动态特效
令人晕眩的动态特效

33. pushMatrix()popMatrix()同样可以在for和while循环中被嵌套,制造牛逼的特效,见下例:
float theta = 0;

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

void draw() {
background(255);
stroke(0);

translate(width/2,height/2);

// 由0度循环到360度 (2*PI弧度)
for(float i = 0; i < TWO_PI; i += 0.2) { // Push, 旋转并画一条线! // 变形状态通过for循环在每个循环开始之初被保存背在结尾被还原。 // 试着将所有pushMatrix()和popMatrix()注释掉看不同! pushMatrix(); rotate(theta + i); line(0,0,100,0); // 由0度循环到360度 (2*PI弧度) for(float j = 0; j < TWO_PI; j += 0.5) { // Push, 旋转并画一条线! pushMatrix(); translate(100,0); rotate(-theta-j); line(0,0,50,0); // 如果我们完成了内部循环, 则pop掉! popMatrix(); } // 如果我们完成了外部循环, 则pop掉! popMatrix(); } theta += 0.01; }

34. Daniel出题:把你的无论是金字塔还是立方体做入一个类,让每个对象都呼叫它们各自的pushMatrix()popMatrix()。你能做一个在3D里各自独立旋转的对象的数组吗?

35. 下边我们把本课学到的所有知识汇总,做一个微缩太阳系,是上边地月系的升级版,并有两大改变:
◎ 每个行星都是一个对象,是Planet类的一员
◎ 一个行星数组将围绕太阳旋转
// 一个由8个行星对象组成的数组
Planet[] planets = new Planet[8];

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

// 行星对象用计数器变量初始化
for (int i = 0; i < planets.length; i++ ) { planets[i] = new Planet(20 + i*10,i + 8); } } void draw() { background(255); // 绘制太阳 pushMatrix(); translate(width/2,height/2); stroke(0); fill(255); ellipse(0,0,20,20); // 绘制所有行星 for (int i = 0; i < planets.length; i++ ) { planets[i].update(); planets[i].display(); } popMatrix(); } class Planet { // 每个行星对象都持续记录其旋转角度. float theta; // 绕日旋转 float diameter; // 行星大小 float distance; // 距太阳距离 float orbitspeed; // 旋转速度 Planet(float distance_, float diameter_) { distance = distance_; diameter = diameter_; theta = 0; orbitspeed = random(0.01,0.03); } void update() { // 加快旋转角度 theta += orbitspeed; } void display() { // 在旋转和位移前, 矩阵的状态被pushMatrix()保存下来. pushMatrix(); // 旋转 rotate(theta); // 位移distance translate(distance,0); stroke(0); fill(175); ellipse(0,0,diameter,diameter); // 行星一旦被绘制, 矩阵被popMatrix()还原,因此其他行星不会受到影响. popMatrix(); } }

36. Daniel出题:为以上行星分别制作一颗卫星,答案见

37. Daniel出题:用sphere()box()替代ellipse(),从而使得太阳系进入三维空间。哦,他开始让我们学着去看p5的官方reference了。

38. 越来越绕了。怎么办 🙁

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

Minim:可播放的(Playable)

[ javadoc | 在线实例 ]

很简单,就是CD唱机的功能。

播放与暂停

手册原文说了一大堆,基本都是废话(其实我早发现这哥们废话多了),播放与暂停是什么玩意,没有不懂的吧?

示范代码(在线实例

import ddf.minim.*;

Minim minim;
AudioPlayer groove;
WaveformRenderer waveform;

void setup()
{
  size(512, 200, P3D);

  minim = new Minim(this);
  groove = minim.loadFile("groove.mp3", 2048);

  waveform = new WaveformRenderer();
  // see the example Recordable >> addListener for more about this
  groove.addListener(waveform);

  textFont(createFont("Arial", 12));
  textMode(SCREEN);
}

void draw()
{
  background(0);
  // see waveform.pde for an explanation of how this works
  waveform.draw();

  if ( groove.isPlaying() )
  {
    text("The player is playing.", 5, 15);
  }
  else
  {
    text("The player is not playing.", 5, 15);
  }
}

void keyPressed()
{
  if ( key == 'l' )
  {
    groove.loop(1);
  }
  if ( key == 'p' )
  {
    groove.play();
  }
  if ( key == 's' )
  {
    groove.pause();
  }
}

void stop()
{
  // always close Minim audio classes when you are done with them
  groove.close();
  // always stop Minim before exiting.
  minim.stop();

  super.stop();
}

重置

rewind()将使你的播放头回到声音最初,它不会改变playable的属性,即,如果你之前设为循环播放,则rewind之后仍然是循环播放。

import ddf.minim.*;

Minim minim;
AudioPlayer groove;
WaveformRenderer waveform;

void setup()
{
  size(512, 200, P3D);

  minim = new Minim(this);
  groove = minim.loadFile("groove.mp3", 2048);
  groove.loop();

  waveform = new WaveformRenderer();
  // see the example Recordable >> addListener for more about this
  groove.addListener(waveform);
}

void draw()
{
  background(0);
  // see waveform.pde for an explanation of how this works
  waveform.draw();
}

void keyPressed()
{
  if ( key == 'r' ) groove.rewind();
}

void stop()
{
  // always close Minim audio classes when you are done with them
  groove.close();
  // always stop Minim before exiting.
  minim.stop();

  super.stop();
}

循环

void loop()
void loop(int numLoops)

没引数的将无限循环,引数为循环次数,记得循环1次,则音频将被播放2次,循环2次,音频播放3次,以此类推。你可以使用isLooping()检查一个声音是否被设为了循环播放。你可以使用loopCount()侦测剩余循环的次数。如果你在声音循环次数之前呼叫了play()则剩余的循环命令将被取消,歌曲从头播放到尾然后停止。这在你想取消循环而不想中途切断声音时很有效。

import ddf.minim.*;

Minim minim;
AudioPlayer groove;
WaveformRenderer waveform;
int loopcount;

void setup()
{
  size(512, 200, P3D);

  minim = new Minim(this);
  groove = minim.loadFile("groove.mp3", 2048);

  waveform = new WaveformRenderer();
  // see the example Recordable >> addListener for more about this
  groove.addListener(waveform);

  textFont(createFont("Arial", 12));
  textMode(SCREEN);
}

void draw()
{
  background(0);
  // see waveform.pde for an explanation of how this works
  waveform.draw();

  text("The player has " + groove.loopCount() + " loops left. Is playing: " + groove.isPlaying() + ", Is looping: " + groove.isLooping(), 5, 15);
}

void keyPressed()
{
  String keystr = String.valueOf(key);
  int num = int(keystr);
  if ( num > 0 && num < 10 )
  {
    groove.loop(num);
    loopcount = num;
  }
}

void stop()
{
  // always close Minim audio classes when you are done with them
  groove.close();
  // always stop Minim before exiting.
  minim.stop();

  super.stop();
}

循环点

你可以呼叫setLoopPoints(int start, int stop)以循环Playable对象的一部分,start和stop是以毫秒表述的时间,这在AudioSnippet里使用是最好的,因为曲子已经实现读入了内存,而在AudioPlayer中,则会有可察觉的滞后,尤以mp3文件明显。

import ddf.minim.*;

Minim minim;
AudioSnippet snip;

int loopBegin;
int loopEnd;

void setup()
{
  size(512, 200, P3D);
  textMode(SCREEN);
  minim = new Minim(this);
  snip = minim.loadSnippet("groove.mp3");
  snip.loop();

  textFont(loadFont("ArialMT-14.vlw"));
}

void draw()
{
  background(0);
  fill(255);
  text("Loop Count: " + snip.loopCount(), 5, 20);
  text("Looping: " + snip.isLooping(), 5, 40);
  text("Playing: " + snip.isPlaying(), 5, 60);
  int p = snip.position();
  int l = snip.length();
  text("Position: " + p, 5, 80);
  text("Length: " + l, 5, 100);
  float x = map(p, 0, l, 0, width);
  stroke(255);
  line(x, height/2 - 50, x, height/2 + 50);
  float lbx = map(loopBegin, 0, snip.length(), 0, width);
  float lex = map(loopEnd, 0, snip.length(), 0, width);
  stroke(0, 255, 0);
  line(lbx, 0, lbx, height);
  stroke(255, 0, 0);
  line(lex, 0, lex, height);
}

void mousePressed()
{
  int ms = (int)map(mouseX, 0, width, 0, snip.length());
  if ( mouseButton == RIGHT )
  {
    snip.setLoopPoints(loopBegin, ms);
    loopEnd = ms;
  }
  else
  {
    snip.setLoopPoints(ms, loopEnd);
    loopBegin = ms;
  }
}

void stop()
{
  // always close Minim audio classes when you are done with them
  snip.close();
  minim.stop();

  super.stop();
}

Cueing(抱歉我实在不知道这词儿怎么翻)

import ddf.minim.*;

Minim minim;
AudioPlayer groove;
WaveformRenderer waveform;

void setup()
{
  size(512, 200, P3D);

  minim = new Minim(this);
  groove = minim.loadFile("groove.mp3", 2048);
  groove.loop();

  waveform = new WaveformRenderer();
  // see the example Recordable >> addListener for more about this
  groove.addListener(waveform);
}

void draw()
{
  background(0);
  // see waveform.pde for an explanation of how this works
  waveform.draw();
}

void keyPressed()
{
  if ( key == 'c' )
  {
    groove.cue(5000);
  }
}

void stop()
{
  // always close Minim audio classes when you are done with them
  groove.close();
  // always stop Minim before exiting.
  minim.stop();

  super.stop();
}

快退快进

skip(100):快进100毫秒,skip(-200):快退200毫秒。如果文件没有处于播放状态,则快进快退不会触发播放。如果快进快退出现错误,播放头将不会改变位置。如果快退位置小于零或快进位置大于歌曲长度,则播放头将位于文件头或尾。

import ddf.minim.*;

Minim minim;
AudioPlayer groove;
WaveformRenderer waveform;

void setup()
{
  size(512, 200, P3D);

  minim = new Minim(this);
  groove = minim.loadFile("groove.mp3", 2048);
  groove.loop();

  waveform = new WaveformRenderer();
  // see the example Recordable >> addListener for more about this
  groove.addListener(waveform);
}

void draw()
{
  background(0);
  // see waveform.pde for an explanation of how this works
  waveform.draw();
}

void keyPressed()
{
  if ( key == 'f' )
  {
    groove.skip(100);
  }
  if ( key == 'r' )
  {
    groove.skip(-500);
  }
}

void stop()
{
  // always close Minim audio classes when you are done with them
  groove.close();
  // always stop Minim before exiting.
  minim.stop();

  super.stop();
}

位置和长度

方法position以毫秒返回当前播放头的位置。方法length以毫秒返回音频文件的长度。下例结合了这两种方法将播放头位置视觉化:

import ddf.minim.*;

Minim minim;
AudioPlayer groove;
WaveformRenderer waveform;

void setup()
{
  size(512, 200, P3D);

  minim = new Minim(this);
  groove = minim.loadFile("groove.mp3", 2048);

  waveform = new WaveformRenderer();
  // see the example Recordable >> addListener for more about this
  groove.addListener(waveform);
  groove.loop();
}

void draw()
{
  background(0);
  // see waveform.pde for an explanation of how this works
  waveform.draw();

  float x = map(groove.position(), 0, groove.length(), 0, width);
  stroke(255, 0, 0);
  line(x, height/2 - 30, x, height/2 + 30);
}

void stop()
{
  // always close Minim audio classes when you are done with them
  groove.close();
  // always stop Minim before exiting.
  minim.stop();

  super.stop();
}

音频Meta数据

你可以通过呼叫getMetaData()由一个音频文件得到一个AudioMetaData对象。随即,你便可以呼叫以下方法:

String album()
String author()
String comment()
String composer()
String copyright()
String date()
String disc()
String encoded()
String fileName()
String genre()
int length()
String orchestra()
String publisher()
String title()
int track()

如果没有信息可用,以上方法将返回一个空的列或-1,基于返回类型。

import ddf.minim.*;

Minim minim;
AudioPlayer groove;
AudioMetaData meta;

void setup()
{
  size(512, 256, P3D);

  minim = new Minim(this);
  groove = minim.loadFile("groove.mp3");
  meta = groove.getMetaData();

  textFont( loadFont("serif.vlw") );
  textMode(SCREEN);
}

int ys = 15;
int yi = 15;

void draw()
{
  background(0);
  int y = ys;
  text("File Name: " + meta.fileName(), 5, y);
  text("Length (in milliseconds): " + meta.length(), 5, y+=yi);
  text("Title: " + meta.title(), 5, y+=yi);
  text("Author: " + meta.author(), 5, y+=yi);
  text("Album: " + meta.album(), 5, y+=yi);
  text("Date: " + meta.date(), 5, y+=yi);
  text("Comment: " + meta.comment(), 5, y+=yi);
  text("Track: " + meta.track(), 5, y+=yi);
  text("Genre: " + meta.genre(), 5, y+=yi);
  text("Copyright: " + meta.copyright(), 5, y+=yi);
  text("Disc: " + meta.disc(), 5, y+=yi);
  text("Composer: " + meta.composer(), 5, y+=yi);
  text("Orchestra: " + meta.orchestra(), 5, y+=yi);
  text("Publisher: " + meta.publisher(), 5, y+=yi);
  text("Encoded: " + meta.encoded(), 5, y+=yi);
}

void stop()
{
  // always close Minim audio classes when you are done with them
  groove.close();
  // always stop Minim before exiting
  minim.stop();

  super.stop();
}

Processing:数学(上)

1. “如果你不相信数学有多简单,是你还没有认识到生活有多复杂——John von Neumann”

2. 模数(Modulus):20 模(modulo,也就是除以,用百分号表示) 6 等于 2 或者写为 20 % 6 = 2。如果A = B % C,那么A永远不可能大于C(当然。。)。因此,模 可以用在你需要将一个计数器变量循环至0的情况。下列代码:
x = x + 1;
if (x >= limit){
x = 0;
}

可以被替换为:
x = (x + 1) % limit;
这在你想要每次一个的顺数数组中的元素时很有用,当你达到数组长度的时候总是返回0。一个例子:
// 4个随机数字
float[] randoms = new float[4];
int index = 0; // 我们正在使用的数字

void setup() {
size(200,200);
// 用随机值填充数组
for (int i = 0; i < randoms.length; i ++ ) {
randoms[i] = random(0,256);
}
frameRate(1);
}

void draw() {
// 我们每帧调用数组的一个元素
background(randoms[index]);
// 然后继续下一个
index = (index + 1) % randoms.length; // 使用模运算符(modulo operator)将计数器循环到0。
}

随机柱状图
随机柱状图

3. random()函数产出的随机值称为“统一(uniform)”分部,举例说明:如果我们要一个在0~9间的随机值,数字0~9出现的机率最高为10%。下例可证明此点:
// 一个记录随机数摘取频率的数组
float[] randomCounts;

void setup() {
size(200,200);
randomCounts = new float[20];
}

void draw() {
background(255);

// 选取一个随机数并增加计数
int index = int(random(randomCounts.length));
randomCounts[index] ++ ;

// 绘制直方图
stroke(0);
fill(175);
for (int x = 0; x < randomCounts.length; x ++ ) {
rect(x*10,0,9,randomCounts[x]);
}
}

通过一些小把戏,我们可以用random()产出非统一性分部的随机值,并且产出使特定事件发生的可能性。比如说,使一个sketch的背景色变黄的机率为10%而变蓝的机率为90%。

4. 可能性是什么?可能性就是某件事发生的可能性。比如投掷硬币,结果是正面与反面朝上的可能性各为50%。出现三次正面朝上的可能性为:
(1/2) * (1/2) * (1/2) = 1/8 (或者 0.125)
换句话说,每8次投掷硬币,正面朝上的情况可能会有3次。

5. Daniel出题,在一副扑克牌内抽中两张A的可能性是多少?答案为:0.00452488688。怎么算出来的?
一副扑克牌有52张牌,其中有4张A,因此抽中第一张A的可能性为:
4 / 52 = 0.0769230769
抽中第二张A的可能性为:
3(剩下的A) / 51(剩下的扑克牌) = 0.0588235294
因此抽中两张A的可能性为:
(4/52)*(3/51) = 0.00452488688

6. 我们可以用random()玩些小把戏:
int[] stuff = new int[5];

stuff[0] = 1;
stuff[1] = 1;
stuff[2] = 2;
stuff[3] = 3;
stuff[4] = 3;
int index = int(random(stuff.length));
if (stuff[index] == 1){
// 做爱做的事
}

如果运行上边这段代码,我们得到1和3的机率都将为40%,而得到2的机率则为20%。

7. 另一个策略是,要一个随机值,并且在我们指定的范围内才使用它:
float prob = 0.10; // 可能性为10%
float r = random(l); // 0~1间的随机浮点值

if (r < prob) { // 如果我们的随机值小于0.1
/*在此让事件发生*/
}

8. 同样的技术同样可用于多个输出:
Outcome A — 60% | Outcome B — 10% | Outcome C — 30%。
用代码实现这个,我们这么搞:
• 介于0.00~0.60间 (10%) → 输出 A.
• 介于0.60~0.70间 (60%) → 输出 B.
• 介于0.70~1.00间 (30%) → 输出 C .

三色可能性
三色可能性

接下来看例子:
void setup() {
size(200,200);
background(255);
smooth();
noStroke();
}

void draw() {

// 3种不同情况的可能性
// 它们加起来应当为100%!
float red_prob = 0.60; // 60%的机会红色
float green_prob = 0.10; // 10%的机会绿色
float blue_prob = 0.30; // 30%的机会蓝色

// 选取一个0~1间的随机值
float num = random(1);

// 如果随机值小于0.6
if (num < red_prob) {
fill(255,53,2,150);
// 如果随机数在0.6~0.7之间
} else if (num < green_prob + red_prob) {
fill(156,255,28,150);
// 所有其他的情况 (比如介于0.7~1.0)
} else {
fill(10,52,178,150);
}

// 绘制圆形
ellipse(random(width),random(height),64,64);
}

9. Perlin噪声,唧唧歪歪说了一堆,我只关注它可用于生成一系列有趣的效果,包括云、山水、大理石纹理等等。下图为Perlin噪声图(x轴代表时间;注意曲线有多么光滑)与纯随机数图的对比:

Perlin噪音  VS. 随机(random)
Perlin噪音 VS. 随机(random)

10. 如果你参考P5官网关于噪音(noise)的参考,你会发现噪音是基于若干“八度(octave)”计算出来的,你可以呼叫noiseDetail()改变八度的数量以及它们相关的重要性。这将改变噪音函数的行为。你可以在这里阅读更多Ken Perlin与噪音协作的东西。

11. 你可以使用函数 noise( )在P5内调用Perlin噪音算法。引数为三个,分别代表一、二、三维,这里只讲一维,剩下两维请参阅P5官网。一维的Perlin噪音反复制造一个值的线性序列。例如:0.364, 0.363, 0.363, 0.364, 0.365

12. Perlin噪音在P5中的使用:a. 呼叫函数noise();b. 传递当前的“时间”为其引数:
float t = 0.0;
float noisevalue = noise(t); // 从时间0开始噪音

我们可以在draw()内使其循环:
float t = 0.0;
void draw() {
float noisevalue = noise(t);
println(noisevalue);
}

但你也许会发现,输出的总是同样的值,那是因为时间没有变化,如果我们让时间增加的话:
float t = 0.0;
void draw() {
float noisevalue = noise(t);
println(noisevalue);
t + = 0.01;
}

输出的值将开始平滑变化。时间变化的快慢决定了噪声曲线的平滑度,越快则越不平滑。

13. 你也许会发现,noise()返回的值总是介于0.0~1.0之间的浮点值,这是不可变的,但如果我们需要更大的值,那么用乘法即可,这个例子阐述了这一关点,请注意球的缩放多么舒服和平滑。

Processing:库

1. 在计算机科学领域,一个库指的是一个“帮助者”代码的集合。一个库可能包含函数、变量和对象。

2. 在绝大多数汇编语言中,你都会被要求在代码之初便指定你要用到的库。

3. 这么着导入:import processing.core.*;
“ Import ”表明我们将使用一个库,准备使用的库是 “processing.core.”。 “ .* ”是通配符,意味着我们将使用库里的所有东西。以后还会更多的接触这类东西,现在,我们仅需要知道“ processing.core ”是库的名字。

4. 你可以到这里下载pr的众多第三方库。

5. 通过Sketch -> Import Library导入你想使用的库。

6. 安装:将下载到的库文件夹放入你的sketchbook文件夹(在偏好里设置)下的libraries文件夹(如果不存在,则自己创建一个)内。重启Processing,安装成功并可以开始使用。

Processing:除错 调试

1. 小窍门#1:休息一下
我没有开玩笑。离开你的电脑。睡一觉。出去慢跑一圈。吃个橘子。玩一下拼字游戏。总之,做一些除了日代码之外的事。我无法告诉你究竟有多少次我盯着代码数小时而无法搞定那些bug,然后第二天一早醒来五分钟就解决了战斗。

2. 小窍门#2:让他人介入
把你的问题讲给你的朋友,将你展示代码的流程讲给一个程序员(甚至是非程序员)并且顺着过一遍它的逻辑,这往往能揭示问题所在。在很多情况下,因为你是如此熟悉你的代码,因此便看不到很多很明显的东西。通过你将其阐述给第二人的过程,会促使你更慢的将你的代码重新过一遍。如果你身边暂时没有朋友,你同样可以将这个流程大声的告诉你自己。是的,看起来像傻逼,但却很有帮助的。

3. 小窍门#3:简化
在上一章(算法),我们学会了分部分块的完成程序的方法,分得越细,最后出错的机率便越小。这么做的方法之一,是将程序中的某些部分“评论(comment)”掉。

4. 小窍门#4: println( )是你的朋友
运用信息窗口显示变量的值可以说事非常有用。如果一个对象完全从萤幕消失了并且你想知道为什么,你可以打印出它位置变量的值。它看起来可能像这样:
println( “x:” + thing.x + “y:” + thing.y);
如果结果是:
x: 9000000 y: – 900000
x: 9000116 y: – 901843
x: 9000184 y: – 902235
x: 9000299 y: – 903720
x: 9000682 y: – 904903

那么很显然,这些值并非合理的像素坐标,因此该对象在计算其位置的时候也许出错了。或者alpha值为0的时候,你当然也看不到该对象。

另外,printIn()还可以用来侦测你的部分代码是否已被传到。比如,之前那个滚动小球的例子,如果球到了右手边但却不滚回来,这是什么情况呢?问题可能是a. 你没有正确定义它怎样才算碰边,或者b. 在它碰边后你没有做对。要知道是哪种情况,你可以写:
if (x > width) {
println( ” X is greater than width. This code is happening now! ” );
xspeed *= –1;
}

如果你运行程序却没有看到这则消息,则你的布尔表达式可能存在瑕疵。