見出し画像

【ジェネラティブアート】 Processingで様々なサイズの正方形を敷き詰めるレイアウトスクリプトを作る

思えば、自分がジェネラティブアートに手を出すきっかけは、「規則的・複数・ランダムの3つを備わったコンテンツを手作業で作るのはしんどいし面白みがない」と、After effectsを弄りながら思ったことである。

そうしたときに「スクリプトでどうにかならない?」なんて考えから、その練習にとProcessingを触り始めた。

で、ここから本日の本題に繋がる話だ。

じゃあProcessingで最初にどんなものを作るかと考えた時、真っ先に思い浮かんだのは、「様々なサイズのマス目(区画)を作り、そこに何かを似て非なるものを埋め込む」というものだった。
そして、「とりあえず、まずはやってみよう」と、早速スクリプトの記述にとっかかり始めた。

一番最初に作った「正方形分割スクリプト」

シンプルな分割ルール・コードで作ってみる

まずは最初に作ったスクリプト(というか肝になる関数)を記しておく。私がProcessingを触りはじめて数日経った頃に、以下の記事を参考に、書いたものだ。

  1. まず分割したい区画の左上の座標(x,y)と、区画の幅サイズ(w)を用意する。

  2. gridNumbersに設定された分割値をランダムでチョイスし、分割後の幅サイズ(gSize)を設定。

  3. for文で、wサイズの区画から、gSizeの区画へ分割。
    もし「gSizeが一定未満のサイズ」「乱数が66以下」であれば、分割した区画をさらに分割するよう再帰処理する。

void drawRandomGrid(int x, int y, int w){
 int[] gridNumbers = {1,2,4};
 int gridNum = gridNumbers[int(random(0,360)%gridNumbers.length)];
 
 int gSize = w/gridNum;
 
 for(int i=x;i<x+w;i+=gSize){
   for(int j=y;j<y+w;j+=gSize){
     if(random(100)<66 && gSize > width/(gridNumbers[gridNumbers.length - 1])){
       drawRandomGrid(i,j,gSize);
     }else{
       //ここに描きたいコンテンツを記述する
     }
   }
 }
}


シンプルで簡単だけど、あまり融通も効かない

ぱっと見、問題ないようにみえる。しかし、色々と弄っていると以下のような問題が気になってしまった。

  • キャンバスが正方型でないと見切れる / 大きな余白ができる

  • このスクリプト、要は「正方形を何度もX等分することを繰り返す」ものであり、ランダムに区画された感じがしない

  • また、分割値(gridNumbers)に、3以上の奇数が含まれる場合、描画がバグる。(gSizeに対して、綺麗に割り切れない数値ゆえ、発生する)

こだわらなければこれでも十分いいとは思う。だが、「もう少し、こっちが想定していないようなランダム感のある区画を描画したい」という気持ちがあった。


その後作り直した「改:正方形分割スクリプト」

その後、ちょっとスパンを開けて、根本的なところから作り直し。

※ちなみに、以下のコードは、自分のサイトに組み込むタイミングで作り直したものなので、Processingではなくp5.jsで記述しております。ご了承下さい。

var cWidth, cHeight; 
var cStartX, cStartY;
var splitMinNum;
var splitWNum, splitHNum;
var boxes = [];

function setup() {
  smooth();
  createCanvas(726,366);
  noLoop();
  splitMinNum = 8;
  adjustFromSplitNum(0,0,width,height,splitMinNum);
  generateRandomGrid(cStartX,cStartY,cWidth,cHeight, splitWNum,splitHNum);
}

function draw() {
  background(0);
  for(let i =0; i<boxes.length;i++){
    box = boxes[i];
    push();
    drawBoxContent(box.lx, box.ly, box.lwidth, box.lheight, box.c);
    pop();
  }
}

function generateRandomGrid(_x,_y,_w,_h,_sw,_sh){
  
  if(((_w==_h) && (_w != cWidth)) || ((_w==_h) && (_h != cHeight))){
    c =color(random(0,255));
    boxes.push(new Box(_x,_y,_w,_h,c));
  }
  
  if((_w > _h) || (_w==cWidth)){
    
    let b1sw = int(random(1, _sw-1));
    let b2sw = _sw - b1sw;
    
    let w1 = _w * b1sw/_sw;
    let w2 = _w - w1;

    generateRandomGrid(_x, _y, w1, _h, b1sw, _sh);
    generateRandomGrid(_x + w1, _y, w2, _h, b2sw, _sh);
  }else if((_w < _h) || (_h==cHeight)){
    
    let b1sh = int(random(1, _sh-1));
    let b2sh = _sh - b1sh;
    
    let h1 = _h * b1sh/_sh;
    let h2 = _h - h1;
    generateRandomGrid(_x, _y, _w, h1, _sw, b1sh);
    generateRandomGrid(_x, _y + h1, _w, h2, _sw, b2sh);
  }
}


function drawBoxContent(_x, _y, _w, _h, _c){
  push();

  //ここに描きたいコンテンツを記述する。
  
  pop();
}


function adjustFromSplitNum(_x,_y,_w,_h,_minS){
  let _maxS;
  adjustLength(_x,_y,_w,_h);

  if(cWidth >cHeight ){
    _maxS =  int(cWidth/(cHeight /_minS));
    cStartX = cStartX + (cWidth - _maxS*(cHeight /_minS))/2;
    cWidth = cWidth - (cWidth - _maxS*(cHeight /_minS));
    splitWNum = _maxS;
    splitHNum = _minS;
    
  }else if(cWidth < cHeight ){
    _maxS = int(cHeight/(cWidth/_minS));
    cStartY = cStartY + (cHeight -_maxS*(cWidth /_minS))/2;
    cHeight = cHeight - (cHeight -_maxS*(cWidth /_minS));
    splitWNum = _minS;
    splitHNum = _maxS;
  }else{
    _maxS = _minS;
    splitWNum = _maxS;
    splitHNum = _minS;
  }
}


function adjustLength(_x,_y,_w,_h){
  if(_w%10!=0){
    cWidth = _w-(_w%10);
    cStartX = _x+((_w%10)/2);
  }else{
    cWidth = _w;
    cStartX = _x;
  }
  
  if(_h%10!=0){
    cHeight = _h - (_h%10);
    cStartY = _y+((_h%10)/2);
  }else{
    cHeight = _h;
    cStartY = _y;
  }  
}

class Box{
  constructor(_lx,_ly,_lwidth,_lheight,_c){
    this.lx = _lx;
      this.ly = _ly;
      this.lwidth = _lwidth;
      this.lheight = _lheight;
      this.c = _c;
  }
}


まずは上記コードをざっくりと解説

上記コードについて大雑把な説明すると、描画するまでに大きく3つの処理を踏んでいる。

1.adjustFromSplitNum()で、描画する箇所・分割数の設定
まずは、描画コンテンツが見切れたりしないように、adjustLength()を使い、createCanvasで設定したキャンバスサイズから、描画箇所のサイズ・座標を設定している(cWidthなどがその描画箇所に関する変数である)。

描画箇所の設定後は、描画箇所の短辺に対してsplitMinNumの値で分割する場合、長辺の分割数がいくつになるのかを算出する。

2.generateRandomGrid()で、描画箇所を分割し、各区画をBoxオブジェクトとして生成
adjustFromSplitNum()で設定した描画箇所を基点に、区画を再帰的に分割していく。分割して区画が正方形になったらBoxオブジェクトとして生成していく。
なお_sw, _shというのは、generateRandomGrid()に渡した区画における縦横それぞれの分割可能数みたいなものである。渡した区画が正方形でない場合の分割処理において、この値で分割数を制御している。

3.drawBoxContent()で、各区画にコンテンツを描画
各区画(Boxオブジェクト)の座標とサイズ情報などを渡して、コンテンツ描写。ここに直接描画のコードを書いてもよいし、別途functionを書いて呼び出すもよい。


「改:正方形分割スクリプト」による作例

現在の私のサイトのサイドバーでランダムで表示されるジェネラティブアートのうちのひとつが、まさにそれ。
スクリプトを実行するたびに、以下のように、分割パターンや図形や背景色がランダムに描画されるようになっている。


このコードにすることの3つのメリット

このコードの良いところは、挙げるとすれば3点かなと思っている。

より複雑な敷き詰めができるようになった
以前のスクリプトは「等分を繰り返す」ため、例えば「縦にサイズが4,2,1,1の正方形が並ぶ」というように、何かキレイにまとまってしまう感があった。それに対し、このスクリプトであれば「縦にサイズが1,4,3の正方形が並ぶ」というような、不規則な分割・敷き詰めを実現できるようになった。

キャンバスサイズや分割数による制約が緩和された
setup()のタイミングでキャンバスサイズや分割数を調整するため、最初に書いたコードと比べて、だいぶ融通の効いた描画をしてくれる。

・・・と思っていたが、実は7とか13のような数字を分割数におくとエラーを吐いたりするので、まだまだ改良する余地あり。多分adjustLength()の処理について、splitMinNumを基準とした(約数となるような)サイズ調整をするようにすれば解消できるだろう。

Boxオブジェクトを使うことでアニメーションにも対応できる
Boxオブジェクトを使って各区画の座標と幅の情報を保持しておけば、区画の位置をずらすことなく再描画できる。そのため、Boxオブジェクト内でアニメーションを描画しても、安定的に表示することもできる。

「静止画でも問題ないよー。」という話であれば、メモリを食いそうなBoxオブジェクトをわざわざ使う必要はないので、ここは用途に応じて書き換えてもOK。


まとめ

記事を書くにあたりコードを改めて見直してみると、なんでこんな書き方してるんだろうか・・・という部分も多かった。例えば「drawBoxContent()は、座標など個別に渡さずboxオブジェクトを丸々渡せばいいのでないか」みたいなところ。

また、ぱっと見、上記コードでなんとか上手くできているようにみえるが、結構無駄が多い書き方だったり、まだまだ改善の余地(というか想定しうるエラーを潰すための修正)がある。

自分の目的が実現できれば細かいことはどうでもよい、って話ではあるが、どこかのタイミングで上記のコードをアップデートしたいなー、と思う。また、もし誰か改良版とか思いついたら、記事にして僕に連絡ください(参考にさせていただきます)。

この記事が気に入ったらサポートをしてみませんか?