Vue.jsとcanvasで画像ジェネレーターを作る



## 前置き

たまにTwitterなどで見かける「〇〇画像ジェネレーター」ってどうやって作ってるんだろうと思ったので実際に作ってみることにしました。


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">写真や画像を恋愛ゲーム風にできるサイトを作りました。現実の写真をゲームの世界へ…！<br><br>普通の画像をギャルゲーや乙女ゲーっぽくしてみたり、好きなキャラクターの画像を使って自由にセリフを言わせてみたりして遊んでみてください！<br><br>恋愛ゲーム風ジェネレーター <a href="https://t.co/3To63WsdtP">https://t.co/3To63WsdtP</a> <a href="https://t.co/XHmcICPVub">pic.twitter.com/XHmcICPVub</a></p>&mdash; megaya (@megaya0403) <a href="https://twitter.com/megaya0403/status/1085828567223611393?ref_src=twsrc%5Etfw">January 17, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


作ったのはこちらです。写真とかをシュミレーションゲーム画面っぽくできるようなサイトを作りました。

恋愛ゲーム風ジェネレーター
https://love-game-generator.megaya.net/

Vue.js + canvasで作りました。フロントのみでサーバ側は作っていません。

本当は画像をサーバにアップロードして、ツイートにそのまま画像をだせるようにしたほうがいいのだろうけど、今回は画像ジェネレーター作りたかっただけなのでやっていません。Vue.jsを使う理由は、僕が普段から使っていて楽だからです。

簡単にどうやって作ったのか説明します。

## 画像を切り取ってcanvasに描画する


![恋愛ゲーム風ジェネレーター.png](https://qiita-image-store.s3.amazonaws.com/0/59008/ba8cf498-e99e-e70c-489c-2141a8482d3f.png "恋愛ゲーム風ジェネレーター.png")

画像をユーザが切り取ってそれを編集させるようにしたかったので、cropper.jsを使用することにしました。cropper.jsは画像の切り取りなどが簡単にできるライブラリです。

cropper.js
https://fengyuanchen.github.io/cropperjs/

いきなりVue.jsでやるまえに、まずはcropper.jsとcanvasだけで適当にコードを書いて試すことにしました。

下記は自分用にテストで書いたやつだけど、多分そのまま書いてブラウザで起動すれば動くはずです。コードの汚さはめんどくさいのでそのままにしてあります。すいません。


```html
<!DOCTYPE html>
<html>
  <head>
    <title>Cropper</title>
    <html lang="ja">
    <meta charset="utf-8"/>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.4.3/cropper.min.css" rel="stylesheet" type="text/css" media="all"/>
    <!-- JS -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.4.3/cropper.min.js"></script>
  </head>

  <body>
		<div class="container">
			<h1>Crop on canvas with Cropper</h1>
			<h3>Image</h3>

      <div class="upload"><input type="file" name="file" id="file"></div>

			<div id="img-container">
				<img id="image" src="" width="980">
			</div>
			<h3>Canvas</h3>
			<div>
				<canvas id="canvas" class="img-canvas" width="980" height="580"></canvas>
			</div>
		</div>

    <a href="" id="download_link">ダウンロード</a>

		<script>
         var _cropper;
         function start() {
           var image = document.getElementById('image');
			  _cropper = new Cropper(image);
 		  }
        function loadLocalImage(e) {
          // ファイル情報を取得
          var fileData = e.target.files[0];
          // FileReaderオブジェクトを使ってファイル読み込み
          var reader = new FileReader();
          reader.onload = function() {
          // ブラウザ上に画像を表示する
          image = document.getElementById('image');
          image.src = reader.result;
          var canvas = document.getElementById('canvas');
          if (image.complete) {
            start.call(image);
          } else {
            image.onload = start;
          }
        }
        // ファイル読み込みを実行
        reader.readAsDataURL(fileData);
      }

      var file = document.getElementById('file');
      file.addEventListener('change', this.loadLocalImage, false);
      var imageContainer = document.getElementById('img-container');

      var imageContainer = document.getElementById('img-container');
      imageContainer.addEventListener('click', function() {
        //getDataは用意されたオプション
        var cropperData = _cropper.getData();

        var data = {
          x: Math.round(cropperData.x),
          y: Math.round(cropperData.y),
          width: Math.round(cropperData.width),
          height: Math.round(cropperData.height),
          vectorX: 1,
          vectorY: 1
        };

        var image = document.getElementById('image');
        var canvas = document.getElementById('canvas');
        canvas.getContext('2d').drawImage(
          image,
          data['x'],
          data['y'],
          data['width'],
          data['height'],
          0,0,//切り出されるCanvas内での座標指定
          data['vectorX'] * 980, //切り出される画像の横幅
          data['vectorY'] * 580 //切り出される画像の縦幅
        );
        var canvasElement = document.getElementById('canvas');
        var dlLink = document.getElementById('download_link');
        dlLink.href = canvasElement.toDataURL();
        dlLink.download = 'sample.jpg';
      });
		</script>
	</body>
</html>

```

単純にcropper.jsの領域内をクリックされたときにcanvasに描画されるようにしました。


```javascript
function loadLocalImage(e) {
  // ファイル情報を取得
  var fileData = e.target.files[0];
  // FileReaderオブジェクトを使ってファイル読み込み
  var reader = new FileReader();
  reader.onload = function() {
	  // ブラウザ上に画像を表示する
	  image = document.getElementById('image');
	  image.src = reader.result;
	  var canvas = document.getElementById('canvas');
	  if (image.complete) {
	    start.call(image);
	  } else {
	    image.onload = start;
	  }
  }
  // ファイル読み込みを実行
  reader.readAsDataURL(fileData);
}

```

一点だけ気をつけるべきポイントは、画像の読み込むを待つことです。ここ少しハマりました。

`reader.onload`を入れて、画像が読み込まれたときに`start`されるようにすると上手くいきます。

canvasに描画した画像のダウンロードはaタグのsrcにぶちこんでいるだけです。

## Vue.jsでcropper.jsを扱う

canvas + cropper.jsが使えるとわかったので、Vueと組み合わせて完成させます。

cropper.jsはvue-cropperjsというラッパーがあるのでそれを使用しました。

```

<div @click="drawCanvas">
  <vue-cropper
      ref='cropper'
      :guides="true"
      :view-mode="2"
      drag-mode="crop"
      :auto-crop-area="0.5"
      :background="true"
      :rotatable="true"
      :src="cropperOptions.img"
      alt="Source Image"
      :img-style="{ 'width': '600', 'height': '400px' }">
  </vue-cropper>
</div>

<script>
import VueCropper from 'vue-cropperjs'  

// 省略
</script>

```

`components: { VueCropper }`のようにコンポーネントを呼び出せて使えるのでめちゃくちゃ楽でした。初めからVue.jsで開発すればよかったなぁ…とこのとき思いました。

ただ、Vueだろうが素のjsだろうが基本はやることは一緒です。cropperで切り取ったものは先ほどのコードと同じようにCanvasに描画していきます。

![恋愛ゲーム風ジェネレーター.png](https://qiita-image-store.s3.amazonaws.com/0/59008/2046f559-1dff-b854-a304-d032b4ad52db.png "恋愛ゲーム風ジェネレーター.png")

```html
<canvas id="canvas" class="img-canvas" width="680" height="480" ref="canvas"></canvas>  

```


```vue.js

drawCanvas() { 
  this.canvasContext = this.$refs.canvas.getContext('2d'); 
  this.canvasContext.clearRect(0,0, this.canvasContext.canvas.width, this.canvasContext.canvas.height); // 一旦canvasをすべてクリアする
  
  // drawImageやfillTextでcanvasに文字を描画していく
}
```


あとはボタンが押されたり、文字が入力されるごとに再描画するようにして、リアルタイムで反応するようにしました。
だから<input>タグや<button>タグには`@click="drawCanvas"`のようなイベントが多く書いてあります。

canvasの大きさに対してボタンや文字の位置を調整しようと思ったのですが、画像を描画するときの高さや幅などはベタ打ちにしました。妥協点。


```html
<img :src="imageSrc">
```

```vue.js
this.imageSrc = this.$refs.canvas.toDataURL('image/jpeg'); 
```

完成した画像は<img>のsrcに入れて表示するようにしてあります。


## 失敗した点

* canvasをレスポンシブにすることを考えていなかったので、PCとスマホで共通して同じ画面幅になってしまった。
* 画像のダウンロード方法がよくない
	* 最初はリンクを押したときにダウンロードさせていたが、スマホのLINEなどのブラウザで起動しなかった。おそらく多重ブラウザをひらけないのが原因ぽい。
* ソース全部公開する予定だったけど汚すぎてやめた


## 終わり

かなり勢いで作ったのでこの記事の説明も適当で申し訳ありません。
Canvas自体をそんなに触ったことがなかったのでいい勉強になりました。またなにか作ります。