Processing
16. 描画の軌跡を考える
「13. 描画の単位を考える」「14. ベジエ曲線を利用する」の項目では描画範囲を縦横に並べています。横に並べる、縦に並べるというのはx座標、y座標の値どちらか一方を変化させれば良いので比較的簡単です。
ここでは斜めに配置する方法について考えてみます。
斜めの軌跡に添い描画単位を配置する1
void setup() {
size( 500, 500 );
background( 255 );
smooth();
noLoop();
colorMode( HSB, 360, 100, 100, 100 );
}
void draw() {
int shapeSize = 80;
int firstPointX = 1;
int firstPointY = 1;
float plusX = 20;
float plusY = 40;
int boundsX = width + shapeSize;
int boundsY = height + shapeSize;
stroke( 150, 60, 80, 100 );
fill( 150, 60, 80, 70 );
float drawPointX = firstPointX;
float drawPointY = firstPointY;
while( drawPointX < boundsX && drawPointY < boundsY ) {
drawShape( drawPointX, drawPointY, shapeSize );
drawPointX = drawPointX + plusX;
}
drawPointX = firstPointX;
drawPointY = firstPointY;
while( drawPointX < boundsX && drawPointY < boundsY ) {
drawShape( drawPointX, drawPointY, shapeSize );
drawPointX = drawPointX + plusX;
drawPointY = drawPointY + plusY;
}
}
void drawShape( float centerX, float centerY, int drawSize ) {
noFill();
ellipse( centerX, centerY, drawSize, drawSize );
}
簡易に行うのであれば、繰返し描画を行う中で、x座標の値とy座標の増分を異なる数値にすれば、斜めに描画単位を配置することができます。
27、35行目、whileの後の()の中の条件の書き方ですが、&&などでつなげて、条件を2つ書くことができます。drawPointX < boundsX && drawPointY < boundsYとは、drawPointXもdrawPointYも描画範囲の最大値以下(drawPointXもwidth+shapeSize以下、drawPointYもheight+shapeSize以下)の場合に繰返しが続きます。つまりどちらか一方がオーバーしたら描画を止めます。

描画単位に応じて、この方法で要件を満たすのであれば、この簡易的な方法でも良いでしょう。
void drawShape( float centerX, float centerY, int drawSize ) {
noStroke();
float tmpSize = random( drawSize/4, drawSize/2 );
float tmpX = random( -tmpSize/2, tmpSize/2 );
float tmpY = random( -tmpSize/2, tmpSize/2 );
int drawRepeat = 4;
ellipse( centerX + tmpX, centerY + tmpY, tmpSize, tmpSize );
for( int i = 0; i < drawRepeat; i++ ) {
tmpSize = random( drawSize/8, drawSize/4 );
tmpX = random( -drawSize/4, drawSize/4 );
tmpY = random( -drawSize/4, drawSize/4 );
ellipse( centerX + tmpX, centerY + tmpY, tmpSize, tmpSize );
}
}

drawShape()の内容だけ、「13. 描画の単位を考える」で試みた内容に置き換えています。
斜めに配置するこの簡易的な方法の問題点は、「x座標とy座標の値を繰返しの中で増やすので、ある点からある点までの直線に添わせる、という書き方が難しい」、「斜線上の距離をもとに描画単位のx座標とy座標を決めることができない」という点にあります。
例えば上記の描画ですが、x座標の増分が同じで、描画単位同士の間隔は異なってしまっています。
それらの問題は三角関数を利用して座標を求めることで解決されます。
斜めの軌跡に添い描画単位を配置する2
ここでは多少複雑ですが、2つの点を結ぶような直線を仮想して、それに添って、描画単位を配置することを考えてみます。
詳細は割愛しますが、点1と点2を結ぶ直線の傾きは、以下のように求めることができます。
angleが角度(ラジアン)、firstPointX:点1のx座標、firstPointY:点1のy座標、secondPointX:点2のx座標、secondPointY:点2のy座標です。
angle = atan2( secondPointY - firstPointY, secondPointX - firstPointX )
そして、その直線上で例えば40の距離を取りたいとします。その場合の水平の距離(plusX)と垂直の距離(plusY)は、次のようになります。
plusX = cos( angle ) * 40
plusY = sin( angle ) * 40
したがって、以下のように書けば、ある点からある点までの距離を指定して、描画単位を描画することができます。
void setup() {
size( 500, 500 );
background( 255 );
smooth();
noLoop();
colorMode( HSB, 360, 100, 100, 100 );
}
void draw() {
int shapeSize = 40;
int firstPointX = 1; //点1のx座標
int firstPointY = 1; //点1のy座標
int secondPointX = 500; //点2のx座標
int secondPointY = 125; //点2のy座標
int thirdPointX = 1; //点3のx座標
int thirdPointY = 500; //点3のy座標
float shapePitch = 40; //描画単位を描く間隔
float angle = 0; //角度
float plusX = 0; //x座標の増分
float plusY = 0; //y座標の増分
int boundsX = width + shapeSize;
int boundsY = height + shapeSize;
stroke( 150, 60, 80, 100 );
fill( 150, 60, 80, 70 );
//描画位置を点1にする
float drawPointX = firstPointX;
float drawPointY = firstPointY;
//点1と点2を結ぶ直線の角度を求める
angle = atan2( secondPointY - firstPointY, secondPointX - firstPointX );
//その直線上の間隔がshapePitchの場合のxとyを求める
plusX = cos( angle ) * shapePitch;
plusY = sin( angle ) * shapePitch;
//描画範囲画面より少し大きい間だけ描画する
while( -shapeSize < drawPointX && drawPointX < boundsX &&
-shapeSize < drawPointY && drawPointY < boundsY ) {
drawShape( drawPointX, drawPointY, shapeSize );
drawPointX = drawPointX + plusX;
drawPointY = drawPointY + plusY;
}
//描画位置を点2にする
drawPointX = secondPointX;
drawPointY = secondPointY;
//点2と点3を結ぶ直線の角度を求める
angle = atan2( thirdPointY - secondPointY, thirdPointX - secondPointX );
//その直線上の間隔がshapePitchの場合のxとyを求める
plusX = cos( angle ) * shapePitch;
plusY = sin( angle ) * shapePitch;
//描画範囲画面より少し大きい間だけ描画する
while( -shapeSize < drawPointX && drawPointX < boundsX &&
-shapeSize < drawPointY && drawPointY < boundsY ) {
drawShape( drawPointX, drawPointY, shapeSize );
drawPointX = drawPointX + plusX;
drawPointY = drawPointY + plusY;
}
//補助のため直線を描画
stroke( 150, 0, 50, 100 );
line( firstPointX, firstPointY, secondPointX, secondPointY );
line( secondPointX, secondPointY, thirdPointX, thirdPointY );
}
void drawShape( float centerX, float centerY, int shapeSize ) {
noFill();
ellipse( centerX, centerY, shapeSize, shapeSize );
}
34〜51行目、1つ目の軌跡に添って描画しています。点1( 0, 0 )から点2(500, 125)まで間隔40で円が描かれます。
53〜70行目、2つ目の軌跡に添って描画しています。点2(500, 125)から点3( 0, 500 )まで間隔40で円が描かれます。
なお46、65行目、whileの条件は4つに増やしています。

曲線の傾きが変わっても、円の間隔は等しく描画されています。
座標を配列に格納する
次々と線を描いていったり、描画のための数値がいくつかに決まっているような場合には、変数よりも配列を使った方が便利な場合があります。
void setup() {
size( 500, 500 );
background( 255 );
smooth();
noLoop();
colorMode( HSB, 360, 100, 100, 100 );
}
void draw() {
int shapeSize = 40;
int pointNum = 5;
//座標を格納するための配列を作る
//x座標とy座標を格納するため二次元の配列にする
int[][] pointArray = new int[pointNum][2];
pointArray[0][0] = 1; //点1のx座標
pointArray[0][1] = 1; //点1のy座標
pointArray[1][0] = 500; //点2のx座標
pointArray[1][1] = 75; //点2のy座標
pointArray[2][0] = 1; //点3のx座標
pointArray[2][1] = 150; //点3のy座標
pointArray[3][0] = 500; //点4のx座標
pointArray[3][1] = 270; //点4のy座標
pointArray[4][0] = 1; //点5のx座標
pointArray[4][1] = 500; //点5のy座標
float shapePitch = 40; //描画単位を描く間隔
float angle = 0; //角度
float plusX = 0; //x座標の増分
float plusY = 0; //y座標の増分
int boundsX = width + shapeSize;
int boundsY = height + shapeSize;
stroke( 150, 60, 80, 100 );
fill( 150, 60, 80, 70 );
for( int i = 0; i < pointNum-1; i++ ) {
//描画位置を配列から取得
float drawPointX = pointArray[i][0];
float drawPointY = pointArray[i][1];
//現在の点と次の点を結ぶ直線の角度を求める
angle = atan2( pointArray[i+1][1] - pointArray[i][1], pointArray[i+1][0] - pointArray[i][0] );
//その直線上の間隔がshapePitchの場合のxとyを求める
plusX = cos( angle ) * shapePitch;
plusY = sin( angle ) * shapePitch;
//描画範囲画面より少し大きい間だけ描画する
while( -shapeSize < drawPointX && drawPointX < boundsX &&
-shapeSize < drawPointY && drawPointY < boundsY ) {
drawShape( drawPointX, drawPointY, shapeSize );
drawPointX = drawPointX + plusX;
drawPointY = drawPointY + plusY;
}
}
//補助のため直線を描画
stroke( 150, 0, 50, 100 );
for( int i = 0; i < pointNum-1; i++ ) {
line( pointArray[i][0], pointArray[i][1], pointArray[i+1][0], pointArray[i+1][1] );
}
}
void drawShape( float centerX, float centerY, int shapeSize ) {
noFill();
ellipse( centerX, centerY, shapeSize, shapeSize );
}
配列は多次元にすることもできます。
ここでいう多次元とは、例えばpointArray[0]にも、pointArray[1]にも、それぞれ複数の値を格納できるようにすることです。
上記であれば、x座標とy座標を格納するとき、pointArray[0]にx座標、pointArray[1]にy座標…などと格納するよりも、pointArray[0][0]にx座標、pointArray[0][1]にy座標…と格納できた方が、始めの数値が何番目の点かを示し、2つめの数値が0であればx座標、1であればy座標となるので理解、管理がしやすくなります。
17行目、int[][] pointArray = new int[pointNum][2];という書き方は、pointArrayを5つ、[0] 〜[4]までを作り、そのそれぞれにさらに2つずつ[0]と[1]を作るようなイメージです。
このようにすると、46〜67行目のように、for文で繰返し座標を指定しての描画が行えるので便利です。

描画単位を置き換えたとしても、中心座標から軌跡の距離は等しくなるので、同じ速さの筆の動きで描写したようになります。
void setup() {
size( 500, 500 );
background( 255 );
smooth();
noLoop();
colorMode( HSB, 360, 100, 100, 100 );
}
void draw() {
int shapeSize = 80;
int pointNum = 5;
int[][] pointArray = new int[pointNum][2];
pointArray[0][0] = 1; //点1のx座標
pointArray[0][1] = 1; //点1のy座標
pointArray[1][0] = 500; //点2のx座標
pointArray[1][1] = 75; //点2のy座標
pointArray[2][0] = 1; //点3のx座標
pointArray[2][1] = 150; //点3のy座標
pointArray[3][0] = 500; //点4のx座標
pointArray[3][1] = 270; //点4のy座標
pointArray[4][0] = 1; //点5のx座標
pointArray[4][1] = 500; //点5のy座標
float shapePitch = 40; //描画単位を描く間隔
float angle = 0; //角度
float plusX = 0; //x座標の増分
float plusY = 0; //y座標の増分
int boundsX = width + shapeSize;
int boundsY = height + shapeSize;
stroke( 150, 60, 80, 100 );
fill( 150, 60, 80, 70 );
for( int i = 0; i < pointNum-1; i++ ) {
//描画位置を配列から取得
float drawPointX = pointArray[i][0];
float drawPointY = pointArray[i][1];
//現在の点と次の点を結ぶ直線の角度を求める
angle = atan2( pointArray[i+1][1] - pointArray[i][1], pointArray[i+1][0] - pointArray[i][0] );
//その直線上の間隔がshapePitchの場合のxとyを求める
plusX = cos( angle ) * shapePitch;
plusY = sin( angle ) * shapePitch;
//描画範囲画面より少し大きい間だけ描画する
while( -shapeSize < drawPointX && drawPointX < boundsX &&
-shapeSize < drawPointY && drawPointY < boundsY ) {
drawShape( drawPointX, drawPointY, shapeSize );
drawPointX = drawPointX + plusX;
drawPointY = drawPointY + plusY;
}
}
}
void drawShape( float centerX, float centerY, int shapeSize ) {
noStroke();
float tmpSize = random( shapeSize/4, shapeSize/2 );
float tmpX = random( -tmpSize/2, tmpSize/2 );
float tmpY = random( -tmpSize/2, tmpSize/2 );
int drawRepeat = 4;
ellipse( centerX + tmpX, centerY + tmpY, tmpSize, tmpSize );
for( int i = 0; i < drawRepeat; i++ ) {
tmpSize = random( shapeSize/8, shapeSize/4 );
tmpX = random( -shapeSize/4, shapeSize/4 );
tmpY = random( -shapeSize/4, shapeSize/4 );
ellipse( centerX + tmpX, centerY + tmpY, tmpSize, tmpSize );
}
}

ここまでくると、描画単位とは例えばとある種類の画材(筆など)の特徴であり、それを軌跡にそって描画することはその画材で線を描くことに相当することが理解されるかと思います。
この方法に限りませんが、「描画単位+軌跡」の組み合わせを独自にどのように考えるかが、プログラムで行うヴィジュアル生成の1つの作り方になります。