Help us understand the problem. What is going on with this article?

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

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

はじめに

その1では、お絵描きできるCanvasの実装まで行います。
その1の内容はこのデモページをvue用に書き換えただけです。
また、MacOSを使って実装してます。

最終的な完成画面イメージはこんなん。
モードでペン、消しゴム、直線を切り替えられるようにして、各種ボタン押下時はそれに応じた処理ができればなと。
image.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

キャンバスの外側を作る

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

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
バッチグー。
ロゴが邪魔な人は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
キャンバスが表示されて、描画もできます。

image.png

その2ではキャンバスに画像を表示して、その画像に落書きできるようにしたいと思います。
(冒頭の完成イメージでいうと、「令和」に落書きできるようにするイメージ)
その2:キャンバスに画像表示および落書き

Butterthon
いろんなことに興味あります。 最近のマイブームはPython, Django, Vue, Ruby(今更) スキル:C#, Java(Seasar2), Python(Django), Ruby(Rails), JavaScript, jQuery, Vue.js, AWS
https://twitter.com/Butterthon
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした