Canvas库Fabric.js的基础功能使用

最近在搞一个类似于微信截图的图片审核工具。具体的功能能支持Canvas放大缩小、移动、画框,箭头,自由画笔和输入文字,且最后能导出一张标注后的截图。功能操作区如图所示

安装fabric.js

npm install fabric --save

项目中如何使用

import { fabric } from 'fabric';

初始化

const canvas = new fabric.Canvas(canvasRef.current, options)// 如果需要Canvas上的元素可以拖动,则需要设置canvas.selection = true

加载背景图

我们的场景是对图片进行标注,所以第一步必须先加载一张图片。我加载的图片可能会比Canvas大,初始时先对图片做了缩放校验。

css 复制代码
fabric.Image.fromURL(
  '图片路径',
  (img: any) => {
     // 图片的宽或者高超过了画布,因此缩放图片,以适应当前画布
     if (img?.width > canvas.width || img?.height > canvas.height) {
          const scaleX = canvas.width / img.width;
          const scaleY = canvas.height / img.height;
          const scale = Math.min(scaleX, scaleY);
          img.scale(scale);
      }
      // 图片居中展示
      const oImg = img.set({ left: 0, top: 0, angle: 0 });
      canvas.centerObject(oImg);
      canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); // 设置为背景图
      canvas.renderAll();
  },
  // 添加属性,支持图片跨域,不然使用导出截图功能时会报错
  { crossOrigin: 'anonymous' },
);

形状、文字绘制

获取当前坐标:const pointer = canvas.getPointer(event.e); 在我场景中允许Canvas缩放,所以需要获取转换后的坐标,getPointer返回的就是转换后的x, y。

矩形绘制
less 复制代码
origX,origY在我的代码中是mouse:down的鼠标位置
 const activeRect = new fabric.Rect({
    left: origX,
    top: origY,
    fill: 'transparent', // 创建线条rect
    stroke: 'red',
    width: pointer.x - origX,
    height: pointer.y - origY,
    originX: 'left',
    originY: 'top',
    selectable: true, // 是否可以移动
    hasControls: true, // 是否显示句柄
    strokeWidth: 'red',
    rx: 4, // 设置半角弧度
    ry: 4,
  });
  canvas.add(activeRect);
箭头绘制

用的是Path绘制的箭头,也可以用Line和Triangle组合来绘制。

为啥最后用Path来绘制呢??是因为绘制出来的箭头需要支持移动,使用Line和Triangle组合绘制的箭头主体和头分离,拖动的时候不能一起拖。。

如果想要一起拖动Line和Triangle,可以使用new fabric.Group([line, triangle])把它俩组合起来。但是遇到了一个问题:在mouse:move事件中实时改变箭头位置时不会添加到Canvas上。

ini 复制代码
// 利用Path来绘制箭头,参考:https://github.com/Couy69/vue-fabric-drawingboard
const drawArrowPath = (x1: number, y1: number, x2: number, y2: number) => {
  const w = x2 - x1;
  const h = y2 - y1;
  const sh = Math.cos(Math.PI / 4) * 16;
  // eslint-disable-next-line no-restricted-properties
  const sin = h / Math.sqrt(Math.pow(w, 2) + Math.pow(h, 2));
  // eslint-disable-next-line no-restricted-properties
  const cos = w / Math.sqrt(Math.pow(w, 2) + Math.pow(h, 2));
  const w1 = (16 * sin) / 4;
  const h1 = (16 * cos) / 4;
  const centerx = sh * cos;
  const centery = sh * sin;
  /**
   * centerx,centery 表示起始点,终点连线与箭头尖端等边三角形交点相对x,y
   * w1 ,h1用于确定四个点
   */
  let path = ' M ' + x1 + ' ' + y1;
  path += ' L ' + (x2 - centerx + w1) + ' ' + (y2 - centery - h1);
  path += ' L ' + (x2 - centerx + w1 * 2) + ' ' + (y2 - centery - h1 * 2);
  path += ' L ' + x2 + ' ' + y2;
  path += ' L ' + (x2 - centerx - w1 * 2) + ' ' + (y2 - centery + h1 * 2);
  path += ' L ' + (x2 - centerx - w1) + ' ' + (y2 - centery + h1);
  path += ' Z';
  return path;
};
自由画笔

设置Canvas上的一些参数就可以了。

canvas.isDrawingMode = true;

canvas.freeDrawingBrush.color = 'red';//设置画笔颜色

canvas.freeDrawingBrush.width = 4;//设置画笔宽度

可编辑文字
php 复制代码
const iText = new fabric.IText('要插入的文字', {
    fill: 'red',
    fontSize: '16px',
    top: pointer.y,
    left: pointer.x,
    hasControls: false,
});
canvas.add(iText);
// 设置为活动对象并进入编辑模式
canvas.setActiveObject(iText);
iText.enterEditing();

// 可监听的失焦事件
iText.on('editing:exited', () => {})
canvas支持缩放、移动操作
ini 复制代码
canvas.on('mouse:wheel', (opt: any) => {
  const delta = opt.e.deltaY;
  // 缩放
  if (opt.e.ctrlKey) {
    let zoom = canvas.getZoom();
    zoom *= 0.99 ** delta;
    if (zoom > 20) zoom = 20;
    if (zoom < 0.01) zoom = 0.01;
    // 获取鼠标当前的位置
    const pointer = canvas.getPointer(opt.e);
    const pos = new fabric.Point(pointer.x, pointer.y);
    // 设置缩放的中心点为鼠标的位置
    canvas.zoomToPoint(pos, zoom);
  } else {
    // 移动
    const activeObject = canvas.getActiveObject();
    const horizontalScroll = -opt.e.deltaX;
    const verticalScroll = -opt.e.deltaY;
    const newLeft = Number(canvas.viewportTransform[4]) + horizontalScroll;
    const newTop = Number(canvas.viewportTransform[5]) + verticalScroll;
    // 更新画布的位置
    canvas.viewportTransform[4] = newLeft;
    canvas.viewportTransform[5] = newTop; // 更新画布的top位置
    // 渲染画布
    canvas.requestRenderAll();
  }
  // 阻止默认事件
  opt.e.preventDefault();
  opt.e.stopPropagation();
});
撤销

在图片上涂涂画画之后,还允许撤销(Cmd+Z)操作。主要实现:把每一步的数据(canvas.toObject)保存到stateStack中,执行撤销操作时加载(loadFromJSON)上一步的所有内容。

fabric.js提供了 loadFromJSON 方法,支持一次性加载所有的数据。但是!!在有渲染图片的场景中使用loadFromJSON方法,可能页面会有闪烁的情况,因为有加载图片过程。

解决办法:1、获取canvas.getObjects()清空当前场景中的所有内容。2、获取到保存的上一场景内容的objects,利用canvas.add()循环加载一遍。

ini 复制代码
// 编辑状态记录,用于撤销
  const canvasState = useRef<CanvasState>({
    currentState: null,
    stateStack: [], // 所有操作的集合,可以添加一个空白数据,可在加载image后添加,不然图片不会出现在里面
  }).current;
  
  // 1、添加操作:在add完形状之后把当前场景的内容添加进去 (我是在mouse:up中添加的)
  // 2、更新操作:对图形移动,文字编辑之后把当前场景的内容添加进去,监听object:modified事件
  
  // 添加当前场景的方法 (在初始化canvas时要在stateStack中添加一个空数据,不然撤退不到最原始状态)
 function pushStateIfChanged() {
    const state = canvas.toObject();
    const { currentState, stateStack } = canvasState;
    if (!isEqual(state, currentState)) {
      stateStack.length = currentState ? stateStack.indexOf(currentState) + 1 : 0;
      stateStack.push(state);
      canvasState.currentState = state;
    }
 }
 
 // 监听键盘事件
  function handleUndo() {
    const { currentState, stateStack } = canvasState;
    const i = currentState ? stateStack.indexOf(currentState) : -1;
    if (stateStack[i - 1]) {
      const data = stateStack[i - 1];
      const { objects } = data;
       // 先清空所有的内容(除背景图)不要使用clear(), 它会清空画布所有内容
        toolCanvas.current.getObjects().forEach((o: any) => {
          toolCanvas.current.remove(o);
        });

        // 再重绘一遍所有的图形,这样就不会发生闪烁情况
        for (let n = 0; n < objects.length; n++) {
          fabric.util.enlivenObjects(
            [objects[n]],
            (enlivenedObjects: any) => {
              enlivenedObjects.forEach((obj: any) => {
                toolCanvas.current.add(obj);
              });
              toolCanvas.current.renderAll();
            },
            'fabric',
          );
        }
      canvasState.currentState = stateStack[i - 1] || null;
    }
    updateOperateBtnStates();
  }
  
导出截图

fabric.js提供了 canvas.toDataURL() 方法,可直接导出当前canvas的内容。

但是,又有个问题!

如果我当前的canvas是处于放大、移动的状态,上面👆🏻的方法会导出canvas视图里面的内容。。但这不是我想要的效果。

我的解决办法是:把当前canvas的数据导出,再新建一个copyCanvas加载所有内容,再利用 toDataURL 导出截图就行啦。

但是,还有一个问题!

如果你当前截图是在小屏幕操作的,后面再去到大屏幕看刚才的截图,图片就会很小(我希望的效果是大小屏应该同等大小)。

  • 如果用 原canvas 的宽高作为 copyCanvas 的宽高的话会出现图片大小不一致问题。
  • 如果用图片的真实宽高作为 copyCanvas 的宽高的话,一些标注在图片之前的框就不会被导出。

如何解决上面👆🏻的问题呢? 欢迎给出不同的答案。

相关推荐
一颗松鼠3 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds23 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱1 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai1 小时前
uniapp
前端·javascript·vue.js·uni-app
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓3 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4113 小时前
无网络安装ionic和运行
前端·npm
理想不理想v3 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云3 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js