Nuxt.jsで文字画像ジェネレータをつくる

Nuxt.js と Konva.js を使って、入力したテキストを**リアルタイムに**描画してくれる文字画像ジェネレータを作ります。**※モバイル未対応**

![generator.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633216/ff942f68-e6b4-1133-5b68-f2b096460fd7.gif)

GitHub のリポジトリとデモサイトはこちら。
Repo: https://github.com/HarukiKinoshita/nuxt-text-image-generator
Demo: https://nuxt-text-image-generator.vercel.app

# 構成
- nuxt: 2.0.0
- nuxt-buefy: 0.3.2
- konva: 7.2.5

# Vue.js / Nuxt.jsとは
[Vue.js](https://jp.vuejs.org/index.html) は Javascript フレームワークの1つで、[Nuxt.js](https://ja.nuxtjs.org/) は Vue.js をベースに様々な機能を盛り込んだフレームワークです。

Vue は **リアクティブ** なシステムと呼ばれ、コンポーネントが保持している値が更新されると、ビューが更新されます。また **双方向データバインディング** といって、DOM 要素の値と Vue インスタンスの値を連動させてくれます。

このジェネレータでは、これらの特徴を活かして、フォームにテキストを入力すると即座に変数の値を書き換えて Konva に渡し、canvas 要素に反映して描画結果を更新することができます。

# Konva.js とは
[Konva.js](https://konvajs.org/docs/overview.html) は、HTML5 の canvas 要素を操作してくれる Javascript ライブラリです。今回は Vue のリアクティブとバインディングに対応した [Vue Konva](https://github.com/konvajs/vue-konva) を利用します。

# 1. インストール

まずは Nuxt.js プロジェクトを作成します。

```shell:terminal
// yarn
yarn create nuxt-app <project-name>

// npx
npx create-nuxt-app <project-name>

// npm
npm init nuxt-app <project-name>
```

なおサンプルコードでは UI フレームワークに **Buefy** を使っています。`create-nuxt-app` の途中で Buefy を選択すればインストールされます。

続けて、下記コマンドを実行してvue-konvaとkonvaをインストールします。

```shell:terminal
// yarn
yarn add vue-konva konva --save

// npm
npm install vue-konva konva --save
```

#2. pluginsに追加
`plugins` 配下に下記のような js ファイルを作成します。ここではファイル名を `vue-konva.js` としてグローバルで登録しておきます。

```javascript:~/plugins/vue-konva.js
import Vue from 'vue'
import VueKonva from 'vue-konva'

Vue.use(VueKonva)
```

#3. nuxt.config.jsにパスを追加

```configuration:nuxt.config.js
plugins: [
  {
    src: '~/plugins/vue-konva',
    mode: 'client'
  }
],
```

#4. 入力フォームをつくる
まずは UI から実装していきます。

まずはじめに `index.vue` や `default.vue` などに Buefy のデフォルトのテンプレートが適用されているので、これらを一掃しておきます (しなくても特に問題はありませんが) 。

ここでは、ユーザがテキストを入力するフォームと、文字の大きさを調節する [スライダー](https://buefy.org/documentation/slider/) を実装します。`<b-field>` や `<b-input>` は Buefy 独自のタグで、HTMLの `<form>` や `<input>` だと思ってもらえれば問題ありません。

```vuejs:~/pages/index.vue
<template>
  <section class="section container">
    <h1 class="title">Text Image Generator</h1>

    <!-- 入力フォーム -->
    <b-field
      label="テキスト"
      position="is-centered"
    >
      <b-input
        placeholder="Type Something"
        type="text"
        v-model="inputText"
      />
    </b-field>

    <!-- 文字サイズを調節するスライダー -->
    <b-field label="Font Size">
      <b-slider v-model="fontsize" :min="24" :max="48" :step="4" ticks rounded>
      </b-slider>
    </b-field>

    <!--
       描画領域をここに実装
    -->

  </section>  
</template>
```
`v-model="inputText"` で、フォームに入力された文字列を、後ほど定義する変数 `inputText` にバインディングしています。

#5. 描画領域をつくる
続いて Konva が描画する領域を作成します。

Konva は、ステージ・レイヤー・シェイプ (画像やテキストなど) という3つの階層で構成されており、Vue Konva ではそれぞれ `v-stage`, `v-layer`, `v-image` (画像) `v-text` (テキスト) というタグに対応しています。Vuetifyを使っていると紛らわしいですね。

ここでは、背景画像を設定する `v-image` と、ユーザがフォームに入力した文字列を設定する `v-text` を使って、下記のようなシンプルな構成にしてみます。

```
v-stage ref="stage"
└── v-layer ref="layer"
    ├── v-image
    └── v-text
```


```vuejs:~/pages/index.vue
<template>
  <section class="section>

  <!-- 省略 -->

  <div id="konvaCanvas" style="margin: auto;">

    <!-- ステージ -->
    <v-stage ref="stage"
    :config="{
      width: 600,
      height: 315,
    }">

      <!-- レイヤー -->
      <v-layer ref="layer">

        <!-- 画像 -->
        <v-image :config="{
          image: bgImage,
          scaleX: 0.5,
          scaleY: 0.5
        }"/>

        <!-- テキスト -->
        <v-text
        :config="{
          text: `${this.inputText}`, 
          align: 'center',
          verticalAlign: 'middle',
          offsetY: 20,
          fill: '#b0bec5',
          fontFamily: 'Helvetica',
          fontSize: `${fontsize}`,
          fontStyle: 'bold',
          lineHeight: 1.4,
          padding: 50,
          width: 600,
          height: 315,
        }"/>
      </v-layer>
    </v-stage>
  </div>
  <b-button @click="save" class="button is-primary" icon-left="download">
    画像を保存
  </b-button>
</section>
```

ユーザがフォームとスライダーで指定した値が `inputText` と `fontSize` にバインディングされます。

`align: 'center'` と `verticalAlign: 'middle'` でテキストを縦横中央揃えにします。さらにその位置から `offsetY: 50` で **縦方向** に **50px** だけ **上に** ずらしています。これらの基準となるのは `v-text` に指定した `width` と `height` の値であり、ステージの幅・高さではないので注意が必要です。

`v-text` には他にも様々なオプションを指定できます。
[Konva Class: Text](https://konvajs.org/api/Konva.Text.html)

#6. scriptを書く
背景画像を読み込む部分と、描画されたステージを画像として保存する部分を実装します。
ここでは背景画像として **1200x630** の画像を使用します。[ここ](https://nuxt-text-image-generator.vercel.app/background.png)からダウンロードして、staticフォルダの直下に保存してください。  
なお、このままのサイズでブラウザ上に描画すると大き過ぎて、画面が小さいと見切れてしまいます。レスポンシブにするのがベストですが、ここでは単純のために Konva の scale プロパティを使って拡大縮小することにします。ステージのサイズを **600x315** に設定し、画像を0.5倍にして読み込み、書き出し時にステージを2倍にします。

```vuejs:~/pages/index.vue
<script>
export default {
  data() {
    return {
      bgImage: '',
      inputText: '',
      fontSize: 36
    }
  },
  created () {
    if (process.client) {
      const image = new window.Image();
      image.src = "background.png";
      image.onload = () => {
        this.bgImage = image;
      }
    }
  },
  methods: {
    save () {
      // v-stage ref="stage" の中身を JSON オブジェクトとして取得する
      const stage = this.$refs.stage.getStage();

      // ダウンロード
      var link = document.createElement('a');
      link.download = 'image_' + this.inputText.slice(0,10).replace(/ /g, "_");
      link.href = stage.toDataURL({
        // ステージを2倍に拡大する
        pixelRatio: 2
      })
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
    }
  },
}
</script>
```

まず `v-text` の `config` などで利用する変数を `data` の中で定義しておきます。

次に `created` の中で、DOM が構成される前に背景画像を読み込みます。`window` を利用するので、クライアントサイドのみで動くように `if (process.client)` を入れています。

画像の保存に関しては、`<b-button @click="save">` で `save()` が呼び出されます。[Vue Konva の公式ドキュメント](https://konvajs.org/docs/vue/Save-Load.html) に載っている方法を試したところうまくいかなかった（そもそもデモが動いていない）ので、 [React Konva の公式ドキュメント](https://konvajs.org/docs/react/Canvas_Export.html) に載っている方法を利用しました。後半は無理やり感が否めないですが、このやり方は [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#Using_the_download_attribute_to_save_a_canvas_as_a_PNG) にも載ってたりします...

#7. 完成

ここまで仕上がったら、ローカルサーバーを起動して `localhost:3000` にアクセスしてみてください。

```shell:terminal
// yarn
yarn dev

// npm
npm run dev
```

下記のような画面が表示されれば、完成です！
![スクリーンショット 2021-05-31 23.40.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633216/5638bf4f-56d2-db06-9e88-2ec22185ffe4.png)

[オリジナルのフォントを設定](https://konvajs.org/docs/sandbox/Custom_Font.html) したり、背景画像に合わせて `v-text` の位置をうまく調節したりすれば、クソコラ画像ジェネレータが作れそうですね(作れるとは言っていない)。他にも、ソースコードのスクリーンショット風画像を生成してくれる [Carbon](https://carbon.now.sh/) みたいなのも作れそうです。


#8. 補足

iOS や Android では上記の方法＋静的ホスティングだと画像をローカル (カメラロール) に保存できません... 

#9. 参考
公式ドキュメント
- [Getting started with vue and canvas via Konva](https://konvajs.org/docs/vue/index.html)
- [How to save a drawing from react-konva?](https://konvajs.org/docs/react/Canvas_Export.html)