Vue.js で 2D お絵かき
HTML5 Canvas Library である Fabric.js を
Vue.js に組み込んでみました.
Web クライアントで 2D 描画をする方法の一つです.
Fabric.js は、HTML5 の Canvas を js で扱うためのライブラリです。
2D 図形や描画したものをオブジェクトとして制御することができるようになります。 また、普通の HTML要素じゃちょっと難しいかなレベルの細かい図形描画が可能です。
どんなことができるのかは、 [Fabric.js のDemo] ページあたりを見てください。
Fabric.js の描画要素は javascript object になります。 土台になるキャンバスは、 fabric.Canvas
で作成します。
その上に、例えば四角を描きたかったら、 fabric.Rect
を作成し、キャンバスに add
すると、後は勝手に描画してくれます。
Fabric.js の [イントロダクション] にあるサンプルコードだと、こんな感じです。
// create a wrapper around native canvas element (with id="c")
var canvas = new fabric.Canvas('c');
// create a rectangle object
var rect = new fabric.Rect({
left: 100,
top: 100,
fill: 'red',
width: 20,
height: 20
});
// "add" rectangle onto canvas
canvas.add(rect);
ここでは、 [オブジェクトの制御のデモ] に似たようなことを Vue.js でやってみます。
適当なプロジェクトを作成します。 今回は typescript
にします。
$ vue create vue-fabric-test
typescript
にするには、オプションを手動で設定していきます。

Typescript
サポートを組み込みます。

一点だけ。 typescript
を Vue.js に組み込む時に、 component
をクラスとして定義するか 、
object
として定義するかの2通りから選べます (Use class-style component syntaxの項目)。
ここでは、 object
として定義するので、 No
にしてます。 それ以外はデフォルトです。

出来上がったら、Fabric.js を組み込みます。
$ npm install fabric --save
$ npm install @types/fabric --save-dev
今回は、あまり複雑な描画はしないので、簡単に fabric のクラスを、Vue.js の component にマッピングすることで実現します。
まず、描画の土台になるキャンバスの準備です。キャンバスは、 HTMLの canvas
タグで定義します。
<template>
<div>
<canvas id="base-canvas" />
</div>
</template>
typescript
なので、変数等の型定義が必要になります。
<script lang="ts">
import Vue from 'vue';
import { fabric } from "fabric";
interface DataType {
canvas: fabric.Canvas | undefined;
background: string;
}
パラメータ類は、 props
で渡します。
export default Vue.extend({
name: "FabricCanvas",
props: {
width: Number,
height: Number,
},
data() : DataType {
return {
canvas: undefined,
background: 'lightgray',
};
},
実行時 (mounted) になったら、 fabric.Canvas
を作って、 data
に保存しておきます。
fabric.Canvas
の最初の引数は、 HTML側の canvas
タグの id
です。 2番目は、オプションパラメータになります。
mounted: function () {
this.$nextTick(function () {
const canvas = new fabric.Canvas("base-canvas", {
width: this.width,
height: this.height,
backgroundColor: this.background,
});
canvas.preserveObjectStacking = true;
canvas.stateful = true;
this.canvas = canvas;
});
},
});
</script>
canvas.preserveObjectStacking
で、キャンバスに追加したオブジェクトが選択されても、手前にこないようにしています。
canvas.stateful
は、今回は使いませんがオブジェクトが、オブジェクトに対するイベントハンドリング時に、操作前の状態が記録されて渡されるようになります。
FabricCanvas
の準備できたら、 App.vue
を変更して、組み込みます。
<template>
<div id="app">
<FabricCanvas :width=600 :height=400 />
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import FabricCanvas from './components/FabricCanvas.vue';
export default Vue.extend({
name: 'App',
components: {
FabricCanvas
}
});
</script>
lightgray
の背景色のキャンバスが描画されているはずです。
次にキャンバス上に四角を描画します。 四角は、 fabric.Rect
です。
<template>
は空にします。 Canvas 上の描画については、全て fabric 側で行われます。
<template>
<div />
</template>
<script lang="ts">
import Vue, { PropType } from 'vue';
import { fabric } from "fabric";
interface DataType {
rect: fabric.Rect | null;
}
export default Vue.extend({
name: "FabricRect",
props: {
coord: Array as PropType<number[]>,
fillColor: String,
canvas: Object as PropType<fabric.Canvas>,
handleCoordChange: Function as PropType<(coord:number[]) => void>,
},
data(): DataType {
return {
rect: null,
};
},
watch: {
// eslint-disable-next-line no-unused-vars
canvas: function (_val, _oldVal) {
const rect = new fabric.Rect({
left: this.coord[0],
top: this.coord[1],
width: this.coord[2],
height: this.coord[3],
fill: this.fillColor,
});
this.canvas.add(rect);
this.rect = rect;
},
},
});
</script>
FabricCanvas
側を以下のように変更して、 FabricRect
を追加します。
<template>
<div>
<canvas id="base-canvas">
<FabricRect :coord="[100, 100, 200, 200]" :canvas="canvas" fillColor="red"/>
</canvas>
</div>
</template>
FabricCanvas の mounted
で canvas
が生成されたら、 FabricRect の props
に渡ってきます。
FabricRect の watch
で、 canvas
を見ているので、そこで fabric.Rect
が作成されることになります。
生成された fabric.Rect
はキャンバスに追加されることで描画されるという流れになります。
できあがると、こんな画面になるはずです。キャンバス上に描画した四角をコントローラで制御するインタラクティンブな描画が簡単に実現できます。

ソースコードは、 [github の vue-fabric-test (basic) ] に置いてあります。
基本的な描画はできるようになりました。 fabric では描画要素をユーザが操作することができます。 (もちろん静的に描画するだけってのも可能です)
今回の実装では、 fabric オブジェクトは Vue の値で作成するだけで、その後は両方の座標値などのプロパティは独立しています。 きちんと実装するなら、 Vue のプロパティと fabric オブジェクトのプロパティを双方向バインディングすると良いのですが、ちょっと面倒です。
そこで、簡易的に fabric のイベントハンドラーで Vue 側の値を書き換えるようにしてみます。
最終的なソースコードは、[vue-fabric-test (developブランチ) ] にあります。
fabric は、ユーザ操作後にイベントを発生しますので、ハンドラを定義して、Vue側のプロパティを書き換えるようにします。
FabricRect
で fabric オブジェクトを生成した後にハンドラを登録します。
rect.on('moved', () => {
const coord = [
Math.round(rect.left!),
Math.round(rect.top!),
Math.round(rect.width! * rect.scaleX!),
Math.round(rect.height! * rect.scaleY!),
];
this.$emit('coord-change', coord, 'moved');
});
rect.on('scaled', () => {
const coord = [
Math.round(rect.left!),
Math.round(rect.top!),
Math.round(rect.width! * rect.scaleX!),
Math.round(rect.height! * rect.scaleY!),
];
this.$emit('coord-change', coord, 'scaled');
});
moved
/ scaled
は、それぞれオブジェクトの移動後、拡大縮小後におきます。 他にもイベントがありますので、きちんとやるなら
そちらもハンドルした方が良いですが、今回は省略。
また、ハンドラの使い方として、 moving
/ scaling
などでユーザの操作中にフックする等も可能です。
一覧については、 [fabric のイベント] あたりを参照。 objects events
に分類されている
ものがオブジェクトの操作時に起きます。
ハンドラの中身を見ていきます。 fabric の座標値は浮動小数です。 Vue 側では整数値で処理しているので、 Math.round
で四捨五入します。
width
/ height
についてですが、 fabric では拡大縮小すると scaleX
/ scaleY
の倍率が変更されるだけになります。
実際の数値を掛け算で求めてから、四捨五入して Vue 側の値を算出します。
$emit
でイベントとして送出します。
この記事の最初の方では、座標値を数値の配列 (numnber[]
)として扱っていましたが、 ここでは、FabricRect
の座標値として、 App.vue
に interface
を定義しています。
export interface rectRecord {
x: number;
y: number;
w: number;
h: number;
fill: string;
}
interface DataType {
rects: rectRecord[];
}
App.vue
で rects: rectRecord[]
を定義して、 FabricCanvas
に渡します。 FabricCanvas
では、 FabricRect
を v-for
で生成します。
さて、 fabric 側で変更された値は、 FabricRect
から coord-change
と名前をつけて $emit
したのは上で見ました。
それを FabricCanvas
で受けて、どの FabricRect
から発報されたものか index
を追加して、 App
に流します。
<template>
...
@coord-change="(coord, event) => $emit('coord-change', index, coord, event)" />
...
<template>
App.vue
では、それを受けて、 handleCoordChange
メソッドを呼び出します。
<template>
...
@coord-change="handleCoordChange" />
...
<template>
そこで、 data
の内部値を書き換えるようになります。 これで fabric での値の変更を Vue 側に反映することができるようになります。

このスクリーションショットでは、 chrome に Vue.js devtools
を組み込んでいます。 devtools
を使うと、 chrome の デベロッパーツールから、 Vue の内部プロパティ等をリアルタイムでデバッグできるようになります。 (ただ、開発サーバ上しか動作しません。)
残りとして、Vue 側での値に reactive に fabric を合わせることで双方向バインディングできるようになりますが、そちらは省略します。
ヒントとしては、 fabric オブジェクトに値を set()
してから、 setCoord()
を使うことで、オブジェクトを変更に合わせて描画させることができます。
reactive なフレームワークを使う利点としては、 Single Source of Truth が実現しやすいことが挙げられるので、是非その辺挑戦してみてください。