はじめに
Fabric.js本体やVueのリアクティブな機能を連係する実装について試した。
準備
プロジェクト作成
vue create
でVue 3 の Composition API を TypeScript で記述するように選択してプロジェクトを作成。
Lintルール調整
個人的な好みによりLintルールを調整。
本記事で前提となるLintルール
// comma-dangleとsemiを追加
rules: {
'comma-dangle': ['error', {
arrays: 'always-multiline',
objects: 'always-multiline',
imports: 'never',
exports: 'never',
functions: 'never',
}],
semi: ['error', 'always'],
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
App.vue更新
プリセットのコンポーネントとスタイルは除去して検証用のコンポーネントのみにする。
更新後のApp.vue
<template>
<MyCanvas/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MyCanvas from '@/components/MyCanvas.vue';
export default defineComponent({
name: 'App',
components: {
MyCanvas,
},
});
</script>
※MyCanvas.vue
については後述
検証
Fabric.jsインストール
fabric本体と型情報をそれぞれ npm install
する。
npm install fabric
npm install --save-dev @types/fabric
Fabric.jsによる描画
まずは チュートリアル に従い四角形を配置してみる。
※下記ではチュートリアルに従いidによりcanvasを決定しているが、Vueでやるならテンプレート参照を使ったほうがよいかも(後述)
<template>
<canvas id="myCanvas"></canvas>
</template>
<script lang="ts">
import { defineComponent, onMounted } from 'vue';
import { fabric } from 'fabric';
export default defineComponent({
setup: () => {
// create a rectangle object
const rect = new fabric.Rect({
left: 200,
top: 100,
fill: 'red',
width: 31,
height: 31,
});
onMounted(() => { // ★1
// create a wrapper around native canvas element
const canvas = new fabric.Canvas('myCanvas');
// "add" rectangle onto canvas
canvas.add(rect);
});
},
});
</script>
<style>
.canvas-container { /* ★2 */
border: 1px solid grey;
}
</style>
- ★1:
fabric.Canvas()
はDOM操作を伴うためonMounted
内で実行する必要がある - ★2: fabricによりcanvas要素がdiv要素によってwrapされる(
.canvas-container
)のでそれにスタイルを定義している
Elementで指定するパターン
Canvasのコンストラクタ引数にはidだけでなくHTMLCanvasElementでの指定もできる。テンプレート参照により取得したElementで指定した方がVueらしい記述になる。
+ const canvasElm = ref<HTMLCanvasElement>();
onMounted(() => {
// create a wrapper around native canvas element
- const canvas = new fabric.Canvas('myCanvas');
+ const canvas = new fabric.Canvas(canvasElm.value!);
// "add" rectangle onto canvas
canvas.add(rect);
});
+ return {
+ canvasElm,
+ };
- <canvas id="myCanvas"></canvas>
+ <canvas ref="canvasElm"></canvas>
画像の表示と注意点
fabric.Image
を使用する場合は元となるimg要素が必要になるのでfabric.Canvas
と同様にonMounted
でインスタンスを作成する。
+ const img1Elm = ref<HTMLImageElement>();
+ const img2Elm = ref<HTMLImageElement>();
const canvasElm = ref<HTMLCanvasElement>();
onMounted(() => {
// create a wrapper around native canvas element
const canvas = new fabric.Canvas(canvasElm.value!);
// "add" rectangle onto canvas
canvas.add(rect);
+ const img1 = new fabric.Image(img1Elm.value!, { left: 10, top: 20, scaleX: 0.4, scaleY: 0.4 });
+ const img2 = new fabric.Image(img2Elm.value!, { left: 60, top: 60, scaleX: 0.4, scaleY: 0.4 });
+ canvas.add(img1, img2);
});
return {
canvasElm,
+ img1Elm,
+ img2Elm,
};
+ <img ref="img1Elm" src="@/assets/sun.png" class="hidden">
+ <img ref="img2Elm" src="@/assets/moon.png" class="hidden">
<!-- class="hidden"は当該img要素自体を画面表示させないために付与 -->
ただし、上記の記述のみではbase64にコンパイルされた画像にのみ有効だがコンパイルされてない画像はうまく表示することができない。Vueのデフォルト設定ではassetsに配置したとしても一定以上のサイズの画像はコンパイルされずにpublicと同様の扱いになるため注意が必要。
実行結果は以下の通り。(初期状態ではsun.pngが表示されず、次のレンダリングが走ったタイミングで反映される)(←moon.pngはコンパイルされ、sun.pngはコンパイルされていない)
これを防ぐためには以下のいずれかの処理が必要になる
- コンパイルされるようにwebpackの設定を変更する
-
v-on:load
を使用して画像の読み込み完了を検知してからインスタンスをadd
する(またはrenderAll
などで再レンダリングする) - 画像をpublic配下に置き、img要素経由ではなく
fabric.Image.fromURL
から作成する
参考
オブジェクトのプロパティについて
オブジェクトのコンストラクタ引数には任意のプロパティを設定可能。(公式ドキュメント:Rect参照)
const rect = new fabric.Rect({
left: 200,
top: 100,
fill: 'red',
width: 31,
height: 31,
+ lockRotation: true,
});
また、インスタンスを経由して後から参照・設定することも可能。
<button @click="square">正方形にする</button>
const square = () => {
rect.set('scaleY', rect.scaleX);
canvas.renderAll();
};
※上では省略しているがcanvasの宣言をomMountedの外に出している
setup: () => {
// create a rectangle object
const rect = new fabric.Rect({
left: 200,
top: 100,
fill: 'red',
width: 31,
height: 31,
lockRotation: true,
});
let canvas: fabric.Canvas;
onMounted(() => {
// create a wrapper around native canvas element
canvas = new fabric.Canvas('myCanvas', { width: 500, height: 300 });
// "add" rectangle onto canvas
canvas.add(rect);
});
const square = () => {
rect.set('scaleY', rect.scaleX);
canvas.renderAll();
};
return {
square,
};
},
Canvasについてもコンストラクタの第2引数に任意のプロパティを設定可能。(公式ドキュメント:Canvas参照)
canvas = new fabric.Canvas('myCanvas', { width: 500, height: 300 });
実行結果は以下の通り。(回転不可、移動・拡大/縮小は可能、ボタン押下で拡大率を揃える)
Vueとの連係
const rect = new fabric.Rect({
left: 200,
top: 100,
fill: 'red',
width: 31,
height: 31,
- lockRotation: true,
+ hasControls: false, // ◆1
});
// ★2
+ const rectPos = reactive({ x: rect.getCenterPoint().x, y: rect.getCenterPoint().y });
+ rect.on('moving', () => { // ★3
+ rectPos.x = rect.getCenterPoint().x;
+ rectPos.y = rect.getCenterPoint().y;
+ });
<template>
<canvas id="myCanvas"></canvas>
<div>
- <button @click="square">正方形にする</button>
+ (x, y) = ({{rectPos.x}}, {{rectPos.y}})
</div>
</template>
- ◆1: 拡大/縮小が絡むとややこしいのでコントロールを出さないようにしておく
- ★2: リアクティブオブジェクトを作成する
- ★3: Fabric側の機能であるイベント購読機能を利用する(利用可能なイベントはRectおよびそのスーパークラスObject参照)
参考