Fabric.js の使い方メモ
Canvas 内に画像やテキストを配置したりするのに便利な Fabric.js の自分用メモです。
インストール
yarn add fabric
いちばん簡単な表示の例
<template lang="pug">
canvas(ref="canvas_el" width="200" height="200")
</template>
<script>
import { fabric } from "fabric"
export default {
mounted() {
const canvas = new fabric.Canvas(this.$refs.canvas_el)
const rect = new fabric.Rect({width: 50, height: 50, fill: "blue"})
canvas.add(rect)
rect.center()
},
}
</script>
<style lang="sass">
canvas
border: 1px dashed blue
</style>
- Vue.js のオールドスタイルで書いてる
- 最初に
fabric.Canvas
のインスタンスを生成する - それに対して他の要素を
add
していく -
center()
はadd
してからでないと効かなかった (それはそう)
ライブラリのお約束的なこと
インタンスには生成時のオプションと同じアクセサがあるっぽい。なので次の2つは同じこと(だと勝手に思っている)。
const canvas = new fabric.Canvas(html_element, {selection: false})
const canvas = new fabric.Canvas(html_element)
canvas.selection = false
背景画像の読み込み
方法1. とにかく読み込む
const url = "https://dummyimage.com/600x400/008/FFF.png"
canvas.setBackgroundImage(url, () => canvas.renderAll())
-
fabric.Image.fromURL
で読み込んだものをcanvas.setBackgroundImage
に渡してもよい - 第二引数の謎
- マニュアルには
canvas.renderAll.bind(canvas)
と書いてある - ググってもそればかりでてくる
- 意味がわからないので
() => canvas.renderAll()
に変えたら動いたのでよしとする
- マニュアルには
-
renderAll()
を呼ばないと表示されない -
toDataURL
できない罠あり (後述)
方法2. 背景画像にキャンバスを合わせる
const url = "https://dummyimage.com/600x400/008/FFF.png"
canvas.setBackgroundImage(url, e => {
canvas.setDimensions({width: e.width, height: e.height})
canvas.renderAll()
})
- キャンバスのサイズは変わるけど隙間はできない
- キャンバスが巨大化する恐れあり
- キャンバスのサイズはあとから変更できないと思ってたけど勘違いだったようだ
方法3. キャンバスに背景画像を合わせる
const url = "https://dummyimage.com/600x400/008/FFF.png"
fabric.Image.fromURL(url, e => {
e.scaleToWidth(canvas.width)
canvas.setBackgroundImage(e, () => canvas.renderAll())
})
- 上は横幅をキャンバスに収める場合の例
- 縦幅をキャンバスに収める場合は
e.scaleToHeight(canvas.height)
とする
toDataURL が失敗する問題
const url = "https://dummyimage.com/600x400/008/FFF.png"
canvas.setBackgroundImage(url, e => {
canvas.renderAll()
})
const data_url = canvas.toDataURL()
- 上のように外部の画像を読み込むと
toDataURL
が失敗する - 「キャンバスが汚染されている」というだけの不親切なエラーメッセージが出る
対策1. 要素に crossOrigin 指定
const url = "https://dummyimage.com/600x400/008/FFF.png"
canvas.setBackgroundImage(url, e => {
e.crossOrigin = "anonymous"
canvas.renderAll()
}
対策2. setBackgroundImage のオプションに crossOrigin 指定
const url = "https://dummyimage.com/600x400/008/FFF.png"
canvas.setBackgroundImage(url, e => {
canvas.renderAll()
}, {
crossOrigin: "anonymous",
})
上のどちらもでも回避できた。が、似た対処方が複数あってなんともすっきりしない。
参考 🙏
originX originY とは何なのか?
- 公式サイトのサンプルがわかりやすかった
- 要素の座標は要素のどこを基点としているかを表わす
- 組み合わせ的には多いが考慮するのは左上と中央の二つしかない
originX | originY | 基点 | |
---|---|---|---|
left | top | 左上 | 初期値 |
center | center | 中央 |
- Rust の nannou のように、より抽象化されたライブラリは中央基点のことが多い
- fabric も中央が初期値の方がよかった
とりあえずパーツ類を配置するときはこうやっておく。
const el = new fabric.Rect({
originX: "center",
originY: "center",
...
})
canvas.add(el)
要素の中央の座標を求めるのに無駄にがんばってはいけない
仮に 100x100 のキャンバスの中央付近に 50x50 の要素が配置されているときの要素の aCoords
はこんな感じになっている。
{
tl: { x: 24.5, y: 24.5 },
tr: { x: 75.5, y: 24.5 },
br: { x: 75.5, y: 75.5 },
bl: { x: 24.5, y: 75.5 },
}
tl
は左上で br
は右下なので X 座標の中心を求めるなら、
tl.x + (br.x - tl.x) / 2
なので、
24.5 + (75.5 - 24.5) / 2
として 50 になる。同様に Y 座標も計算すると 50 になる。
と、こんな感じでいかにも間違えそうな計算をしないでいいように originX と originY には center を指定しておく。
new fabric.Rect({
originX: "center",
originY: "center",
...
})
すると (left, top)
は最初から (50, 50)
になっている。
見える化する
template(v-if="canvas")
p {{canvas.getActiveObject()}}
b-table(:data="canvas.getObjects()")
template(slot-scope="{row}")
b-table-column(label="type") {{row.type}}
b-table-column(label="x") {{row.top}}
b-table-column(label="y") {{row.left}}
b-table-column(label="w") {{row.width}}
b-table-column(label="h") {{row.height}}
b-table-column(label="raw") {{row}}
b-table-column
button.button(@click="canvas.remove(row)") remove
動作検証している環境が大昔の Vue.js + Buefy で何かをアップデートすると何かが壊れるという悲しい状況なので、かなり古めかしい書き方になってしまった。とりあえず、いろいろ表示しとかないと何がどうなっているのかさっぱりわからない。getObjects()
で保持している要素の配列が返る。
ただ、このままだと再描画されないのでマウス操作を変更のトリガーとして再描画する。
this.canvas.on("mouse:move", () => this.$forceUpdate())
もしくは deep: true
で変更を監視して再描画する。
watch: {
canvas: {
handler() { this.$forceUpdate() },
deep: true,
},
},
テキストを配置する
const element = new fabric.Text("こんにちは", {
originX: "center",
originY: "center",
fill: "black",
fontSize: 64,
fontFamily: "Ariel",
})
canvas.add(element)
element.center()
-
origin(X|Y)
で「こんにちは」の「に」の中心を座標の基点とする(重要) - 背景画像の上に配置する場合は読みやすいようにこれを追加して縁取る
stroke: "white", // 縁取り色
strokeWidth: 1.0, // 縁取り幅
よく使うメソッド
Canvas
Code | 意味 |
---|---|
canvas.add(el) | 要素 追加 |
canvas.remove(el) | 要素 削除 |
canvas.selection = false | グループ選択禁止 |
canvas.setZoom(1.0) | 全体の拡縮 (1.0 が基準) |
canvas.toJSON() | 状態を json 化 (SVGを含む) |
canvas.toDatalessJSON() | 状態を json 化 (SVGを含まない) |
canvas.loadFromJSON(xxx) | json で復元 |
canvas.setDimensions(attrs) | width: height: で w h を指定する |
canvas.setWidth(w) | width 指定 |
canvas.setHeight(h) | height 指定 |
canvas.setBackgroundImage(url, ...) | 背景画像設定。第二引数重要 |
canvas.setActiveObject(el) | 指定の要素をクリック |
canvas.getActiveObject() | 今動かせる要素を返す |
canvas.getObjects() | 要素の配列 |
canvas.size() | 要素の配列の個数 |
要素
Code | 意味 |
---|---|
el.center() | 上下左右を考慮して中央に表示 |
el.set({hasRotatingPoint: false}) | 回転させない |
el.scaleToWidth(50) | 横幅変更 (比率維持) |
el.scaleToHeight(50) | 縦幅変更 (比率維持) |
el.set(left: 50}) | 左から指定ピクセルの位置に移動 |
el.set({selectable: false}) | 選択させない |
el.set({lockScalingFlip: true}) | 裏返しにさせない |
el.set({fill: "#008"}) | 塗り潰し色の指定 |
el.set({opacity: 0.5}) | 非透明度の指定 |
el.moveTo(1) | 優先順位の変更 直接 |
el.bringToFront() | 優先順位の変更 前面へ |
el.sendToBack() | 優先順位の変更 背面へ |
Rect が持っている属性
ダンプしたときに出てきたのがこれっていうだけでもっとあると思われる。hasRotatingPoint
とか、なんで出てこないんだろう?
属性名 | 値 |
---|---|
type | "rect" |
version | "2.6.0" |
originX | "center" |
originY | "center" |
left | 200 |
top | 200 |
width | 50 |
height | 50 |
fill | "cyan" |
stroke | null |
strokeWidth | 1 |
strokeDashArray | null |
strokeLineCap | "butt" |
strokeDashOffset | 0 |
strokeLineJoin | "miter" |
strokeMiterLimit | 4 |
scaleX | 1 |
scaleY | 1 |
angle | 0 |
flipX | false |
flipY | false |
opacity | 1 |
shadow | null |
visible | true |
clipTo | null |
backgroundColor | "" |
fillRule | "nonzero" |
paintFirst | "fill" |
globalCompositeOperation | "source-over" |
transformMatrix | null |
skewX | 0 |
skewY | 0 |
rx | 0 |
ry | 0 |
参照
Discussion