Processing开发实战
上QQ阅读APP看书,第一时间看更新

第2章 语言基础

2.1 变量

变量是储存指定数据类型的空间。在一个程序中,同一变量可以多次引用,变量储存的值也可以更改。你可以把变量理解为存储某种类型东西的箱子,可以把东西放进去,也可以在用的时候取出来。变量的名字就是箱子的标签,这样在取出里面东西的时候会更加方便。使用变量的时候要提前声明。

int variable;      //前面是数据类型,后面是变量的名字

2.1.1 基础变量类型

不同的数据类型就好像不同大小的箱子,可以放不同的数据。数据类型有基础数据类型和复杂数据类型,如表2-1所示。有些数据类型内存空间大小一样,但是它们的取值范围不一样,这说明类型决定了数据在内存空间中的表示方式。计算机中的数据都是以二进制位来储存的,一个位也就是0或1。boolean类型的数据只有一位,也就是占用一个二进制位来储存这种数据。

表2-1 数据类型

2.1.2 变量名字

变量是储存数据的箱子,变量名字是箱子的标签。为了更方便地提取数据,变量的命名有一些规则。

❑变量名字在同一作用域下是独一无二的。

❑变量名字的第一个符号是字母或下划线。

❑变量名字区分大小写,不一样的大小写是不同的变量。

❑构成变量名字的只能是字母、数字、下划线。

❑变量名字最好有实际意义,能表达所储存的内容。

2.1.3 变量赋值

把数据存入变量这个箱子叫作变量赋值。通过运算符“=”来把数据存入变量。有以下两种方式来进行变量赋值。

                                                                            2-1
int variable = 2;
                                                                            2-2
int variable;
variable = 3;

Processing内置了打印函数,可以用print()或println()函数把变量的数值在控制台输出。

示例:

                                                                            2-3
int variable;
variable = 3;
println(variable);        //输出:3

执行上面的例子可以在文本框下的控制台上看到输出结果为3。

2.1.4 系统变量

有一些变量是系统内置的,不需要声明。这些变量不允许赋值,这些变量的值可以让编程人员获得系统在运行时的一些参数。比如,mouseX、mouseY的值是鼠标在画板上停留的坐标。利用好系统变量可以更方便地设计交互效果。表2-2列出了部分Processing系统变量。

表2-2 系统变量举例

示例:

下面例子打印系统变量mouseX、mouseY,并显示在画板上。

                                                                            2-4
void setup()
{
        size(900,600);
        textSize(26);
}
void draw()
{
        background(0);
        text("x:"+mouseX,100,200);
        text("y:"+mouseY,300,200);
}

还有一些关键字,比如final,可以让变量变成常量,当后续代码再修改该变量的时候就会报错。通常用于设置不能修改的常量值。

示例:使用final关键字来声明变量。

                                                                            2-5
final int a = 2;
a = 3;     //出错

2.2 运算符

运算符可以对变量进行基本的运算。运算符包括基本运算符、自增自减运算符、关系运算符逻辑运算符和条件运算符。

2.2.1 基本运算符

表2-3所示是Processing的基本运算符。

表2-3 运算符

加减乘除是常用的基本运算,求余是两个数相除所得的余数。一般都是同类型的数据通过运算符进行运算得出结果。比如两个int类型的1相加得出2。

示例:

                                                                            2-6
int a = 1;
int b = 1;
int c = a+b;
println(c);        //输出:2

同一数据类型的相加得到相同数据类型的结果。还有一些特例是不同数据类型的数相加,如float类型和int类型的数相加,得到的是float数据类型的结果。后面章节会介绍运算符的其他作用,例如:在字符串中可以连接组合字符串。

2.2.2 自增自减运算符

自增“++”、自减“--”运算是非常方便的操作,可以把变量的值自动增加1或者减少1。

示例:

                                                                            2-7
int i = 5;
i++;
println(i);        //输出:6

此处i++等同于i+=1或i=i+1。变量赋值的顺序非常重要,i++和++i在参与运算的时候,前者i是先参与运算再自增,后者是先自增再参与运算,请读者仔细区分两者的差别。

示例:

                                                                            2-8
int i = 1;
int a = 0;
a = i++;
println(i);        //输出:2
println(a);        //输出:1

自减运算同理。

2.2.3 关系运算符

关系运算符可以得到两个数据关系判断的真假,例如4>3是真(true),3>4是假(false)。为了区分等于和赋值,要注意等于用了两个等于号“==”,而赋值是一个等于号“=”。关系运算符如表2-4所示。

表2-4 关系运算符

基本数据类型的变量和变量进行判断时,检查值是否相等而不是数据类型是否相同。

示例:

                                                                            2-9
int a = 65;
float b = 65.0;
char c = ' A' ;
println(a == c);  //输出:true
println(a == c);  //输出:true
println(b == c);  //输出:true

2.2.4 逻辑运算符

逻辑运算符有与、或、非3种,对应的符号分别是&&、‖、! 。它们设定复合判断条件,返回一个boolean值,要么是true,要么是false。

1.与运算

语法结构:

表达式1 && 表达式2

如果表达式1和表达式2的值都是true,那么整个表达式的值也是true;如果两个表达式中任何一个为false,那么整个表达式的值为false。这个运算符可控制整个子表达式的求值顺序。

                                                                            2-10
a>5 && a<10

&&运算符的优先级比>和<运算符低,所以子表达式是按照下面这种方式进行组合的:

                                                                            2-11
(a>5) && (a<10)

但是尽管&&操作符的优先级较低,但它仍会对两个关系表达式施加控制。它的工作原理如下:&&操作符的左操作数总是首先进行求值,如果它为真,就紧接着对右操作数进行求值。如果左操作数为假,那么右边就不再进行求值,因为整个表达式的值一定是假的。

2.或运算

语法结构:

表达式1 ‖ 表达式2

两个表达式中任何一个值为true,或者两个都为true,则返回值为true。如果两个表达式都是false,则返回值为false。

逻辑运算符还可以组合使用:

                                                                            2-12
println((2<3) && (4>1 ‖ 2<5));            //输出:true

3.非运算

语法结构:

!表达式

非运算符可以把一个布尔值变成相反的值,即true变成false, false变成true。

示例:

                                                                            2-13
if(! (2>3)) {
    print("hello");                         //输出:hello
}

2.2.5 条件运算符

条件运算接收3个表达式,它会控制子表达式的求值顺序。

语法结构:

条件表达式1 ?表达式2:表达式3;

条件操作符的优先级很低,所以它的各个表达式即使不加括号也没什么问题。但是为了清楚起见,人们还是倾向在它的各个子表达式两端加上括号。

首先计算的是表达式1,如果它的值是true,那么整个表达式的值就是表达式2的值,表达式3不会进行求值。但是,如果表达式1的值是false,那么整个条件语句就是表达式3的值,表达式2不会进行求值。

示例:

                                                                            2-14
a>5 ? b-6 : c/2

可以读作:“a是不是大于5?如果是,就执行b-6,否则执行c/2”。

2.3 条件语句

在编程时可能会处理一些较复杂的分支情况,需要进行判断,并跳转到不同的语句中去。下面介绍如何使用两种条件语句。

2.3.1 if条件语句

使用条件语句,可以使程序执行某些代码,而跳过另一些代码,可以使程序在符合某些特定条件时才执行特定代码。

if条件语句语法结构:

                                                                            2-15
if (条件表达式)
{
    代码;
}

先判断if后面小括号中表达式的值是真还是假,如果是true执行{}里面的代码,如果是false,则直接跳过{}。

示例:

                                                                            2-16
int a = 2;
if(a>1)
{
    a = 0;
}
println(a);                 //输出:0

上例中因为a的初始值是2,所以进入if语句,a被重新赋值为0。

if else语句语法结构:

                                                                            2-17
if (条件表达式)
{
    代码1;
}else
{
    代码2;
}

如果if后面的条件表达式值为true,程序会运行代码1,否则执行else后面的代码2。

示例:

                                                                            2-18
void setup()
{
    size(200,200);
}
void draw()
{
    background(255);
    if (mouseX>100)
    {
        fill(0);
        rect(100,100,50,50);     //鼠标在右半屏时画方形
    } else
    {
        fill(0);
        ellipse(100,100,50,50);  //鼠标在左半屏时画圆形
    }
}

运行结果如图2-1所示。

图2-1 鼠标移动到左半屏时画圆形,移动到右半屏时画方形

上述代码只有两个分支,但是可以通过嵌套else if来插入更多分支的情况。

else if语句语法结构:

if (条件表达式1)
{
    代码1;
}else if (条件表达式2)
{
    代码2;
}
……
  else if(条件表达式n)
{
    代码n;
}
  else
{
    代码n+1;
}

如果条件表达式1为true,只执行代码1;否则判断条件表达式2,如果条件表达式2为true,只执行代码2;否则,判断条件表达式3,以此类推,直到判断条件表达式n为止。如果条件表达式n为false,则执行最后else{}里面的代码n+1。

示例:

                                                                            2-19
int a = 2;
if(a>1)
{
    println("t");
}
else if(a<-5)
{
    println("f");
}
else
{
    println("m");
}

控制台输出:t。

要注意的是,在使用多个else if条件语句时,程序会按顺序执行判断,从上到下直到返回true。如果在中间某个过程中条件成立,即使后面也有成立的条件,程序都不会执行。

2.3.2 Switch条件语句

switch条件语句用于需要进行多次判断才能做出选择的情况。

语法结构:

switch(条件表达式)
{
    case 常量1:
        代码1;
        break;
    case常量2:
        代码2;
        break;
…
    case常量n:
        代码n;
        break;
    default:
        代码n+1;
        break;
}

switch后面()里的表达式就是要进行判断的条件,switch语句首先计算条件表达式的值,这个值限定了数据类型,只能是byte、char、int三种数据类型,返回其他数据类型程序会报错。

每一个case到break代表一个分支结构,case后面跟的是常量表达式,用来判断是否与条件表达式相等,若相等,就执行分支里面的语句,直到遇见break。若每个分支的值都和表达式的值不相等,程序会执行默认分支default后面的语句。default语句也可以省略,如果分支条件都不成立的话,程序就什么都不执行。

示例:

                                                                            2-20
int a = 2;
switch (a)
{
case 1:
    println("hello");
    break;
case 2:
    println("world");
    break;
}

如果去掉break那么就会从标签位置继续向下执行,而不会执行完分支语句就马上退出。

2.4 循环语句

循环结构可以在满足某一个条件之前反复执行一个语句,让更少的代码完成更多的事情。下面是两种循环结构的使用方式。

2.4.1 while循环语句

用while来实现循环操作,它的语法结构如下:

  while(条件表达式)
 {
    循环体语句;
  }

条件表达式就是这个循环是否继续进行的条件,而循环体语句就是这个循环需完成的事情。while语句首先判断条件表达式的值(布尔型数据的值)。如果表达式的值为true,则执行循环体语句,否则,结束while语句。当循环语句执行完一次时,会再次判断表达式的值,根据表达式的值决定是否要进行下一次循环。如此不断循环,直到表达式的值为false,此时循环结束。

示例:

                                                                            2-21
int i = 10;
void setup()
{
    size(900,600);
}
void draw()
{
    while (i< 900)
    {
        ellipse(i, i,50,50);
        i+=50;
    }
}

运行结果如图2-2所示。

图2-2 用while循环画圆

2.4.2 for循环语句

for循环语句的语法结构:

for(初始化语句;判断循环条件;更新语句)
{
    循环体语句;
}

程序进入for循环语句之后,首先会执行初始化语句,然后计算判断循环条件语句,如果判断循环条件的值为true,则执行循环体语句,再执行更新语句,修改循环控制变量。接着又开始计算判断条件的值,根据其值决定是否需要继续下一次循环:如果判断条件的值为true,则继续下一次循环;反之,则结束整个循环。

示例:

                                                                            2-22
void setup()
{
    size(900,600);
}

void draw()
{
    for (int i = 0; i < 10; i++)
    {
        ellipse(50*i,50*i,50,50);
    }
}

运行结果如图2-3所示。

图2-3 用for循环画圆

2.4.3 加强循环结构

1.跳出循环

循环中可以使用break语句来永久终止循环;也可以使用continue语句,用于永久终止当前那次循环,而在执行之后,重新测试表达式的值,决定是否继续执行循环。

下面例子是输出10以内的偶数:

                                                                            2-23
for (int i = 0; i < 10; i++)
{
    if (i%2 ! = 0)
    {
        continue;
    }
    println(i);   //输出:0 2 4 6 8
}

下面的示例是用break关键字跳出循环:

                                                                            2-24
int a = 0;
for (int i = 0; i < 10; i++)
{
    if (i > 5)
    {
        break;
    }
    a++;
}
println(a);        //输出:6

两条语句任何一条如果出现在了嵌套的循环内部,它只对最内层的循环起作用,无法使用break或continue语句影响外层循环的执行。

2.嵌套循环

嵌套循环指在一个循环之内还有另一个循环。内部循环在外部循环的每个周期做着相同的事情。通过使内部循环的一部分依赖于外部循环,可以使内部循环在每个周期中的表现不同。

示例:

                                                                            2-25
for (int i = 0; i < 5; i++)
{
    for (int j = 0; j < 6; j++)
    {
        println(j);
    }
}

输出:012345012345012345012345012345。

2.5 函数

函数是用于完成特定任务的程序代码的单元。在Processing中很常见的函数有size()函数、line()函数等。这一节将要展示如何通过编写新的函数,来扩展Processing的功能特性。

为什么要使用函数?当要处理的问题越来越复杂,程序越来越庞大的时候,如果把这些程序代码都放到主函数中,将使主函数异常臃肿,这样会给程序的维护带来麻烦。在这种情况下,可以按照功能的不同,把大问题划分成小问题,把大程序划分成小模块。函数则成为模块划分的基本单元,是对一个复杂问题处理过程的一种抽象。

函数的使用可以省去复杂代码的编写。如果程序中需要多次使用某种特定的功能,那么只需写一个合适的函数即可。程序可以在任何需要的位置调用该函数,并且同一个函数可以在不同的程序中调用。即使某些功能在程序中只使用一次,将其以函数的形式实现也是有必要的,因为函数让程序变成模块化,从而有利于程序的阅读、修改和完善。

函数内部的计算是非常复杂的,特别是由多人共同完成的程序,会创建大量的新函数。但函数的优点在于不必了解它们是如何运作的,只需知道如何使用它们就已经足够了,即了解输入的是什么和它们是如何影响输出的。这种抽象使编程人员能专注于程序整体的设计,而无须考虑过多细节。

2.5.1 定义函数

定义函数有3部分:函数名、参数体、返回值。

函数名就是为了标志一个函数而取的名字,通过该函数名可以快速找到这个函数。函数的命名规则和变量的命名规则相同。如果说变量命名重在说明它“是什么”,那么函数的命名则重在说明它要“做什么”。

在程序里函数是可以传入参数的,可以针对函数的不同值,来进行相应的操作。当调用函数时,它会执行{}里的语句,函数{}里的所有语句叫作函数体。

语法结构:

返回值类型 函数名(参数类型1 参数名1,参数类型2 参数名2, ...)
/*可以接收任意个参数,中间用逗号隔开*/
{
    语句;
    return 返回值;
}

函数在执行完任务后,往往需要给它的调用者一个返回值,表示函数运行的结果或者其他意义。如下面的示例,在这个加法函数中,函数名为add,返回值类型为int,表示该函数在完成加法计算后,将计算的结果作为返回值返回给它的调用者。若函数只是执行一系列动作,无需返回值,则可以使用void作为返回值类型。

示例:

                                                                            2-26
void draw()
{
    println(add(5,4));
}
int add(int a, int b)      //此处有两个参数,中间用逗号隔开
{
    return a+b;
}                             //输出:9

还有一些函数是没有返回值的,类型是void。下面的示例是定义一个名为circle的函数,它的作用是画圆。

示例:

                                                                            2-27
void draw()
{
    circle();
}
void circle()
{
    ellipse(50,50,50,50);
}

2.5.2 函数重载

有一些函数功能相同,但是接收的参数不一样,它们就可以用相同的名称。比如一个加法函数,接收两个参数相加一起。而另一个加法函数也实现了相同的功能,只不过变为接收3个参数相加。如果起不同的名字,在调用函数时需要起很多名字,也要记录很多名字,会让人觉得很混乱。函数重载可以让其只有一个名字,调用时,根据参数个数和类型自动识别,调用相应的函数。

示例:两个求最小值的函数,第一个是两个数求最小,第二个是3个数求最小。功能相似,用一个名字更容易使用,减少函数数量。

                                                                            2-28
int minNumber(int a, int b)
{
    int temp = a > b? b:a;
    return temp;
}
int minNumber(int a, int b, int c)
{
    int temp = a > b? b:a;
    temp = temp < c ? temp:c;
    return temp;
}

2.5.3 函数递归

函数递归就是一个函数直接或间接调用自身。递归有一定的适用条件,在某些情况下使用递归可以方便获得想要的结果。当一个函数自己调用自己时,如果编程没有设定可以终止递归的条件检测,它会无限制地进行递归调用,所以要谨慎使用递归。

递归一般可以用循环来代替,但是递归更为强大,有时是循环无法处理的。递归方法使得程序结构优美,但是执行效率却往往没有循环高。

下面的示例是计算阶乘。

示例:

                                                                            2-29
void setup(){
      println(Factorial(8));       //求8的阶乘
}
int Factorial(int i)
{
    int num;
    num=i;
    if(num==1)return1;
    else return num *Factorial(--num);
}

2.6 数组

2.6.1 数组的概念

数组是一组有序且数据类型相同的变量的集合,每一个数组里面的项目被称为元素,每一个索引值标记其在数组中的位置。索引值即元素序号从0开始计数。例如,第一个元素在数组中的索引值是0,第二个元素在数组中的索引值是1,以此类推。如果有20个数值在数组中,那么最后一个元素的索引值就是19。如图2-4所示是一个数组的结构概念图。

图2-4 数据结构概念图

对集合中数组元素的访问可以采用下标的形式,也可以采用指针的形式进行。数组可以是一维的,也可以是多维的。当数组只有一个下标时,这类数组称为一维数组,当下标不止一个时,称为多维数组。常用的数据集基本在三维以内。

创造一个数组可分为3个步骤:

1)声明和定义数据类型。

2)创建新的关键字和数组长度。

3)给每个元素分配值。

使用数组类似于使用单独的变量,但遵循的是同种模式。如,写一个整数变量x如下:

Int   x;

而写一个数组,只需在数据类型后加上括号:

int[ ] x;

例如,下面的代码表示定义了一个intArray整型数组,下标从0~4,共5个数组元素,如图2-5所示。

int[ ] intArray=new int[5];

图2-5 长度为5的整型数组

数组的优点在于只需要一行代码就能写多个变量。例如,下面这行代码是2000个整型变量:

int[ ] x=new int[2000];

注意

每个数组仅可以储存一种类型的数据(布尔、浮点、整数、PImage等)。不能将不同类型的数据混在一个数组中。如果必须这样,可以使用对象来代替。

2.6.2 遍历数组

在Processing中,很多时候需要制作并处理多图片,来达到所需要的图片效果,如果一个一个地对图片的信息进行处理,会消耗很多的精力和时间,并且需要大量的工作量。因此,利用数组,通过for循环语句操作,可以提高工作效率。

1)用for循环遍历数组。

下面就是一个利用for循环语句,绘制单位为10的熊猫来回运动的效果,如图2-6所示。

                                                                            2-30
float [ ] x = new float[10];              //储存每幅图的横向运动坐标组
float [ ] y = new float[10];              //储存每幅图的纵向运动坐标组
float [ ] s = new float[10];              //储存每幅图的偏移量组
void setup() {
    size(300, 300);                         //窗口大小
    smooth();                               //线段光滑
    for (int i = 0; i < x.length; i ++) {
        x[i] = random(0, 300);            //0~300随机赋值
        y[i] = random(0, 300);            //0~300随机赋值
        s[i] = 0.5;                       //偏移量
    }
}
void draw() {
    background(200);                        //背景填充
    for (int i =0; i<x.length; i++) {
        panda(x[i], y[i]);                //载入熊猫函数
        x[i] += s[i];
        if (x[i] > 300 ‖ x[i] <0) {
            s[i] = -s[i];
        }                                     //防止图片移出窗口
    }                                         //for循环运行10次,载入10次熊猫函数
}
void panda(float x, float y) {
    pushMatrix();                           //矩阵移动
    translate(x, y);                        //移动函数
// 熊猫头像绘制函数
    fill(0);
    strokeWeight(1);
    ellipse(-35, -25, 35, 35);            //左耳
    ellipse(35, -25, 35, 35);             //右耳
    fill(255);
    strokeWeight(1);
    stroke(0);
    ellipse(0, 0, 100, 90);               //头部
    fill(0);
    ellipse(-25, 5, 30, 35);              //左眼
    ellipse(25, 5, 30, 35);               //右眼
    fill(255);
    ellipse(-25, 0, 6, 6);                //左眼球
    ellipse(25, 0, 6, 6);                  //右眼球
    fill(0);
    ellipse(0, 25, 7, 5);                  //鼻子
    noFill();
    stroke(0);
    strokeWeight(1);
    bezier(-2.5, 35, -2.5, 37, 2.5, 37, 2.5, 35); //用贝塞尔曲线绘制嘴巴
    popMatrix();                            //矩阵闭合
}

图2-6 数组熊猫运动图

2)用for循环的另一种形式来遍历数组。

语法结构:

for(数据类型 变量名:数组){
    println(变量名);
}

示例:

                                                                            2-31
int[ ] array={0,1,2,3,4}; //创建数组
for(int i:array){
    println(i);
}

输出:0

1

2

3

4

3)通过printArray()函数遍历数组。

printArray()函数用于将信息显示在消息控制台中。每一行都会显示一个数组元素。这个函数只能应用于一维数组。

示例:

                                                                            2-32
float[ ] f = { 0.3, 0.4, 0.5 };
printArray(f);

输出:[0] 0.3

[1] 0.4

[2] 0.5

利用数组绘制五角星,如图2-7所示。

图2-7 五角星

绘图思路:描绘五角星10个顶点坐标,再通过vertex()函数连接相邻点。

实例点坐标:

(50,18)(61,37)(83,43)(69,60)(71,82)(50,73) (29,82)(31,60)(17,43)(39,37)

示例:

                                                                            2-33
int[ ] x={50,61,83,69,71,50,29,31,17,39};
// 数组x储存点横坐标
int[ ] y={18,37,43,60,82,73,82,60,43,37};
// 数组y储存点纵坐标
beginShape();              //发起创建新图形信号
for(int i=0; i<x.length; i++){
    vertex(x[i], y[i]);    //定义x、y轴坐标
}
endShape(CLOSE);           //结束信号

注意

避免在draw()内创建数组,因为在每一帧创建一个数组会降低帧速率。

2.6.3 二维数组

可以这样理解:一维数组解决的是线性问题,而二维数组解决的是面的问题。定义二维数组的语法格式如下:

数据类型[ ][ ] 变量名;

例如,定义一个整型数组,变量名为intArray:

                                                                            2-34
int[][] int Array ;

使用new关键字初始化的语法格式:

new 数据类型[数组长度1][数组长度2] ;

二维数组可以当作矩阵来看待,二维数组中第1个下标表示行,第2个下标表示列。功能可以看作为该数组分配一块连续数组长度1×数组长度2的存储空间。

例如,定义一个存放3个长度为2的整型数组,如图2-8所示。

图2-8 定义一个3行2列的intArray整型数组

                                                                            2-35
int[ ][ ] intArray;                //数组声明
intArray =new int[3][2];          //数组初始化

在第二个[ ]中的数字一般可省略,然后通过下标来分别初始化数组中的数组,以此来得到包含不同长度的二维数组。

String[ ][ ] strArray=new String[5][ ];

对第一个数组进行长度初始化。

strArray[0]=new String[5];

对第二个数组进行长度初始化。

strArray[1]=new String[4];

对第三个数组进行长度初始化。

strArray[2]=new String[3];

…………

例如:

int x[2][5]={{1,2,3,4,5}, {4,5,6,7,8}};

其等效的形式如下:

int x[2][  ]={{1,2,3,4,5}, {4,5,6,7,8}};

注意

数据类型可以是int、float、char等,数组名用标识符充当。数组大小是指构成数组的元素个数。为数组分配的存储空间为一连续的存储空间,这是利用指针访问数组的物理基础。访问数组时,下标始终从0开始。

1)用for循环遍历二维数组。

通过for循环遍历二维数组,编程画一个随机的灰度噪声图,如图2-9所示。

                                                                            2-36
size(300, 300);
int cols = width;
int rows = height;
// 声明二维数组
int[ ][ ] myArray= new int[cols][rows];
// 初始化二维数组变量
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);
    }
}

图2-9 二维数组绘制灰度图

2)用另一种for循环遍历二维数组。

示例:

                                                                            2-37
String[ ][ ] strArray=new String[3][4];
// 创建一个3行4列的字符串数组
for(int i=0; i<strArray.length; i++){
    for(int j=0; j<strArray[i].length; j++){
        strArray[i][j]=i+"_"+j;  //字符串样式
    }
}
for(int i=0; i<strArray.length; i++){
for(String j:strArray[i]){
    print(j+", ");
}
    println();
}

控制台输出:

0_0,0_1,0_2,0_3,

1_0,1_1,1_2,1_3,

2_0,2_1,2_2,2_3,

3)第三种for循环遍历二维数组。

示例:

                                                                            3-38
String[ ][ ] strArray=new String[3][4];
// 创建一个3行4列的字符串数组
for(int i=0; i<strArray.length; i++){
    for(int j=0; j<strArray[i].length; j++){
        strArray[i][j]=i+"_"+j;  //字符串样式
    }
}
for(int i=0; i<strArray.length; i++){
    printArray(strArray[i]);
}

控制台输出:

[0] "0_0"

[1] "0_1"

[2] "0_2"

[3] "0_3"

[0] "1_0"

[1] "1_1"

[2] "1_2"

[3] "1_3"

[0] "2_0"

[1] "2_1"

[2] "2_2"

[3] "2_3"

将一个数组变量值赋值给另一个数组变量,改变其中一个变量的值,会影响另一个变量的值。

                                                                            2-39
int[ ]x={1,3,5,7,9};
int[ ]y=x;
y[1]=101;
println(x[1]);                               //输出:101

2.6.4 数组综合应用

接下来,本书提供了5个数组综合应用的实用范例。

范例一

功能:利用数组,绘制大小变化的圆。

示例:

                                                                            2-40
float[] a=new float[10];                   //定义一个长度为10的浮点型数组
int j=0;
void setup() {
    frameRate(1);                           //程序每秒刷新一次画面
    for (int i=0; i<10; i++)
        a[i]=random(100);                  //把随机生成的数放入数组
}
void draw() {
    background(204);                        //背景设置为灰色
    if (j<10)
        ellipse(50, 50, a[j], a[j]);
    j++;
}

范例二

功能:利用数组的连续遍历功能,制作渐变图效果,如图2-10所示。

示例:

                                                                            2-41
size(600, 600);                             //窗口大小600×600
float[ ] coswave= new float[width];
// 利用new运算符将像素的width值赋予数组
for (int i = 0; i < width; i++) {
    float amount= map(i, 0, width, 0, PI);
// map(value,最小值区间,最大值区间)
    coswave[i] = abs(cos(amount));          //绝对值
}
for (int i = 0; i<width; i++) {
    stroke(coswave[i]*255);                //灰度值深化
    line(i, 0, i, height/3);
}                                           //第一个渐变
for (int i = 0; i<width; i++) {
    stroke(coswave[i]*255/4);              //灰度值变化
    line(i, height/3, i, height/3*2);
}                                           //第二个渐变
for (int i = 0; i<width; i++) {
    stroke(255 - coswave[i]*255);          //灰度值亮化
    line(i, height/3*2, i, height);
}

图2-10 数组绘制渐变图

范例三

功能:为数组分配一块连续数组大小1×数组大小2的存储空间

说明:二维数组可以当作矩阵来看待和处理,二维数组中第1个下标表示行,第2个下标表示列。

示例:

在这个例子中,有两个数组来储存鼠标的状态:一个用于x坐标,一个用于y坐标,这些数组储存鼠标在过去帧内的位置。每一帧新的出现,保存最久的x、y坐标的值都会被现在的mouseX和mouseY代替,新的值则被添加到数组的第一个位置,但是在这之前,数组中的每一个值都会被向右移动(从后到前),从而为新的数值腾出空间。

同样,每一帧,所有的60个坐标都被用于在屏幕上画出一系列的圆,如图2-11所示。

图2-11 鼠标动态圆绘制

示例:

                                                                            2-42
int num=60;                                  //用于定义数组长度
int[ ] x=new int[num];                       //储存x坐标
int[ ] y=new int[num];                       //储存y坐标
void setup() {
    size(240, 120);                          //定义窗口大小
    smooth();                                //线段光滑
    noStroke();                              //禁止描边
}
void draw() {
    background(0);                           //填充刷新背景色
    for (int i=x.length-1; i>0; i--) {
        x[i]=x[i-1];
        y[i]=y[i-1];
    }                                         //从后往前复制数值到数组
    x[0]=mouseX;                              //设置第一个元素
    y[0]=mouseY;                              //设置第二个元素
    for (int i=0; i<x.length; i++) {
        fill(i*4);
        ellipse(x[i], y[i], 40, 40);          //画圆
    }
}

案例分析:

数组存储值分析,如下:

原始数组如下:

开始循环复制第二到最后一个数值到最后的位置。

第二次循环,复制元素2到元素3。

第三次通过循环,复制元素1到元素2。

第四次也是最后一次通过循环,复制元素0到元素1。

复制新的元素到元素0。

范例四

功能:Processing提供PImage类用于加载图片,通过对图片像素提取及重构,实现对图片的基层处理及预想编辑效果。

说明:通过载入原图片像素点RGB,并通过数组循环重排,实现对图片的还原以及镜像处理后所得到的图片,如图2-12所示。

                                                                            2-43
void setup() {
    pic=loadImage("pic.jpg");                      //载入原图片
    size(pic.width*2, pic.height);                //设置窗口大小
    background(0);                                 //背景填充
    smooth();                                      //图片光滑
}
void draw() {
    int x=int(random(pic.width));                  //随机获取原图片横坐标
    int y=int(random(pic.height));                 //随机获取原图片纵坐标
    int sum=x+y*pic.width;                        //像素点矩阵计算
    loadPixels();                                  //载入像素点RGB值
    float r=red(pic.pixels[sum]);                  //复制red值
    float b=blue(pic.pixels[sum]);                 //复制blue值
    float g=green(pic.pixels[sum]);                //复制green值
    int diameter=int(random(8,15));                //绘制像素点大小
    noStroke();                                    //不描边
    fill(r, g, b,100);                             //填充RGB值
    ellipse(x, y, diameter, diameter);             //绘制原图
    ellipse(pic.width*2-x, y, diameter, diameter); // 绘制镜像图片
}

图2-12 镜像处理

分析:载入图片一般存在Processing根目录下,亦可通过设置url值绑定路径。通过对原图片的像素点RGB提取,并重新绘制来得到新图片,同时通过对绘制的路径变化得到所需要的图片效果,亦可通过设置绘制像素的大小来得到精准的图片。当绘制像素大小为1px时,可得到精确原图,基于此方法得到的图片在后期处理上更为完美。

范例五

利用二维数组输出杨辉三角形前10行。

int[][]sz=new int[10][10];           //定义一个10×10列的数组
int i, j;
for( i=0; i<sz.length; i++){
    sz[i][0]=1;
    sz[i][i]=1;
}                                     //输出外围的数值1
for(i=2; i<sz.length; i++){
    for(j=1; j<i; j++){
        sz[i][j]=sz[i-1][j-1]+sz[i-1][j];
    }
}                                     //内嵌循环输入数值
for(i=0; i<10; i++){
    for(j=0; j<=i; j++){
print(sz[i][j]+" ");
        if(j==i)
    println();
    }
}                                     //内嵌循环输入

输出:

1

1 1

1 2 1

1 3 3 1

1 4 6 4 1

1 5 10 10 5 1

1 6 15 20 15 6 1

1 7 21 35 35 21 7 1

1 8 28 56 70 56 28 8 1

1 9 36 84 126 84 36 9 1

2.6.5 数组函数

Processing提供了处理数组的全局函数,详细函数如表2-5所示。读者可以使用这些函数对数组进行处理。

这些函数有个共同的特点,就是它们在调用的时候不会改变原数组元素,而是返回一个新的数组。

表2-5 数组函数

示例一:

                                                                            2-44
String[   ] sa1 = { "A", "B", "C"};
String[   ] sa2 = append(sa1, "D");
println(sa2);                       //输出:A B C D

示例二:

                                                                            2-45
String[ ] sa1 = { "A", "B", "C"};
String[ ] sa2 = { "D", "E", "F"};
String[ ] sa3 = concat(sa1, sa2);
println(sa3);                       //输出 A B C D E F

示例三:

                                                                            2-46
int[] data1 = {0, 1, 3, 4};       //创建并赋值
println(data1.length);
int[] data2 = expand(data1);
// 增加数组长度
println(data2.length);
println(data1[data2.length-5]);
/*输出:
4
8
4 */

示例四:

                                                                            2-47
String[   ] s={ "deer", "elephant", "bear", "aardvark", "cat" };
s = sort(s, 3);                     //对前面3个元素进行排序
println(s);                         //输出:bear deer elephant aardvark cat

示例五:

                                                                            2-48
String[ ] a = { "OH", "NY", "CA" };
a = splice(a, "KY", 1);           //"KY"插入a中a[1]位置
println(a);
String[ ] b = { "VA", "CO", "IL" };
a = splice(a, b, 1);               //数组b全体插入a中a[1]位置
println(a);
/* 输出:
OH KY NY CA
OH VA CO IL KY NY CA
  */

示例六:

                                                                            2-49
String[] sa1 = { "OH", "NY", "CA", "VA", "CO", "IL" };
String[] sa2 = subset(sa1, 1);   //提取sa1中a[1]后的元素
println(sa2);
String[] sa3 = subset(sa1, 2, 3);
// 提取sa1的a[2]后的长度为3的元素
println(sa3);
/* 输出:
NY CA VA CO IL
CA VA CO
*/

2.7 字符串

2.7.1 字符串基本概念

字符串是由多个字符的组成的集合,例如“Good morning”,在引号“”内的都叫字符串。在Processing中字符串被封装成了类String,并且提供了一些成员方法和全局函数来处理字符串数据。

字符串在存储上类似于字符数组,所以它每一位的单个元素都是可以提取的,给编程人员提供了方便,如高精度运算时每一位都可以转化为数字存入数组。

字符串的初始化有两种方式。第一种用双引号来初始化:

String str ="Processing";

第二种用new来初始化,需要把字符数组当作参数传入:

                                                                            2-50
char[ ] charData={' I' , ' l' , ' o' , ' v' , ' e' , ' P' , ' r' , ' o, ' g' , ' r' , ' a' , ' m' };
String str1=new String(charData);
println(str1);
// 输出:IloveProgram

也可把字节数组作为参数:

Byte[ ]   byteData={80,114,111,99,101,115,115,105,110,103} ;
String str2=new String(byteData);
println(str2);
// 输出:Processing

String()类重载了构造函数,使得可以通过参数来指定字符数组的一段字符来初始化字符串。

String(data, offset, length)
/* data:字符数组或者字节数组
offset:开始位置
length:长度 */

示例:

                                                                            2-51
char[ ] charData={' p' , ' e' , ' r' , ' f' , ' e' , ' c' , ' t' };
String str=new String(charData,2,3);
// 将从charDate[2]开始长度为3的元素赋予str
println(str);
// 输出:rfe

用“+”可以连接两个字符串,得到一个新的字符串。

                                                                            2-52
String name="Bob";
String age="twenty";
String str="NAME:"+name+"   AGE:"+age;
println(str);
//输出:NAME:Bob  AGE:twenty

2.7.2 字符串的方法

Processing中,字符串被封装成类,并提供了一些成员方法。

1)length()成员方法返回字符串的长度。

下面例子中,字符串“processing”的长度是12而不是10,因为空格也是一个字符。

                                                                            2-53
String str=“processing”;
println(str.length());
// 输出: 12

2)charAt(index)成员方法返回制定索引位置的字符。

字符串可以通过索引位置返回指定位置的字符,索引位置从0到字符串的长度减1。

index为返回字符的索引位置,类型为int,取值范围是0到字符串的长度减1。

下面的例子为通过charAt()函数返回字符串的每个字符,并判断字符串中大写字母的个数。

                                                                            2-54
String str="Hi, Processing";       //定义字符串并赋值
int count=0;
for (int i=0; i<str.length (); i++) {
    char ch=str.charAt(i);        //索引字符
    if (ch>= ' A' &&ch<=' Z' )    //if判断是否大写字母
        count++;
}
println(count);                   //输出大写字母的个数
// 输出:2

3)indexOf()成员方法返回指定字符串的索引位置

indexOf(str)
indexOf(str, fromIndex)
/*str:要搜索的字符或字符串
fromIndex:开始搜索的索引位置,前面即使有匹配字符也会被忽略 */

程序会从左向右开始搜索返回一个与指定字符串匹配的索引位置,如果没有找到则返回-1。

字符串第一位索引位置为0。

注意

indexOf()函数只返回第一个字符的索引位置,若查找的字符在字符串中多次出现也只返回第一个字符的索引位置。

4)substring()重载函数用于返回字符串中从指定开始位置到结束位置的子字符串。

substring(beginIndex)
substring(beginIndex, endIndex)
/* beginIndex:截取的开始位置
endIndex:截取的结束位置,实际字符串长度减1 */

5)利用indexOf()函数查找定位字符或者字符串,并结合substring()函数即可实现Word中的查找替换功能。

                                                                            2-55
String str="We love Processing.";         //定义并赋值字符串
int index=str.indexOf("love");            //索引所查内容的位置
String repl="beat";                       //定义代替的字符串
if (index! =-1) {
    String restr=str.substring(0, index)+repl+str.substring(index+5);
/* str.substring(0, index)返回love字符前的所有字符
    str.substring(index+5)返回love后面的所有字符*/
}
println(restr);                             //输出新字符串
//输出:We beat Processing.

6)equals(str)成员方法用于判定两个字符串是否相等,如果相等返回true,否则返回false。注意,在processing中不能够使用“==”来判断字符串是否相等。

7)toLowerCase()成员方法返回一个新的字符串,并把字符串中所有大写字母全替换成小写字母。

8)toUpperCase()成员方法返回一个新的字符串,并把字符串中所有小写字母全替换成大写字母。

以下示例对字符串使用凯撒加密,它的基本思想是:通过把字母移动一定的位数来实现加密和解密。明文中的所有字母都在字母表上向后(或向前)按照一个固定数目进行偏移后被替换成密文。例如,当偏移量是3的时候,所有的字母A将被替换成D, B变成E,以此类推,X将变成A, Y变成B, Z变成C。

                                                                            2-56
String str="We loves Processing.";                //定义并赋值字符串
String upstr=str.toUpperCase();                   //转换所有字母为大写字母
char[ ] ch=new char[upstr.length()];              //定义字符数组用于储存加密后的字符
for (int i=0; i<upstr.length (); i++) {
// if判断当前字符是否为大写字母,符合则对字符加密,不符合返回原字符
    if (upstr.charAt(i)>=' A' && upstr.charAt(i)<=' Z' ) {
        ch[i]=char((upstr.charAt(i)-' A' )%26+' A' +3); //字母循环移动
    }
    else ch[i]=upstr.charAt(i);
}
String Caesastr=new String(ch);                   //字符数组作为参数传递给字符串进行赋值
println(Caesastr);
//输出:ZH ORYHV SURFHVVLQJ

2.7.3 字符串处理函数

Processing提供了处理字符串的全局函数,详细函数如表2-6所示。它们为编程者使用字符串处理数据提供很大帮助。

表2-6 字符串函数