単純なパターンとアルゴリズムで、面白い模様を生成できる Truchet Tiling。
この記事では Truchet Tiling とは?から始めて、パターン生成の考え方、自作パターンを使って模様を描けるサンプルコードまでを紹介します。
p5.js や Processing を使って、自作パターンで Truchet Tiling を楽しんでみましょう。
👉 Read this article in English.
Truchet Tiling とは?
Truchet Tiling とは、Truchet タイルを使って作る模様です。 その Truchet タイルというのは、回転対称ではないパターンを持った正方形のタイルです。
Wikipedia : Truchet tiles (英語)
…どうにもうまく説明できてる気がしないので、具体例を見ながら説明させてください。
このような図をご覧になったことがあるのではないでしょうか?
これは、10 x 10個のマス目にタイルをはめ込んだものです。マス目に線を引くとわかりやすいと思います。
ここで使っている Truchet タイルは下図の A です。 隣の B は、A を 90度回転させたものになります。
このたった一種類のタイル A を、ランダムに 90度回転させながらマス目にはめていくことで、こういう図を描くことができます。
この 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度回転させて同じパターンにならなければ、ひとまず成立します。
でも、それだけでは、線が繋がらずに切れるところが出て、あまり面白い絵になりません。
これを、「回転させても 4辺の接合点が同じ位置にくるパターン」にすると、切れることなく繋がります。
4辺の接合点を同じ位置に合わせるには、下記の方法が使えます。
1.ある 1辺上に接合点を決める
2.それを残りの 3辺に 90度づつ回転させながらコピーする
3.上下、あるいは左右反転させる
こうやって作った接合点間に線を引いて結んでいきます。 その際、90度回転させたときに同じパターンにならないように注意して線を引きます。 ここでは手書きで線を引いてみました。
これを使って模様を生成してみると、こんな感じになります。
自作パターンで遊んでみよう
先に示したサンプルコードを使って、自作パターンで模様を描いてみましょう。 サンプルコードの 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() を使って、線ではなく面を塗ってパターンを作りました。元ネタは 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);
}