初めに
本記事では、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/
どんなサービスかは、下の動画を見たほうが早いです
こんなふうに、テキストボックスに入力した文章を筆として扱い、キャンバス上に文を描く(?)ことができます。
「とにかく字をぐりぐり描きたい」というのが一番の動機のため、あまり凝った機能は設けていませんが、フォントや文字サイズを変更したり、アンドゥやダウンロードをしたりといった字描き(?)のための最低限の機能は備えています。
konva、vue-konvaとは
html5のcanvas上で図形を便利に操作するため、konvaというライブラリがあります。
vue-konvaはそれをvueのコンポーネントとして扱えるようにしたものです。
導入方法については、既に素晴らしい記事がありましたのでそちらを参照してください。
- Nuxt.jsにvue-konvaを追加する
https://qiita.com/SatoshiTanoue/items/43270a462407e3561cb8
konvaによる図形の描画
konvaの基本的な使い方ですが、まずkonvaにはstage・layer・その他オブジェクト(基本図形やテキストなど)という三種類のオブジェクトがあり、
- stageにlayerを登録
- layerにその他オブジェクトを登録
- 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ページを作成すると、以下のようにマウスカーソルを当てると巨大化する円が描画されます。
上記の例を元に、ポイントをいくつか紹介します。
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を付ける必要はありません。
- 関連する変数を一つのstate関数名にまとめられ、コードが分かりやすくなる
※例
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>
上記の例を実行すると……
さまざまな円が、マウス操作に従って動き出すアニメーションができました!
このアニメーション、描画を担っているコードは実質、<template>
内の<v-circle>
コンポーネントと<script>
内のcircleConfig()とonMouseEnter()。合計でたったの二十行程度となります。
"vue-konvaは使える"ということが分かってもらえましたでしょうか。
おわりに
自作サービスの紹介から始まり、vue-konvaの紹介からVue composition APIのtipsまで、やや散漫な内容となってしまいました。
しかしこれを機にvueやvue-konvaの魅力が伝わり、新たなサービス開発の一助となれば幸いです。