Vue Composition API×vue-konva+自作サービスで解説するcanvas操作


# 初めに  
　本記事では、canvas上でテキストを操作するサービスをリリースするにあたり得られた、以下の知見を紹介します

* canvasを操作するライブラリ、konva(vue-konva)について
* Vue Composition APIについて

　Vue composition APIとは、次期メジャーバージョンのVueにて追加予定のAPI群です。<br/>　このAPI群を使うことで、これまでのVueの書き方ががらっと変わります（従来の書き方もできます）。<br/>　この機能は現在（2020年5月時点）でも、Vueにパッケージを追加することで試用できます。本記事では、このAPIのポイントについても紹介します。  

##  本記事にて取り上げる内容  
* 自作サービスの紹介
* Vue（Vue2.0+Vue Composition API）
* vue-konva

##  前提条件  
　本記事は、以下の読者を想定しています。
  
* canvasでの自在な描画技術や、vueに興味がある人
* vueの入門程度の知識があるとなおよい

# 自作サービスの紹介  
　今回、Netlify＋Nuxt+Veautifyという構成でWebサービスを作りました。  

　筆の海　https://seaofbrush.netlify.app/  

　どんなサービスかは、下の動画を見たほうが早いです 

 
![intro_lossy.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/207416/8ff1b222-af7a-dc3a-b098-664d4079de94.gif)

　こんなふうに、テキストボックスに入力した文章を筆として扱い、キャンバス上に文を描く（？）ことができます。<br/>「とにかく字をぐりぐり描きたい」というのが一番の動機のため、あまり凝った機能は設けていませんが、フォントや文字サイズを変更したり、アンドゥやダウンロードをしたりといった字描き（？）のための最低限の機能は備えています。

# konva、vue-konvaとは

　html5のcanvas上で図形を便利に操作するため、konvaというライブラリがあります。<br/>　vue-konvaはそれをvueのコンポーネントとして扱えるようにしたものです。<br/>　導入方法については、既に素晴らしい記事がありましたのでそちらを参照してください。  

* Nuxt.jsにvue-konvaを追加する  
https://qiita.com/SatoshiTanoue/items/43270a462407e3561cb8  

## konvaによる図形の描画  

　konvaの基本的な使い方ですが、まずkonvaにはstage・layer・その他オブジェクト（基本図形やテキストなど）という三種類のオブジェクトがあり、   
 
1. stageにlayerを登録  
2. layerにその他オブジェクトを登録  
3. layerを描画  

……という3ステップで図形を描画するようになっています。  

実行例は以下の通り<br/>（ https://konvajs.org/docs/overview.html を元に少し改変)

<details><summary>円を描画するサンプルコード</summary>
<div>

```
// ステージを作成
let stage = new Konva.Stage({
  container: 'container',   // 描画するdivのidを指定
  width: 500,
  height: 500
});

// レイヤーを新規作成
let layer = new Konva.Layer();

// 図形（ここでは円）を作成
let circle = new Konva.Circle({
  x: stage.width() / 2,
  y: stage.height() / 2,
  radius: 70,
  fill: 'red',
  stroke: 'black',
  strokeWidth: 4
});

// レイヤーに図形を追加
layer.add(circle);

// ステージにレイヤーを追加
stage.add(layer);

// レイヤーを描画
layer.draw();

```
</div></details>

## vue-konvaによる図形の描画
　続いてvue-konvaによる図形の描画方法です<br/>　上記のkonvaの例をベースとしつつ、Vue Composition APIの解説も兼ねるため、vueのコードとしてやや無理がある例になっていますがご了承ください。  

<details><summary>単一ファイル(.vue)本体サンプルコード</summary>
<div>

```
<template>
  <div>
    <client-only placeholder="Loading...">
      <v-stage
        ref="stage"
        :config="myState.stageConfig"
        @mouseenter.native="onMouseEnter"
        @mouseleave.native="onMouseLeave"
      >
        <v-layer ref="layer">
          <v-circle
            ref="circle"
            :config="myState.circleConfig"
          />
        </v-layer>
      </v-stage>
    </client-only>
    今の円のサイズは{{myState.text}}です
  </div>
</template>

<script>
  // composition-apiで使用する一連のapiをインポート
  import {
    ref,
    reactive,
    computed,
    watch,
  } from "@vue/composition-api";

  export default {
    name: "test",
    setup(_, context) {
      //ref
      const stage = ref(null);
      const layer = ref(null);
      const circle = ref(null);
      const size = ref(250)
      //reactive
      const myState = reactive({
        stageConfig: computed(() => {
          return {
            width: size.value,
            height: size.value,
          }
        }),
        circleConfig: computed(() => {
          return {
            x: size.value / 2,
            y: size.value / 2,
            radius: size.value / 2,
            fill: 'red',
            stroke: 'black',
            strokeWidth: 4
          }
        }),
        text: "",
      })

      //function
      function onMouseEnter(event) {
        size.value = 500;
      };

      function onMouseLeave() {
        size.value = 250;
      };

      watch(() => size.value, (val, prevVal) => {
        myState.text = String(val);
      })

      return {
        //const
        stage,
        layer,
        circle,
        size,
        myState,
        //func
        onMouseEnter,
        onMouseLeave,
      };
    }
  }
</script>

```
</div></details>

　上記のコードを使いwebページを作成すると、以下のようにマウスカーソルを当てると巨大化する円が描画されます。  

![sample_circle_lossy.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/207416/f8730c08-21a5-8750-f900-9ab07b048310.gif)

　上記の例を元に、ポイントをいくつか紹介します。  

# vue-konvaにおけるポイント  
* new Konva.hogehogeオブジェクトが`<v-hogehoge>`というコンポーネントに置き換わっている
* Konvaオブジェクトの初期値は:configディレクティブで渡す
* refを指定する<br/>
　これはどこのドキュメントやサイトにも載っていないのですが、vue-konvaのコンポーネントはとりあえずref="hogehoge"という形で、何かしらのコンポーネント名を指定しておくと良いです。

※例

```
        <v-layer ref="layer"></v-layer>
```

　理由については後述します。

# Vue composition APIのポイント

## ref()とreactive()
　Vueの特徴といえばリアクティブ、すなわち変数の変更が他の変数と動的に連動する点にあります。<br/>　従来のAPIではdata()内に記述していたリアクティブな変数は、ref()かreactive()関数を使うように変更されました。  

```
//従来の書き方
data() {
  return {
    size: 250
  }
}

//Composition APIの書き方
const size=ref(250);
return {
  size
}
//または
const state = reactive({
  size:250
  })
return {
  state
}
```
　ref()とreactive()、どちらの関数で書いても、もう一方の書き方で表現しなおすことができます。<br/>　どちらの表現も一長一短があり、特性やユースケースで使い分ける必要があるかと思います。

* ref()のメリット
  * 宣言が楽
* ref()のデメリット
  * 値を参照するときや代入するとき、関数名そのままではなく「関数名.value」でアクセスする必要がある


※例

```
const size=ref(500)
console.log(size.value)//=>"500"
```

　この、使うときに.valueを付けなければならないという特性は何かと忘れがちで、バグの原因にもなりがちです。  


* reactiveのメリット
  * 関連する変数を一つのstate関数名にまとめられ、コードが分かりやすくなる <br/>
　上記vue-konvaのコードでは、一連のconfig設定をmyStateという関数名でまとめている例がそれにあたります。<br/>　アクセスする際は「ステート名.変数名」という形式でアクセスします。こちらは.valueを付ける必要はありません。

※例

```
const myState = reactive({
  size:500
  })
console.log(myState.size)//=>"500"
```

* reactiveのデメリット
  * 宣言が若干手間


## function
　従来methodで指定していた各メソッドは、functionという形でそのまま記述するようになりました。  

```
//従来の書き方
methods: {
  onMouseEnter(event) {
    //（中略）
  };
}
//Composition APIの書き方
function onMouseEnter(event) {
  //（中略）
};
```

## watch
　watchも若干書き方が変わりました。  

```
//従来の書き方
watch: {
  size(value) {
    //（中略）
  }
},

//Composition APIの書き方
//(1)引数に値を指定する
watch(() => size.value, (val, prevVal) => {
  //（中略）
})
//または
//(2)特に値を指定しない
watch(() => {
  if(size.value==500){
    //（中略）
  }
})
```
　なお、新しい書き方の場合、さらに(1)引数に値を指定する書き方と(2)しない書き方があるようです。<br/>　指定する書き方のほうが、ウォッチする内容が少ないぶん速いはず（要確認）ですし、コードが分かりやすくなるため、引数に値を指定したほうが良いのではないかと思います。

## return
　コンポーネント内で使用する変数や関数は、return内で指定しないと使えませんのでお気をつけください。

```
return{
  //constを指定
  size,
  myState,
  //functionを指定
  onMouseEnter,
}
```

## ref()についてのtips
### 1. コンポーネントにアクセスする
　コンポーネント内のrefディレクティブと同名のref関数を用意することで、コンポーネントがマウントされた際にそのコンポーネントオブジェクトが自動的に代入されます。<br/>　従来の$refに相当する使い方ですね。

```

<template>
   (中略)
   <v-layer ref="myLayer"/>
</template>

<script>
(中略)
const myLayer=ref(null);//←同じ名前にしておくと、<v-layer>の実体が後で勝手に入る
</script>

```
### 2. konvaのnodeオブジェクトを取得して図形を操作
　一度画面に表示した図形にアニメーションを付ける場合など、後から図形に何か操作する場合、操作対象をnodeオブジェクトとして指定する必要があります。<br/>　nodeオブジェクトは、上記1.で取得したコンポーネントからgetNode()メソッドで取得します。

※例：円が移動するvue-konvaアニメーション（Tween）の場合

```
<template>
(中略)
        <v-circle ref="myCircle"/>
</template>

<script>
(中略)
const myCircle=ref(null);
(中略)
if(myCircle.value){
  //コンポーネントからnodeオブジェクトを取得
  const nodeObj=myCircle.value.getNode();
  //nodeオブジェクトを引数に、Konva.Tween()でアニメーションを設定
  let tweenObj = new Konva.Tween({
    node: nodeObj,
    duration: 1.0,
    x: myState.X
    y: myState.Y
    easing: Konva.Easings.BackEaseOut,
  });
  //アニメーションを実行
  tweenObj.play();
}
</script>
```

　「vue-konvaのコンポーネントはとりあえずrefを指定しておけ」と前述した理由はここにあります。<br/>　konvaを使う動機として、単に図形を表示するだけでなく、何かしらの凝った操作やアニメーションを付けたいというケースが大半だと思います。そのため、refしてコンポーネント取得してgetNodeしてアニメーションを設定、というケースが非常に多いです。


### 3. ref()で配列を扱う

　ref()は配列も扱うことができます。配列内の各要素にアクセスするときは「関数名.value[index]」という形式です。

```
const refArray=ref([]);
refArray.value.push("A")
refArray.value.push("B")
refArray.value.push("C")
console.log(refArray.value[1])//=>"B"

```

### (1～3の応用)複数の図形を自在に操る
　vueの場合、v-forディレクティブで複数のコンポーネントを一括して管理できるわけですが、
これと上記1～3を組み合わせると、複数の図形の一括したアニメーションが非常に容易になります。
　例を以下に示します。前述した円のアニメーションのコードを踏襲していますが、今度の例ではリアクティブにする必要がなくなったconfig設定は単なる定数や関数になっている点にのみご注意ください。


<details><summary>単一ファイル(.vue)本体</summary><div>

```

<template>
  <div>
    <client-only placeholder="Loading...">
      <v-stage
        ref="stage"
        :config="stageConfig"
        @mouseenter.native="onMouseEnter"
      >
        <v-layer ref="layer">
          <v-circle v-for="index in numberArray"
                    :config="circleConfig(index)"
                    ref="circle"/>
        </v-layer>
      </v-stage>

    </client-only>
  </div>
</template>

<script>
  import {
    ref,
    reactive,
    computed,
    watch,
  } from "@vue/composition-api";

  export default {
    name: "test",
    //data
    setup(_, context) {
      //ref
      const stage = ref(null);
      const layer = ref(null);
      const circle = ref(null);
      const stageConfig={
        width: 1000,
        height: 1000,
      }

      //0~99の連番を作成
      const numberArray = [...Array(100).keys()];

      function circleConfig(index) {
        return {
          x: 500,
          y: 500,
          radius: index + 25,
          fill: 'red',
          stroke: 'black',
          strokeWidth: 1
        }
      };

      function onMouseEnter(event) {
        for (let index of numberArray) {
          if(circle.value[index]){
            const nodeObj = circle.value[index].getNode();
            let tweenObj = new Konva.Tween({
              node: nodeObj,
              duration: 1.0,
              x: Math.random() * 1000,
              y: Math.random() * 1000,
              easing: Konva.Easings.BackEaseOut,
            });
            tweenObj.play();
          }
        }
        ;
      }
      return {
        //const
        stage,
        layer,
        circle,
        stageConfig,
        numberArray,
        //func
        circleConfig,
        onMouseEnter,
      };
    }
  }
</script>

```

</div></details>

　上記の例を実行すると……

![sample_circle2_lossy.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/207416/b55773ca-477f-6473-e8dd-073a0a83a3c1.gif)


　さまざまな円が、マウス操作に従って動き出すアニメーションができました！<br/>　このアニメーション、描画を担っているコードは実質、`<template>`内の`<v-circle>`コンポーネントと`<script>`内のcircleConfig()とonMouseEnter()。合計でたったの二十行程度となります。<br/>　"vue-konvaは使える"ということが分かってもらえましたでしょうか。

# おわりに
　自作サービスの紹介から始まり、vue-konvaの紹介からVue composition APIのtipsまで、やや散漫な内容となってしまいました。<br/>　しかしこれを機にvueやvue-konvaの魅力が伝わり、新たなサービス開発の一助となれば幸いです。