- Processing开发实战
- 黄文恺 吴羽 伍冯洁
- 1014字
- 2024-12-23 13:41:09
第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 字符串函数