🖼

Fabric.js の使い方メモ

2022/07/06に公開

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",
})

上のどちらもでも回避できた。が、似た対処方が複数あってなんともすっきりしない。

参考 🙏

https://blog.np-sys.com/tainted-canvases-may-not-be-exported/

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

参照

http://fabricjs.com/docs/

Discussion