Fabric.js图片裁剪教程【源码】

2,973 阅读5分钟

Fabric.js图片裁剪

最近使用Fabric.js做了一个图片编辑器,需求中有一个图片裁剪的功能,这是个很实用的基础功能,当初我做的时候想去寻求一些教程,但找了一圈始终没有找到完整的方案,都是一些比较零碎的功能......

果然,靠人不如靠己,在我不懈的努力下最终将它完整的开发出来了,过程还算顺利。

正所谓前人插秧,后人乘凉,在我开发完这个功能后,第二时间写出了一篇文章分享出来,希望能给予需要它的人一些帮助,当然如果有哪些实现不好的地方也希望大家能够指正

先看一下效果

一、图片裁剪能力分析

上面的效果看起来很难,实际上一点也不简单,我们需要考虑图片的位置蒙层的位置,以及它们的放大缩小拉伸等行为

主要就是做以下几件事:

  • 给图片元素增加裁剪按钮
  • 点击图片后创建蒙层盖在图片上
  • 监听蒙层和图片的拖拽事件,限制它们可拖动的范围
  • 获取到裁剪后的图片并替换之前的图片

image-20240729211408155

二、给图片元素增加裁剪按钮

fabric.Object.prototype.controlsFabric.js 库中所有对象的父类属性,用于包含和管理对象的所有控制点信息

我们可以通过自定义control,设置一个裁剪按钮,并将它应用在图片元素

官方例子: fabricjs.com/custom-cont…

function drawImg(ctx, left, top, img, wSize, hSize, angle) {
  if (angle === undefined) return;
  ctx.save();
  ctx.translate(left, top);
  ctx.rotate(fabric.util.degreesToRadians(angle));
  ctx.drawImage(img, -wSize / 2, -hSize / 2, wSize, hSize);
  ctx.restore();
}

const clipImageEl = document.createElement('img')
clipImageEl.src = "icon的路径"

function clipImage(eventData, transform) {
}

fabric.Object.prototype.controls.centerButton = new fabric.Control({
  y: -0.5,
  mouseUpHandler: clipImage, // 点击事件  
  offsetY: -25,
  offsetX: 0,
  cursorStyle: 'pointer', // 鼠标hover样式  
  render: (ctx, left, top, styleOverride, fabricObject) => {
    // 这里判断了一下元素的类型,只在图片上生效  
    if (fabricObject.type === 'image') {
      drawImg(ctx, left, top, clipImageEl, 30, 30, fabricObject.angle);
    }
  }
})

这样我们就会得到一个只会在图片元素上生效的裁剪按钮

image-20240729211335696

image-20240729211134346

三、创建蒙层

给按钮绑定点击事件,然后创建一个蒙层,将蒙层的大小和位置与图片保持一致

function clipImage(eventData, transform) {
  const image = transform.target;
  const canvas = image.canvas;
  if (image.type !== 'image') return
  const rectLeft = image.left // 矩形的位置X
  const rectTop = image.top // 矩形的位置Y
  const sourceWidth = image.getScaledWidth() // 获取图片的宽
  const sourceHeight = image.getScaledHeight() // 获取图片的高
  image.bringToFront() // 将这个图片的层级移动到顶层
  const selectionRect = new fabric.Rect({
    left: rectLeft,
    top: rectTop,
    fill: 'rgba(0,0,0,0.3)',
    originX: 'left',
    originY: 'top',
    stroke: 'black',
    opacity: 1,
    width: sourceWidth,
    height: sourceHeight,
    hasRotatingPoint: false,
    transparentCorners: false,
    cornerColor: 'white',
    cornerStrokeColor: 'black',
    borderColor: 'black',
    cornerSize: 12,
    padding: 0,
    cornerStyle: 'circle',
    borderDashArray: [5, 5],
    borderScaleFactor: 1.3,
    id: 'currentClipRect',
    lockMovementX: true,
    lockMovementY: true,
    hoverCursor: 'default'
  })
  canvas.add(selectionRect)
  canvas.setActiveObject(selectionRect)
  canvas.renderAll()
}

此外我们开始裁剪的时候还需要计算出原图和裁剪之后的图位置的差异

四、监听拖拽事件

点击裁剪按钮的时候我们需要去监听图片和裁剪方块的拖拽事件,使其紧密相连,不能超出边界

image.on('moving', (e) => {
  const image = e.transform.target
  if (image.left > selectionRect.left) {
    image.set({ left: selectionRect.left })
  }
  if (image.top > selectionRect.top) {
    image.set({ top: selectionRect.top })
  }
  const rectWidth = selectionRect.getScaledWidth()
  if (image.left < selectionRect.left + rectWidth - image.getScaledWidth()) {
    image.set({ left: selectionRect.left + rectWidth - image.getScaledWidth() })
  }
  const rectHeight = selectionRect.getScaledHeight()
  if (image.top < selectionRect.top + rectHeight - image.getScaledHeight()) {
    image.set({ top: selectionRect.top + rectHeight - image.getScaledHeight() })
  }
  canvas.renderAll()
})

五、监听缩放事件

同时,我们还需要监听缩放事件

image.on('scaling', (e) => {
  const image = e.transform.target
  if (image.left > selectionRect.left) {
    image.set({ left: selectionRect.left })
  }
  if (image.top > selectionRect.top) {
    image.set({ top: selectionRect.top })
  }
  if (selectionRect.getScaledWidth() / image.getScaledWidth() > 1) {
    image.set({ scaleX: selectionRect.getScaledWidth() * image.get('scaleX') / image.getScaledWidth() })
  }
  if (selectionRect.getScaledHeight() / image.getScaledHeight() > 1) {
    image.set({ scaleY: selectionRect.getScaledHeight() * image.get('scaleY') / image.getScaledHeight() })
  }
  canvas.renderAll()
})

六、取消裁剪

取消裁剪时需要把蒙层从画布中删除,然后恢复画布中元素的层级

/**
 * 取消裁剪
 */
const cancelClipImage = {
  if (!canvas) return
  let rect = null // 裁剪rect
  let clipImage = null // 被裁剪的图片
  let currentClipImageIndex = null // 被裁剪的图片的index
  canvas.getObjects().forEach((item, index) => {
    // 获取到裁剪rect
    if (item.id === 'currentClipRect') {
      rect = item
    }
    // 获取到被裁剪的图片和被裁剪的图片的index
    if (item.id === clipImageId) {
      clipImage = item
      currentClipImageIndex = index
    }
  })
  if (!clipImage) return
  // 如果点击取消,那么判断有rect就把rect删除
  if (rect) canvas.remove(rect)
  canvas.remove(clipImage)
  const cloneObject = clipImage.get('cloneObject')
  if (!clipImage || !cloneObject) return
  cloneObject.set({
    id: uuid(),
    sourceSrc: clipImage.sourceSrc,
    rawScaleX: clipImage.scaleX,
    rawScaleY: clipImage.scaleY,
    rectDiffLeft: clipImage.rectDiffLeft,
    rectDiffTop: clipImage.rectDiffTop,
  })
  canvas.add(cloneObject)
  if (cloneObject && currentClipImageIndex !== clipRawIndex) {
    // 如果被裁剪的图片层级发生了变化那么需要恢复image之前的层级
    while (currentClipImageIndex !== clipRawIndex) {
      cloneObject.sendBackwards()
      currentClipImageIndex--
    }
  }
  // 完成裁剪,恢复状态
  setIsClipImage(false)
  canvas.renderAll()
}

七、完成裁剪

裁剪我们主要用到的api时图片元素的clipPath属性,clipPath是一个用于创建裁剪路径的属性,它可以将对象或图形的显示区域限制在指定的形状内,只显示该形状内的内容,而隐藏形状外的部分。这个属性允许开发者通过定义复杂的裁剪路径来实现精细的图形裁剪效果。获取裁剪后的图片是base64格式的,如果有其他场景的需求,可以在这里把它上传到oss中存储网络图片地址

const saveClipImage = () => {
  if (!canvas || !workSpace) return
  const scale = canvas.getZoom()
  try {
    let image = null // 被裁剪的图片
    let rect = null // 裁剪rect
    let currentClipImageIndex = null // 当前下标
    canvas.getObjects().forEach((item, index) => {
      // 获取到裁剪rect
      if (item.id === 'currentClipRect') {
        rect = item
      }
      // 获取到被裁剪的图片和被裁剪的图片的index
      if (item.id === clipImageId) {
        image = item
        currentClipImageIndex = index
      }
    })
    if (!image || !rect) return
    setLoading(true)
    // 创建一个新的rect并保持和裁剪rect位置一致
    const newRect = new fabric.Rect({
      left: rect.left,
      top: rect.top,
      width: rect.getScaledWidth(),
      height: rect.getScaledHeight(),
      absolutePositioned: true,
    });
    // 设置图片clipPath(裁剪功能)
    image.clipPath = newRect;

    // 生成一个image
    const cropped = new Image();
    cropped.crossOrigin = 'anonymous'
    // 把旧的rect删除
    canvas.remove(rect);
    // 恢复画布缩放比例
    canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
    // 裁剪图片,位置是newRect的位置
    const base64 = canvas.toDataURL({
      left: newRect.left,
      top: newRect.top,
      width: newRect.width,
      height: newRect.height,
    });
    cropped.src = base64
    // 裁剪完成收回复画布缩放比例
    workSpace?.setZoomAuto(scale)
    // 等待裁剪的图片加载完成
    cropped.onload = function () {
      // 将原图片删除
      canvas.remove(image)
      // 创建新的图片
      const newImage = new fabric.Image(cropped, {crossOrigin: 'anonymous'});
      // 这个原图的src很重要,重新编辑的时候会用到
      // 设置新图片的位置和newRect位置保持一致
      newImage.set({
        id: image.id,
        left: newRect.left,
        top: newRect.top,
        rawScaleX: image.scaleX,
        rawScaleY: image.scaleY,
        sourceSrc: image.sourceSrc,
        rectDiffLeft: newRect.left - image.left,
        rectDiffTop: newRect.top - image.top,
        prevWidth: newRect.getScaledWidth(),
        prevHeight: newRect.getScaledHeight()
      })
      canvas.add(newImage);
      if (currentClipImageIndex !== clipRawIndex) {
        // 如果被裁剪的图片层级发生了变化那么需要恢复image之前的层级
        while (currentClipImageIndex !== clipRawIndex) {
          newImage.sendBackwards()
          currentClipImageIndex--
        }
      }
      canvas.renderAll();
      // 完成裁剪
      setIsClipImage(false)
      setLoading(false)
    };
  } catch (err) {
    setLoading(false)
    workSpace?.setZoomAuto(scale)
    cancelClipImage()
    toast.show('裁剪失败,请稍后重试~')
    console.log(err)
  }
}

八、完整代码

这是一个使用React.js、Fabric.js和Node.js开发的简单图片编辑器组件。它允许用户上传图片,并使用工具栏进行裁剪、旋转、缩放等编辑操作。最后,用户可以将编辑后的图片保存到本地。 项目地址

作者:洞窝-永升