跳到主要内容

第七章 循环

重复是计算机所擅长的事情之一。想象一下使用不同的参数在屏幕上画一千个图形。以当前的编程知识这将耗费我们大量的时间。对于这种以相同或带有变动的方式重复代码的情况,我们可以使用称之为循环的编程结构。循环让我们可以对一个代码块反复的执行。

我们已有熟悉了p5.js中的循环概念。想一下,draw函数就是一个反复执行的循环,直到p5.js程序退出为止。在本章中,我们将学习如何创建我们自己的这类循环。

for 循环

JavaScript中有几种不同的循环结构,但for循环是其中最为常用的。它让我们能以指定次数对运算进行重复。for循环有四个部分组成,示例7-1演示了如何构建一个for循环。

示例7-1. for循环的示例

for (var i = 0; i < 10; i = i + 1) {
//do something
}

在第一部分中,我们初始化了一个变量,用于记录循环执行的次数,我们称之为记数变量。

var i = 0;

按照惯例,循环中将使用简短的变量名,如i或j,尤其是在变量仅用于控制循环流程时。但请随意使用其它变量,只要在你的用例中有具体含义。

第二部分中,我们定义每次循环开始时运行的测试条件。本例中,我们给检查计算变量是否小于数字10.

i < 10;

第三部分中,我们定义一种在循环结束时运行的更新计算变量的方式。本例中,我们获取当前变量i的值并为值加1。

i = i + 1;

最后在大括号内我们编写需重复的代码。一旦计算变量无法满足测试条件时,循环终止,程序返回正常运行。

如果一直能满足测试条件,就会形成无限循环,除非通过外部方式终止程序,否则程序会一直执行、永不退出。p5.js中的 draw函数就是一个无限循环,直到关闭浏览器窗口会一直在屏幕上绘画。

虽然无限循环是一个有效用例,循环更常用的是执行有限次数的运算。我们来使用for循环来在屏幕上画给定数量的圆形(示例7-2和图7-1)。

示例7-2. 使用for循环画圆

function setup() {
createCanvas(800, 300);
}

function draw() {
background(1, 75, 100);

// 圆形属性
fill(237, 34, 93);
noStroke();

for(var i=0; i<10; i=i+1){
ellipse(0, 0, 50, 50);
}
}

图7-1. 示例7-2的输出

图7-1. 示例7-2的输出

本例中,我们在屏幕上画了10个圆形,但通过眼睛是看不出来的,因为每个圆都会在另一个圆之上。这里就使用了循环计数器变量。我们可以在每次循环调用时对变量添加偏移量(示例7-3和图7-2)。

示例7-3. 在for循环中使用循环计数器

function setup() {
createCanvas(800, 300);
}

function draw() {
background(1, 75, 100);

// 圆形属性
fill(237, 34, 93);
noStroke();

for(var i=0; i<10; i=i+1){
ellipse(i*50, 0, 50, 50);
}
}

图7-2. 示例7-3的输出

图7-2. 示例7-3的输出

我们在喂入ellipse函数前对循环变量乘上了10(圆的直径)。这样我们画的形状就不会重叠。

现在执行代码,就可以看到for循环为我们创建的所有圆了。我们这里所创建的重复运算结构的好处在于,我们可以通过修改循环内的条件值为更大来扩大循环规模。把渲染圆的数量从10变成100或1000仅仅需要修改一个值就可以了。但是,如果使用的数很大时我们就会发现性能下降。

我们来调整代码让圆形等布满屏幕上的整排(示例7-4和图7-3)。

如果屏幕宽是800,圆的直径是50,这就表示页面上的一排可以放800 / 50 个圆形。我们会看到页面的最后会有一点空缺,这是因为第一个圆有部分出画面了。我们可以通过在 x 轴添加直径的一半25来进行偏移来去除这个空缺。如你所知,我们其实不需要自己进行计算,而让JavaScript来为我们计算值。

可以看到目前我们代码中很多值都是硬编码,使用变量来替换可增加灵活,下面我们来重构代码实现这一点。

示例7-4. 在屏幕中整排铺满圆形

function setup() {
createCanvas(800, 300);
}

function draw() {
background(1, 75, 100);

// 圆形属性
fill(237, 34, 93);
noStroke();
var diameter = 50;

for(var i=0; i<width/diameter; i=i+1){
ellipse(diameter/2 + i*diameter, 0, diameter, diameter);
}
}

图7-3. 示例7-4的输出

图7-3. 示例7-4的输出

现在只要修改一个值,圆的直径,整个代码都仍会在屏幕上画出足够多的圆。这对我们来说很酷。

假如我们要在屏幕纵向上也铺满屏幕呢?要这么做,我们需要再写一个for循环,将原本横向铺满屏幕的圆在纵向上也铺满。这要求我们在第一个循环内再加一个循环,有效的这一个循环中内嵌另一个循环。参见示例7-5和图7-4。

示例7-5. 圆形铺满全屏

function setup() {
createCanvas(800, 300);
}

function draw() {
background(1, 75, 100);

// 圆形属性
fill(237, 34, 93);
noStroke();
var diameter = 50;

for(var i=0; i<width/diameter; i=i+1){
for(var j=0; j<height/diameter; j=j+1){
ellipse(
diameter/2 + i*diameter,
diameter/2 + j*diameter,
diameter,
diameter);
}
}
}

图7-4. 示例7-5的输出

图7-4. 示例7-5的输出

注意本例中ellipse函数的声明方式。我们将代码写在多行上来增强可读性。JavaScript中空格不会影响执行,所以将代码写在多行上不会产生错误。

这段代码已经非常易用了。首先代码很健壮,我们可以修改绘图区大小或所绘制圆形的数量,但仍会正常运行。

有一点需要注意,在循环中再加循环根据执行的运算次数会让程序变得很慢。同时,内嵌的结构有时也会让程序变得不容易阅读。

random和noise函数

既然我们现在可以创建循环来在每次执行时使用不同值,或许是学习p5.js的random函数不错的时机。p5.js的random函数在每次调用时都会生成一个随机数。这在画图时想要使用随机参数值就非常有用了。

如果我们调用random函数时不传入参数,就会为每个draw函数调用或每帧生成0和1之间的随机值。如果在random函数中传入一个值,就会返回0以上传入值以下的随机值。如果在random函数中传入两个值,就会返回所传入两个值之间的随机值。参见示例7-6来了解这一情况。

示例7-6. 使用random函数的示例

console.log(random()); // 0和1之间的随机数
console.log(random(10)); // 0和10之间的随机数
console.log(random(100, 1000)); // 100和1000之间的随机数

示例7-7是一个以不同方式使用random函数的小脚本。图7-5显示了该脚本的运行结果。显示的数字是随机生成的,在每次代码执行时都会不同。

示例7-7. 使用random函数

function setup() {
createCanvas(800, 300);
textAlign(CENTER, CENTER);
fill(237, 34, 93);
frameRate(1);
}

function draw() {
var random_0 = random();
var random_1 = random(10);
var random_2 = random(100, 1000);
var offset = 40;

textSize(24);
background(255);
text(random_0, width/2, height/2-offset);
text(random_1, width/2, height/2-0);
text(random_2, width/2, height/2+offset);

}

图7-5. 示例7-7的输出

图7-5. 示例7-7的输出

通过示例7-8和图7-6,我们可以更新前面的代码(示例7-5)来使用random函数。

示例7-8. 使用random函数

function setup() {
createCanvas(800, 300);
}

function draw() {
background(1, 75, 100);

// 圆形属性
fill(237, 34, 93);
noStroke();
var diameter = 50;

for(var i=0; i< width/diameter; i=i+1){
for(var j=0; j<height/diameter; j=j+1){
ellipse(
diameter/2 + i * diameter,
diameter/2 + j * diameter,
diameter * random(), // 使用随机函数
diameter
);
}
}
}

图7-6. 示例7-8的输出

我们使用random函数生成的结果乘上圆形的宽,这一随机数是调用random所生成的0和1之间值。因为random函数可在任意帧上使用范围内的数,这样的动画看上去变动非常剧烈。如果我们想要随机值逐渐改变,这样看上去更自然,那么就应该使用noise函数了。

我们可以向noise函数喂入任意数值,这样它会返回0和1之间的半随机值。对于相同给定值它总会返回相同的输出。noise函数的好处大于,如果喂入noise函数的值在递增,输出值也会递增。这样通过获取的随机值过渡更加平滑。

要了解noise函数运作的概念,可以将其想像为拥有无限数量像波一样变化的随机值,传入noise函数的值可看作这些随机值的坐标。基本上我们只是在对已存在噪音进行取样。无论何时我们向noise函数传入相同的值,都会得到相同的半随机返回值。

我们装饰重写上述程序(示例7-8)来使用noise函数进行替换。我们向noise函数传入变量frameCount,因为这是在p5.js中获取有序数字很好的方式。但我们会将frameCount除以100以减慢值改变的速度,这样动画也会慢一些。参见示例7-9和图7-7。

示例7-9. 使用noise函数

function setup() {
createCanvas(800, 300);
}

function draw() {
background(1, 75, 100);

// 圆形属性
fill(237, 34, 93);
noStroke();
var diameter = 50;

for(var i=0; i< width/diameter; i=i+1){
for(var j=0; j<height/diameter; j=j+1){
ellipse(
diameter/2 + i * diameter,
diameter/2 + j * diameter,
diameter * noise(frameCount/100), // 使用noise函数
diameter * noise(frameCount/100) // 使用noise函数
);
}
}
}

图7-7. 示例7-9的输出

注意现在所有的形状都使用相同的动画。如何想要每个形状的噪音都不相同呢?现在我们的值在重复,因为noise函数在接收相同值时,返回的输出也相同。要使每个形状获取不同的输出值,我们需要使用for 循环的 i 和 j 值来重写以上函数,来调整噪音的取样源。参见示例7-10和图7-8。

示例7-10. 对每个圆应用不同的动画

function setup() {
createCanvas(800, 300);
}

function draw() {
background(1, 75, 100);

// 圆形属性
fill(237, 34, 93);
noStroke();
var diameter = 50;

for(var i=0; i< width/diameter; i=i+1){
for(var j=0; j<height/diameter; j=j+1){
ellipse(
diameter/2 + i * diameter,
diameter/2 + j * diameter,
diameter * noise(frameCount/100 + j*10000 + i*10000), // 对每个圆应用不同的动画
diameter * noise(frameCount/100 + j*10000 + i*10000)
);
}
}
}

图7-8. 示例7-10的输出

以上我们使用的乘数10000完全是随机选取的。我们目的是传入noise函数的坐标值相距足够远。

总结

循环是编程中最强大的结构之一。这让我们进入计算机的真实计算能力,大规模的重复运算大有限时间内对人类是不可能完成的任务。

本章中我们学习了如何创建for循环并通过内嵌循环来按行重复形状来替代之前的单行形状。

我们还学习了p5.js的random和noise函数,以及它们之间的区别。

练习

创建一个循环来创建一个颜色由黑到白逐渐变化的一条列矩形(图7-9)。应通过单个变量来控制所绘制矩形数量。

图7-9. 练习图像

图7-9. 练习图像

译者补充,仅供参考:

function setup() {
createCanvas(800, 300);
}

function draw() {
background(255);
num = 10; // 绘制矩形数量

noStroke();

for(i=0; i<num; i=i+1){
// fill(0, 0, 0, noise(i*10000)*255); // 实现随机颜色变化
fill(0, 0, 0, i/num*255); //i为 num-i-1可实现与原书相同顺序的矩形
rect(i*width/num, 0, width/num, height);
}
}