MENU

Fabric.js 简单介绍和使用

2022 年 05 月 25 日 • 开发

参考资料

简介

Fabric.js 是一个功能强大的 Canvas 库,它在原生 Canvas 之上提供了交互式对象模型、多种易用的 API 和 SVG 解析器等,通过简洁的 API 就可以在画布上进行丰富的操作,并且支持多种的事件方法。

注:本文中使用的 Fabric.js 版本为 v5。

常用的对象、属性和方法

常用对象

名称描述
ActiveSelection选区
Group分组
Image图像
Line线段
Rect矩形
Text文本(不可编辑,不换行)
IText可编辑文本(可编辑,不换行)
Textbox文本框(可编辑,自动换行)

常用属性

属性描述
type对象类型
data用于添加自定义的数据
originX对象转换的水平原点(left / center / right)
originY对象转换的垂直原点(top / center / bottom)
left水平坐标
top垂直坐标
width宽度
height高度
angle旋转角度
scaleX水平方向缩放倍数
scaleY垂直方向缩放倍数
stroke线段颜色
strokeWidth线段宽度
fill填充颜色
fontFamily文本字体名称
fontSize文本字体大小
opacity对象的不透明度
borderColor选区边框颜色
borderDashArray选区边框虚线样式
borderOpacityWhenMoving选区拖拽时的边框透明度
cornerColor选区“操作点”填充颜色
cornerSize选区“操作点”大小
cornerStrokeColor选区“操作点”描边颜色
cornerStyle选区“操作点”样式(circle / rect)
transparentCorners选区“操作点”中心是否透明(只有描边)
hasControls是否有“操作点”
lockRotation是否禁止旋转对象
lockMovementX是否禁止水平移动对象
lockMovementY是否禁止垂直移动对象
lockScaleX是否禁止水平缩放对象
lockScaleY是否禁止垂直缩放对象
selectable是否可以被选中
hoverCursor光标在对象上方时的样式(子对象优先)
moveCursor光标在可移动对象上方时的样式(子对象优先)

常用方法

画布(Canvas)

方法描述
add(...object)添加对象到画布
insertAt(object, index, nonSplicing)添加对象到画布指定层级
moveTo(object, index)更改对象所在的层级
remove(...object)移除画布上的对象
discardActiveObject()取消选中对象
getActiveObject()获取选中的对象
setActiveObject()更改选中的对象
getPointer()传递事件对象,获取相对于画布的坐标
fire()触发画布事件
forEachObject()遍历画布上的对象
getZoom()获取画布缩放比例
setZoom()设置画布缩放比例
on()监听画布事件
requestRenderAll()刷新画布
set()设置画布属性

对象(Object)

方法描述
bringToFront()改变对象层级,置顶
bringForward()改变对象层级,上移
sendBackwards()改变对象层级,下移
sendToBack()改变对象层级,置底
intersectsWithObject()检测两个对象是否相交(重叠)
isContainedWithinObject()检测对象是在另外一个对象内
on()监听对象事件
set()设置对象属性
toDataURL()转换为 Base64

创建对象

画布

let canvas = new fabric.Canvas({
  // 画布宽度
  width: 800,
  // 画布高度
  height: 480,
  // 背景颜色
  backgroundColor: '#FFFFFF',
  // 激活对象时不将对象置顶
  preserveObjectStacking: true,
  // 选区框样式
  selectionBorderColor: '#4CAF50',
  selectionColor: '#4CAF5040',
  selectionFullyContained: true,
  selectionLineWidth: 1,
  // 关闭按比例缩放
  uniformScaling: false,
  // 其他配置选项
  ...
});

矩形

// 创建对象
let el = new fabric.Rect({
  left: 100,   // 相对于画布左侧的距离
  top: 100,    // 相对于画布顶部的距离
  width: 200,  // 对象宽度 100px
  height: 100, // 对象高度 100px
  fill: '#FFFFFF',   // 填充颜色
  stroke: '#1E88E5', // 边框颜色
  strokeWidth: 2,    // 边框粗细
});
// 添加对象
canvas.add(el);

线段

// 创建对象
let el = new fabric.Line([
  100, 100, // 起始点坐标
  200, 100, // 结束点坐标
], {
  stroke: '#1E88E5', // 线段颜色
  strokeWidth: 2,    // 线段粗细
});
// 添加对象
canvas.add(el);
// 创建对象
let el = new fabric.Line([], {
  stroke: '#1E88E5',
  strokeWidth: 2,
});
// 设置位置和长度
el.set({
  left: 100,
  top: 100,
  width: 100,
  // 注:因为是线段,高度为 0
  height: 0,
});
// 添加对象
canvas.add(el);

图片

let img = new Image();

// 监听图片加载,加载完成后再添加对象
img.onload = function() {
  // 创建对象
  let el = new fabric.Image(img, {
    top: 100,
    left: 100,
    width: 200,
    height: 150,
  });
  // 添加对象
  canvas.add(el);
};
// 设置 crossorigin 属性,开启 CORS 校验
//   若不设置,无法将画布或画布上的对象导出为图片。
//   值设置为 anonymous,表示对此元素的 CORS 请求将不设置凭据标志。
img.setAttribute('crossorigin', 'anonymous');
// 设置图片地址
img.setAttribute('src', 'http://fabricjs.com/assets/69.svg');

文本框

// 创建对象
let el = new fabric.Textbox('文本内容', {
  top: 100,
  left: 100,
  // 设置宽度后,文本内容超出宽度后会自动换行。
  // 注意:文本只会在空格处自动换行。
  // 高度为自适应,不需要设置。
  width: 200,
});
// 添加对象
canvas.add(el);

场景和实现方法

画布内容撤销、恢复

主要通过 Fabric.js 画布对象的 toObject()loadFromJSON() 方法实现。

为了提高性能,默认情况下 toObject() 只会导出对象上常用的属性,其他属性(例如 dataselectable)不会被导出,需要手动在其 propertiesToInclude 参数中指定(例如 canvas.toObject(['data', 'selectable']))。

画布内容导出为图片

将画布(显示区域)或对象导出为图片:

toDataURL(options)

参数:options(可选)

属性名数据类型默认值简介
formatStringpng导出图片的格式,可选值:jpeg、png
qualityNumber1图片质量(0 ~ 1),仅 jpeg 格式可用
multiplierNumber1缩放比例
leftNumber 裁剪左偏移量(1.2.14+)
topNumber 裁剪上偏移量(1.2.14+)
widthNumber 裁剪宽度(1.2.14+)
heightNumber 裁剪高度(1.2.14+)
enableRetinaScalingBoolean 为克隆图像启用 Retina 缩放(2.0.0+)

用法示例:

let dataURL = canvas.toDataURL({
  format: 'jpeg',
  quality: 0.8,
});
let dataURL = rect.toDataURL({
  format: 'png',
});

注意:

若导出的对象中包含使用 URL(非 DataURL)加载的图片对象,需要使用 crossOrigin 属性解决资源跨域问题,否则会出现以下错误提示:

The canvas has been tainted by cross-origin data.

其中一种解决方式是先使用 new Image() 设置 crossorigin 属性,加载图片,再创建 fabric.Image 对象(参考“创建对象 - 图片”)。

画布自适应父 DOM 元素宽高

方案一:监听浏览器窗口大小变化事件

// 父元素(DOM)
let el = vm.$el;
// 画布对象
let canvas = vm.canvasInstance;
// 防抖定时器
let debounce = null;

// 事件处理函数
const handler = function () {
  clearTimeout(debounce);
  debounce = setTimeout(() => {
    if (canvas) {
      // 设置宽高
      canvas.setWidth(el.clientWidth);
      canvas.setHeight(el.clientHeight);
      // 刷新画布(正常情况下设置宽高后会自动刷新)
      // canvas.requestRenderAll();
    } else {
      console.error('处理失败,画布对象不存在!');
    }
  }, 500);
};

// 添加事件监听
window.addEventListener('resize', handler);

// 移除事件监听
window.removeEventListener('resize', handler);
clearTimeout(debounce);

方案二:使用 ResizeObserver 监听 DOM 元素大小更改

参考资料:ResizeObserver - Web API 接口参考 | MDN
方案优点:在窗口大小不变化的情况下也能实现自适应

// 父元素(DOM)
let el = vm.$el;
// 画布对象
let canvas = vm.canvasInstance;
// 防抖定时器
let debounce = null;

/**
 * @desc 观察器回调函数
 * @type {ResizeObserverCallback}
 */
const handler = function (entries) {
  clearTimeout(debounce);
  debounce = setTimeout(() => {
    const entry = entries[0];

    if (canvas && entry) {
      const { width, height } = entry.contentRect;
      // 设置宽高
      canvas.setWidth(width);
      canvas.setHeight(height);
      // 刷新画布(正常情况下设置宽高后会自动刷新)
      // canvas.requestRenderAll();
    } else {
      console.error('处理失败,Canvas 或 ResizeObserverEntry 不存在!');
    }
  }, 500);
};

/**
 * @desc 观察器配置选项
 * @type {ResizeObserverOptions}
 */
const options = {
  // 设置观察器以哪种盒子模型来观察变化
  //   可选值:content-box(默认)、border-box、device-pixel-content-box
  box: 'border-box',
};

// 创建观察器,并传递回调函数
const observer = new ResizeObserver(handler);

// 传递配置选项
observer.observe(el, options);

// 停止观察器
observer.disconnect();
clearTimeout(debounce);

解决画布缩放后,对象显示模糊

把对象的 objectCaching 属性设置为 false,关闭缓存即可。
例如:

const rect = new fabric.Rect({
  left: 100,
  top: 100,
  width: 200,
  height: 200,
  fill: '#FFFFFF',
  stroke: '#66CCFF',
  strokeWidth: 2,
  objectCaching: false,
});

canvas.add(rect);

注意:不建议大量使用,以免影响性能。

实现对象溢出隐藏(裁剪)效果

主要用到了 clipPath 功能,设置对象的裁剪路径。

// 创建用于作为“容器”的对象
const container = new fabric.Rect({
  left: 0,
  top: 0,
  width: 800,
  height: 480,
  fill: '#FFFFFF',
  stroke: '#000000',
  strokeWidth: 2,
  // 将该属性设置为 true 后,裁剪的区域会动态更新
  absolutePositioned: true,
});

// 创建子对象
const item = new fabric.Rect({
  left: 100,
  top: 100,
  width: 200,
  height: 100,
  fill: '#FFFFFF',
  stroke: '#66CCFF',
  strokeWidth: 2,
  // 将 container 对象设为该对象的 clipPath
  clipPath: container,
});

关于 absolutePositioned 属性:

仅当对象作为 clipPath 使用时才有意义。如果为 trueclipPath 的位置将会相对于画布,且不受对象变换影响。(JSDoc: Class: Object#absolutePositioned

注意:

后续通过 .set() 更新对象的 clipPath 属性时,建议同时将其 dirty 属性设置为 true,以便清除缓存,防止裁剪区域没有更新。(JSDoc: Class: Object#dirty

item.set({ clipPath: null, dirty: true });

鼠标滚轮缩放画布

主要用到了画布的鼠标事件(mouse:wheel)以及调用画布的 zoomToPoint() 方法改变视图缩放。

// 监听事件:鼠标滚轮
canvas.on('mouse:wheel', function (ev) {

  const evt = ev.e;
  const cursorX = evt.offsetX;
  const cursorY = evt.offsetY;

  // 若按下 Ctrl 键,则为缩放
  if (evt.ctrlKey) {
    evt.preventDefault();
  } else {
    return;
  }

  // 若按下 Alt 键,则为重置缩放
  if (evt.altKey) {
    canvas.zoomToPoint({
      x: cursorX,
      y: cursorY,
    }, 1);
    return;
  }

  // 正数:向下滚动
  // 负数:向上滚动
  const delta = evt.deltaY;

  // 画布当前缩放值
  let zoom = canvas.getZoom();
  let min = 0.1;
  let max = 10;

  zoom += zoom * (delta > 0 ? -0.2 : 0.2);

  // 限制缩放范围
  (zoom < min) && (zoom = min);
  (zoom > max) && (zoom = max);

  // 设置画布缩放比例
  // 参数1:将画布的缩放点设置为鼠标当前位置
  // 参数2:传入缩放值
  canvas.zoomToPoint({
    x: cursorX,
    y: cursorY,
  }, zoom);

});

鼠标拖拽移动画布

主要用到了画布的鼠标事件(mouse:downmouse:movemouse:up)以及调用画布的 setViewportTransform() 方法改变视图偏移量。
注意:在拖拽前,若鼠标选中了对象,需要将对象的 lockMovementXlockMovementY 属性设置为 true,锁定对象的移动,否则拖拽后对象的位置会变化。

// 监听事件:鼠标按下
canvas.on('mouse:down', function ({ e: evt, target }) {

  // 仅在按下了 Ctrl 键时为拖拽画布
  if (!evt.ctrlKey) {
    return;
  }

  if (target) {
    if (target.lockMovementX || target.lockMovementY) {
      // 记录原始锁定状态
      target.isLock = true;
    } else {
      // 记录原始锁定状态
      target.isLock = false;
      // 锁定对象的移动
      target.set({
        lockMovementX: true,
        lockMovementY: true,
      });
    }
  }

  canvas.set({
    // 关闭选区框
    selection: false,
    // 自定义属性,标记拖拽状态
    isDragCanvas: true,
    // 自定义属性,记录起始位置
    startPosX: evt.clientX,
    startPosY: evt.clientY,
    startVpt: [...canvas.viewportTransform],
  });

});

// 监听事件:鼠标移动
canvas.on('mouse:move', function (ev) {
  // 检测是否为拖拽画布模式
  if (canvas.isDragCanvas) {
    const evt = ev.e;
    const vptOld = canvas.startVpt;
    const vptNew = [...vptOld];

    // 计算新的偏移量
    vptNew[4] += (evt.clientX - canvas.startPosX);
    vptNew[5] += (evt.clientY - canvas.startPosY);

    // 更新视图偏移量
    canvas.setViewportTransform(vptNew);
  }
});

// 监听事件:鼠标松开
canvas.on('mouse:up', function ({ target }) {
  // 解锁对象的移动(如果可以)
  if (target && !target.isLock) {
    target.set({
      lockMovementX: false,
      lockMovementY: false,
    });
  }
  // 更新画布属性
  canvas.set({
    // 开启选区框
    selection: true,
    // 自定义属性,标记拖拽状态
    isDragCanvas: false,
  });
});

鼠标事件中获取分组(Group)内的目标对象

默认情况下,对于分组,ev.target 只能获取到整个分组,并不能具体到其中的对象。
在创建分组时,将 subTargetCheck 属性设置为 true,即可通过 ev.subTargets 获取触发事件时的子对象。
对于支持该功能的事件(mouse:downmouse:upmouse:move 等),该属性为 fabric.Object 数组;
对于不支持该功能的事件,该属性为 undefined
例如:

const group = new fabric.Group([obj1, obj2], {
  subTargetCheck: true,
  ...
});

canvas.add(group);

canvas.on('mouse:down', function (ev) {
  console.log(ev.subTargets);
});

文本框强制自动换行

Fabeic.js 的文本框对象(Textbox)默认支持文本自动换行,但只能在空格处换行,对于类似中文句子这种不带空格的字符串并不会换行,且文本框的宽度会自动被撑开。
在 Fabric.js 2.6.0 版本中,新增了一个 splitByGrapheme 属性,将其设置为 true 即可启用在任意字符之间自动换行(参考:JSDoc: Global),例如:

const textbox = new fabric.Textbox('文本框', {
  left: 100,
  top: 100,
  width: 200,
  splitByGrapheme: true,
});

canvas.add(textbox);

选中并聚焦对象(将对象置于视图的中心)

// 获取对象的中心点坐标
//   坐标值相对于画布左上角起始点
//   坐标值不受缩放比例影响
const {
  x: elCenterX,
  y: elCenterY,
} = el.getCenterPoint();

// 获取画布当前的缩放比例
const zoom = canvas.getZoom();

// 计算新的视图坐标
//   中心点坐标值需要与画布缩放比例相乘,与视图统一
//   画布坐标的起始点在左上角,坐标值需要减去画布宽高的一半
const point = {
  x: elCenterX * zoom - canvas.width / 2,
  y: elCenterY * zoom - canvas.height / 2,
};

// 移动视图
canvas.absolutePan(point);
// 选中对象
canvas.setActiveObject(el);

选中多个对象,创建选区

主要用到了 Fabric.js 的 ActiveSelection 对象,手动创建选区:

// 画布对象
const canvas = this.canvasInstance;

// 选区中包含的对象
const items = [a, b, c, ...];

// 新建一个选区对象
const sel = new fabric.ActiveSelection(items, {
  // 选区所属的画布
  //   必须包含该参数,否则会出现奇怪的问题(不会报错)
  canvas,
  // 其他配置选项,例如:
  //  borderColor,
  //  borderDashArray,
  //  opacity,
  //  hasControls,
  ...
});

// 使选区生效
canvas.setActiveObject(sel);
最后编辑于: 2023 年 06 月 24 日