最近、ジェネレーティブアートという言葉は定着しつつありますね。言語はProcessingがよく使われていて、そのコミュニティーは活発ですね。今回はJavaScriptを使ってみます。
さて、ちょっと怪しげな、こんな模様を見たことはありませんか?

これは1次元セルオートマトン1を呼ばれるものです。仕組みについては検索すれば小難しい記事がいくらでも出てきます。その仕組みはアナログに手を動かして模様を実際に描いてみれば理解することは簡単です。
手書き
5ミリ方眼紙と鉛筆を用意します。縦横8×15マスの四角い枠を書きます。一番上の行の真ん中のマスを黒く塗りつぶします。

2行目から順番に下へ1行づつ規則に従ってマスを黒く塗りつぶしていきます。1行は15マスありまが、その1マス毎にそのマスの左上と真上と右上マスの計3マスの状態によって、そのマスを黒く塗りつぶすかどうか決まります。
上記の模様の規則は、左上と右上マスのいずれか片方が黒く塗りつぶされている場合に下マスを黒く塗りつぶします。両方の場合は塗りつぶしません。真上マスはどちらでも構いません。
塗りつぶす状態は4つで下図のようになります。

実際に描いてみてくださいね。
描いてみると気付くのですが、上図の右側2つの状態は描くのに使われません。
それでは次に、縦横9×16マスの四角い枠を書き、1番上の行の好きなマスを黒く塗りつぶし、同じように規則に従って黒く塗りつぶしてみます。

描いてみると左右の端のマスの場合、左上もしくは右上のマスがありません。いくつか対処方法はあります。例えば、黒く塗りつぶしているマス、もしくは塗りつぶしていないマスとするなどです。今回は、右端マスの場合は左上端マスを、左端マスの場合は右上端マスを代用することにします。つまり循環していることにします。上図の場合、各行を順に描いていくと下図のような模様が描けます。1番上の行を変えると模様も色々と変わってきます。でも、だいたいの場合、途中で模様が終わってしまい黒く塗りつぶすマスは無くなってしまいます。これは後で考えてみたいと思います。
規則をいろいろと変えたら様々な模様が描けるのではないか、規則の種類はどのくらいあるのか、と疑問に思うのではないでしょうか?
規則の理解
まず左上と真上と右上の計3マスの状態は何種類あるのかの考えてみます。

上図のように8種類あります。
組合せ数学を知っていればすぐに解ります。2つの状態から重複を許し3つ取る重複順列の総数は$2^3=8$です。8種類の状態のそれぞれにおいて次に黒く塗りつぶすかしないかの2種類あるので、同じように重複順列の総数は$2^8=256$となります。つまり規則は256種類あることが解ります。
これを2進数で考えてみます。
2進数を簡単に説明します。普段使っている数は10進数です。1から順に数えていくと9の次は1桁上がり10となります。8進数の場合は8の次は1桁上がり10となり、その次が11になります。4進数の場合は4の次は10となり、その次が11になります。2進数の場合は1の次が10になり、その次が11になり、その次がまた桁上がりして100になります。それぞれの進数を比較すると下の表のようになります。
| 10進数 | 8進数 | 4進数 | 2進数 |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 1 |
| 2 | 2 | 2 | 10 |
| 3 | 3 | 3 | 11 |
| 4 | 4 | 10 | 100 |
| 5 | 5 | 11 | 101 |
| 6 | 6 | 12 | 110 |
| 7 | 7 | 13 | 111 |
| 8 | 10 | 100 | 1000 |
| 9 | 11 | 101 | 1001 |
| 10 | 12 | 102 | 1010 |
黒く塗りつぶしたマスを1とし、塗りつぶさないマスを0とします。そして3つのマスにおける8つの状態と先の規則を表にしてみます。
| 状態(2進数) | 10進数 | 桁数 | 先の規則 |
|---|---|---|---|
000 |
0 | 1 | 0 |
001 |
1 | 2 | 1 |
010 |
2 | 3 | 0 |
011 |
3 | 4 | 1 |
100 |
4 | 5 | 1 |
101 |
5 | 6 | 0 |
110 |
6 | 7 | 1 |
111 |
7 | 8 | 0 |
上の表の先の規則を桁数を参照して並べてみると01011010となります。これを10進数に直してみます。
例えば10進数で1234は
1*1000+2*100+3*10+4*1
=1*10^3+2*10^2+3*10^1+4*10^0
ですので、2進数で01011010は同様に
0*2^7+1*2^6+0*2^5+1*2^4+1*2^3+0*2^2+1*2^1+0*2^0
=2^6+2^4+2^3+2^1
=64+16+8+2
=90
よって先の規則は90という10進数で表すことができました。規則の総数は2進数で全て塗りつぶしなしの00000000から全て塗りつぶしの11111111です。11111111を10進数で表すと
2^7+2^6+2^5+2^4+2^3+2^2+2^1+2^0
=128+64+32+16+8+4+2+1
=255
0から255まで規則があるので総数は重複順列の総数と同じ$255+1=2^8=256$を求めることができました。
規則の実装
それでは実際にJavaScriptで実装してきます。
規則から次の状態を求める関数
規則の数値ruleと左上マスleftと真上マスcenterと右上マスrightの状態(0か1)を引数に次の状態を計算する関数は以下のように書けます。
function state(rule, left, center, right) {
if ((rule & (2 ^ (left * (2 ^ 2) + center * (2 ^ 1) + right * (2 ^ 0)))) !== 0) {
return 1;
} else {
return 0;
}
}
三項演算子とビット演算子を使えば以下のように書き換えられます。
function state(rule, left, center, right) {
return (rule & (1 << ((left << 2) | (center << 1) | right))) !== 0 ? 1 : 0;
}
1行の状態をランダムに作る。
状態を配列で1行の状態を表します。1行の状態をランダムに作る関数は以下のように書けます。引数は1行の長さlengthで、rateは1となる確率で0から1の値を与えます。0.5のときはほぼ半々の確率で1になります。ランダムに生成した配列を返します。
function field(length, rate = 0.5) {
const field = [];
for (let i = 0; i < length; i++) {
field[i] = Math.random() < rate ? 1 : 0;
}
return field;
}
1行の状態を規則に従って変更する関数
引数は規則の数値ruleと1行の状態を表す配列fieldです。今回は破壊メソッドとし配列の中身を変更します。左端マスから始めるのですが、右端マスで再び左端マスの状態を使うので、変更される前にedgeを保持しておきます。
function next(rule, field) {
const length = field.length, edge = field[0];
let left = field[length - 1];
for (let i = 0; i < length - 1; i++) {
const center = field[i];
field[i] = state(rule, left, center, field[i + 1]);
left = center;
}
field[length - 1] = this.state(left, field[length - 1], edge);
return field;
}
規則クラスにまとめる
class Rule {
constructor(id) {
this.id = id;
}
next(field) {
const length = field.length, edge = field[0];
let p = field[length - 1];
for (let i = 0; i < length - 1; i++) {
const u = field[i];
field[i] = this.state(p, u, field[i + 1]);
p = u;
}
field[length - 1] = this.state(p, field[length - 1], edge);
}
state(left, center, right) {
return (this.id & (1 << ((left << 2) + (center << 1) + right))) !== 0 ? 1 : 0;
}
field(length, rate = 0.5) {
const field = [];
for (let i = 0; i < length; i++) {
field[i] = Math.random() < rate ? 1 : 0;
}
return field;
}
}
模様の描画
引数に規則クラスのインスタンスruleと描画設定optionを与えると、画像としてcanvasを返します。描画設定optionは初期値DRAW_DEFを参考にしてください。
const DRAW_DEF = {
width: 320,
height: 320,
pixel: 1,
rate: 0.5,
on: "#000000",
off: "#f8f8f8"
};
const draw = (rule, option = {}) => {
for (const [name, value] of Object.entries(DRAW_DEF)) {
if (option[name] === undefined) option[name] = value;
}
if (option.field !== undefined) {
option.field = [...option.field];
option.width = option.field.length;
}
const
{ width, height, field, pixel, rate, edge, off, on } = option,
cv = document.createElement("canvas"),
ct = cv.getContext("2d"),
wh = [width * pixel, height * pixel],
vs = field ?? rule.field(width, rate);
[cv.width, cv.height] = wh;
ct.fillStyle = off;
ct.fillRect(0, 0, ...wh);
ct.fillStyle = on;
for (let i = 0, y = 0; i < height; i++, y += pixel) {
for (let j = 0, x = 0; j < width; j++, x += pixel) {
if (vs[j] === 1) ct.fillRect(x, y, pixel, pixel)
}
rule.next(vs);
}
return cv;
};
様々な規則の描画
実装した規則クラスRuleと描画関数drawを使って模様を描画してみます。
document.body.appendChild(draw(new Rule(105), { pixel: 4 }));
規則の総数は256であることが解っています。その全てを描画してみます。
それぞれの画像をクリックすると大きく描画した画像のページ遷移します。
総数256個といっても一覧にするとかなり多く感じます。よく見ると似たような画像も並んでいます。もう少し種類を分けてスッキリさせようと思います。
反転している規則
白黒が反転してネガポジみたいな画像が上記一覧にあります。規則を2進数で表すと0と1の8桁の羅列になりますが、その0と1を反転させた規則がこのネガポジの関係になります。
その2つのいずれか一方を一覧から削除しようと思います。
例えば規則90は2進数で01011010ですが反転させると10100101になり10進数では165になります。数が大きい方を一覧から削除したいと思います。数値を反転させて比較するのは面倒でないのですが、一番左端の値に注目して0*******を反転させると1*******となり必ず大きくなります。つまり規則0から規則127(01111111)までだけを一覧にすれば反転規則の重複は防げます。これで一覧の総数は128個となりました。
左右対称の規則
左斜めや右斜めの模様になっている画像が上記一覧にあります。規則をよく見てみると左右反転している状態があります。
| 10進数 | 2進数 | 左右対称性 | 左右反転 | コード |
|---|---|---|---|---|
| 0 | 000 |
O | O |
|
| 1 | 001 |
X | 100 |
} |
| 2 | 010 |
O | I |
|
| 3 | 011 |
X | 110 |
] |
| 4 | 100 |
X | 001 |
{ |
| 5 | 101 |
O | V |
|
| 6 | 110 |
X | 001 |
] |
| 7 | 111 |
O | X |
左右対称に規則を変換するは、1と4、3と6の状態を入れ変えることになります。非対称の規則42(00101010)は規則112(01110000)になります。こちらも同様に数の大きい方を一覧から削除します。
また左右対称に規則を変換しても変わらない規則があります。例えば規則90(01011010)です。このような規則を左右対称規則とし、一覧で別のグループにまとめます。さらに、10進数や2進数で表された規則はぱっと見解り辛いのでコード化してみます。上記表のコードを使い[{OIVX}]の順で1の時だけ抜き出すことによってコード化してみます。例えば規則90は[{}]となります。実装はこんな感じです。
const
CODE = [
{ str: "{", bit: 4 },
{ str: "[", bit: 6 },
{ str: "O", bit: 0 },
{ str: "I", bit: 2 },
{ str: "V", bit: 5 },
{ str: "X", bit: 7 },
{ str: "]", bit: 3 },
{ str: "}", bit: 1 }
];
class Rule {
//...
code() {
let c = "";
for (const o of CODE) {
if (bit(this.id, o.bit) !== 0) c += o.str;
}
if (c.length === 0) c = "-";
return c;
}
flip() {
return new Rule(~this.id & 255);
}
mirror() {
return new Rule((this.id & 165)
| ((this.id & 2) << 3) | ((this.id & 16) >> 3)
| ((this.id & 8) << 3) | ((this.id & 64) >> 3));
}
}
グループ分けした一覧
左右対称規則が計32個、左右非対称規則が計48個の計80個だけになりました。CODEと画像を比較してみるのその関係性が掴めるかと思います。
だいぶスッキリした感じですが、まだまだ模様の周期性など詳細に見ていけばグループ分けできそうです。カオスの縁と呼ばれるものです。
さくっと書くつもりが意外と長くなってしまいました。丸一日かかりました。疲れた。規則の分析も面白そうですが、さらなる発展として規則の拡張、例えば3マスを5マスに増やすとか、2状態を3状態に増やすとか、続きはまた今度。
-
英語ではElementary cellular automatonと呼びます。初級セルオートマトンって感じです。 ↩

