自作パターンで Truchet Tiling を楽しもう

2022年3月1日火曜日

p5.js Processing テクニック 作例 初心者向け

t f B! P L
自作パターンで Truchet Tiling

単純なパターンとアルゴリズムで、面白い模様を生成できる Truchet Tiling。

この記事では Truchet Tiling とは?から始めて、パターン生成の考え方、自作パターンを使って模様を描けるサンプルコードまでを紹介します。

p5.js や Processing を使って、自作パターンで Truchet Tiling を楽しんでみましょう。

👉 Read this article in English.

 

Truchet Tiling とは?

Truchet Tiling とは、Truchet タイルを使って作る模様です。 その Truchet タイルというのは、回転対称ではないパターンを持った正方形のタイルです。

Wikipedia : Truchet tiles (英語)

…どうにもうまく説明できてる気がしないので、具体例を見ながら説明させてください。

このような図をご覧になったことがあるのではないでしょうか?

よく見る Truchet Tiling.

これは、10 x 10個のマス目にタイルをはめ込んだものです。マス目に線を引くとわかりやすいと思います。

よく見る Truchet Tiling にマス目を入れる

ここで使っている Truchet タイルは下図の A です。 隣の B は、A を 90度回転させたものになります。

一枚の Truchet タイル

このたった一種類のタイル A を、ランダムに 90度回転させながらマス目にはめていくことで、こういう図を描くことができます。

よくある Truchet Tiling.32 x 32

この Truchet タイルを使って図を描くサンプルコードを p5.js で書きました。


/*
 * Truchet Tiling example code.
 * p5.js
 * @author @deconbatch
 * @version 0.1
 * created 2022.02.28
 * license CC0
 */

const w = 640;
const h = w;
const num = 10;

function setup() {
  createCanvas(w, h);
  imageMode(CENTER);

  const cell = getPattern(floor(w / num));

  background(224);
  for (let x = 0; x < num; x++) {
    for (let y = 0; y < num; y++) {
      push();
      translate((x + 0.5) * w / num, (y + 0.5) * height / num);
      if (random(1.0) < 0.5) {
        rotate(HALF_PI);
      }
      image(cell, 0, 0);
      pop();
    }
  }
}

function getPattern(_size) {
  g = createGraphics(_size, _size);
  g.background(224);
  g.noFill();
  g.stroke(96);
  g.strokeWeight(_size * 0.1);
  g.circle(0, 0, _size);
  g.circle(_size, _size, _size);
  return g;
}

 

パターン生成の考え方

単純なパターンのタイルを単純なルールで並べていくことで面白い絵ができ上がるのは、ちょっとした驚きがあって楽しいですね。自分で作ったパターンを使えたら、もっと楽しいのではないでしょうか。

パターンは、90度回転させて同じパターンにならなければ、ひとまず成立します。

あまり良くない Truchet タイル例

でも、それだけでは、線が繋がらずに切れるところが出て、あまり面白い絵になりません。

あまり良くない Truchet タイルでのパターン

これを、「回転させても 4辺の接合点が同じ位置にくるパターン」にすると、切れることなく繋がります。

良い Truchet タイル例
良い Truchet タイルで作った模様

4辺の接合点を同じ位置に合わせるには、下記の方法が使えます。
1.ある 1辺上に接合点を決める

1辺上の点

2.それを残りの 3辺に 90度づつ回転させながらコピーする

1辺上の点を残りの3辺にコピー

3.上下、あるいは左右反転させる

点を上下反転させてコピー

こうやって作った接合点間に線を引いて結んでいきます。 その際、90度回転させたときに同じパターンにならないように注意して線を引きます。 ここでは手書きで線を引いてみました。

手書きの Truchet tile.

これを使って模様を生成してみると、こんな感じになります。

手書きの Truchet タイルで作った模様

 

自作パターンで遊んでみよう

先に示したサンプルコードを使って、自作パターンで模様を描いてみましょう。 サンプルコードの getPattern() を改変するだけで、独自の模様を生成できますよ。

もし、手書きのパターンを使いたければ、getPattern() ではなく、loadImage() で手書きパターンの画像を読み込むとよいです。そのとき、マス目の大きさに画像を resize() するのを忘れずに。


let img;
function preload() {
  img = loadImage('ptn.jpg');
}

function setup() {
  createCanvas(w, h);
  imageMode(CENTER);

  const cell = img.resize(floor(w / num), floor(w / num));


 

Processing での作例

'bezier()' 関数で作った Truchet タイリング

この作例では、bezier() を使って、線ではなく面を塗ってパターンを作りました。元ネタは AltEdu202201 イベントの「Day 22. これまで使ったことがない関数を用いて」で作ったコードです。

AltEdu202201 イベントについてはこちらをご覧ください。

回転でも作れるんですが、回転の場合、軸がずれて隙間が出ることがあるので、この作例では左右上下反転を使いました。

タイルの間に隙間が出来た例隙間が出ちゃってる

このコードは GPL3 のライセンスで公開します。ライセンス条項にのっとってご自由にお使いください。


/**
 * Magic Carpet Ride.
 * auto pattern generated Truchet Tiling.
 * 
 * @author @deconbatch
 * @version 0.1
 * @license GPL Version 3 http://www.gnu.org/licenses/
 * Processing 3.5.3
 * 2022.02.20
 */

void setup() {

  size(900, 900);
  colorMode(HSB, 360, 100, 100, 100);
  imageMode(CENTER);

  int   frmMax  = 3;
  int   divBase = 15;
  int   start   = floor(random(frmMax));
  float hueBase = random(360.0);

  for (int frmCnt = 0; frmCnt < frmMax; frmCnt++) {

    int div = divBase + ((start + frmCnt) % frmMax) * 5;
    float hueVal = hueBase + frmCnt * 120.0;

    PImage cell = getPattern(floor(width / div), hueVal);
    background(hueVal % 360.0, 5.0, 80.0, 100.0);
    for (int x = 0; x < div; x++) {
      for (int y = 0; y < div; y++) {
        pushMatrix();
        translate((x + 0.5) * width * 1.0 / div, (y + 0.5) * height * 1.0 / div);
        //  rotate(floor(random(4.0)) * HALF_PI);
        if (random(1.0) < 0.5) {
          scale(-1.0, 1.0);
        }
        if (random(1.0) < 0.5) {
          scale(1.0, -1.0);
        }
        image(cell, 0, 0);
        popMatrix();
      }
    }

    casing(100);
    saveFrame("frames/" + String.format("%04d", frmCnt + 1) + ".png");

  }
  
  exit();
  
}

/** 
 * getPattern : returns random generated pattern
 */
PImage getPattern(int _size, float _hue) {
  // connect points
  PVector[][] points = {
    {new PVector(0.0, 0.5), new PVector(0.5, 1.0)},
    {new PVector(0.5, 0.0), new PVector(1.0, 0.5)},
    {new PVector(0.0, 0.25), new PVector(0.25, 0.0)},
    {new PVector(0.75, 1.0), new PVector(1.0, 0.75)},
    {new PVector(0.75, 0.0), new PVector(1.0, 0.25)},
    {new PVector(0.0, 0.75), new PVector(0.25, 1.0)},
  };
  int pNum = points.length;

  PGraphics g = createGraphics(_size, _size);

  g.beginDraw();
  g.colorMode(HSB, 360, 100, 100, 100);
  g.blendMode(BLEND);
  g.background(_hue % 360.0, 5.0, 80.0, 100.0);
  g.noStroke();
  for (int i = 0; i < pNum; i++) {
    PVector[] p = points[i];
    float hueVal = (_hue + i * 30.0) % 360;
    float pRnd = random(0.1, 0.9);
    g.fill(hueVal, 40, 60, 100);
    g.bezier(
             p[0].x * _size, p[0].y * _size,
             pRnd * _size, pRnd * _size,
             (1.0 - pRnd) * _size, (1.0 - pRnd) * _size,
             p[1].x * _size, p[1].y * _size
             );
  }
  g.endDraw();

  return g;

}

/**
 * casing : draw fancy casing
 */
public void casing(int _m) {
  fill(0.0, 0.0);

  strokeWeight(_m * 2 - 10.0);
  stroke(0.0, 0.0, 95.0, 100.0);
  rect(0.0, 0.0, width, height);

  strokeWeight(_m * 2 - 32.0);
  stroke(0.0, 0.0, 80.0, 100.0);
  rect(0.0, 0.0, width, height);

  strokeWeight(20.0);
  stroke(0.0, 0.0, 50.0, 100.0);
  rect(0.0, 0.0, width, height);

  strokeWeight(16.0);
  stroke(0.0, 0.0, 100.0, 100.0);
  rect(0.0, 0.0, width, height);
}

 

QooQ