Generative Art with JavaScript #2 実践編

![4.png](https://qiita-image-store.s3.amazonaws.com/0/248832/c43f977b-1ca2-2677-6397-990b0979bdaf.png)

[前回は Generative Art の概要](https://qiita.com/mimonelu/items/c8147fd07b70d060a4bb) を説明しました。
今回は実際に簡単な画像を JavaScript と Canvas で制作していきます。
なお、 [同等のコードが実際に動作しているページ](https://mimonelu.net/generative-art/practice/) はこちらです。

早速ですが、私は画像そのものの描画ロジック（固有部分）と、それ以外のロジック（共通部分）を分離してコーディングします。今回はそれにならい、まずは共有部分のコーディングから始めましょう。

## 共通部分のコーディング

適当な場所に適当な名前で HTML ファイルを作成してください。中身はこんな感じで良いでしょう。ほぼ必要最低限です。

```html
<body></body>
<style>

body {

    /* 背景色を暗めにする */
    background-color: #202020;

    /* 直下の要素を中央に配置する */
    display: flex;
    align-items: center;
    justify-content: center;

    /* 隙間を潰す */
    margin: 0;

}
canvas {

    /* ページ内に収まるようにする */
    max-width: 100vw;
    max-height: 100vh;

}

</style>
<script>

// canvas 要素の作成
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);

// context の作成
const context = canvas.getContext('2d');

// 固有部分がここに入る

</script>
```

現段階では何も表示されませんが、とりあえずブラウザで開いておきます。
CSS については、 canvas サイズが大きくなると全体を確認できなくなるため記述しています。

## 固有部分のコーディング

### canvas サイズを決める

今回はこんな感じです。

```javascript
canvas.setAttribute('width', 1280);
canvas.setAttribute('height', 792);
```

リサイズしても正常に画像が描画されるようにコーディングするのがまた難しいのですが、今回は触れません。

### 背景を塗りつぶす

塗り潰さないと背景が完全な透明になります。透過 PNG を作りたい際はそうしてください。今回は白で塗り潰します。

```javascript
context.fillStyle = '#ffffff';
context.fillRect(0, 0, canvas.width, canvas.height);
```

`context.fillStyle = 'rgba(255, 255, 255, 0.5)';` などとすれば半透明の白になりますね。

### 円を描く

とりあえず円を描いてみましょう。

```javascript
context.beginPath();
context.arc(canvas.width / 2, canvas.height / 2, canvas.height / 2, 0, Math.PI * 2);
context.strokeStyle = 'rgba(0, 0, 0, 0.5)';
context.stroke();
```

![1.png](https://qiita-image-store.s3.amazonaws.com/0/248832/349192ac-8443-d42d-3c73-f65cf09d8485.png)

円が描かれました。しかしつまらないですね。
ここからはセンスが問われる領域ですが、今回はよくある手法を使って面白くしましょう。

### 再帰関数で円を描く

先程の円のコードを削除し、次のコードを記述してください。

```javascript
const render = (x, y, r, level) => {

    // 残りの再帰回数が 0 であれば終了
    if (level <= 0) return;

    // 現在の円を描く
    context.beginPath();
    context.arc(x, y, r, 0, Math.PI * 2);
    context.strokeStyle = 'rgba(0, 0, 0, 0.5)';
    context.stroke();

    // 現在の円の中に複数の円を描く
    r /= 2;
    for (let i = 0; i < 2; i ++) {
        let v = i / 2 * Math.PI * 2;
        for (let a = 0, aa = 8; a < aa; a ++) {
            let av = a * Math.PI / aa;
            let nx = x + Math.sin(v + av) * r;
            let ny = y + Math.cos(v + av) * r;

            // 再帰する
            render(nx, ny, r, level - 1);

        }
    }

};
render(canvas.width / 2, canvas.height / 2, canvas.height * 0.5, 2);
```

次のような円が描かれるはずです。ちょっと面白くなってきましたね。

![2.png](https://qiita-image-store.s3.amazonaws.com/0/248832/c0104434-d3f3-f39a-b237-fc5a82a6a3bb.png)

では、最後の行の `2` を `3` とか `4` にしてみてください。さらに面白くなると思います（ `5` 以上は指定しないでください。ブラウザがハングアップします… ）。

![3.png](https://qiita-image-store.s3.amazonaws.com/0/248832/cdfeae7c-d2df-6b90-ff42-32222bc4c00a.png)

詳しい説明はできませんが、再帰関数を上手く使うと簡単に複雑な図形を描画できます。面白いのは、作った本人でも予想が付きにくい点です。これは Generative Art の醍醐味と言えるでしょう。

### 「最後の一工夫」

でも今の絵は黒い部分が多すぎますね。もう少し白い部分がほしいと思いませんか？つまり、もう少し面白くできる余地があるように思えます。
では、 `// 現在の円を描く` のコードブロックを下記のコードに差し替えてみてください。

```javascript
// 現在の円を描く
context.beginPath();
context.arc(x, y, r, 0, Math.PI * 2);
if (level === 1) {
    context.lineWidth = 1;
    context.strokeStyle = 'rgba(0, 0, 0, 0.5)';
    context.stroke();
} else if (level === 2) {
    context.lineWidth = 2;
    context.strokeStyle = 'rgba(255, 255, 255, 1.0)';
    context.stroke();
}
```

![4.png](https://qiita-image-store.s3.amazonaws.com/0/248832/c43f977b-1ca2-2677-6397-990b0979bdaf.png)

かなり面白くなりました！何となく質感が増して、オブジェっぽくなりましたね。
特に再帰関数を使った描画ロジックでは、色分けが重要なファクターになります。今回は再帰回数に応じて色と線の太さを変えてみました。もちろん、何度か試行錯誤を繰り替えた結果です。
このように「最後の一工夫」によって見た目が良くなることは頻繁にあります。面白くできる余地がなくなるまで（実際はなくなりませんが）試行錯誤してみると良いでしょう。

### ちょっとした反省

今回のサンプルコードではコアロジックに再帰関数を使い、状況に応じて面白くなるよう改変していくというスタイルを取りました。
これはこれで良いのですが、 Generative Art はあくまでアートです。自分の描きたい絵を頭の中で決めておき、その絵を実現するために試行錯誤するというスタイルこそ本来あるべきスタイルです。既存の数式やアルゴリズムに「使われない」ように気を付け…とは言え、今回はサンプルなので良しとしましょう^ｐ^

## 付録 1: 注意点

* デスクトップの Safari では `globalCompositeOperation = 'lighter'` が機能しません。
* やや古い iOS では `fillStyle` などにオブジェクトを代入した際、 `toString()` が暗黙の内に呼び出されません。 `toString()` を prototype に持つオブジェクトを使う場合は留意しておいた方が良いかもしれません。
* 画像ファイルを読み込んで加工する際はローカルサーバを立てる必要があります。 `file:///` プロトコルでは Canvas のセキュリティポリシーに違反してエラーが発生するためです。
* 普通、 canvas 要素は直接 DOM ツリーに追加するものですが、スマートフォンでは（少なくとも iPhone では） canvas 要素を画像として保存できません（以前はできたような気がするのですが…）。このため、ウェブで公開する際は canvas データを img 要素に出力した方が良いと思います。サンプルコードで言うと、こんな感じです。

```javascript
// img 要素の作成と body 要素への追加
const imageNode = document.createElement('img');
imageNode.setAttribute('src', canvas.toDataURL('image/png'));
document.body.appendChild(imageNode);
```

## 付録 2: 公開の場について

Generative Art を公開するにはどこが良いのか、まとめてみました。

* 静止画は Medium に投稿します。 [Generative Art タグ](https://medium.com/tag/generative-art/latest) を付けて投稿すれば 👏 がもらえると思います。
* 動画は Vimeo に投稿します。こちらにも [Generative Art タグ](https://vimeo.com/search?q=generative+art) がありますが、相当クォリティ高いですね… YouTube でも良いかもしれません。
* インタラクティブな作品は自サイト上に展示します。自サイトがない方は [GitHub Pages](https://pages.github.com/) を使いましょう。 **※実際に動作するものを展示する際は処理速度に注意してください。特にスマートフォンの処理能力はデスクトップに遠く及ばないため、クレームにつながります。**
* ついでに Twitter にツイートします。 [#generativeart タグ](https://twitter.com/search?f=tweets&vertical=default&q=%23generativeart&src=typd) を付けてツイートすれば、関連 Bot にフォローされ、毎回自動的にリツイートしてくれるかもしれません。

---

以上、 [前回](https://qiita.com/mimonelu/items/c8147fd07b70d060a4bb) と今回で Generative Art をざっくり説明してきました。
もっとも、本稿では静止画しか取り扱いませんでしたが、今後はインタラクティブな作品も折を見て公開していきたいと思います。