VueでKonva.jsとcanvasを使ってお絵描き（その1）

vueとkonva.jsを使った記事が全然ないので作ってみました。
（vue-konvaを使ってる記事はちょいちょいあるんですけどね。）

# はじめに
その1では、お絵描きできるCanvasの実装まで行います。
その1の内容は[このデモページ](https://konvajs.org/docs/sandbox/Free_Drawing.html)をvue用に書き換えただけです。
また、MacOSを使って実装してます。

最終的な完成画面イメージはこんなん。
モードでペン、消しゴム、直線を切り替えられるようにして、各種ボタン押下時はそれに応じた処理ができればなと。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/6286c919-8b1f-2583-2d0f-e3e6c8ade62d.png)

  
# バージョン情報
vue・・・2系
node・・・11.14.0
npm・・・6.7.0

# vueプロジェクト作成
「vue init webpack <プロジェクト名>」でvueのプロジェクトを作成します。
最初の質問（Project Name）だけ、vueプロジェクトを作成するプロジェクトの名前を指定してあげて、あとはエンターで問題ないです。
（僕はkonvastudyというプロジェクトに作成したので、「konvastudy」としました。）

```
$ vue init webpack konvaproject

? Project name konvastudy
? Project description A Vue.js project
? Author Butterthon <~.0805@gmail.com>
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests Yes
? Pick a test runner jest
? Setup e2e tests with Nightwatch? Yes
? Should we run `npm install` for you after the project has been created? (recommended) npm
```
  
作成できたら、vueプロジェクトのディレクトリに移動してサーバーを起動してみます。
ブラウザが立ち上がって、以下のような画面が表示されれば成功です。

```
$ cd konvaproject
$ npm run dev
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/5ff137f7-aa1e-b1df-22b8-f390a1fcfc04.png)


# キャンバスの外側を作る
冒頭の完成イメージでいう、モードやペンの色、各種ボタンのことです。
その前に、Vue Materialやkonvaを使う準備をしましょう。
Vue Materialは色々なUIコンポーネントが提供されており、それっぽいデザインが簡単に作れるので便利。
Vue Materialの公式ページは[ここから](https://vuematerial.io/)

```
npm install --save vuex vue-material@1.0.0-beta-10.2 babel-plugin-transform-object-rest-spread vue-style-loader ts-loader@3.5.0 css-loader sass-loader node-sass konva
```

上記プラグインをinstallしたら、main.jsに以下を追記します。

```main.js
...
..
.
import VueMetarial from 'vue-material'
import 'vue-material/dist/vue-material.min.css'
import 'vue-material/dist/theme/default.css'

Vue.use(VueMetarial)

...
..
.

```
  
  
では外側の作成に入っていきます。
src直下に「paint」フォルダを作成し、その中にCallCanvas.vueを用意します。
中身はこんな感じ

``` src/paint/CallCanvas.vue
<template>
    <div>
        <div class="md-layout md-gutter" style="margin-left: 340px">
          <div class="md-layout-item">
            <md-field style="float: left">
              <label for="mode">モード</label>
              <md-select v-model="mode" name="mode" id="mode">
                <md-option value="brush">ペン</md-option>
                <md-option value="eraser">消しゴム</md-option>
                <md-option value="line">直線</md-option>
              </md-select>
            </md-field>

            <md-field style="float: left">
              <label for="brushColor">ペンの色</label>
              <md-input type="color" v-model="brushColor" />
            </md-field>

            <md-field style="float: left; margin-top: -8px">
              <md-button class="md-dense md-raised md-primary">
                戻る
              </md-button>
            </md-field>

            <md-field style="float: left; margin-top: -8px">
              <md-button class="md-dense md-raised md-primary">
                進む
              </md-button>
            </md-field>

            <md-field style="float: left; margin-top: -8px">
              <md-button class="md-dense md-raised md-primary">
                  リセット
              </md-button>
            </md-field>

            <md-field style="float: left; margin-top: -8px">
              <md-button class="md-dense md-raised md-primary">
                  保存
              </md-button>
            </md-field>
          </div>
        </div>
    </div>
</template>

<script>
export default {
  name: 'CallCanvas',
  data: () => ({
    mode: '',
    brushColor: '',
    defaultMode: 'brush',
    defaultBrushColor: '#FFFFFF'
  }),
  mounted: function() {
    this.init();
  },
  methods: {
    // モードとペンの色を初期状態にする
    init: function() {
      this.mode = this.defaultMode;
      this.brushColor = this.defaultBrushColor;
    }
  }
}
</script>

<style lang="scss" scoped>
  .md-field {
    max-width: 110px;
  }
</style>

```
  
てっとり早くサーバー立ち上げたときに確認したいので、サーバー立ち上げて最初に表示される「HelloWorld.vue」を修正します。

```src/components/HelloWorld.vue
<template>
  <div>
    <!-- componentに追加したものはタグとして使用できるようになる -->
    <CallCanvas />
  </div>
</template>

<script>
import CallCanvas from '../paint/CallCanvas.vue' // 作成したCallCanvas.vueをインポート

export default {
  name: 'HelloWorld',
  components: {
    CallCanvas // componentsにインポートしたCallCanvasを追加する。
  }
}
</script>
```
  
サーバー起動して確かめます（npm run devを実行）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/24c83fd2-7d59-29ef-4fe5-49f79e9f055d.png)
バッチグー。
ロゴが邪魔な人はApp.vueから該当する行を削除したら消えます。

  
# Canvasを実装してお絵描きしてみる
CallCanvas.vueと同じフォルダに、「FreeDrawing.vue」を用意します。
中身はこんな感じ（propsやthis.$emit(...)は今は気にしなくていいです。）

```src/paint/FreeDrawing.vue
<template>
  <div>
    <div ref="container">
      <canvas
        :width="width/2"
        :height="height/2"
        ref="canvas">
      </canvas>
    </div>
  </div>
</template>

<script>
import Konva from 'konva';

export default {
  name: 'FreeDrawing',
  // propsは親の「CallCanvas.vue」から値を受け取るためのプロパティ
  props: {
    mode: {
      type: String,
      default: ''
    },
    brushColor: {
      type: String,
      default: ''
    },
    backgroundImage: {
      type: String,
      default: ''
    }
  },
  data: () => ({
    width: window.innerWidth,
    height: window.innerHeight,
    stage: null,
    canvas: null,
    context: null,
    drawingLayer: null,
    drawingScope: null,
    lastPointerPosition: {},
    localPos: {
      x: 0,
      y: 0
    },
    pos: null,
    isPaint: false
  }),
  mounted: function () {
    var container = this.$refs.container;
    this.stage = new Konva.Stage({
      container,
      width: this.width,
      height: this.height
    })
    this.drawingLayer = new Konva.Layer()
    this.stage.add(this.drawingLayer)

    this.canvas = this.$refs.canvas
    this.drawingScope = new Konva.Image({
      image: this.canvas,
      x: this.width / 4,
      y: 5,
      stroke: 'black'
    })
    this.drawingLayer.add(this.drawingScope)
    this.stage.draw()

    this.context = this.canvas.getContext('2d')
    this.context.strokeStyle = this.brushColor
    this.context.lineJoin = 'round'
    this.context.lineWidth = 5

    // イベント追加
    this.drawingScope.on('mousedown', this.mousedown)
    this.stage.addEventListener('mouseup', this.mouseup)
    this.stage.addEventListener('mousemove', this.mousemove)
    this.drawingScope.on('touchstart', this.mousedown)
    this.stage.addEventListener('touchend', this.mouseup)
    this.stage.addEventListener('touchmove', this.mousemove)
  },
  methods: {
    mousedown: function () {
      this.isPaint = true

      // マウスダウン時の座標を取得しておく
      this.lastPointerPosition = this.stage.getPointerPosition()
    },
    mouseup: function () {
      this.isPaint = false
    },
    mousemove: function () {
      if (!this.isPaint) {
        return;
      }
      // ペンモード時
      if (this.isTargetMode('brush') || this.isTargetMode('line')) {
        this.context.globalCompositeOperation = 'source-over';
      }
      // 消しゴムモード時
      if (this.isTargetMode('eraser')) {
        this.context.globalCompositeOperation = 'destination-out';
      }

      this.context.beginPath()

      this.localPos.x = this.lastPointerPosition.x - this.drawingScope.x()
      this.localPos.y = this.lastPointerPosition.y - this.drawingScope.y()

      // 描画開始座標を指定する
      this.context.moveTo(this.localPos.x, this.localPos.y)
      
      this.pos = this.stage.getPointerPosition()
      this.localPos.x = this.pos.x - this.drawingScope.x()
      this.localPos.y = this.pos.y - this.drawingScope.y()

      // 描画開始座標から、lineToに指定された座標まで描画する
      this.context.lineTo(this.localPos.x, this.localPos.y)
      this.context.closePath()
      this.context.stroke()
      this.drawingLayer.draw()

      this.lastPointerPosition = this.pos
    },
    onClearCanvas: function () {
      this.context.globalCompositeOperation = 'destination-out'
      this.context.fillRect(0, 0, this.width, this.height)
      this.drawingLayer.draw()

      this.$emit('on-reset')
    },
    // 現在のモードが指定されたモードと一致するかどうか
    isTargetMode: function (targetMode) {
      return this.mode === targetMode
    }
  },
  watch: {
    // ペンの色変更
    brushColor: function () {
      this.context.strokeStyle = this.brushColor
    }
  }
}
</script>
```
  
  
これをCallCanvas.vueに組み込みます。
・FreeDrawing.vueをインポート
・インポートしたものをcomponentsに指定する
・templateに記述する（厳密にはそのなかのdivタグ内）

```src/paint/CallCanvas.vue
<template>
    <div>
        ...
        ..
        .
        <FreeDrawing />
    </div>
</template>

<script>
import FreeDrawing from './FreeDrawing.vue'

export default {
  name: 'CallCanvas',
  components: {
    FreeDrawing
  },
  ...
  ..
  .
}
</script>

...
..
.
```

サーバー起動してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/10244385-8417-ba8a-9994-0070d22da7ba.png)
キャンバスが表示されて、描画もできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/f50556d0-a345-fafa-779a-8f47bd03ee6c.png)

その2ではキャンバスに画像を表示して、その画像に落書きできるようにしたいと思います。
（冒頭の完成イメージでいうと、「令和」に落書きできるようにするイメージ）
[その2：キャンバスに画像表示および落書き](https://qiita.com/Butterthon/items/157c1356428b6020bc4e)