Processing:数据输入(下)

25. P5不仅可以分析来自URL的小块信息,还可以分析来自新闻feed、文章、演讲或整本书的大块文本。Daniel引用的txt网站估计是被和谐了,总之我这此时此刻是打不开,因此,我胡乱搜索了一个P5官方版本变更说明的txt作为下例引用的txt地址,同样的,如果你在阅读本笔记的时候无法打开我现在使用的这个在线txt地址的话,你也可以上网自己搜索一个来用:

PFont f;              // 字体变量
String[] revisions;    // 承载所有文本的数组
int counter = 1509;   // 我们在文本文档中的位置

// 把标点和符号用于分界符
String delimiters = " ,.?!;:[]+/()\"-#=_";

void setup() {
  size(200,200);
  
  f = createFont("Lucida Sans", 16);
  
  // 将文本文档载入字符串数组
  String url = "http://processing.org/download/revisions.txt";
  String[] rawtext = loadStrings(url);
  
  // 将数组整合为一个巨长的字符串
  String everything = join(rawtext, "" );
  
  // 文本文档中的所有行先是组合成一个长字符串然后分解成一个独立单子的数组
  // 注意我们使用splitTokens()将全部标点和符号作为分界符.  
  revisions = splitTokens(everything,delimiters);
  frameRate(1);
}

void draw() {
  background(255);
  
  // 从文本文档内取一个词
  String theword = revisions[counter];
  
  // 计数那个词在文本文档中出现的次数
  int total = 0;
  for (int i = 0; i < revisions.length; i ++ ) {
    if (theword.equals(revisions[i])) {
      total ++;
    }
  }
  
  // 显示文本及其出现的总次数
  textFont(f);
  fill(0);
  text(theword,10,90);
  text(total,10,110);
  stroke(0);
  fill(175);
  rect(10,50,total/4,20);
  
  // 继续下一个词
  counter = (counter + 1) % revisions.length;
}

一个好玩的:在上例中我发现,在P5版本变更说明文件中出现频率最高的一个词是"bug",竟然高于"the"和"processing"出现的频率。

26. 练习18-10,找出并显示上文txt文档中字母a~z出现的频率。well,暂时没啥一阵见血的主意,想出来来更新。我的解答见

27. 如我们所见,loadStrings()可以被用于从网页检索原始数据。但是,除非你只需要在整个setup()的过程中载入一次数据,否则问题就来老。比方说,你每5分钟通过XX网站的XML来检索是否有新的种子放出。当你每次调loadStrings(),sketch在接收数据的时候都会停掉,所有动画都会傻逼掉。这是因为loadStrings()是一个“粘连”函数,换句话说,sketch在loadStrings()完成它的任务前都会一直停在那条代码上。如果是一个本地的文本文件,则会很快。但是,一个对于网页的请求是异步的(asynchronous),意味着服务器将自行掌握返回你需要数据的时间。你知道loadStrings()需要多长时间完成自己的任务?不知道,也没有人知道,此刻你在与服务器的较量中明显处于劣势!

28. 于是一个被称为simpleML的库跳出来准备拯救你于水火之中。它可以平行执行sketch并向服务器提出请求,实现sketch多任务化并在数据检索进行中同时继续动画。这里属于高阶应用环节(或者也许几乎不会出现于我们目前的使用中),感兴趣的请自行参考原书或上边那个链接,这里我就偷懒跳过了。

29. 当然,我们可以如之前那个例子一般去手动在XML中搜寻我们想要的数据。但是,一个更方便的方式是使用一个支持XML分析的库。XML用树形结构组织信息。比如一堆学生,每人都有各自的id,名字,电邮,地址什么的,那么他们的XML树看起来就应像下图一般:

学生树
学生树

XML源文件(只以列两名学生为例)即:

<?xml version = "1.0" encoding = "UTF-8 " ?>       
<students>     
           <student>       
                      <id>001</id>        
                      <name>ww</name>        
                      <phone>55-555-5555</phone>     
                      <email>ww@coding.im</email>        
                      <address>      
                               <street>123 Processing Way</street>       
                               <city>Kunming</city>
                               <state>Yunnan</state>        
                               <zip>01234</zip>     
                      </address>       
</student>
<students>     
           <student>       
                      <id>002</id>        
                      <name>sulu</name>        
                      <phone>66-666-6666</phone>     
                      <email>sulu@coding.im</email>        
                      <address>      
                               <street>123 Processing Way</street>       
                               <city>Chenzhou</city>
                               <state>Hunan</state>        
                               <zip>01234</zip>     
                      </address>       
</student>

请注意它与面向对象编程的相似。我们可以如下去想一个XML树。XML文档讲述了一个学生对象的数组。每个学生对象都有很多信息,id,名字,邮址,地址....邮址同样是一个有很多数据的对象,比如接到、城市和邮编。

30. 回到天气的例子,现在我们来重看它的源XML代码(人为简化后):

<?xml version = "1.0" encoding = "UTF-8" standalone = "yes"?>  
<rss version = "2.0" xmlns:yweather = "http://xml.weather.yahoo.com/ns/rss/1.0">
         <channel>      
      <item>       
         <title>Conditions for New York, NY at 3:51 pm EST</title>        
         <geo:lat>40.67< /geo:lat>        
         <geo:long>-73.94</geo:long>      
         <link>http://xml.weather.yahoo.com/forecast/USNY0996_f.html </link>        
         <pubDate>Mon, 20 Feb 2006 3:51 pm EST</pubDate>       
         <yweather:condition text = "Fair"  code = "34" temp = "35"  date = "Mon, 20 Feb 2006 3:51 pm EST " />     
         <yweather:forecast day = "Mon"  date = "20 Feb 2006" low = "25"  high = "37" text = "Clear" code = "31" />     
       </item>           
            </channel>    
</rss>

这个数据结构如果整理成树型的话,如下:

天气树
天气树

31. 接下来,教材又开始讲simpleML库。。同样的,感兴趣的同学请自学,这段跳过。

32. 说半天,Daniel告诉我们simpleML库真的很simple,因为它不能完成一些高级的任务...OK,现在我们还有两个选择:使用Christian Riekoff写的proXML,或者使用P5内建的XML库(我X,你早点说这个会死啊- -b):

import processing.xml.*;

33. 当库被导入后,第一步是创建一个XMLElement对象。这个对象将导入来自网络或本地XML文档的数据。构造器要求两个引数,"this"以及XML的文件名或URL。

String url = "xmldocument.xml"; 
XMLElement xml = new XMLElement(this,url);

34. 这个XML库在载入文档的时候将会让sketch暂停,如果你需要异步同步,则需要去研究proMXL。

35. 一个XMLElement对象代表XML树中的一个元素。当一个文档最先载入,那个元素对象将一直称为根元素。simpleXML库自动遍历整棵树为我们找到正确的信息。但用P5的XML库,我们不得不手动做这个事。尽管这看起来更复杂了,但也意味着同时,我们可以更好、更精确的控制我们的搜索。

36. 回望之前那个天气的树状图,我们可以经由以下路径找到温度:
1)树的根元素是"RSS"。
2)"RSS"有一个子元素名叫"Channel"。
3)"Channel"的第十三个子元素是"item"。(图标被简化为只显示channel的一个子元素)
4)"item"的第六个子元素是“yweather:condition”。
5)温度由属性'temp“表示存于“yweather:condition”中。

37. 一个元素的子元素可以由一个索引(从0开始,与一个数组相同)传递进入getChild()函数来进行访问。一个元素的内容由getContent()检索,属性被读取作任一数字(原文:attributes are read as either numbers,不知道怎么翻译才好,有知道的请联系我更正)—— getIntAttribute(), getFloatAttribute() —或文本— getStringAttribute()

// 访问根元素的第一个子元素 
XMLElement channel = xml.getChild(0);

遵循上一条的1~5步,我们可以搞出如下代码:

XMLElement xml = new XMLElement(this, url); 
XMLElement channel = xml.getChild(0);
// 一个元素的第十三个子元素在索引中列第十二位 
XMLElement item = channel.getChild(12); 
XMLElement condition = item.getChild(5); 
temp = condition.getIntAttribute("temp");

38. 其他有用的能够调用XMLElement对象的函数是:
getChildCount() — 返回一个XMLElement子元素的总数.
getChildren() — 以数组的形式返回XMLElements的全部子元素.

39. 在之前那个气泡的例子中,我们用txt文档中的数字作为气泡的数据。一个XML文档同样具备这样的功能。让我们按颜色来整理一个XML档:

<?xml version = "1.0" ?>
<bubbles>      
  <bubble>       
    <diameter>40</diameter>       
    <color red = "75"  green = "255" />      
  </bubble>       
  <bubble>       
    <diameter>20</diameter>       
    <color red = "255"  green = "75" />      
  </bubble>
  <bubble>       
    <diameter>80</diameter>       
    <color red = "100"  green = "150" />      
  </bubble>
</bubble

40. 我们可以使用getChildren()来接收一个"Bubble"元素的数组并从中制造一个气泡对象。以下为示例(与我们早先使用的Bubble类完全一样,因此在下边的代码里就不写这个类以免吓到你我了):

import processing.xml.*;

// 一个气泡对象数组
Bubble[] bubbles;

void setup() {
  size(200,200);
  smooth();
  // 载入一个XML文档
  XMLElement xml = new XMLElement(this, "bubbles.xml" );
  
  // 使用getChildCount()得到气泡对象的总数.  
  int totalBubbles = xml.getChildCount(); 
  
  // 做个同样大小的数组
  bubbles = new Bubble[totalBubbles];
  
  // 获取全部子元素
  XMLElement[] children = xml.getChildren();
  
  for (int i = 0; i < children.length; i ++ ) {
  // 直径是子元素0
    XMLElement diameterElement = children[i].getChild(0);
    
    // 直径是第一个元素的内容,红和绿是第二个元素的属性
    int diameter = int(diameterElement.getContent());
    
    // 颜色是子元素1
    XMLElement colorElement = children[i].getChild(1);
    int r = colorElement.getIntAttribute("red");
    int g = colorElement.getIntAttribute("green");
    
    // 用XML文档的值创建一个新的气泡对象
    bubbles[i] = new Bubble(r,g,diameter);
  }  
}

void draw() {
  background(255);
  // 显示并移动全部气泡
  for (int i = 0; i < bubbles.length; i++ ) {
    bubbles[i].display();
    bubbles[i].drift();
  }
}

41. 载入HTML和XML文档能够很方便的从网上拽出信息,但是,对于更复杂的程序来说,很多网站都会提供一个API。一个API(Application Programming Interface,应用程序接口)是一个用于软件间访问彼此服务的接口。有很多API可用于P5,你可以在P5官网库页面的“Data / Protocols”环节获取更多信息。

42. 接下来,我们要运用Yahoo应用程序接口来看一个网络搜索的例子。尽管我们可以直接访问Yahoo API,但是这里我们要使用Daniel同学自己写的一个P5库来使事情变得简单一些。介绍和下载在这里

43. Well,如果你和我一样傻逼,在雅虎SDK下载包中没找到那个.jar文件,那么你可以在这里找到它。

44. 你可以在这里注册你的雅虎API Key,请注意,application id = API key

45. 呼呼呼,终于万事俱备了,你安好了库,申请好了API Key,现在来玩个有趣的例子:

import pyahoo.*;

// 创建一个YahooSearch对象. 你需要填入雅虎给你的API key.
YahooSearch yahoo; 

void setup() {
  size(100,100);
  
  // Make a search object, pass in your key
  yahoo = new YahooSearch(this, "用你获得的API Key替换这句话");
}

// 鼠标按下进行搜索
void mousePressed() {
  // 搜索关键词。默认将返回10个结果 
  // 如果你想要更多或更少的结果,你可以写: yahoo.search("oneway post rock post rock china", 30);
  yahoo.search("oneway post rock china"); 
}

void draw() {
  noLoop();
}

// 当搜索完成后
void searchEvent(YahooSearch yahoo) {
  
  // 获取标题和URL
  String[] titles = yahoo.getTitles();
  // 搜索结果以一个字符串数组返回. 
  // 你同样可以用getSummaries()获取摘要.
  String[] sum = yahoo.getSummaries();
  String[] urls = yahoo.getUrls(); 
  
  for (int i = 0; i < titles.length; i++) {
    println( "__________" );
    println(titles[i]);
    println(sum[i]);
    println(urls[i]);
  }  
}

如何?在p5的环境中建立自己的yahoo搜索引擎的感觉怎么样?

46. 这个库还可以用于做简单的视觉(样图)。接下来的例子搜索五个名字并为每个名字画一个圆(大小与搜索结果捆绑):

import pyahoo.*;

YahooSearch yahoo;
PFont f;

// 欲搜索的人名,一个“气泡”对象的数组
String[] names = { "sulu" , "Cobain" , "Axl" , "Wang" , "Wong" };
Bubble[] bubbles = new Bubble[names.length];

int searchCount = 0;

void setup() {
  size(500,300);
  yahoo = new YahooSearch(this, "用你获得的API Key替换这句话" );
  f = createFont("Lucida", 20, true);
  textFont(f);
  smooth();
  
  // 搜索全部名字
  for (int i = 0; i < names.length; i++) {
    // search()函数在数组中被每个名字调用
    yahoo.search(names[i]); 
  }
}

void draw() {
  background(255);
  
  // 显示全部气泡
  for (int i = 0; i < searchCount; i++) {
    bubbles[i].display();
  }
}

// 搜索一次进行一个
void searchEvent(YahooSearch yahoo) {
  
  // 每个搜索结果的总数
  // getTotalResultsAvailable()返回雅虎每个搜索结果的总数. 
  // 这些数字也许可以非常大,因此在作为一个圆的大小前要被按比例缩小.
  int total = yahoo.getTotalResultsAvailable(); 
  
  // 缩小总数使其可见
  float r = sqrt(total)/75;
  
  // 创建一个新的气泡对象
  // 搜索数据被用于使一个气泡对象可见.
  Bubble b = new Bubble(yahoo.getSearchString(), r,50 + searchCount*100,height/2);
  bubbles[searchCount] = b;
  searchCount++;
}

// 简单的描述搜索结果的"Bubble"类
class Bubble {
  
  String search;
  float x,y,r;
  
  Bubble(String search_, float r_, float x_, float y_) {
    search = search_;
    r = r_;
    x = x_;
    y = y_;
  }
  
  void display() {
    stroke(0);
    fill(0,50);
    ellipse(x, y, r, r);
    textAlign(CENTER);
    fill(0);
    text(search,x,y);
  }
}

如上可见,关键字“Wong”比“Wang”返回的结果要多,可见装逼犯还是广泛存在的。

47. 我们可以方便的在本地调用网络数据,但当你将你的程序作为一个浏览器的applet使用时,有一些安全方面的要求。
1)如果applet存于你的站点:http://www.我的域名.com,那可以使用没问题:

// 将能在你的浏览器里工作
String[] lines = loadStrings( "http://www.我的域名.com/数据.html");

2)但是,如果你要求一个来自不同域的URL,你就不那么走运了:

// 将无法在你的浏览器里工作
String[] lines = loadStrings( "http://www.立花里子的域名.com/数据.htm");

48. 关于此点的一个解决办法是,在你的服务器上创建一个代理脚本与你的applet共存,链接外部URL并将信息传递回applet —— 实际上,你是在忽悠你的applet,让它认为它正在检索的只有本地信息。

49. 另一个办法是为你的applet签名。签名一个小程序即说“你好,我叫ww,我制作了这个小程序。如果你信任我请允许我访问它通常不让别人访问的的资源”的过程。

50. 如果你对以上问题感兴趣或者恰巧在使用中需要解决它们,可以访问这里以获取更多的提示。

Processing:数据输入(上)

1. 方法indexOf()返回一个关键字(或词)在字符串中的位置,它有一个引数:关键字。例如下例将返回数值3:

String search = "def";  
String toBeSearched = "abcdefghi"; 
int index = toBeSearched.indexOf(search);

2. 逐一println()一下下边诸例,数一数(记住一个字符串的第一位是0而不是1,因为他是一个数组,所以你应当从0数起),你便会了解它的用法:

String sentence = "The quick brown fox jumps over the lazy dog." ;  
println(sentence.indexOf("quick"));  
println(sentence.indexOf("fo"));  
println(sentence.indexOf("The"));  
println(sentence.indexOf("blah blah"));

3. 是的,试到上边最后一个例子的时候,你会发现它返回的值是-1。这是一个很好的选择,因为在数组序号中不存在-1,因此,当indexOf()找不到一个关键字的时候,返回值-1来表述是再合适不过的。

4. 一个字符串的一部分被称为子串(substring),一个子串可以藉由substring()函数得到。它有两个引数:开始和结束的位置。比如下例的结果为“def”:

String alphabet = "abcdefghi" ;   
String sub = alphabet.substring(3,6);

5. 通过上例我们发现,子串开始于给定的开始位置(3),却结束于给定的结束位置减一(6)。减一的好处是,如果你需要的子串结束于字符串末尾,你可以方便的使用thestring.length(),同时,你可以用结束位置减去起始位置来简单的计算子串的长度。

6. 你可以这样从整个字符串中获取到”quick brown fox jumps over the lazy dog”:

String sentence = "The quick brown fox jumps over the lazy dog."; 
int foxIndex=sentence.indexOf("fox jumps over the lazy dog");  
int periodIndex = sentence.indexOf(".");  
println(sentence.substring(foxIndex,periodIndex));

当然,如果你愿意数的话,你也可以这样写:

String sentence = "The quick brown fox jumps over the lazy dog.";  
println(sentence.substring(16,(sentence.length()-1)));

第二个方式,虽然现在看起来比第一种写得更少,但是设想一下如果要在一篇文章中截取某个部分的话,硬数还不数死你?!

7. 下例实现了用户键盘输入录入:

PFont f;

// 保存输入文字的变量
String typing = "";

// 保存回车后保存文字的变量
String saved = "";

void setup() {
  size(300,200);
  f = createFont("Arial",16,true);
}

void draw() {
  background(255);
  int indent = 25;
  
  // 设置字体和文本颜色
  textFont(f);
  fill(0);
  
  // 显示一切
  text("点击这个小程序并用键盘输入。\n敲回车保存你的输入。", indent, 40);
  text(typing,indent,90);
  text(saved,indent,130);
}

void keyPressed() {
  // 如果回车键按下, 保存并清除字符串
  if (key == '\n' ) {
    saved = typing;
    // 可以通过将一个字符串设置等于""实现将其清除
    typing = ""; 
  } else {
    // 否则,串联字符串
    // 用户输入的每个字符都被加到String变量之后.
    typing = typing + key; 
  }
}

8. p5提供了其他关于字符串组合及拆分的函数:split()join()。顾名思义,split()用来将一个长字符串拆解为一个字符串数组,它有两个引数:将被拆解的字符串和分界符(delimiter):

// 用空格作为界定符拆分一个字符串 
String spaceswords = "oNEwAY is a Post Rock band from Kunming." ;   
String[] list = split(spaceswords, " ");   
for (int i = 0; i < list.length; i++) {   
  println(list[i] + " " + i);      
}

或者用逗号作为界定符:

// 用逗号作为界定符拆分一个字符串    
String commaswords = "oNEwAY,is,a,Post,Rock,band,from,Kunming." ;   
String[] list = split (commaswords, ',');
for (int i = 0; i < list.length; i++) {   
  println(list[i] + " " + i);      
}

二者运行后在信息窗口均返回结果:
oNEwAY 0
is 1
a 2
Post 3
Rock 4
band 5
from 6
Kunming. 7

9. 如果要使用多于两个分界符,你需要使用splitTokens()函数,它与split()的用法基本一致,除了一点区别:字符串中的任何元素都有作为一个界定符的机会:

// 多分界符拆分字符串 
String stuff = "hats  &  apples, cars + phones % elephants dog.";   
String[] list = splitTokens(stuff, "& , + .");   
for (int i = 0; i < list.length; i++) {   
  println(list[i] +  " " + i);      
}

运行后信息窗口返回结果:
hats 0
apples 1
cars 2
phones 3
% 4
elephants 5
dog 6

由此可见,作为界定符的元素将不会出现在数组中。

10. 如果你要拆分一个字符串中的数字,可用int()函数将结果数组转化为一个整数数组:

// 计算字符串内各数字之和 
String numbers = "8,67,5,309";   
// 转化字符串数组为整数数组
// 字符串中的数字并非数字并不可用于数学计算,除非事先将它们转为数字类型
int[] list = int(split(numbers, ','));   
int sum = 0;   
for (int i = 0; i < list.length; i++) {   
  sum = sum + list[i];      
}
println(sum);

11. join()的过程与split()正相反,它是将一个字符串数组整合为一个长字符串。同样是两个引数:欲结合数组,分隔符。
假设这样一个数组:

String[] lines = {"It","was","a","dark","and","stormy","night."};

用“+”和for循环,我们可以这样连接它们:

// 手工连接 
String onelongstring = " ";  
for (int i = 0; i < lines.length; i++) {  
  onelongstring = onelongstring + lines[i] + " ";     
} 

使用join(),一切得到最大简化:

String onelongstring = join (lines, " ");

12. p5可以读取一个外部txt档:

String[] lines = loadStrings("text.txt");   
println ("there are " + lines.length + " lines");
for (int i = 0; i < lines.length; i++){   
  println(lines[i]);      
}

是的,仍然,作为常识,你需要将text.txt文件放入这个sketch下的data文件夹内。

13. 下例为一个可视化效果的初级例子,将data.txt中的数据转化为柱状图显示:
13.1. 创建并保存data.txt,内容(当然,你可以随便写):131,85,87,16,169,140,153,72,113,123
13.2. 用p5运行如下代码:

int[] data;

void setup() {
  size(200,200);
 // 文件中的文字被载入一个数组. 
  String[] stuff = loadStrings("data.txt");
  // 这个数组只有一个元素因为它只有一行. 
  // 用逗号作为分界符,并将数组转化为整数数组
  data = int(split(stuff[0], ',' ));
}

void draw() {
  background(255);
  stroke(0);
  
  for (int i = 0; i < data.length; i ++ ) {
    // 数组元素被用于设置矩形的颜色和高度.
    fill(data[i]); 
    rect(i*20,0,20,data[i]);
  }
  noLoop();
}

14. 继续一个稍微高级的例子,将文本文件内的数据传入一个对象的构造器:

Bubble[] bubbles;

void setup() {
  size(200,200);
  smooth();
  
  // 以字符串数组的方式载入文本文件
  String[] data = loadStrings("data.txt");
  
  // 气泡对象数组的大小由文本文件中总的行数决定
  bubbles = new Bubble[data.length]; 
  for (int i = 0; i < bubbles.length; i ++ ) {
    // 每行都被拆分为一个小数数组.
    float[] values = float(split(data[i], "," )); 
    // 数组中的值被传递入气泡类的构造器.
    bubbles[i] = new Bubble(values[0],values[1],values[2]); 
  }
}

void draw() {
  background(255);
  
  // 显示并移动所有气泡
  for (int i = 0; i < bubbles.length; i ++ ) {
    bubbles[i].display();
    bubbles[i].drift();
  }
}

class Bubble {
  float x,y;
  float diameter;
  float speed;
  float r,g;

  // 构造器初始化颜色和大小
  // 随机决定位置
  Bubble(float r_, float g_, float diameter_) {
    x = random(width);
    y = height;
    r = r_;
    g = g_;
    diameter = diameter_;
  }

  // 显示气泡
  void display() {
    stroke(0);
    fill(r,g,255,150);
    ellipse(x,y,diameter,diameter);
  }

  // 移动气泡
  void drift() {
    y += random(-3,-0.1);
    x += random(-1,1);
    if (y < -diameter*2) {
      y = height + diameter*2;
    }
  }
}

上例中data.txt内的内容为(当然,你可以随便写):
88,149,22
193,78,8
90,152,56
136,18,37
47,2,55
36,142,57
10,61,31
9,121,49
156,60,12
71,200,21

15. 现在我们已经学会舒服的载入信息,然后准备问接下来的这个问题:我们该如何存储信息然后在下一次程序运行的时候载入新信息?比如说,让上例的气泡们当鼠标在其上滚动的时候变化。

// Bubble类中rollover()函数依据引数(mx, my)是否位于气泡内返回一个布尔值(真或假)
boolean rollover(int mx, int my) {   
  if (dist(mx,my,x,y) < diameter/2) {   
    return true;      
  } else {   
    return false;      
  }       
}

我们将鼠标位置(mx, my)到气泡圆心的距离与气泡半径的距离作比较,便可以确定鼠标是否位于圆内。

检查鼠标是否位于气泡内
检查鼠标是否位于气泡内
for (int i = 0; i < bubbles.length; i++) {  
  bubbles[i].display(); 
  bubbles[i].drift(); 
  if (bubbles[i].rollover(mouseX,mouseY)) {  
    bubbles[i].change(); 
  }       
}

一旦我们执行change()函数以调整Bubble的变量,我们便可以使用p5的saveStrings()函数将新信息存储到一个文本文件中。saveStrings()函数从本质上与loadStrings()相反,它接受一个文件名和一个字符串数组并且将那个数组保存到文件中。

String[] stuff = {"Each String", "will be saved", "on a", "separate line"};  
saveStrings("data.txt", stuff);

保存并运行上边这段代码,将在你的文件夹根目录内(注意不是data文件夹)创建如下txt文档。

data.txt
data.txt

如果你想要将此文件写入data文件夹,那么你需要指定路径。同样,如果该文件已经存在,它将被重写。

16. OK,具备以上知识之后,我们便可以为气泡例子整一个saveData()函数来改变"data.txt"中的信息。在本例中,我们将在鼠标点击后存储新的数据。

// 一个气泡对象的数组
Bubble[] bubbles;

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

  // 将文本文件作为一个字符串数组载入
  String[] data = loadStrings("data.txt");

  // 气泡对象数组的大小由文本文件中总的行数决定
  bubbles = new Bubble[data.length]; 
  for (int i = 0; i < bubbles.length; i ++ ) {
    // 每行都被拆分为一个小数数组.
    float[] values = float(split(data[i], "," )); 
    // 数组中的值被传递入气泡类的构造器.
    bubbles[i] = new Bubble(values[0],values[1],values[2]); 
  }
}

void draw() {
  background(255);

  // 显示并移动所有气泡
  for (int i = 0; i < bubbles.length; i ++ ) {
    bubbles[i].display();
    bubbles[i].drift();

    // 如果鼠标在气泡上滚动,则改变气泡
    if (bubbles[i].rollover(mouseX,mouseY)) {
      bubbles[i].change();
    }
  }
}

// 当鼠标按下时保存新的气泡数据
void mousePressed() {
  saveData();
}

void saveData() {
  // 对每个气泡存储一个字符串
  String[] data = new String[bubbles.length];

  for (int i = 0; i < bubbles.length; i ++ ) {
    // 连接气泡变量
    data[i] = bubbles[i].r + " , " + bubbles[i].g + " , " + bubbles[i].diameter;
  }

  // 存储到文件
  // 指向data文件夹使用saveStrings()重写同一个文件
  path to saveStrings().
  saveStrings("data/data.txt", data); 
}

// Bubble类
class Bubble {
  float x,y;
  float diameter;
  float speed;
  float r,g;
  
  Bubble(float r_,float g_, float diameter_) {
    x = random(width);
    y = height;
    r = r_;
    g = g_;
    diameter = diameter_;
  }
  
  // 检查鼠标是否位于气泡内
  boolean rollover(int mx, int my) {
    if (dist(mx,my,x,y) < diameter/2) {
      return true;
    } else {
      return false;
    }
  }
  
  // 改变气泡变量
  void change() {
    r = constrain(r + random(-10,10),0,255);
    g = constrain(g + random(-10,10),0,255);
    diameter = constrain(diameter + random(-2,4),4,72);
  }
  
  // 显示气泡
  void display() {
    stroke(0);
    fill(r,g,255,150);
    ellipse(x,y,diameter,diameter);
  }
  
  // 气泡向上漂流
  void drift() {
    y += random(-3,-0.1);
    x += random(-1,1);
    if (y < -diameter*2) {
      y = height + diameter*2;
    }
  }
}

17. 我们还可以使用一个统一资源定位器(URL)当作loadStrings()的参数:

String[] lines = loadStrings("http://www.douban.com/artist/oneway");

运行它的结果将返回你输入URL页面的源文件。要继续接下来的学习,你无须是一个HTML专家,但如果你完全不熟悉HTML,你可能需要阅读:http://en.wikipedia.org/wiki/HTML

18. 不像以逗号划界的文本文件,将HTML源文件存入一个字符串数组是不实用的(每个元素代表源文件中的一行)。将数组转存为一个长的字符串可以让事情简单一些。之前说过的,用join()可以做到这一点:

String onelongstring = join(lines, "  " );

19. 当获取到网页源文件后,很多情况我们仅需要其中的一小段。也许是气象信息,股市行情,或者一个新闻标题。我们可以使用indexOf(), substring(), 和length()来为我们从一对文字中找到我们需要的数据。用以下字符串作为例子:

String stuff = "Number of apples:62. Boy, do I like apples or what!";

比如说我们要从以上字符串中提取苹果的数量,我们的算法应该如下:
1)找到子字符串的结尾“apples:”。称其为开始。
2)找到 “apples:”之后的第一个句号。称其为结束。
3)在开始和结束间创建一个子字符串。
4)将字符串转换至一个数字(如果我们希望如此使用它)
写成代码:

// 一个字符串的“结束点”可以通过搜索那个字符串的索引并加上其长度(本例中“apples:”长度为7)来获得
int start = stuff.indexOf("apples:") + 7;        // 第一步   
int end = stuff.indexOf(".", start);      // 第二步   
String apples =  stuff.substring(start,end);        // 第三步   
int apple_no = int(apples);        // 第四步

20. 以上代码属于小聪明,但我们应当更谨慎,以免因无法找到子字符串而产生错误。我们可以加入一些查错的代码,然后将它们做入一个函数:

// 在两个子字符串间返回一个子字符串的函数 
String giveMeTextBetween(String s, String startTag, String endTag) {   
  String found = " ";   
  // 找到开始标签的索引   
  int startIndex = s.indexOf(startTag);   
  // 如果我们一无所获   
  if (startIndex == –1) return " ";   
  // 移动到开始标签的末尾   
  startIndex += startTag.length();   
  // 找到结束标签的索引 
  int endIndex = s.indexOf(endTag, startIndex); 
  //  如果我们一无所获 
  if (endIndex == –1) return " ";  
  // 返回其间的文字 
  return s.substring(startIndex,endIndex); 
}

21. 然而了解HTML的同学都该知道,要从它里边提取出有用的数据是如此困难的一件事。其实对于从网站获取数据来说,一个XML(扩展标记语言)的feed将更可靠和易于分析。我们首先从雅虎天气获取XML feed:
http://xml.weather.yahoo.com/forecastrss?p=CHXX0076&u=c
在p5中,从一个XML feed获取数据的方式是使用XML库。但是,为了从一个较低的层面映证字符串分析,作为一个练习,我们将使用我们的loadStrings()手动剥离提取技术。

22. 透过上边那个XML的源代码(建议用Mac的同学使用FireFox查看,Safari无法查看或我还不知道如何查看,因为Safari会主动将这个链接转换为"feed://"打头的),我们可以看到2010年1月28日晚上十点昆明的气温是11摄氏度(也~比起你们来说很暖和吧~)。

<yweather:condition  text="Fair"  code="33"  temp="11"  date="Thu, 28 Jan 2010 10:00 pm CST" />

气温总在变化而XML的格式不会,因此我们可以推论我们搜索的开始标签应该是:

temp="

结束标签是:

"

23. 知道了开始和结束标签,我们便可使用上边那个giveMeTextBetween()来提取温度了:

String url = "http://xml.weather.yahoo.com/forecastrss?p=CHXX0076&u=c";   
String[] lines = loadStrings(url);   
// 为搜索整个页面而处理数组   
String xml = join(lines," ");   
// 搜索温度   
String tag1 = "temp = \"";   
String tag2 = "\"";   
temp = int(giveMeTextBetween (xml,tag1,tag2));   
println(temp);

注意,要在Java中显示一个引号,需要在要显示引号前加上一个右斜杠:

String q = "这个字符串有一个\"号";

24. 下例从雅虎天气XML feed中检索温度并将之显示于萤幕上。这个例子同样使用了面向对象编程,将所有字符串分析功能做入一个WeatherGrabber类:

PFont f;
String[] zips = { "CHXX0076" , "21209" , "90210" };
int counter = 0;

// WeatherGrabber对象为我们服务!
WeatherGrabber wg;

void setup() {
  size(200,200);
  
  // 创建一个WeatherGrabber对象
  wg = new WeatherGrabber(zips[counter]);
  // 告诉它申请天气
  wg.requestWeather();
  
  f = createFont( "Lucida" ,16,true);
}

void draw() {
  background(255);
  textFont(f);
  fill(0);
  
  // 获取要显示的值
  String weather = wg.getWeather();
  int temp = wg.getTemp();
  
  // 显示所有我们想要显示的东西
  text(zips[counter],10,160);
  text(weather,10,90);
  text(temp,10,40);
  text("点击转换城市代码. " ,10,180);
  
  // 基于温度画一个小温度计
  stroke(0);
  fill(175);
  rect(10,50,temp*6,20);
}

void mousePressed() {
  // 增大计数器并获取下一城市代码的气温
  counter = (counter + 1) % zips.length;
  wg.setZip(zips[counter]);
  // 每当鼠标点击,这个数据被一个新的城市代码再次请求
  wg.requestWeather(); 
}

// WeatherGrabber类
class WeatherGrabber {
  
  int temperature = 0;
  String weather = "";
  String zip;
  
  WeatherGrabber(String tempZip) {
    zip = tempZip;
  }
  
  // 设置一个新的城市代码
  void setZip(String tempZip) {
    zip = tempZip;
  }
  
  // 获取温度
  int getTemp() {
    return temperature;
  }
  
  // 获取天气
  String getWeather() {
    return weather;
  }
  
  // 创建实际的XML请求
  void requestWeather() {
    // 将所有HTML/XML源代码放入一个字符串数组
    // (每行是数组中的一个元素)
    String url = "http://xml.weather.yahoo.com/forecastrss?p=" + zip + "&u=c";
    String[] lines = loadStrings(url);
    
    // 将数组转入一个长的字符串
    String xml = join(lines, ""); 
    
    // 搜索气候状况
    String lookfor = "yweather:condition  text= \"";
    String end = "\"";
    weather = giveMeTextBetween(xml,lookfor,end);
    
    // 搜索气温
    lookfor = "temp=\"";
    temperature = int(giveMeTextBetween (xml,lookfor,end));
  }
  
  // 一个从两个子字符串之间返回子字符串的函数
  String giveMeTextBetween(String s, String before, String after) {
    String found = "";
    int start = s.indexOf(before);    // 找到开始标签的索引
    if (start == - 1) return"";       // 如果一无所获,返回一个空字符串
    start += before.length();         // 移动到开始标签的末尾
    int end = s.indexOf(after,start); // 找到结束标签的索引
    if (end == -1) return"";          // 如果一无所获,返回一个空字符串
    return s.substring(start,end);    // 返回两者之间的文本
  }  
}

无限崩溃中,碎了 T_T

Processing:文本

1. 本课我们要学一个新的类,称为字符串(string)。对我们来说,这并不是一个全新的概念,之前我们也曾接触过它们:

println("printing some text to the message window!");      // 列印一个字符串
PImage img = loadImage("filename.jpg");   // 为文件名使用一个字符串

这样看起来,我们似乎可以简单的将字符串理解为“在双引号内的内容”。

2. 字符串的核心其实是储存字母数组,如果没有string类,那么我们要写一行字的代码可能会变成:

char[] sometext = { 'H', 'e', 'l', 'l' , 'o' , ' ', 'W' , 'o' , 'r', 'l' ,  'd'};

这么搞绝对会被搞死,如果使用string对象,世界立马变得简单美好起来:

String sometext = " 如何做一个字符串? 在引号间输入字符!";

3. 我们必须记住,一个string是一个伴随方法(method)的对象。类似于PImage对象可以在存储一个图像数据的同时,还可以使用方法copy(), loadPixels()等等。对于string来说,一个方法是 charAt(),它可以返回一个字符串里给定序号的字母。同数组一样,字符串的第一个字母同为第0号。

4. 通过Daniel随后出的一题我意外的发现,在一个字符串内,空格也算一个“字母”,也占序号,无论这个空格出现在字符串内的任何地方。

5. 另一有用的方法是length()。这常会与数组length的属性混淆。不过你记得按一下方式呼叫它就对了:

String message = "这个字符串有十五个字符那么长。";   
println(message.length());

通过笨办法——数数的方式,我们看到,句号也会被当作一个“字母”。因此我们可以推论,在一个字符串内任何占位符都会被当作一个“字母”被进行计数。

6. 顺序列印一个字符串的每一个字母:

String message = "一堆文字."; 
    for(int i = 0; i < message.length(); i++) {
         char c =  message.charAt(i); 
         println(c);

7. toUpperCase()可以使字符串内字母全大写:

String message = "a bunch of text here.";
String uppercase = message.toLowerCase();   
println(uppercase);

8. toLowerCase()可以使字符串内字母全小写,用法同上。

9. 细心的你可能会发现,在第7条的例子当中,为什么我们要用println(uppercase); ,而不用println(message);?因为string对象是一种特殊的不可变对象。所谓“不可变”是指它的数据永远都不可变。这就意味着任何时候我们要改变一个字符串,我们必须创建一个新的字符串。因此在转换大写的时候,方法toUpperCase()返回的是一个全大写的字符串对象的副本。

10. 最后,我们看看equals()。说到“等于”,也许你首先会想到这么写:

String one = "hello";  
String two = "hello";  
println(one == two);

从技术角度讲,当对对象使用"==",它比较的是每个对象的内存位置。尽管每个字符串包含相同的数据——"hello"——如果它们是不同的对象实例,则"=="可能会返回一个错误的对比。这样的时候,就是equals()发挥作用的时候,它用来对比两个字符串是否包含相同的数据,而无论它们存储的位置在哪里。

String one = "hello";  
String two = "hello";
println(one.equals(two));

尽管以上两种方法都会返回正确的结果,但是用equals()更保险。基于string对象不同的创建方式,"=="不会始终保持正确。

11. 字符串还可用“+”号连接。

String helloworld = "Hello" + "World";

变量照使:

int x = 10;   
String message = "x的值是:" + x;

“图像”一课载入编号图像数组是这一用法一个很好的例子。

12. 要在萤幕上显示一些文本,我们需要做以下简单几步:
12.1. Tools > Creat Font 选择一个子体。这将创建并将字体文件放入你的data文件夹。p5的字体格式很特殊——"vlw",它使用图像来显示每个字母。因为如此,你同时还要指定你想要显示文字的大小。
12.2. 申明一个PFont类型的对象:

PFont f;

12.3. 用loadFont()载入字体。载入一次就够了,所以我门将它放在setup()里,和载入图像一个道理。

f = loadFont("ArialMT-16.vlw");

12.4. 用textFont()指定字体。两个引数,第一个字体变量,第二个字体大小(可选)。如果不指定字体大小,则字体会以最初载入的大小显示。指定一个与创建字体大小不同的字体大小会影像它的显示(和放大缩小图像一样)。

textFont(f,36);

12.5. 用fill()指定颜色。
12.6. 呼叫text()函数显示文本。三个引数:文本,x位置,y位置。

text( "呜呜呜... 字符串...  " ,10,100);

12.7. 上完整的例子:

// Step 2: 申明PFont变量
PFont f;  

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

  // Step 3: Load Font
  f = loadFont( "ArialMT-16.vlw" ); 
} 

void draw() {
  background(255);
  textFont(f,16); // Step 4: 指定要用的字体
  fill(0);        // Step 5: 指定字体颜色

  // Step 6: 显示文本
  text ( "oNEwAY is a Chinese Post-Rock Band" ,10,100); 
}

通过上例,我发现这玩意不会自动换行,,多出来的部分就会显示在窗口之外而不可见。

13. 字体还可被createFont()函数创建:

myFont = createFont( "Georgia", 24, true);   // 三个引数分别是:字体名字,字体大小,是否开启平滑的一个布尔表达式

createFont()允许你创建安装于本机却在p5默认选项中不支持的字体。另外,createFont()允许字体缩放到任意大小而不影响显示质量。更多介绍请参看p5在线手册.

14. PFont.list()可以查看所有可用的字体:

println(PFont.list());     // 列印全部可用于createFont() 的字体。

15. 下面这个练习在弹跳的球旁边伴随显示球的x,y位置(当然一个更好的显示方式是将文本位置固定):

PFont f;

float x = 100; 
float y = 0; 
float speed = 0; 
float gravity = 0.05;

void setup() {
  size(400,200);
  f = createFont("Arial", 16, true);
  smooth();
}

void draw() {
  background(255);
  stroke(175);
  rectMode(CENTER);
  ellipse(x,y,10,10);
  
  y = y + speed;
  
  // Add gravity to speed.
  speed = speed + gravity;
  
  // If square reaches the bottom
  // Reverse speed
  if (y > height) {
    // Multiplying by -0.95 instead of -1 slows the square down each time it bounces (by decreasing speed).  
    // This is known as a "dampening" effect and is a more realistic simulation of the real world (without it, a ball would bounce forever).
    speed = speed * -0.95;  
  }
  
  textFont(f,10);
  fill(0);
  // 将浮点值转为整数并显示 
  text((int)x+", "+(int)y,x+8,y-8);
}

16. textAlign() —— 指定文本对齐模式:

PFont f;

void setup() {
  size(400,200);
  f = createFont("Arial", 16, true);
}

void draw() {
  background(255);
  stroke(175);
  line(width/2,0,width/2,height);
  textFont(f);
  fill(0);
  
  // textAlign()设置文本对齐模式. 它有一个引数: CENTER, LEFT, 或者 RIGHT.
  textAlign(CENTER); // 居中
  text("这是居中的文字." ,width/2,60);
  textAlign (LEFT) ; // 居左
  text("这是左对齐的文字." ,width/2,100);
  textAlign(RIGHT); // 居右
  text("这是右对齐的文字." ,width/2,140);
}

17. textWidth() —— 计算并返回任何字母或文本字符串的宽度。比如说我们要在萤幕下方做一个从右至左的新闻滚动条。当文字全部滚入左边后,从右边重新开始滚动。如果我们知道文本开头的x位置和文本的宽度,我们便可知道它什么时候会完全滚入左边。textWidth()给我们的正是这个宽度。
首先,我们在setup()内申明新闻,字体,x变量,并初始化它们。

//   一条新闻   
String headline = "New study shows computer programming lowers cholesterol.";   
PFont f;   // 全局字体变量   
float x;   // 新闻的水平位置   
void setup()  {   
  f = createFont( "Arial",16,true); // 载入字体   
  x = width; // 将x放到右边 
} 

draw()里边:

// 在x位置显示新闻   
textFont(f,16);   
textAlign(LEFT);   
text(headline,x,180);

让x减小,以使文本向左走:

x = x – 3;

稍微复杂一点的一步,测试文本是否已经全部滚入左侧,如果已经离开萤幕,重置其到最右边:

// 如果x小于文本宽度的负值,则这段文本便完全处于萤幕外
float w = textWidth(headline);   
if (x < -w)  {  
  x = width;      
}

下例即以上代码的合体,唯一不同的是,它分别滚动两段不同的文本:

// 新闻数组
String[] headlines = {
  "Processing downloads break downloading record." ,
  "New study shows computer programming lowers cholesterol." ,
};

PFont f; // 全局字体变量
float x; // 水平位置
int index = 0;

void setup() {
  size(400,200);
  f = createFont( "Arial" ,16,true);
  
  // 初始化x到最右
  x = width;
}

void draw() {
  background(255);
  fill(0);
  
  // 在x位置显示新闻
  textFont(f,16);
  textAlign (LEFT);
  
  // 基于"index"变量的值显示数组的一个特定字符串
  text(headlines[index],x,180); 
  
  // 递减x
  x = x - 3;
  
  // 如果x小于文本宽度的负值,则这段文本便完全处于萤幕外
  // textWidth()被用来计算当前字符串的宽度.
  float w = textWidth(headlines[index]); 
  if (x < -w) {
    x = width;
    // 递增index,这样当当前字符串离开萤幕便显示一个新的字符串。
    index = (index + 1) % headlines.length; 
  }
}

18. 除了textAlign()和textWidth()外,还有textLeading(), textMode()和textSize()可用,自己看p5在线参考for more。

19. 习题17-6,做一个接头接尾持续滚动的文本条。看似简单,我却不会,,已经给了Daniel邮件,等他答案。

20. 结合像素数组,我们可以用字母做一个视频马赛克,代码长度不短:

import processing.video.*;

// 缩放值
int videoScale = 14;
// 我们系统中行和列的数量
int cols, rows;
// capture对象
Capture video;

// 马赛克模式中的源文本. 越长的字符串也许会出现越有趣的结果.
String chars = "fuckyoubaby" ; 

PFont f;

void setup() {
  size(640,480);
  // 建立行和列
  cols = width/videoScale;
  rows = height/videoScale;
  video = new Capture(this,cols,rows,15);
  
  // 载入字体
  // 使用一个固定宽度的字体. 对大多数字体来说, 不同的字母有不同的宽度. 
  // 在固定宽度字体钟, 所有字母等宽. 
  // 这对本例很有用,因为我们要在空间平均排布这些字母 
  f = loadFont("Courier-Bold-20.vlw");
}

void draw() {
  background(0);
  
  // 从摄像头读取图像
  if (video.available()) {
    video.read();
  }
  video.loadPixels();
  
  // 使用一个计数器变量来数字符串中的字母
  int charcount = 0;
  // 开始循环行
  for (int j = 0; j < rows; j ++ ) {
    // 开始循环列
    for (int i = 0; i < cols; i ++ ) {
      
      // 我们在哪?
      int x = i*videoScale;
      int y = j*videoScale;
      
      // 在像素数组内寻找合适的颜色
      color c = video.pixels[i + j*video.width];
      // 显示字符串中的各个字母来替代矩形
      textFont(f);
      fill(c);
      
      // 文本内的一个字母在像素位置显示并被染色. 
      // 一个计数器变量—— "charcount"被用来每次触发一个字符串内的字母.
      text(chars.charAt(charcount),x,y);
      
      // 继续下一个字母
      charcount = (charcount + 1) % chars.length();
    }
  }
}

21. 让字母根据亮度调整大小,你只需加入以下几行(如果你不想要彩色而只要黑白,那么将关于颜色的几行注释掉即可),不过记得使用createFont()函数创建字体,因为这样创建的字体不会因为缩放而失真:

亮度调整字体大小
亮度调整字体大小

float b = brightness(video.pixels[i + j*video.width]);
float fontSize = 36 * (b / 255);
textSize(fontSize);

虽然能运行成功,但我却持续的得到错误提示:“Thu Dec 3 23:53:34 xxx.local java[431] : CGAffineTransformInvert: singular matrix.”。关于这点,我正在等Daniel回答。

22. 文本同样可以实现位移与旋转:

PFont f;
String message = "这段文本正在旋转";
float theta;

void setup() {
  size(200,200);
  f = createFont("Arial", 20, true);
}

void draw() {
  
  background(255);
  fill(0);
  textFont(f);                 // 设置字体
  translate(width/2,height/2); // 由中点旋转
  rotate(theta);               // 以theta旋转
  textAlign(CENTER) ;
  
  // 在平移和旋转后这段文本居中对齐并显示在(0,0). 
  // 如果你忘了平移和旋转,返回去复习一下吧.
  text(message,0,0); 
  
  // 增加旋转(加 为顺时针)
  theta += 0.05; 
}

23. 是的,如上例可见,p5支持并可正确显示中文,但是它并不允许你在程序中直接输入中文,没事,在文本编辑器里写好再把它们粘贴进去就好了~

24. 如果你要新起一行,可以这么写:

String message = "这是第一行\n这是第二行";

“\”意思是"新的一行"。在Java中,不可见字母可以与一个“转义序列(escape sequence)”一起北河并到一个字符串内——一个斜杠"\"伴随一个字母。以下是一些基本用法:
\n—新的一行
\r—回车
\t—tab
\’—单引号
\”—双引号
\\—斜杠

25. 在一些制图软件中,要求一串文本中每个字符都得独立渲染。在p5中,这样的解决方式是循环一个字符串,每次显示一个字符。

// 第一个字母在位于10个像素的水平位置
int x = 10;   
for (int i = 0; i < message.length(); i++ )  {
  //  使用charAt()函数使每次显示一个字符 
  text(message.charAt(i), x, height/2);
  // 每个字符间间隔10个像素   
  x += 10; 
}

26. 呼叫每个字符的text()函数给了我们之后的例子更多的发挥空间(染色,设置大小,在一个字符串内单独放置字符)。10个像素的间隔不一定准确,因为不一定每个字符的宽度都是10。准确的间隔在下例中由textWidth()实现。注意它是如何在随机字符大小的情况下仍然保持合适的间隔的:

PFont f;   
String message = "oNEwAY是一支来自昆明的后摇滚乐队";   

void setup()  {   
  size(500,200);   
  f = createFont( " Arial " ,12,true);      
}    

void draw()  {  
  background(255);
  fill(0);
  textFont(f,12);  
  int x = 10;   
  for (int i = 0; i < message.length(); i++ )  {   
    textSize(random(12,36));   
    text(message.charAt(i), x, height/2);   
    x += textWidth(message.charAt(i)); 
  }
}

我注意到如果让上边这段代码一直跑的话,最后会停止(因为出错了..),解决办法是,要么你加个noLoop();,要么你把它从draw()里边给整出来:

PFont f;   
String message = "oNEwAY是一支来自昆明的后摇滚乐队";   

size(500,200);   
f = createFont( " Arial " ,12,true);
background(255);
fill(0);
textFont(f,12);  
int x = 10;   
for (int i = 0; i < message.length(); i++ )  {   
  textSize(random(12,36));   
  text(message.charAt(i), x, height/2);   
  x += textWidth(message.charAt(i));    
}

然后我发现在这样的情形下,使用"\n"换行的方式是没用的。

27. 用自适应间隔重做第一个字母马赛克的例子:

import processing.video.*;

// 缩放比例
int videoScale = 16;
// 行数和列数
int cols, rows;
// capture对象
Capture video;

// 一个字符串和字体
String chars = "oNEwAYisapostrockbandfromkunmingyunnanchina";
PFont f;

void setup() {
  size(640, 480);
  //建立行和列
  cols = width/videoScale;
  rows = height/videoScale;
  video = new Capture(this,cols,rows,15);

  // 载入字体
  f = createFont("Arial",18,true);
}

void draw(){ 
  // 从摄像头读取图像
  if (video.available()) {
    video.read(); 
  }
  video.loadPixels();
  //image(video,0,0,width,height);

  background(0);

  // 使用一个计数器
  int charcount = 0;

  // 开始循环行
  for ( int j = 0; j < rows;j++) {
    // 现在开始循环列, 而不是逐一循环像素
    // 我们基于字符宽度使用一个浮点变量x来进行移动
    float x = 0;
    while (x < width) {
      // 我们垂直位置在哪?
      int y = j*videoScale;
      // 我们水平位置在哪? 转化到整数, 缩小, 并确保我们始终位于萤幕内 
      int pix = constrain((int) (x / videoScale),0,cols-1);

      // 在像素数组内选取正确的颜色
      color c = video.pixels[pix+j*video.width]; 

      // 显示字符串中的独立字符
      // 替代矩形
      textFont(f);
      fill(c);
      char ch = chars.charAt(charcount);
      text(ch,x,y);
      // 继续下一个字符, 到最末的时候重新从0开始循环
      charcount = (charcount + 1) % chars.length();
      // 依据字符宽度移动x
      x += textWidth(ch);
    }
  }
}

28. 这种方法同样可以用于各个字符独立运动的案例。下边这个例子使用面向对象得方式制作,让字符串中的每一个字符成为一个对象,使它们既能在适当的位置显示又能在萤幕内独立运动:

PFont f;
String message = "oNEwAY是一支来自云南昆明的后摇滚乐队";

// 字母数组
Letter[] letters;

void setup() {
  size(460,200);
  
  // 载入字体
  f = createFont("Arial", 20, true);
  textFont(f);
  
  // 创建与字符串同样大小的数组
  letters = new Letter[message.length()];
  
  // 在正确的x位置初始化字母
  int x = 66;
  for (int i = 0; i < message.length(); i ++ ) {
    // 字符对象依它们在字符串中的位置及显示初始化
    letters[i] = new Letter(x,100,message.charAt(i)); 
    x += textWidth(message.charAt(i));
  }
}

void draw() {
  background(255);
  for (int i = 0; i < letters.length; i ++ ) {
    
    // 显示全部字符
    letters[i].display();
    
    // 如果鼠标按下,字符开始抖动
    // 如果放开,它们回归原位
    if (mousePressed) {
      letters[i].shake();
    } else {
      letters[i].home();
    }
  }
}

// 一个描述单独字符的类
class Letter {
  char letter;
  
  // 对象知道它最初的位置
  float homex,homey;
  
  // 以及它现在的位置
  float x,y;
  
  Letter(float x_, float y_, char letter_) {
    homex = x = x_;
    homey = y = y_;
    letter = letter_;
  }
  
  // 显示字符
  void display() {
    fill(0);
    textAlign(LEFT);
    text(letter,x,y);
  }
  
  // 随机移动字符
  void shake() {
    x += random(-2,2);
    y += random(-2,2);
  }
  
  // 在任何一点, 通过呼叫home()函数便可将位置从当前重置到最初。
  void home() { 
    x = homex;
    y = homey;
  }
}

29. 这种方法还允许我们将文字附于一个曲线上。在这么做之前,我们先看看如何将一系列矩形附于一个曲线上:

PFont f;

// 圆形的半径
float r = 100;

// 矩形的长、宽
float w = 40;
float h = 40;

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

void draw() {
  background(255);
  
  // 从中心开始并画圆
  translate(width/2, height/2);
  noFill();
  stroke(0);
  // 这个圆就是我们的曲线
  ellipse(0, 0, r*2, r*2); 
  // 10个矩形围绕曲线
  int totalBoxes = 10;
  // 我们必须保持记录围绕曲线的位置
  float arclength = 0;
  // 为每个矩形
  for (int i = 0; i < totalBoxes; i ++ ) {
    // 每个盒子都居中,因此我们将它们移动半个身位
    arclength += w/2; 
    
    // 角的弧度等于弧长除以半径
    float theta = arclength / r;
    
    pushMatrix();
    // 极坐标到笛卡尔坐标的转换
    translate(r*cos(theta) , r*sin(theta));
    // 旋转矩形
    rotate(theta);
    
    // 显示矩形
    fill(0, 100);
    rectMode(CENTER);
    rect(0, 0, w, h);
    popMatrix();
    
    // 再次移动半个身位
    arclength += w/2;
  }
}

30. 尽管你觉得这个例子的数学可能有点难,但它的运行结果已经向我们揭示了下一步。我们需要做的只是用字符来填入各个矩形。并且因为每个字符并非同等宽度,所以相对"w"这个常量,我们需要使用一个变量,这个宽度由textWidth()函数得到:

// 被显示的文本
String message = "围绕曲线的汉字们";
PFont f;

// 圆半径
float r = 100;

void setup() {
  size(320,320);
  f = createFont("Georgia",40,true);
  textFont(f);
   // 文本必须居中!
  textAlign(CENTER);
  smooth();
}

void draw() {
  background(255);
  
  // 从中心开始并绘制圆形
  translate(width/2, height/2);
  noFill();
  stroke(0);
  ellipse(0, 0, r*2, r*2);
  
  // 保存位置
  float arclength = 0;
  
  // 为每个字
  for (int i = 0; i < message.length(); i ++ ) {
    
    // 字符及其宽度
    char currentChar = message.charAt(i);
    // 我们查询每个字符的宽度来替代固定的宽度.
    float w = textWidth(currentChar); 
    // 每个字都是居中对齐的,所以我们移动半个身位
    arclength += w/2;
    
    // 角的弧度等于弧长除以半径
    // 通过增加PI由圆形左边开始
    float theta = PI + arclength / r;
    
    pushMatrix();
    
    // 极坐标到笛卡尔坐标的转换允许我们找到沿着曲线的点.
    translate(r*cos(theta), r*sin(theta)); 
    // 旋转文字 (抵消旋转90度)
    rotate(theta + PI/2); 
    
    // 显示文字
    fill(0);
    text(currentChar,0,0);
    
    popMatrix();
    
    // 再次移动半个身位
    arclength += w/2;
  }
}

31. 下边是书里的练习,让随机位置的文本归位,然后继续上边那个例子,鼠标点击,抖动,放开,归位。还要用到一些新函数,哦也,我不会做,直接看答案,慢慢学习吧。。。:

PFont f;
String message = "呼啦啦啊呼啦啦,随机文字们各归其位!";
// 文字对象数组
Letter[] letters;

void setup() {
  size(400, 200);
  // 载入字体
  f = createFont("Georgia",20,true);
  textFont(f);
  
  // 以字符串大小创建数组
  letters = new Letter[message.length()];
  // 在正确的x位置初始化数组
  int x = 30;
  for (int i = 0; i < message.length(); i++) {
    letters[i] = new Letter(x,height/2,message.charAt(i)); 
    x += textWidth(message.charAt(i));
  }
}

void draw() { 
  background(255);
  for (int i = 0; i < letters.length; i++) {
    // Display all letters
    letters[i].display();
    
    // If the mouse is pressed the letters shake
    // If not, they return to their original location
    if (mousePressed) {
      letters[i].shake();
    } else {
      letters[i].home();
    }
  }
}

// A class to describe a single Letter
class Letter {
  char letter;
  // The object knows its original "home" location
  float homex,homey;
  // As well as its current location
  float x,y;
  // And an angle of rotation
  float theta;

  Letter (float x_, float y_, char letter_) {
    homex = x = x_;
    homey = y = y_;
    x = random(width);
    y = random(height);
    theta = random(TWO_PI);
    letter = letter_; 
  }

  // Display the letter
  void display() {
    fill(0);
    textAlign(LEFT);
    // 用户位移和旋转来绘制文字
    pushMatrix();
    translate(x,y);
    rotate(theta);
    text(letter,0,0);
    popMatrix();
  }

  // 随机移动文字
  void shake() {
    x += random(-2,2);
    y += random(-2,2);
    theta += random(-0.5,0.5);
  }

  // 用lerp让文字归位!
  void home() {
    x = lerp(x,homex,0.05);
    y = lerp(y,homey,0.05);
    theta = lerp(theta,0,0.05);
  }
}

lerp()——依一个特殊的增量计算两个数字间的一个数字。参数amt是两个值间的篡改(interpolate)总数,比如,0.0等于第一个值,0.1非常靠近第一个值,0.5在它们之间,等等。lerp函数便于创建直线运动以及绘制点状线。更多
是的,貌似比较难理解,但我想了个笨办法,列印lerp出来的值,然后慢慢去琢磨它的用法。还是挺管用的~

Processing:视频

1. 本章的学习需要一个外部摄像头(如果你使用PC则还需安装版本7或以上的QuickTime播放机,并在自定义安装时勾选”QuickTime for Java“。另外,你还需安装vdig(数字视频转换机)在Win系统内捕捉视频)的参与配合。具体安装方式请查阅你自己的摄像头使用帮助。

2. 是的,现在你会发现比起PC,Mac真是轻松许多。Mac的本子自带iSight摄像头,预装QuickTime,Mac用户无需进行任何恼人步骤便可轻松进入下一步。

3. 要在p5内使用视频,遵循以下步骤:
◎ 导入video库:

import processing.video.*;

◎ 申明一个捕捉对象:

Capture video; 

◎ 初始化捕捉对象:

video = new Capture(this,320,240,30);

※ this — 自参考指令(self-referential statement),原书唧唧歪歪扯一大堆,暂不考试,记得这里放this就OK。
※ 320 — 摄像头捕捉视频的宽度
※ 240 — 摄像头捕捉视频的高度
※ 30 — 帧速率
◎ 从摄像头读取图像

4. 有两种方式从摄像头读取帧。两种方式遵循的基本原则都是:我们只在当一个新帧准备好被读取时,从摄像头读取一个图像。要查询一个图片是否可用,我们使用available()函数,它以是否有东西存在返回真或假。如果有,函数read()被呼叫并且摄像头的帧被读入内存。我们在draw()反复这么做,总是查询是否有新的图像可用。

    void draw()  {    
         if (video.available())  {    
              video.read();   
          }    
     }

5. 第二种方式,使用”事件(event)“的方式,即某个事件发生的时候便执行。比如鼠标点击或键盘输入。在视频这里,我们可以选择使用函数captureEvent(),使得每当一个捕捉动作发生时被执行,即,来自摄像头的一个新帧可用了。

void captureEvent(Capture video)  {    
   video.read();   
} 

6. 显示视频图像,当然,最简单的部分,你可以将Capture对象看做是一个始终处于变化的PImage,事实上,一个Capture对象可以以与PImage对象完全一致的方式来利用。

image(video,0,0); 

7. 把上边说的加一块:

// Step 1. 载入video库
import processing.video.*;

// Step 2. 申明一个Capture对象
Capture video;

void setup() {
  size(320,240);
  
// Step 3. 通过构造器初始化Capture对象 Constructor
// 视频性质:320 x 240, @15 fps
video = new Capture(this,320,240,15);
}

void draw() {
  
  // 产看一个新帧是否已准备完毕
  if (video.available()) {
    // 如果是的话, Step 4. 从摄像头读取图像.
    video.read();
  }
  
 // Step 5. 显示视频图像.
 image(video,0,0);
}

8. 再重复一次,所有能在PIamge身上用的招数,我们全可以用在PImage上。只要我们从那个对象上read(),视频图像就将像我们操作它一样的更新。

// Step 1. 导入video库
import processing.video.*;

Capture video;

void setup() {
  size(320,240);
  video = new Capture(this,320,240,15);
  background(255);
}

void draw() {

  if (video.available()) {
    video.read();
  }

  // 用鼠标位置改变色调
  tint(mouseX,mouseY,255);

  //一个视频图像同样可以像PImage一样被改色及重设大小.
  image(video,0,0,mouseX,mouseY);   
}

9. ”图像“一课内的所有例子都可以在本章使用,以下为稍微改写后那个改变亮度的例子:

// Step 1. 导入video库
import processing.video.*;

// Step 2. 申明一个Capture对象
Capture video;

void setup() {
  size(320,240);
  
  // Step 3. 用构造器初始化Capture对象
  // 图像属性:320 x 240, @15 fps
  video = new Capture(this,320,240,15); 
  background(0);
}

void draw() {
  
  // 检查是否有新帧可用
  if (video.available()) {
    // 如果有,读取它.
    video.read();
  }
  
  loadPixels();
  video.loadPixels();
  
  for (int x = 0; x < video.width; x++) {
    for (int y = 0; y < video.height; y++) {
      
      // 由2D网格计算出其1D位置
      int loc = x + y*video.width;
      
      // 从图像获取R,G,B值
      float r,g,b;
      r = red (video.pixels[loc]);
      g = green (video.pixels[loc]);
      b = blue (video.pixels[loc]);
      
      // 依距鼠标距离计算改变亮度的总量
      float maxdist = 100;// dist(0,0,width,height);
      float d = dist(x,y,mouseX,mouseY);
      float adjustbrightness = (maxdist-d)/maxdist;
      r *= adjustbrightness;
      g *= adjustbrightness;
      b *= adjustbrightness;
      
      // Constrain RGB to make sure they are within 0-255 color range
      r = constrain(r,0,255);
      g = constrain(g,0,255);
      b = constrain(b,0,255);
      
      // Make a new color and set pixel in the window
      color c = color(r,g,b);
      pixels[loc] = c;
    }
  }
  updatePixels();
}

10. 是的,再来一个,这是小丹出题ww改进版:

// Step 1. 导入video库
import processing.video.*;

int pointillize = 10;

// Step 2. 申明一个Capture对象
Capture video;

void setup() {
  size(200,130);
  
  // Step 3. 用构造器初始化Capture对象
  // 图像属性:320 x 240, @15 fps
  video = new Capture(this,200,130,15); 
  background(0);
}

void draw() {
  
  // 检查是否有新帧可用
  if (video.available()) {
    // 如果有,读取它.
    video.read();
  }
  
loadPixels();
// 与PImage的关键区别就是多了下边这行 
video.loadPixels();
  // 沿y轴每隔5个像素循环所有像素
  for (int y = 0; y < video.height; y+=5) {
    // 沿x轴每隔5个像素循环所有像素
    for (int x = 0; x < video.width+5; x+=5) {
      // 从一个2D网格计算1D位置
      int loc = x + y*video.width;
      // 以原图填色
      stroke(video.pixels[loc]);
      fill(video.pixels[loc]);
      // 画圆
      ellipse(x,y,3,3);
    }
  }
}

在你套上一课例子的时候,我们发现只需将”img“改为”video“,然后加上”video.loadPixels();“这么一行即可。只是注意updatePixels()的使用,有时候你加上它,当摄像头打开p5窗口什么都没有。我在p5官网参考里查到:如果你仅是从像素数组中读取像素,则没有必要呼叫updatePixels(),除非出现变化。

11. 显示录制好的视频大部分遵循与实时视频相同的结构。p5的视频库仅接收QuickTime格式的电影。
◎ 申明一个Movie对象替代Capture对象:

Movie movie;

◎ 初始化Movie对象:

movie = new Movie(this, "xxx.mov");

同样的,电影文件应当储存在你sketch目录里的data文件夹内
◎ 开始电影播放。同样有两个选择,play()播放电影一次,loop()持续循环播放。

movie.loop();

◎ 从电影读取帧。与捕捉完全一致。

     void draw()  {   
          if (movie.available())  {   
               movie.read();   
           }    
      }

或者

     void movieEvent(Movie movie)  {   
          movie.read();   
      }

◎ 显示电影

image(movie,0,0);

12. 全部放一块儿:

import processing.video.*;

// Step 1. 申明Movie对象
Movie movie; 

void setup() {
  size(320,240);
  
  // Step 2. 初始化Movie对象
  // 电影文件应放在data文件夹下
  movie = new Movie(this, "xxx.mov"); 
  
  // Step 3. 开始电影播放
  movie.loop();
}

// Step 4. 从电影读取新帧
void movieEvent(Movie movie) {
  movie.read();
}

void draw() {
  // Step 5. 显示电影.
  image(movie,0,0);
}

是的,运行上边的代码,不出意外的话,你将得到错误:"ArrayIndexOutOfBoundsException: Coordinate out of bounds!" 。然后错误高亮框选在“image(myMovie, 0, 0);”上。哦也,bug!于是我查找p5讨论区,被这个打败的同学还真不少。最后终于发现一个解决之道,即:用“size(160,120,P2D);”替代 “size(160,120);”。这样修改后,mov文件便可正常显示了。按理说P2D应是留空默认引数,至于在这里为何非要特殊申明才能使程序运转正常,小弟就不得而知了。

13. 当然,p5不可能只能做播放电影的事,下边这个例子使用jump()(跳到影片内特定的点)和duration()(返回电影的长度)函数实现了前进后退的功能:

// 如果mouseX是0, 到影片开始   
// 如果mouseX是宽度, 到影片末尾   
// 所有其他的位置,在开始与末尾间徘徊   

import processing.video.*;

Movie movie;

void setup() {
  size(160,120,P2D);
  movie = new Movie(this, "xxx.mov");
}

void draw() {

  // mouseX相对宽度的比率
  float ratio = mouseX / (float) width;

  // jump()函数允许你快速的跳到影片特定的时间点. 
  // duration()以秒为单位返回影片的长度.  
  movie.jump(ratio*movie.duration()); 
  
  // 读取帧
  movie.read(); 
  // 显示帧
  image(movie,0,0); 
}

注意,这里你的size必须严格按照影片真实尺寸写,否则程序会崩溃掉(在本章里p5真是bug频出啊..);还有一种可能是size不能写太大,太大会造成程序崩溃(回头我真实测试一下便知)。

14. 我们现在要做一个玩意,用80×60像素捕捉视频,然后渲染到640×480的窗口内。首先,我们来做一个布满矩形的网格:

// 网格内每一格的大小,依视频和窗口的大小成比例缩放。
// 80 * 8 = 640
// 60 * 8 = 480
int videoScale = 8;

// 我们系统内行和列的数目
int cols, rows;

void setup() {
  size(640,480);
  
  // 初始化行和列
  cols = width/videoScale;
  rows = height/videoScale;
}

void draw() {
  
  // 开始循环列
  for (int i = 0; i < cols; i++) {
    // 开始循环行
    for (int j = 0; j < rows; j++) {
      
      // 在 (x,y) 按比例放大绘制矩形
      int x = i*videoScale;
      int y = j*videoScale;
      fill(255);
      stroke(0);
      // 对于每一行每一列, 一个矩形被绘制于一个 (x,y) 位置,并由videoScale限制大小.
      rect(x,y,videoScale,videoScale); 
    }
  }
}

现在,我们便可捕捉一个80×60的视频了。这很有用,因为相比起来,640×480视频捕捉的速度就很慢。我们仅仅想用我们sketch需要的分辨率来捕捉颜色信息。

import processing.video.*;

// 网格内每一格的大小,依视频和窗口的大小成比例缩放。
int videoScale = 8;
// 我们系统内行和列的数目
int cols, rows;
// 承载变量的Capture对象
Capture video;

void setup() {
  size(640,480);
  
  // 初始化行和列
  cols = width/videoScale;
  rows = height/videoScale;
  video = new Capture(this,cols,rows,30);
}

void draw() {
  // 从摄像头读取图像
  if (video.available()) {
    video.read();
  }
  video.loadPixels();
  
  // 开始循环列
  for (int i = 0; i < cols; i++) {
    // 开始循环行
    for (int j = 0; j < rows; j++) {
      
      // 我们在哪?
      int x = i*videoScale;
      int y = j*videoScale;
      // 在pixel数组内寻找合适的颜色
      color c = video.pixels[i + j*video.width];
      fill(c);
      stroke(0);
      rect(x,y,videoScale,videoScale);
    }
  }
}

15. 在下例中,我们只使用黑与白。视频中较亮的部分较大,较暗的则较小。

// 每个源于视频源的像素都基于亮度被绘制为一个矩形

import processing.video.*;

// 每格的大小
int videoScale = 10;
// 我们系统内行和列的数量
int cols, rows;
// Capture对象
Capture video;

void setup() {
  size(640,480);
  // 初始化行和列
  cols = width/videoScale;
  rows = height/videoScale;
  smooth();
  // 构造Capture对象
  video = new Capture(this,cols,rows,15);
}

void draw() {
  if (video.available()) {
    video.read();
  }
  background(0);

  video.loadPixels();

  // 开始循环列
  for (int i = 0; i < cols; i++) {
    // 开始循环行
    for (int j = 0; j < rows; j++) {

      // 我们在哪?
      int x = i*videoScale;
      int y = j*videoScale;

      // 反向x使其镜像图像
      // 为了镜像图像, 列基于如下公式反向:
      // 镜像列 = 宽度 - 列 - 1
      int loc = (video.width - i - 1) + j*video.width;

      // 每个矩形被染白,并基于亮度确定大小
      color c = video.pixels[loc];

      // 一个矩形的大小被计算为一个像素亮度的函数. 
      // 亮的十大的,暗的是小的.
      float sz = (brightness(c)/255.0)*videoScale; 
      rectMode(CENTER);
      fill(255);
      noStroke();
      rect(x + videoScale/2,y + videoScale/2,sz,sz);
    }
  }
}

那个镜像图像是干啥吃的?常规的视频显示是与你的动作反向的,比如你的头向右移动,则视频窗口内显示你的头便向左移动。如果你在向右摇头时希望窗口内的影像随你一同向右,你就需要加上:

// 设i为列,j为行
int loc = (video.width - i - 1) + j*video.width;
color c = video.pixels[loc];
fill(c);

有心的可以自己算算为什么会这样子,没心没肺只想用的,记住这三行即可。

16. 思考软件镜像(software mirrors)常常通过两步。这将同样帮助我们比更明显的将像素映射到网格的形状上想得更远。
第一步:开发一个可以覆盖整个窗口的有趣模式(pattern)
第二步:查找视频像素渲染那个模式
比方说第一步,创建一个在窗口内胡乱书写的随机线。
◎ 以位于萤幕中心的x和y作为起始坐标
◎ 无限重复以下:
— 选取一个新的x和y
— 从新的(x,y)到旧的(x,y)画一条直线
— 保存新的(x,y)

// 两个全局变量
float x;
float y;

void setup() {
  size(320,240);
  smooth();
  background(255);
  // 从中心开始x和y
  x = width/2;
  y = height/2;
}

void draw() {
  // 通过当前(x,y)位置加减一个随机数获取新的x,y位置. 
  // 新位置被限制于窗口像素内.  
  float newx = constrain(x + random(-20,20),0,width);
  float newy = constrain(y + random(-20,20),0,height);
  
  // 由x,y到newx,newy绘制一条线
  stroke(0);
  strokeWeight(4);
  line(x,y,newx,newy);
  
  // 我们将新位置存入(x,y)以无限循环这个程序.
  x = newx; 
  y = newy;
}

ok,现在让我们用视频图像的颜色来替代stroke():

import processing.video.*;

// 两个全局变量
float x;
float y;

// 承载变量的Capture对象
Capture video;

void setup() {
  size(320,240);
  smooth();
  background(255);
  
  // 从中心开始x和y
  x = width/2;
  y = height/2;
  
  // 开始捕捉程序
  // 如果窗口更大 (比如说 800 x 600), 我们可能会要引入一个videoScale变量,因此我们便无需捕捉如此大的一个图像了.
  video = new Capture(this,width,height,15); 
}

void draw() {
  // 从摄像头读取图像
  if (video.available()) {
    video.read();
  }
  video.loadPixels();
  
  // 选取一个新的x和y
  float newx = constrain(x + random(-20,20),0,width-1);
  float newy = constrain(y + random(-20,20),0,height-1);
  
  // 找到线的中点
  int midx = int((newx + x) / 2);
  int midy = int((newy + y) / 2);
  
  // 从事品选取颜色, 反向x
  color c = video.pixels[(width-1-midx) + midy*video.width];
  
  // 由x,y到newx,newy绘制一条线
  stroke(c);
  strokeWeight(4);
  line(x,y,newx,newy);
  
  // 将newx, newy存入x,y
  x = newx;
  y = newy;
}

17. 摄像头为我们提供了大量的信息,下边我们将试着将其用作传感器。我们在其他例子里已经看到,一个单独像素的亮度值可由brightness()函数检索,返回一个从0~255的浮点值。下边这行像素返回视频图像内第一个相素的值:

float brightness = brightness(video.pixels[0]);

当然,我们还可以计算总体亮度:

   video.loadPixels(); 
   // 由总体亮度为0开始 
   float totalBrightness = 0; 
   // 求出每个像素的亮度总和 
   for (int i = 0; i < video.pixels.length; i++ ) {  
        color c = video.pixels[i]; 
        totalBrightness += brightness(c); 
    }  

   // 计算平均值
   // 平均亮度=总亮度/总像素
   float averageBrightness = totalBrightness / video.pixels.length;     
   // 以平均亮度显示背景 
   background(averageBrightness);

这是一个基于视频的算法的很好的例子。总之,一个视频图像不仅是一个颜色的集合,而且还是一个具有空间导向的颜色的集合。通过开发搜索像素和识别模式的算法,我们便可开始开发更高级的计算机视觉程序。

18. 跟踪最亮的颜色是一个很好的起步。想象一个昏暗的房间里一个移动的光源。在我们接下来将要学习的技术中,这个灯光将被作为鼠标的替代,以成为一种互动的形式。首先,我们将测试如何贯穿一个图像然后找到最亮像素的x,y位置。

    // 程序之初的记录为0   
    float worldRecord = 0.0;   
    // 哪个像素将赢得奖牌?   
    int xRecordHolder = 0;  
    int yRecordHolder = 0;  
    for (int x = 0; x < video.width; x++) {    
         for (int y = 0; y < video.height; y++ )  {    
              // 当前亮度是什么   
              int loc = x + y*video.width;   
              float currentBrightness = brightness(video.pixels[loc]);   
              if (currentBrightness > worldRecord)  {    
                   // 设置一个新纪录   
                   worldRecord = currentBrightness;   
                   // 这个像素保持记录! 
                   // 当我们发现了新的最亮的像素,我们必须保存下数组中那个像素的(x,y)位置以备之后使用。  
                   xRecordHolder = x;   
                   yRecordHolder = y;   
               }    
          }    
     }

现在我们来做抓取一个特定的颜色,而不是简单的亮度。比如,我们可以寻找视频中最绿或最黄的部分。为了做这类分析,我们需要开发一个比较颜色的方法论。让我们创建两个颜色,c1和c2.

color c1 = color(255,100,50);   
color c2 = color(150,255,0);

颜色们仅能比较它们各自红、绿、蓝的成分,所以我们必须首先分离这些值:

float r1 = red(c1);
float g1 = green(c1);
float b1 = blue(c1);
float r2 = red(c2);
float g2 = green(c2);
float b2 = blue(c2);

ok,现在,对比它们,有一个策略是获取它们差值总数的绝对值:

float diff = abs(r1-r2) + abs(g1-g2) + abs(b1-b2);

还有一个更精确的方法是比较颜色之间的距离,是的,我说真的。我们可以将颜色想象为三维空间中的一个点,用rgb替代xyz。如果两个颜色在这个颜色空间相近,则它们便相似;较远,则它们不同。

float diff = dist(r1,g1,b1,r2,g2,b2);

尽管更精确,但因为dist()函数在其计算中包含了一个平方根,因此它的计算要慢于绝对值的方式。一个解决方法是不用平方根写你自己的颜色距离:

colorDistance = (r1-r 2)*(r1-r2)+(g1-g2)* 
(g1-g2)+(b1-b2)*(b1-b2)

19. 比如说,要找到一个图像最红的像素,也就是找到最接近红色的颜色——(255,0,0)。下例就是一个颜色抓取的展示,用户可以用鼠标自行点取想要捕捉的颜色:

import processing.video.*;

// 捕捉设备的变量
Capture video;

// 一个给我们要寻找的颜色的变量.
color trackColor; 

void setup() {
  size(320,240);
  video = new Capture(this,width,height,15);
  // 从抓红色开始
  trackColor = color(255,0,0);
  smooth();
}

void draw() {
  // 捕捉并显示视频
  if (video.available()) {
    video.read();
  }
  video.loadPixels();
  image(video,0,0);

  // 在我们开始搜索前, 为最接近颜色设置的“世界纪录”被设置为一个很高的值以此容易击败第一个像素。
  float worldRecord = 500; 

  // 最接近颜色的XY坐标
  int closestX = 0;
  int closestY = 0;

  // 开始在整个数组内循环
  for (int x = 0; x < video.width; x ++ ) {
    for (int y = 0; y < video.height; y ++ ) {
      int loc = x + y*video.width;
      // 当前颜色是什么
      color currentColor = video.pixels[loc];
      float r1 = red(currentColor);
      float g1 = green(currentColor);
      float b1 = blue(currentColor);
      float r2 = red(trackColor);
      float g2 = green(trackColor);
      float b2 = blue(trackColor);

      // 使用欧氏距离(euclidean distance)比较颜色
      float d = dist(r1,g1,b1,r2,g2,b2);

      // 如果当前颜色比“世界纪录”颜色更类似记录的颜色,保存当前位置和当前差值。
      if (d < worldRecord) {
        worldRecord = d;
        closestX = x;
        closestY = y;
      }
    }
  }

  // 我们仅仅在颜色距离小于10的时候才认为颜色被找到了. 
  // 阀值10是任意的,你可以基于你要求的抓取精度来任意给这个值。
  if (worldRecord < 10) { 
    // 在抓取到的像素位置画一个圈
    fill(trackColor);
    strokeWeight(4.0);
    stroke(0);
    ellipse(closestX,closestY,16,16);
  }
}

void mousePressed() {
  // 当鼠标在trackColor变量内点击时,保存那个颜色
  int loc = mouseX + mouseY*video.width;
  trackColor = video.pixels[loc];
}

20. 一个延伸运用,效果很奇妙,代码很长。。,基本做法时,在包含鼠标互动的例子里,用上例中的颜色捕捉替代鼠标。哦呀,今天头晕得很,这里边貌似错综复杂的东西很多,我现在大脑一片空白,去看部傻逼大片,明日继续吧~

背景移除
背景移除

21. ok,失意归来,继续,可我今天感觉也有点困,昨晚没睡好。是的,状况总是频出不穷,借口也总是那么容易找,,今天我们要学习一个技术——背景移除。我们准备这么干:
◎ 记录一个背景图像
◎ 在当前视频帧呢检查每一个像素。如果它与背景图像相应像素的差别很大,那么证明它为前景像素。如果不是,则它为背景像素。仅显示前景像素。
为证明以上算法,我们将以这样来显示它:移除背景图像,并用绿色填充背景。
21.1. 第一步是记忆背景,背景其实是视频的一个快照。因为视频图像总是始终出于变化中,我们必须将视频一帧的一个副本存入一个单独的PImage对象中。

PImage backgroundImage; 

void setup() {   
   backgroundImage = createImage(video.width,video.height,RGB); 
}

当backgroundImage被创建后,它是一副与视频尺寸一致大小的空图。通常这样的形式是没有用的,所以当我们需要记忆一个背景的时候,我们需要从摄像头copy一个图像进入背景图像里。让我们在鼠标按下时这么做:

void mousePressed()  {    
  // 将当前视频帧复制到backgroundImage对象
  // copy()允许你从一个图像到另一个图像复制像素。注意在新像素被复制后应当呼叫updatePixels()
  // 注意copy有五个引数:   
  // 源图像   
  // 被复制源区域的x,y,宽度和高度 
  // 复制目标的x,y,宽度和高度
  backgroundImage.copy(video,0,0,video.width,video.height,0,0,video.width,video.height);   
  backgroundImage.updatePixels(); 
}

21.2. 一旦我们的背景图像被保存,我们可以在当前帧循环其所有像素并用距离计算比较它们与背景。对于任何给定的像素(x,y),我们使用如下代码:

int loc = x + y*video.width;  // Step 1, 1D位置是什么    
color fgColor = video.pixels[loc];  // Step 2, 前景色是什么    
color bgColor = backgroundImage.pixels[loc];  // Step 3, 背景色是什么    

// Step 4, 比较前景与背景色 
float r1 = red(fgColor); float g1 = green(fgColor); float b1 = blue(fgColor);   
float r2 = red(bgColor); float g2 = green(bgColor); float b2 = blue(bgColor);   
float diff  =  dist(r1,g1,b1,r2,g2,b2);   

// Step 5, 前景色相比背景色显得不同吗    
if (diff > threshold)  {    
   // 如果是,显示前景色    
   pixels[loc] = fgColor;   
}  else  {    
   // 如果不是,显示绿色    
   pixels[loc] = color(0,255,0);   
}

21.3. 以上代码假设了一个名为”阀值“的变量。阀值越低,对于一个像素来讲就越容易被作为前景像素。它不需要与背景像素有很大的差异。以下是以阀值作为一个全局变量的例子:

// 点击鼠标以记录当前背景图像
import processing.video.*;

// 捕捉设备的变量
Capture video;

// 保存背景
PImage backgroundImage;

// 差异有多大才能将一个像素作为前景像素
float threshold = 20;

void setup() {
  size(320,240);
  video = new Capture(this, width, height, 30);
  // 创建一个与视频大小一致的的空白图像
  backgroundImage = createImage(video.width,video.height,RGB);
}

void draw() {
  // 捕捉视频
  if (video.available()) {
    video.read();
  }
  
  // 我们正看着视频的像素, 记录的backgroundImage的像素, 同时也进入显示的像素. 
  // 因此我们必须loadPixels()全体!
  loadPixels();
  video.loadPixels(); 
  backgroundImage.loadPixels();
  
  // 开始在每个像素间循环
  for (int x = 0; x < video.width; x ++ ) {
    for (int y = 0; y < video.height; y ++ ) {
      int loc = x + y*video.width;  // Step 1, 1D位置是什么    
      color fgColor = video.pixels[loc];  // Step 2, 前景色是什么    
      color bgColor = backgroundImage.pixels[loc];  // Step 3, 背景色是什么 
      
      // Step 4, compare the foreground and background color
      float r1 = red(fgColor);
      float g1 = green(fgColor);
      float b1 = blue(fgColor);
      float r2 = red(bgColor);
      float g2 = green(bgColor);
      float b2 = blue(bgColor);
      float diff = dist(r1,g1,b1,r2,g2,b2);
      
      // Step 5, 前景色相比背景色显得不同吗
      if (diff > threshold) {
        // 如果是,显示前景色
        pixels[loc] = fgColor;
      } else {
        // 如果不是,显示绿色
        pixels[loc] = color(0,255,0); // 我们当然可以在绿色之外让背景显示为其他东西!
      }
    }
  }
  updatePixels();
}

void mousePressed() {
  // 将当前视频帧复制到backgroundImage对象
  // copy()允许你从一个图像到另一个图像复制像素。注意在新像素被复制后应当呼叫updatePixels()
  // 注意copy有五个引数:   
  // 源图像   
  // 被复制源区域的x,y,宽度和高度 
  // 复制目标的x,y,宽度和高度
  backgroundImage.copy(video,0,0,video.width,video.height,0,0,video.width,video.height);
  backgroundImage.updatePixels();
}

运行以上代码,你需要先位于摄像头范围之外,然后鼠标点击选取背景,然后你再进去。当然,这方式不可能100%的严丝合缝,而且很多时候还会达不到抹掉背景的效果,试着调整你的位置、背景的复杂度以及阀值。是的,之前也说了,你可以用其他东西替换掉背景,由于这招也不是那么好使,所以有兴趣的可以在这里看如何用一副图换掉背景。

22. 接下来是动作侦测。Daniel说今天将是一个“happy day”。为什么呢?因为动作侦测与背景移除使用的是同一个算法。动作是如何产生的?在一个像素颜色与上一帧有极大变化的时候产生。唯一的不同,与背景移除仅记录一次背景相比,我们需要持续的记录视频的上一帧。以下代码几乎与背景移除的例子相同,但有一个重要的变化——当一个新帧可以使用的时候,视频上一帧持续的被存储下来。

    // 捕捉视频   
    if (video.available())  {    
      // 为动作侦测保存上一帧!!   
      prevFrame.copy(video,0,0,video.width,video.height,0,0,video.width,video.height); 
      video.read();   
     }

23. 看例子:

import processing.video.*;
// 捕捉设备变量
Capture video;
// 前一帧
PImage prevFrame;
// 一个像素要如何不同才能被界定为一个“动作”像素
float threshold = 50;

void setup() {
  size(320,240);
  video = new Capture(this, width, height, 30);
  //创建一个与视频大小相同的空图
  prevFrame = createImage(video.width,video.height,RGB);
}

void draw() {
  
  // 捕捉视频
  if (video.available()) {
    // 为动作侦测保存上一帧!!
    prevFrame.copy(video,0,0,video.width,video.height,0,0,video.width,video.height); // 在我们读取新帧前, 始终保存上一帧作为对比!
    prevFrame.updatePixels();
    video.read();
  }
  
  loadPixels();
  video.loadPixels();
  prevFrame.loadPixels();
  
  // 开始在每一帧间循环
  for (int x = 0; x < video.width; x ++ ) {
    for (int y = 0; y < video.height; y ++ ) {
      
      int loc = x + y*video.width;            // Step 1, 1D位置是什么
      color current = video.pixels[loc];      // Step 2, 当前颜色是什么
      color previous = prevFrame.pixels[loc]; // Step 3, 之前的颜色是什么
      
      // Step 4, 比较严色 (之前的 vs. 现在的)
      float r1 = red(current); float g1 = green(current); float b1 = blue(current);
      float r2 = red(previous); float g2 = green(previous); float b2 = blue(previous);
      float diff = dist(r1,g1,b1,r2,g2,b2);
      
      // Step 5, 颜色的差别有多大?
      // 如果那一点的像素改变了, 那么那个像素就运动了.
      if (diff > threshold) { 
        // 如果动了,显示黑色
        pixels[loc] = color(0);
      } else {
        // 如果没动,显示白色
        pixels[loc] = color(255);
      }
    }
  }
  updatePixels();
}

24. 如果我们要知道在一个空间内动作的总量,该怎么办?还记得上边那个计算平均亮度的例子吗?
平均亮度 = 总亮度/像素总数
同样的,我们也可以这么计算平均动作:
平均动作 = 总动作/像素总数

25. 下例显示了一个根据动作平均量变化颜色和大小的圆。再次注意,你不需要为了分析视频而display(显示)它。

import processing.video.*;

// 捕捉设备变量
Capture video;
// 前一帧
PImage prevFrame;

// 一个像素要如何不同才能被界定为一个“动作”像素
float threshold = 50;

void setup() {
  size(320,240);
  // 使用默认捕捉设备
  video = new Capture(this, width, height, 15);
  // 创建一个与视频大小相同的空图
  prevFrame = createImage(video.width,video.height,RGB);
}

void draw() {
  background(0);
  
  // 你不需要显示以分析它!
  image(video,0,0);
  
  // Capture video
  if (video.available()) {
    // 为动作侦测保存上一帧!!
    prevFrame.copy(video,0,0,video.width,video.height,0,0,video.width,video.height);
    prevFrame.updatePixels();
    video.read();
  }
  
  loadPixels();
  video.loadPixels();
  prevFrame.loadPixels();
  
  // 开始在每个像素间循环
  // 从总数为0开始
  float totalMotion = 0;
  
  // 计算像素的亮度总和
  for (int i = 0; i < video.pixels.length; i ++ ) {
    // Step 2, 当前颜色是什么
    color current = video.pixels[i];    
    // Step 3, 之前颜色是什么
    color previous = prevFrame.pixels[i];
    
    // Step 4, 对比颜色 (之前的 vs. 现在的)
    float r1 = red(current); 
    float g1 = green(current);
    float b1 = blue(current);
    float r2 = red(previous); 
    float g2 = green(previous);
    float b2 = blue(previous);
    
    // 独立像素的运动即其之前和现在颜色的差异.
    float diff = dist(r1,g1,b1,r2,g2,b2);
    // totalMotion(动作总和)即所有颜色差异的总和. 
    totalMotion += diff; 
  }
  
  // averageMotion(动作均值).
  float avgMotion = totalMotion / video.pixels.length; 
  
  // 基于动作均值绘制一个圆形
  smooth();
  stroke(255,0,0);
  fill(0);
  float r = avgMotion*2;
  ellipse(width/2,height/2,r,r);
}

26. 小丹出题:创建一个侦测动作平均位置的sketch。您能搞一个跟伴随你手的甩动的圆吗?官网没答案。。。不过我相信我清理一下头脑应该还是能做出来的,不过现在我们还是暂时继续下边的内容吧。

27. 出于安全考虑,例如使用摄像头这样的事请,在一个web applet中常常是禁用的,如果你不得不这么干的话,请参照这里

Processing:图像

1. 载入一张图片:
Code

// 申明一个PImage类型的变量, 一个我们可用的存储于p5核心库内的类.
PImage img;

void setup() {
  size(320,240);
  // 通过载入一个图片文件创建一个新的PImage实例
  img = loadImage("fuckyoubaby.jpg");
}

void draw() {
  background(0);
   //image()函数在一个位置显示图片-本例位于点(0,0).
  image(img,0,0);
}

这里,手贱的我又发现一点:如果你将你的这个sketch命名为PImage的话,点击运行p5会返回你错误:”cannot convert from PImage to PImage”。。。直到你重命名后才能正常运行。

2. 在上例内放入一张比size大的图片,运行,它将以原尺寸从左上角开始显示图片,你的size有多大,他就显示多大,没有缩放。

3. 要显示的图片必须位于本sketch文件夹下的data文件夹内(这也是p5载入外部文件的统一规矩)。

4. 我们需要注意,从硬盘载入图片相对来说是较慢的,因此我们应当确保我们的程序仅执行其一次,因此,应当将它放到setup()而不是draw()里。

5. 一旦图片被载入,它将被image()函数显示出来,这个函数必备三个引数:被显示的图片、x位置和y位置,另外,后边还可接两个备选参数:图像的宽和高(全图缩放)。

6. 让图片动起来:
Code

PImage dick; // 为图片文件准备的变量
float x,y;   // 图片位置变量
float rot;   // 图片旋转变量

void setup() {
  size(200,200);
  
  // 载入图像, 初始化变量
  dick = loadImage("fuckyoubaby.jpg");
  x = 0.0;
  y = width/2.0;
  rot = 0.0;
}

void draw() {
  background(255);
  
  // 位移和形变
  translate(x,y);
  rotate(rot);
  
  // 图片可以像其他常规形状一样使用变量、translate(), rotate()等等进行动画制作.
  image(dick,0,0);
  
  // 调整动画变量
  x += 1.0;
  rot += 0.02;
  if (x > width+head.width) {
    x = -head.width;
  }
}

7. 小丹出题:用类重写上例,是的,和你一样,目前为止我对类的使用还不是那么舒服,well,一起看答案吧,我始终坚信看着看着看多了也就会了。。

8. 如果你要改变图片显示的样子,tint()可以帮你,tint()之于图片就像fill()之于形状。tint()的引数可以简单指定每个像素可以使用的颜色数量以及透明度。

tint()
tint()

◎ 如果tint()只接收到一个引数,只有图像的亮度会受到影响。

//原始亮度
tint(255);  
image(sunflower,0,0);
//变暗  
tint(100);  
image(sunflower,0,0);

◎ 第二个引数将改变图片的alpha透明度

//设置图片不透明度为50%
tint(255,127);  
image(sunflower,0,0);

◎ 三个引数影响红、绿、蓝三色组成的亮度。

//没有红色,大部分为绿色,全部为蓝色
tint(0,200,255)
image(sunflower,0,0);

◎ 最后,添加第四个引数同样控制alpha透明度。tint()值的范围可用colorMode()指定。

//设置图片为红色调并透明
tint(255,0,0,100);
image(sunflower,0,0);

9. 一张图片不爽,我们要一堆图片!我们从这个点子开始搞,载入五张图片,每点击鼠标一次更换一张。
首先,我们建立一个图片的数组作为全局变量:

PImage[] images = new PImage[5];

第二,我们在setup()内将每张图片塞入数组内正确的位置:

images[0] = loadImage( " cat.jpg " );  
images[1] = loadImage( " mouse.jpg " );  
images[2] = loadImage( " dog.jpg " );  
images[3] = loadImage( " kangaroo.jpg " );  
images[4] = loadImage( " porcupine.jpg " );

是的,单独手动载入一张张图片的做法显得并不十分优雅,而且假设我们要载入100张图片你还不得傻逼了啊?一个解决办法是将文件名储存于一个字符串内然后使用for循环初始化全部数组元素:

String [] filenames = {"cat.jpg" , "mouse.jpg", "dog.jpg" , "kangaroo.jpg" , "porcupine.jpg" );
for (int i = 0; i < filenames.length; i++)  {    
    images[i] = loadImage(filenames[i]);  
}
知识串联:在字符串内:“Happy” + “ Trails” = “Happy Trails”,“2” + “2” = “22”。

更棒的是:如果我们将图片按顺序命名 ( “animal1.jpg” , “animal2.jpg” , “animal3.jpg” , 等等),我们的代码长度将得到大大减短:

for (int i = 0; i < images.length; i++) {  
  images[i] = loadImage( "animal" + i + ".jpg");
}

当图片载入完毕,我们将进入draw()显示它:

image(images[0],0,0);

当然,按顺序一个个把图片顺序的值写出来的是傻逼。我们需要一个可以动态变动的变量来代替它,以应对不同时刻点击的图片显示:

image(images[imageindex],0,0);

全部代码:
Code

int maxImages = 10; // 图片的总数
int imageIndex = 0; // 最初的图片最先显示

// 申明一个图片数组.
PImage[] images = new PImage[maxImages];

void setup() {
  size(200,200);
  
  // 将图片载入数组
  // 别忘了将图片放入data文件夹!
  for (int i = 0; i < images.length; i ++ ) {
    images[i] = loadImage( "animal" + i + ".jpg" );
  }
}

void draw() {
  // 显示一副图
  image(images[imageIndex],0,0);
}

void mousePressed() {
  // 当鼠标点击后随机选取一副图片显示
  // 注意数组的索引必须是整数!
  imageIndex = int(random(images.length));
}

10. 顺序动态播放一组图片:
Code

int maxImages = 10; // 图片的总数
int imageIndex = 0; // 最初的图片最先显示

// 申明一个图片数组.
PImage[] images = new PImage[maxImages];

void setup() {
  size(200,200);
  
  // 将图片载入数组
  // 别忘了将图片放入data文件夹!
  for (int i = 0; i < images.length; i ++ ) {
    images[i] = loadImage( "animal" + i + ".jpg" );
  }
  frameRate(5);
}

void draw() {
  
  background(0);
  image(images[imageIndex],0,0);
  
  // 每次循环为索引加一
  // 一旦到达数组末尾,使用模 " % "返回0
  imageIndex = (imageIndex + 1) % images.length;
}

11. 我们知道我们的萤幕是由像素组成的,我们之所以能用line()绘制两点间的直线,是因为p5为我们自动填满了这两点间所有像素的点。像素数组只有一维,从左之右、从上往下数序排列。

像素布局
像素布局

下例在每个像素上填上了随机灰色值。pixles数组和其他数组一样,唯一不同即我们无需申明它,因为它是p5的内置变量:
Code

size(200,200);

// 在我们处理像素前
loadPixels();

// 在每个像素间循环
for (int i = 0; i < pixels.length; i++ ) { // 我们可以像处理其他数组那样得到像素数组的长度
  
  // 选取一个随机数, 0到255
  float rand = random(255);
  
  // 基于随机数创建一个灰色
  color c = color(rand);
  
  // 为每个像素设置随机色
  pixels[i] = c; // 我们可以通过索引进入像素数组的单独元素,与处理其他数组并无二致.
}

// 当我们完成像素处理工作
updatePixels();

12. 如上例,我们无需担心像素的具体位置,但是相素的XY位置在很多制图软件中是至关重要的信息。一个简单的例子:设置每个偶数列像素为白、每个奇数列像素为黑。通过一个一维像素数组你如何做到这一点?你如何知道任意给定的像素位于第几行第几列?当使用像素编程的时候,我们需要设想每个像素都存在于二维世界中,但是持续在一维接收数据。我们可以用如下公式:
1)假设一个窗口或图像大小为给定的宽度和高度。
2)于是我们知道了像素总数为宽*高。
3)对于任何给定的X、Y点,它在我们的一维像素数组中的位置是:位置=X+Y*宽度

像素位置及其x、y位置关系
像素位置及其x、y位置关系

13. 好的,现在让我们来处理上边那个奇数和偶数列的问题:
Code

size(200,200);
loadPixels();

// 两个循环允许我们访问到每个列 (x) 和行 (y).

// 在每个像素列间循环
for (int x = 0; x < width; x++ ) {
  // 在每个像素行间循环
  for (int y = 0; y < height; y++ ) {
    
    // 使用公式找到一维位置
    int loc = x + y * width;
    
    if (x % 2 == 0) { // 如果我们位于偶数列
      pixels[loc] = color(255);
    } else { // 如果我们位于奇数列
      pixels[loc] = color(0); //   我们使用列数字 (x) 来决定颜色是黑是白
    }
  }
}

updatePixels();

14. 这个练习的效果很有意思:
Code

size(255, 255);
loadPixels();
for (int x = 0; x < width; x++) {
  for (int y = 0; y < height; y++) {
    int loc = x+y*width;
    float distance =dist(x,y,width/2,height/2);
    pixels[loc] = color(distance);
  }
}
updatePixels();

15. 还有这个:
Code

size(255, 255);
loadPixels();
for (int x = 0; x < width; x++) {
  for (int y = 0; y < height; y++) {
    int loc = x + y*width;
    if (x < width/2) {
      pixels[loc] = color(x);
    } else {
     pixels[loc] = color(y);
    }
  }
}
updatePixels();

是的,这个有点难以理解,不过还是继续向下吧,现在想不通不等于以后想不通。。

16. 之前的图像例子都是基于任意计算的像素值。现在我们将学习如何通过从一个已知PImage对象中找到的来设置像素,下边是一些伪码:
1)将一个图像载入PImage对象。
2)对于PImage中的每个像素,检索其颜色并用那个颜色颜色像素。
PImage包含很多有用的储存图像数据的域(field)——宽度、高度和像素,我们可以使用点语法(dot syntax)进入这些域:

PImage img = createImage(320,240,RGB); // 创建一个 PImage对象
println(img.width); // Yields 320
println(img.height); // Yields 240
img.pixels[0] = color(255,0,0); // 设置第一个像素颜色为红色

17. 进入这些域允许我们循环一个图像的全部像素并将它们显示在萤幕上。
Code

PImage img;

void setup() {
  size(320 ,214);
  img = loadImage("fuckyoubaby.jpg");
}

void draw() {
  loadPixels();

  // 由于要读取它的像素,我们必须同样在PImage呼叫loadPixels()
  img.loadPixels();
  for (int y = 0; y < height; y++ ) {
    for (int x = 0; x < width; x++ ) {
      int loc = x + y*width;
      // 函数red(), green(), and blue()将三个颜色的混合色显示在各个像素上.
      float r = red(img.pixels [loc]);
      float g = green(img.pixels[loc]);
      float b = blue(img.pixels[loc]);

      // 颜色处理将前往这里
      // 如果我们想更改RGB值, 我们可以在这里做(在设置像素显示于萤幕前).

      // 设置显示像素到图片像素上
      pixels[loc] = color(r,g,b);
    }
  }
  
  updatePixels();
}

强调一点,在上例中,因为显示区域与原图大小相同,所以这么写没问题。但是如果二者大小不同的话,你需要进行两个像素位置的计算,一个为原图,一个伪显示区

int imageLoc = x + y*img.width;  
int displayLoc = x + y*width;

18. 是的,如果我们可以控制每个像素,也就意味着我们可以基于此玩出更多的把戏,请看下例,鼠标水平位置控制图像亮度:
Code

PImage img;

void setup() {
  size(320,214);
  img = loadImage("fuckyoubaby.jpg");
}

void draw() {
  loadPixels();

img.loadPixels();
  for (int x = 0; x < img.width; x++ ) {
    for (int y = 0; y < img.height; y++ ) {

      // 计算1D像素位置
      int loc = x + y*img.width;

      // 获取图像R,G,B值
      float r = red (img.pixels[loc]);
      float g = green (img.pixels[loc]);
      float b = blue (img.pixels[loc]);

      // 用鼠标位置和乘法计算一个0.0到8.0的范围
      // 这个乘法改变了每个像素的RGB值.      
      float adjustBrightness = ((float) mouseX / width) * 8.0;
      r *= adjustBrightness;
      g *= adjustBrightness;
      b *= adjustBrightness;

      // 在设置为新颜色前,RGB值被限制在0到255间.      
      r = constrain(r,0,255);
      g = constrain(g,0,255);
      b = constrain(b,0,255);

      // 制作一个新颜色并在窗口设置像素
      color c = color(r,g,b);
      pixels[loc] = c;
    }
  }
  
  updatePixels();  
}

19. 由于我们在每个像素的基础上改变图像,因此不需要平等对待每一像素。例如,我们可以根据其距离鼠标的距离改变每个像素的亮度:
Code

PImage img;

void setup() {
  size(320,214);
  img = loadImage( "1.jpg" );
}

void draw() {
  loadPixels();
  
  // We must also call loadPixels() on the PImage since we are going to read its pixels.  img.loadPixels();
  for (int x = 0; x < img.width; x++ ) {
    for (int y = 0; y < img.height; y++ ) {
      
      // 计算1D像素位置
      int loc = x + y*img.width;

      // 获取图像R,G,B值
      float r = red (img.pixels[loc]);
      float g = green (img.pixels[loc]);
      float b = blue (img.pixels[loc]);
      
      // 基于与鼠标的接近度计算一个总量以改变亮度
      float distance = dist(x,y,mouseX,mouseY);
      
      // 距离鼠标越近的像素, 其"distance"值越低
      // 我们要更近的像素更亮, 因此我们将公式的值反向: float adjustBrightness = (50-distance)/50
      // 距离鼠标50 (或更大)的像素的亮度为0.0 (或复数,这里为0)
      // 距离鼠标0的像素亮度为1.0.
      float adjustBrightness = (50-distance)/50;
      r *= adjustBrightness;
      g *= adjustBrightness;
      b *= adjustBrightness;
      
      // 将RGB限制在0~255
      r = constrain(r,0,255);
      g = constrain(g,0,255);
      b = constrain(b,0,255);
      
      // 制作一个新颜色并在窗口设置像素
      color c = color(r,g,b);
      pixels[loc] = c;
    }
  }
  
  updatePixels();  
}

20. 我们之前的例子都是从原图读取每个像素并且只接在p5窗口内写下新像素。然而,我们常常使用一个更方便的方式将新的像素写到一个新的目标图像上(然后使用image()函数显示出来)。

21. 阀值(threshold)滤镜仅以两种状态显示一个图像的每个像素:黑或白(其实看下例,我们可以将它们设置为任意的两种颜色)。阀值可以自定义,如果像素的亮度高于阀值,则以白色来显示之,低于则用黑色。下例为阀值为100的情况:
Code

PImage source;    // 原图  
   PImage destination; // 目标图
  
   void setup()  {  
        size(200,200);  
        source = loadImage( "fuckyoubaby.jpg" );
       //目标图被创建为一个与原图大小相当的空白图像
        destination = createImage(source.width, source.height, RGB);  
    }
  
   void draw()  {  
        float threshold = 100;  
        // 我们将看一下全部图像的像素  
        source.loadPixels();  
        destination.loadPixels();

    for (int x = 0; x < source.width; x++)  {  
       for (int y = 0; y < source.height; y++)  {  
          int loc = x + y*source.width;  
         // 基于阀值比较亮度
         // brightness()返回一个0~255间的值,像素颜色的整体亮度。
          if (brightness(source.pixels[loc]) > threshold) {  
            destination.pixels[loc] = color(255); // 白  
           }  else  {  
             destination.pixels[loc] =  color(0);   // 黑
          }  
      }  
  }  
      // 基于目标图更换像素
     destination.updatePixels();  
      // 显示目标图  
      image(destination,0,0);  
   }

这个特殊的功能在p5的fliter()函数内并不可用。理解更低层代码,在你想要在fliter()之外制作你自己想要的图像处理算法时是至关重要的。如果你想做的仅仅是设置一个阀值,那么下例将更加简便:

// 绘制图像  
image(img,0,0);  
// 使用阀值滤镜  
// 0.5意味着阀值是亮度的50%
filter(THRESHOLD,0.5);

22. filter()函数提供一系列滤镜。这将改变图像显示的效果。模式除了THRESHOLD,还有GRAY, INVERT, POSTERIZE, BLUR, OPAQUE, ERODE 和 DILATE。参考p5官网看它们各自的例子。使用方法:

filter(mode);
filter(mode,level);

23. 之前我们的学习都是基于像素对像素,而为了进行更进一步的图像处理,我们必须进入像素组处理。
让我们先从基于原图两个像素(一个像素及其左边相邻的像素)创建一个新像素开始。
如果我们的像素位于(x,y):

int loc = x + y*img.width;
color pix = img.pixels[loc];

它左边的邻居便位于( x - 1, y):

int leftLoc = (x-1) + y*img.width;
color leftPix = img.pixels[leftLoc];

接着我们基于两个像素间的差异创建一个新颜色:

float diff = abs(brightness(pix) - brightness(leftPix));
pixels[loc] = color(diff);

下例是完全的算法:
Code

// 由于我们要找的是左邻
   // 因此我们跳过第一列
   for (int x = 1; x < width; x++) {  
     for (int y = 0; y < height; y++ ){  
        // 像素位置和颜色
        int loc = x + y*img.width;
        color pix = img.pixels[loc];
        
        // 左邻像素的位置和颜色
        int leftLoc = (x –1) + y*img.width;
        color leftPix = img.pixels[leftLoc];
              
        //  像素及其左邻的差是新颜色
        float diff = abs(brightness(pix) - brightness(leftPix));  
        pixels[loc] = color(diff);  
          }  
     }

上例是普通的垂直边缘侦测算法。当像素与其相邻像素差别很大的时候,它们更像是“边缘”像素。例如,你将一张白纸放在一个黑色的桌面上。纸的边缘即颜色差异最大的地方,即白色与黑色交界处。

24. 上例我们查找两个像素找到边缘。然而更复杂的算法通常是寻找更多的相邻像素。总之,每个像素都有八个直接的邻居:上下左右,左上,左下,右上,右下。这些图形处理算法通常被称为“空间回旋(spatial convolution)”。处理使用一个输入像素及其相邻像素的加权平均计算一个输出像素。换句话说,那个新的像素是一个区域内像素的函数。可以使用不同的大小的相邻区域,比如3 × 3或5 × 5的矩阵等等。
各个像素加权的不同组合差生多样的效果。比如,我们可以通过减小相邻像素值并增加中心点像素值来锐化一个图像,通过平均所有相邻像素值来模糊一个图像。(注意回旋矩阵中的值总和为1)
举例,
锐化:
–1 –1 –1
–1 9 –1
–1 –1 –1
模糊:
1/9 1/9 1/9
1/9 1/9 1/9
1/9 1/9 1/9

25. 下例展示了一个使用2D数组存储一个3 × 3矩阵像素加权的回旋。这可能使我们到目前为止在本教程内碰到的最先进的例子,因为它使用了如此多的元素(嵌套循环,2D数组,PImage像素,等等),哦也,我看得也很崩溃,因为它太长了,放这慢慢研究吧…
Code

PImage img;
int w = 80;
//一个"模糊"效果的回旋矩阵以一个3 x 3二维数组储存.
float[][] matrix = { { 1.0/9.0, 1.0/9.0, 1.0/9.0 } ,
                              { 1.0/9.0, 1.0/9.0, 1.0/9.0 } ,
                              { 1.0/9.0, 1.0/9.0, 1.0/9.0 } } ;

void setup() {
  size(320,214);
  img = loadImage( "fuckyoubaby.jpg" );
}

void draw() {
  
  // 我们将仅处理图像的一部分
  // 因此让我们将全图设为背景先
  image(img,0,0);
  
  // 在本例中我们仅仅处理图像的一部分—一个围绕鼠标的80 x 80矩形(鼠标分别向上下左右扩展40).  
  int xstart = constrain(mouseX-w/2,0,img.width);
  int ystart = constrain(mouseY-w/2,0,img.height);
  int xend = constrain(mouseX + w/2,0,img.width);
  int yend = constrain(mouseY + w/2,0,img.height);
  int matrixsize = 3;
  
  loadPixels();
  // 为每个像素开始我们的循环
  for (int x = xstart; x < xend; x++ ) {
    for (int y = ystart; y < yend; y++ ) {
      // 每个像素位置(x,y)开始进入一个名为convolution()的函数
      // convolution()函数返回一个新的待显示的颜色.
      color c = convolution(x,y,matrix,matrixsize,img);
      int loc = x + y*img.width;
      pixels[loc] = c;
    }
  }
  updatePixels();
  stroke(0);
  noFill();
  rect(xstart,ystart,w,w);
}

color convolution(int x, int y, float[][] matrix, int matrixsize, PImage img) {
  float rtotal = 0.0;
  float gtotal = 0.0;
  float btotal = 0.0;
  int offset = matrixsize / 2;
  
  // 在回旋矩阵内循环
  for (int i = 0; i < matrixsize; i++ ) {
    for (int j = 0; j < matrixsize; j++ ) {
      
      // 我们正在测试的像素是?
      int xloc = x + i-offset;
      int yloc = y + j-offset;
      int loc = xloc + img.width*yloc;
      
      // 确保我们没有走出像素数组的边缘
      // 在查找相邻像素时确保我们没有偶然走出像素数组的边缘常常是很好的。.
      loc = constrain(loc,0,img.pixels.length-1);
      
      // 计算回旋
      // 我们通过乘以回旋矩阵的值以合计所有的相邻像素.
      rtotal += (red(img.pixels[loc]) * matrix[i][j]);
      gtotal += (green(img.pixels[loc]) * matrix[i][j]);
      btotal += (blue(img.pixels[loc]) * matrix[i][j]);
    }
  }
  
  // 确保RGB的范围
  rtotal = constrain(rtotal,0,255);
  gtotal = constrain(gtotal,0,255);
  btotal = constrain(btotal,0,255);
  
  // 返回结果颜色
  return color(rtotal,gtotal,btotal);
}
随机点阵
随机点阵

26. 是的,你会和我有一样的疑问,上边这些难道PS不可以做么?为什么还要写这么大堆的代码?是的,它们只是帮助我们去理解做一些特效的原理,以及更底层的概念——像素。以下几个例子在PS里就很难完成了。抛开繁杂的编码,看下面这个绘制p5形状的算法。每循环一次draw(),我们以随机的位置画下一个圆,各个圆的颜色取自原图对应位置的颜色,来创建一种点阵图的特效。

PImage img;
int pointillize = 10;

void setup() {
  size(320,214);
  img = loadImage("fuckyoubaby.jpg");
  background(255);
  smooth();
}

void draw() {
  
  // 选取一个随机点
  int x = int(random(img.width));
  int y = int(random(img.height));
  int loc = x + y*img.width;
  
  // 从原图查找RGB颜色
  loadPixels();
  float r = red(img.pixels[loc]);
  float g = green(img.pixels[loc]);
  float b = blue(img.pixels[loc]);
  
  // 回到形状! 相对于设置一个像素, 我们使用一个像素的颜色来画一个圆.
  noStroke();
  fill(r,g,b,100);
  ellipse(x,y,pointillize,pointillize);
}
2D转3D
2D转3D

27. 再看一个例子,我们从一个二维图像中获取数据,用3D位移技术在三维空间内的每一个像素渲染一个矩形。z的位置取决于颜色的亮度。越亮的看起来越靠近观者,越暗的则越远。

PImage img;       // 原图
int cellsize = 2; // 点阵每个单元的尺寸
int cols, rows;   // 我们系统中行和列的数目

void setup() {
  size(320,214,P3D);
  img = loadImage( "fuckyoubaby.jpg" ); // 载入图像
  cols = width/cellsize;              // 计算列的数目
  rows = height/cellsize;             // 计算行的数目
}

void draw() {
  background(255);
  img.loadPixels();

  // 开始循环列
  for (int i = 0; i < cols; i++ ) {
    // 开始循环行
    for (int j = 0; j < rows; j++ ) {
      int x = i*cellsize + cellsize/2; // x位置
      int y = j*cellsize + cellsize/2; // y位置
      int loc = x + y*width;           // 像素数组位置
      color c = img.pixels[loc];       // 截取颜色

      // 依mouseX和像素亮度计算z位置
      float z = (mouseX/(float)width) * brightness(img.pixels[loc])- 100.0;

      // 位移到相应位置, 设置填充和边框, 并绘制矩形
      pushMatrix();
      translate(x,y,z);
      fill(c);
      noStroke();
      rectMode(CENTER);
      rect(0,0,cellsize,cellsize);
      popMatrix();
    }
  }
}
三角三角!
三角三角!

28. 是的,可以用任何图形进行类似的填充,比如三角形:

PImage img;

void setup() {
  size(320, 214);
  // 载入图像
  img = loadImage("fuckyoubaby.jpg");  
  smooth();
}

void draw() {
  background(255);
  // 处理像素前先呼叫loadPixels
  loadPixels();  
  // 沿y轴每隔10个像素循环所有像素
  for (int y = 0; y < img.height; y+=10 ) {
    // 沿x轴每隔5个像素循环所有像素
    for (int x = 0; x < img.width+5; x+=5) {
      // 从一个2D网格计算1D位置
      int loc = x + y*img.width;
      // 以原图填色
      stroke(img.pixels[loc]);
      fill(img.pixels[loc]);
      // 顶点朝上或顶点朝下绘制三角形
      if (x %10 == 0) triangle(x-5,y,x,y+10,x+5,y);
      else triangle(x-5,y+10,x,y,x+5,y+10);
    }
  }
}

基于上例换一些其他形状进去,你会得到很多有趣的、PS难以做到的特效。

Processing:平移和旋转

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

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

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. 看这个例子:
Code

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);

最后,我们可以用它照做一开始那个矩形变大的例子:
Code

// 一个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()函数对相对一个给出的中点绘制的一系列形状很有用。
Code

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()画一个立体的金字塔,共五面,四面三角形,底座一个矩形。

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

Code

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)的值来计算一个旋转的角度出来。
Code

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是这么个由上而下的代码执行顺序呀)
Code

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()实现的矩形增大实验:
Code

   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轴转的矩形,是的,独处的时候,二者都没有问题,但是一旦放到一块:
Code

    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)显示第二个矩形。
用以上思路重写上例:
Code

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. 接下来我们如法炮制用数组做一堆旋转的矩形:
Code

// 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()是如何的有用,下例为一个行星系统的模拟:
Code

// 围绕太阳及行星旋转的角度
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类的一员
◎ 一个行星数组将围绕太阳旋转
Code

// 一个由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. 越来越绕了。怎么办 🙁