3 Jan 2014

Processingで正多角形を描く



最近Processingで久々に遊んでいます。以前はライブラリを組み合わせて”わーできたー”位のことをやっておりましたが、最近基礎力を身につけたいと思って幾何学的なことも軽く学んでいます。
さて、文化というかよく見かけるProcessingのサンプルは縦に長ーくて変数の値がなんだったのか途中から忘れてしまうようなものが多いです。
例えば折れ線グラフを書くようなケースの場合、

統計情報をデータソースから取得する > X軸、Y軸を描く > X、Yの最大値を求めてスケールを決める > ちょうどいい単位に目盛りを引く > 折れ線グラフを描く

といったステップを分けて踏むとコードがすっきりするのですが、一方でX軸、Y軸のディスプレイ上の座標や、XやYの最大値やなどの変数はスケッチを通して保持したいもので、そのことが縦に長ーいプログラムだとコードを複雑にしがちです。Proceccingの世界だとおそらく一回使い切りのプロジェクトもしくは次回使うことがあってもコピペして修正すれば割とすんでしまうようなことが多いのかもしれません。

でも、やっぱり再利用可能な部品を作りたいですね。コードを関数に区切ると変数のスコープが狭まるのでミニマムなことにフォーカスできます。

下記のコードでは最初に以下の引数で図形を調整できるものという条件を固持するものとして書きました。

- 正N角形のN
- 半径


# どこから読めばいいのか?
まずはsetupとdrawを読んでください。特にdrawを読むことで大まかな流れがつかめるでしょう。

  1. // 半径200pxの6角形を描く  
  2.   polygon = new EquilateralPolygon(6200);  
  3.   // 頂点を取得  
  4.   polygon.vertexes();  
  5.   // 頂点を描く  
  6.   polygon.drawVertexes();  
  7.   // 番号を描く  
  8.   polygon.drawIndexes();  


肝になるところだけ軽く触れておきましょう。

# #xVertex, #vertexes()

vertexesは頂点の座標を集めています。

  1. public float xVertex(int i, int extra) {  
  2.     return cos(angles[i]) * (radius + extra);  
  3.   }  
  4.   
  5.   public Coordinate[] vertexes() {  
  6.     vertexes = new Coordinate[numVertexes];  
  7.     float angleForSingleArc = 2 * PI / numVertexes;  
  8.     
  9.     for(int i = 0; i < numVertexes; i++) {  
  10.       // Sum up angles  
  11.       angles[i] = HALF_PI - angleForSingleArc * i;  
  12.         
  13.       float x = xVertex(i, 0);  
  14.       float y = - yVertex(i, 0);  
  15.         
  16.       vertexes[i] = new Coordinate(x, y);  
  17.     }  
  18.       
  19.     return vertexes;  
  20.   }  


ある頂点のX座標は三角関数を使って以下のように求められます。sinとcosを使った辺の長さの求め方

# xVertex

cos(角度) * 半径


y座標はsinを使って以下のように求められます

# yVertex
sin(角度) * 半径

xVertex中では半径だけを使ってって計算しています。これは円の中心を起点の0,0としてその差分を座標として計算し描写しているからです。ただ描写する正確な座標ははプログラマが意識しないですむようにtranslate関数であらかじめずらしています


# 拡張点の座標の求め方
angles[i] = HALF_PI - angleForSingleArc * i;


PIはπを意味するあらかじめProcessingに用意された定数です。上記のx, yの関数で角度が0の場合processingは右端になります。そこから始まって反時計回りに角度を表します。

0 => QUARTER_PI(45度) => HALF_PI(90度) => PI(180度) => 2 * PI(360度)




一つずつの頂点について図の一番上を起点としてそこからの角度をi毎に取得します。時計回りに点を配置することにしたので マイナスを付けて逆方向に回転させます。
angles[i] = HALF_PI - angleForSingleArc * i;

// Size of display
int DISPLAY_WIDTH = 500;
int DISPLAY_HEIGHT = 500;
// Colours
int BLACK = 0;
int WHITE = 255;
int CENTER_X = 0;
int CENTER_Y = 0;
EquilateralPolygon polygon;
// This class saves coordinate
class Coordinate {
int x;
int y;
// The Constructor is defined with arguments.
Coordinate(float _x, float _y) {
x = int(_x);
y = int(_y);
}
public String toString() {
return x + ", " + y;
}
}
class EquilateralPolygon {
int numVertexes;
int radius;
float[] angles;
Coordinate[] vertexes;
EquilateralPolygon(int _numVertexes, int _radius) {
numVertexes = _numVertexes;
angles = new float[numVertexes];
radius = _radius;
}
public float xVertex(int i, int extra) {
return cos(angles[i]) * (radius + extra);
}
public float yVertex(int i, int extra) {
return sin(angles[i]) * (radius + extra);
}
public Coordinate[] vertexes() {
vertexes = new Coordinate[numVertexes];
float angleForSingleArc = 2 * PI / numVertexes;
for(int i = 0; i < numVertexes; i++) {
// Sum up angles
angles[i] = HALF_PI - angleForSingleArc * i;
float x = xVertex(i, 0);
float y = - yVertex(i, 0);
vertexes[i] = new Coordinate(x, y);
}
return vertexes;
}
// Put tiny circles onto each vertex
public void drawVertexes() {
smooth();
textAlign(CENTER);
int r = 20;
for (int i = 0; i < numVertexes; i++) {
// Draw black circle for the first vertex and draw gray ones for the rest
int colorValue = i == 0 ? 0 : 200;
stroke(colorValue);
strokeWeight(1);
ellipse(vertexes[i].x, vertexes[i].y, r, r);
}
}
public void drawIndexes() {
for (int i = 0; i < numVertexes; i++) {
fill(BLACK);
text(i, vertexes[i].x, vertexes[i].y);
noFill();
}
}
}
void setup() {
size(DISPLAY_WIDTH, DISPLAY_HEIGHT);
background(WHITE);
noLoop();
}
void draw() {
// Displace the drawn polygon by half width and half height of `display`, which means center.
// With `translate`, we can calculate coodinate of each vertex as distance from the center of a
// diagram, where it's coordinate is as (0, 0), and then, displace the polygon to actual coordinate
// of `display`.
translate(DISPLAY_WIDTH / 2, DISPLAY_HEIGHT / 2);
polygon = new EquilateralPolygon(6, 200);
polygon.vertexes();
polygon.drawVertexes();
polygon.drawIndexes();
saveFrame("result.jpg");
}




# なぜOOPを使ってシンプルに書くのが難しいのか?

なぜProcessingではOOPをつかってシンプルに書くのがむずかしいのでしょうか?コードを書いていて思ったことがあります。

- 文脈依存の関数が多い

例えばこのコードに出てくる以下のようなコードは文脈依存です

fill(BLACK);
text(i, vertexes[i].x, vertexes[i].y);
noFill();

noLoop()

translate(DISPLAY_WIDTH / 2, DISPLAY_HEIGHT / 2)


全て後述の処理に影響します。


- 一個のループに何でもまとめてしまう

計算量が気になってしまい一個のループに何でもまとめてしまうのでそのループを含む関数がいろんなことをやってしまいがちです。lambdaを使ってループのある関数内で座標計算や描写などを遅延評価させてあげればいいんですかね。
http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/Lambda-QuickStart/index.html?cid=7180&ssid=3993398328344#section2


以上、久々のJava勉強になりました。