🎢

【p5.js】 グリッド芸、あるいはとある作品の製作過程について

に公開

本記事は Processing Advent Calendar 2025 に参加しています。

はじめに

本記事では、「グリッド芸」(と私が勝手に呼んでいるやつ) について僅かに触れ、残りで「グリッド芸」の一例としてある作品の製作過程を紹介します。段階を追うのでハンズオンとしての側面もあります。

とりあげる作品は以下です (...拙作ですが)

https://x.com/SnowEsamosc/status/1980975397480403432?s=20


このコードをより読みやすくしたものが以下になります [1]。本記事ではこちらを取り扱います。

// キャンバスのサイズ
let W=500
function setup(){
  createCanvas(W,W)
  noStroke()
}
function draw(){
  for(let x=0;x<W;x+=10)
  for(let y=0;y<W;y+=10){
    let isWhite=true
    let angle = atan2(y-250,x-250)
    let distance = dist(x,y,250,250)
    
    for(let sqSize=0;sqSize<W;sqSize+=30){
      let rotatedAngle = angle + frameCount*sqSize/1e4
      
      if(sqSize/max(abs(cos(rotatedAngle)),abs(sin(rotatedAngle))) < distance){
        isWhite = !isWhite
      }
    }
    fill(isWhite?255:0)
    
    rect(x,y,10)
  }
}

また、このコードは p5.js WebEditor に 上がっています のでぜひ参考にしてください。

グリッド芸とは?

グリッド芸とは、キャンバスを小さめの格子状に分割し、それらのパーツを各々塗分けていく描画方法です。先ほど紹介したものでは、10 ピクセルごとに分割しています。

単純に情報量が増え、予想もしない視覚効果が生まれます

... という御託はどうでもいいので早速作品の製作過程を紹介します。

手順

ベース

まずはグリッド芸をするため、キャンバス一面に正方形を敷き詰める、基本となる形を作りましょう。

// キャンバスのサイズ
let W=500
function setup(){
  createCanvas(W,W)
}
function draw(){
  for(let x=0;x<W;x+=10)
  for(let y=0;y<W;y+=10){
    rect(x,y,10)
  }
}

p5.js は、デフォルトで図形を黒線で囲むことになっているので、書いた図形が分かりやすくて良いですね。
以降、このパーツを塗り分けることで描画していきます。

各正方形の、中心からの距離と角度を計算する

各正方形について、中心からの距離と角度を知っておくと後々都合がいい [2] ので、計算します。

// キャンバスのサイズ
let W=500
function setup(){
  createCanvas(W,W)
}
function draw(){
  for(let x=0;x<W;x+=10)
  for(let y=0;y<W;y+=10){
    let angle = atan2(y-250,x-250)
    let distance = dist(x,y,250,250)
    
    rect(x,y,10)
  }
}

(実行結果は変わりません)

ここで、距離の計算には dist を、角度の計算には atan2 [3] を使います。

こうして計算した angledistance を使用して色を決めることで、「条件を回転させる」ことが簡単にできるようになります。このことはまた後で説明します。

正方形の内側だけ白にする

さて、正方形の内側を白くする条件を追加します。

...ここが一番難しいところで、これを超えればあとは割とすんなりと飲み込めると思います。

// キャンバスのサイズ
let W=500
function setup(){
  createCanvas(W,W)
}
function draw(){
  for(let x=0;x<W;x+=10)
  for(let y=0;y<W;y+=10){
    let isWhite = true                                // 追加
    let angle = atan2(y-250,x-250)
    let distance = dist(x,y,250,250)
    
    let sqSize = 200                                            // 追加
    if(sqSize/max(abs(cos(angle)),abs(sin(angle))) < distance){ // 追加
      isWhite = false                                           // 追加
    }                                                           // 追加
    
    fill(isWhite?255:0) // 追加
    rect(x,y,10)
  }
}

グリッドを塗り分けることで正方形を描画したいのですが、後のために xy を使わず、angledistance を使うだけで色塗りの判定をしたいです。

ここで役に立つのが 「極座標表示」というやつです。

極座標表示の考え方によれば、一辺の長さが 2a である正方形は、中心から角度\theta 方向に直進した時に、長さ \frac{a}{max(|\cos\theta|, |\sin\theta|)} 地点で境界線に当たることが知られています。 [4]

つまり、中心からの距離がそれよりも遠いパーツは黒で塗ればよいわけです。それを表したのが以下の部分です。

if(sqSize/max(abs(cos(angle)),abs(sin(angle))) < distance){ 
  isWhite = false                                           
}                                                           

回す

さて、さっきのようにわざわざ面倒な手順を踏んでまで xy を条件に使わなかったために、以下のようなことが出来るようになります。

// キャンバスのサイズ
let W=500
function setup(){
  createCanvas(W,W)
}
function draw(){
  for(let x=0;x<W;x+=10)
  for(let y=0;y<W;y+=10){
    let isWhite = true                                
    let angle = atan2(y-250,x-250)
    let distance = dist(x,y,250,250)
    
    let sqSize = 200                                            
    let rotatedAngle = angle + frameCount/300 // 追加 (frameCountを使って回す)
      
    if(sqSize/max(abs(cos(rotatedAngle)),abs(sin(rotatedAngle))) < distance){ // 変更
      isWhite = false                                           
    }                                                           
    
    fill(isWhite?255:0)
    rect(x,y,10)
  }
}

angle を使って条件を指定できたということは、渡す角度にいくらか加えてやれば、別の角度の条件を取り出して使用することが出来るようになるわけです。いわゆる、極座標の回転操作に当たります。

この回転を frameCount を使って回すことで、正方形がくるくる回る感じになります。

増やす

ここまでくればもう一息です。

まずは、sqSize を for 文で回すことで、正方形を増殖させます。

// キャンバスのサイズ
let W=500
function setup(){
  createCanvas(W,W)
}
function draw(){
  for(let x=0;x<W;x+=10)
  for(let y=0;y<W;y+=10){
    let isWhite = true                                
    let angle = atan2(y-250,x-250)
    let distance = dist(x,y,250,250)
    
    //let sqSize = 200 // 削除して...
    for(let sqSize=0;sqSize<W;sqSize+=30){ // ...追加
      let rotatedAngle = angle + frameCount/300
      if(sqSize/max(abs(cos(rotatedAngle)),abs(sin(rotatedAngle))) < distance){
        isWhite = false                                           
      }                                                           
    }
    
    fill(isWhite?255:0)
    rect(x,y,10)
  }
}

...おや...?

sqSize をfor で回した影響で、「最も小さい正方形の条件が優先された」状態になってしまっています。

重なりをうまく表現するためには、「条件が満たされるたびに、色を反転させる」のが良いでしょう。


// キャンバスのサイズ
let W=500
function setup(){
  createCanvas(W,W)
}
function draw(){
  for(let x=0;x<W;x+=10)
  for(let y=0;y<W;y+=10){
    let isWhite = true                                
    let angle = atan2(y-250,x-250)
    let distance = dist(x,y,250,250)
    
    for(let sqSize=0;sqSize<W;sqSize+=30){
      let rotatedAngle = angle + frameCount/300
      if(sqSize/max(abs(cos(rotatedAngle)),abs(sin(rotatedAngle))) < distance){
        isWhite = !isWhite //変更                                         
      }                                                           
    }
    
    fill(isWhite?255:0)
    rect(x,y,10)
  }
}

正方形ごとに回る速度を変える

全部の正方形が同じ速度で回るのはちょっとつまらないので、正方形ごとに回る速度を変えましょう

// キャンバスのサイズ
let W=500
function setup(){
  createCanvas(W,W)
}
function draw(){
  for(let x=0;x<W;x+=10)
  for(let y=0;y<W;y+=10){
    let isWhite = true                                
    let angle = atan2(y-250,x-250)
    let distance = dist(x,y,250,250)
    
    for(let sqSize=0;sqSize<W;sqSize+=30){
      let rotatedAngle = angle + frameCount*sqSize/1e4 // 変更
      if(sqSize/max(abs(cos(rotatedAngle)),abs(sin(rotatedAngle))) < distance){
        isWhite = !isWhite                     
      }                                                           
    }
    
    fill(isWhite?255:0)
    rect(x,y,10)
  }
}

先ほど frameCount を使うことでくるくる回すようにしましたが、その速度を sqSize を使うことで変えれば、「大きい正方形ほど早く回る」ようになります。


あとはお好みで枠線を消しましょう

// キャンバスのサイズ
let W=500
function setup(){
  createCanvas(W,W)
  noStroke() // 追加
}
function draw(){
  for(let x=0;x<W;x+=10)
  for(let y=0;y<W;y+=10){
    let isWhite = true                                
    let angle = atan2(y-250,x-250)
    let distance = dist(x,y,250,250)
    
    for(let sqSize=0;sqSize<W;sqSize+=30){
      let rotatedAngle = angle + frameCount*sqSize/1e4
      if(sqSize/max(abs(cos(rotatedAngle)),abs(sin(rotatedAngle))) < distance){
        isWhite = !isWhite                                       
      }                                                           
    }
    
    fill(isWhite?255:0)
    rect(x,y,10)
  }
}

これで完成です!

おわりに

グリッド芸をやるには普段 p5.js で描画するのとは別の考え方が必要になってきますが、上手く行くと楽しいです。ぜひあなたもやってみて、作品を見せてください!

裏話

...説明が大分淡白だな、と思いましたか?んーすみません、反省点です。実は一回 30% ぐらい書いたんですが保存を忘れて吹っ飛んでしまいました。再度書くのに際して説明が少なめになってしまった感は否めません。

脚注
  1. 読みやすくしたついでに、処理も少し変えていますが、元のコードとほぼ等価です ↩︎

  2. 距離と角度を知っておくと今回のような回転する図形を描画するのに大分楽ができます。 ↩︎

  3. atan2 を使うというのは若干しっくりこないかもしれません。詳しい説明は他の記事に譲るとして、p5.js のドキュメントにも角度を求めるのが主目的であるかのように書かれていますから、そういうものだ、と思って読んでいくのでも構いません ↩︎

  4. なんと拙著のこの記事 はこのための布石なのでした! ↩︎

Discussion