LoginSignup
2
1

More than 3 years have passed since last update.

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

Posted at

初めに

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

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

 Vue composition APIとは、次期メジャーバージョンのVueにて追加予定のAPI群です。
 このAPI群を使うことで、これまでのVueの書き方ががらっと変わります(従来の書き方もできます)。
 この機能は現在(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

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

konva、vue-konvaとは

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

konvaによる図形の描画

 konvaの基本的な使い方ですが、まずkonvaにはstage・layer・その他オブジェクト(基本図形やテキストなど)という三種類のオブジェクトがあり、

  1. stageにlayerを登録
  2. layerにその他オブジェクトを登録
  3. layerを描画

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

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

円を描画するサンプルコード
// ステージを作成
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();

vue-konvaによる図形の描画

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

単一ファイル(.vue)本体サンプルコード
<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>

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

sample_circle_lossy.gif

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

vue-konvaにおけるポイント

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

※例

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

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

Vue composition APIのポイント

ref()とreactive()

 Vueの特徴といえばリアクティブ、すなわち変数の変更が他の変数と動的に連動する点にあります。
 従来の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()、どちらの関数で書いても、もう一方の書き方で表現しなおすことができます。
 どちらの表現も一長一短があり、特性やユースケースで使い分ける必要があるかと思います。

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

※例

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

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

  • reactiveのメリット
    • 関連する変数を一つのstate関数名にまとめられ、コードが分かりやすくなる
       上記vue-konvaのコードでは、一連のconfig設定をmyStateという関数名でまとめている例がそれにあたります。
       アクセスする際は「ステート名.変数名」という形式でアクセスします。こちらは.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)しない書き方があるようです。
 指定する書き方のほうが、ウォッチする内容が少ないぶん速いはず(要確認)ですし、コードが分かりやすくなるため、引数に値を指定したほうが良いのではないかと思います。

return

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

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

ref()についてのtips

1. コンポーネントにアクセスする

 コンポーネント内のrefディレクティブと同名のref関数を用意することで、コンポーネントがマウントされた際にそのコンポーネントオブジェクトが自動的に代入されます。
 従来の$refに相当する使い方ですね。


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

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

2. konvaのnodeオブジェクトを取得して図形を操作

 一度画面に表示した図形にアニメーションを付ける場合など、後から図形に何か操作する場合、操作対象をnodeオブジェクトとして指定する必要があります。
 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を指定しておけ」と前述した理由はここにあります。
 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設定は単なる定数や関数になっている点にのみご注意ください。

単一ファイル(.vue)本体

<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>

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

sample_circle2_lossy.gif

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

おわりに

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1