返回

Fabric.js透视变形: 旋转后形变问题及解决方案

javascript

Fabric.js 结合 Perspective.js 旋转后形变问题解析与解决

在 Fabric.js 中集成 Perspective.js 用于图像透视变换是一个常见的需求,然而当图像发生旋转后,透视变换效果往往会出现错乱,产生形变。这是因为 Perspective.js 默认基于未经旋转的原始坐标系计算变换,而 Fabric.js 的旋转操作会改变图像在画布上的实际位置和方向。解决此问题的核心在于将透视变换的控制点坐标进行相应的旋转和偏移调整,使其适应旋转后的图像。

问题分析

Perspective.js 通常需要用户提供四个控制点(左上、右上、右下、左下)的坐标来实现透视变换。在 Fabric.js 中,对象旋转会影响这四个控制点的相对位置。因此,直接使用 Fabric.js 对象的 getBoundingRect() 或者其他直接获取的未经处理的绝对坐标,进行透视变换,会导致错位。控制点必须转换为基于旋转后对象的新局部坐标。

解决方案一:基于 Fabric.js 对象变换矩阵的控制点转换

此方法利用 Fabric.js 对象本身的变换矩阵,将透视变换控制点从画布全局坐标系转换到对象局部坐标系,并在透视变换完成后,将其反向变换回全局坐标系。这种做法,巧妙地解决了因为旋转导致的坐标错乱的问题。

步骤:

  1. 获取 Fabric.js 对象的变换矩阵: 使用 object.calcTransformMatrix() 获取 Fabric.js 对象的变换矩阵,包含了对象的缩放、旋转和偏移信息。
  2. 获取初始的透视控制点坐标: 考虑没有旋转时四个角的控制点,这些控制点是以 Canvas 坐标表示。
  3. 反向变换原始控制点到局部坐标系: 利用上一步获取的变换矩阵的逆矩阵 transformMatrix.invert() 对控制点进行反向变换,将其从 Canvas 坐标转换为物体旋转前的局部坐标。
  4. 基于局部坐标执行透视变换: 使用 Perspective.js 对转换后的控制点坐标进行透视变换。
  5. 执行透视变换 执行透视变换,注意在透视变换之后画布中的位置,依然是旋转之后的物体所在的位置和形状,即被拉伸过的矩形的位置和形状,在绘制完成之后需要反向进行计算,找到原来对象绘制的范围。
  6. 将结果反向变换回全局坐标系: 计算得出新的四个角的顶点坐标。注意Perspective.js处理之后新的透视画布位置是会变化的。
  7. 更新 Fabric.js 对象: 使用透视变换后的图像数据更新 Fabric.js 对象的 texture 并重绘。

代码示例:

function applyPerspective(fabricObject, newCorners){
    // 1. 获取变换矩阵
    const transformMatrix = fabricObject.calcTransformMatrix();
     //2. 创建矩阵
    const m = new fabric.Matrix(transformMatrix[0],transformMatrix[1],transformMatrix[2],transformMatrix[3],transformMatrix[4],transformMatrix[5])
    //3. 获取对象原始四个角的顶点位置(画布坐标),这个可以通过object.getBoundingRect来获得
   const bounds = fabricObject.getBoundingRect()
  const topLeft = new fabric.Point(bounds.left, bounds.top);
  const topRight = new fabric.Point(bounds.left + bounds.width, bounds.top);
    const bottomRight = new fabric.Point(bounds.left + bounds.width, bounds.top + bounds.height);
    const bottomLeft = new fabric.Point(bounds.left, bounds.top+bounds.height)


   const oldPoints = [
     topLeft,
     topRight,
     bottomRight,
     bottomLeft,
   ]

    const invertMatrix = m.invert();

      const originalPoints  =  oldPoints.map(p=>{
             let  tranP  = fabric.util.transformPoint(p,invertMatrix);
              return  tranP
        })


      const transformPoints = newCorners
        .map((corner) => new fabric.Point(corner.x, corner.y))
        .map((p) => {
            const pt = new fabric.Point(p.x, p.y);
            let point= fabric.util.transformPoint(pt,invertMatrix);
            return {
                 x:point.x,
                 y:point.y
              }
      });



  const perspective = new Perspective();
    let newCanvas=document.createElement('canvas');

      newCanvas.width=fabricObject.getScaledWidth();
      newCanvas.height=fabricObject.getScaledHeight()


    let  tempCanvas = document.createElement('canvas');
      tempCanvas.width =  fabricObject.canvas.width
       tempCanvas.height =  fabricObject.canvas.height
      const tmpCanvasContext =  tempCanvas.getContext("2d")
        const ctxd=tempCanvas.getContext('2d');
    perspective.ctxd =ctxd
       let source= document.createElement("img")

    source.onload =()=>{
        //4.基于原始的坐标计算旋转
       perspective.draw( {
         topLeftX: transformPoints[0].x,
         topLeftY: transformPoints[0].y,
         topRightX: transformPoints[1].x,
         topRightY:transformPoints[1].y,
         bottomRightX: transformPoints[2].x,
          bottomRightY: transformPoints[2].y,
        bottomLeftX:transformPoints[3].x,
           bottomLeftY: transformPoints[3].y,
          canvas: tempCanvas
        }
        );
             //计算变换后图像的新尺寸
    let dest_topLeft= new fabric.Point(transformPoints[0].x, transformPoints[0].y);
       let dest_topRight = new fabric.Point(transformPoints[1].x, transformPoints[1].y);
           let dest_bottomRight = new fabric.Point(transformPoints[2].x, transformPoints[2].y);
          let dest_bottomLeft= new fabric.Point(transformPoints[3].x, transformPoints[3].y)

           const dest_points= [dest_topLeft,dest_topRight,dest_bottomRight,dest_bottomLeft]



      //  利用变换矩阵 将新的四个顶点 转换到画布坐标系,用于计算变换后对象的尺寸
    const destCanvasPoints =  dest_points.map(p=>{
               let  tranP  = fabric.util.transformPoint(p,m);
               return tranP
           })

     let leftArr =   destCanvasPoints.map((x)=>x.x);
      let topArr=   destCanvasPoints.map((y)=>y.y)
       let dest_minX = Math.min(...leftArr)
         let dest_maxX = Math.max(...leftArr)

        let dest_minY= Math.min(...topArr);
         let dest_maxY=Math.max(...topArr)

      // 计算变换之后 最小包围矩阵
          let boundingBoxWidth =   dest_maxX - dest_minX;
            let boundingBoxHeight = dest_maxY -  dest_minY;



  fabricObject.set('clipPath',new fabric.Path(`M ${destCanvasPoints[0].x} ${destCanvasPoints[0].y} L  ${destCanvasPoints[1].x} ${destCanvasPoints[1].y} L  ${destCanvasPoints[2].x} ${destCanvasPoints[2].y}  L ${destCanvasPoints[3].x} ${destCanvasPoints[3].y} Z`)).set('dirty', true);
  fabricObject.set({
       left:dest_minX,
      top:dest_minY
     }).setCoords()
           fabricObject.canvas.requestRenderAll();
           fabricObject.set('needsReDraw',false) //确保绘制之后,再次操作是正确的


    }


    //注意在进行drawImage之前需要提前初始化图片资源,以避免异步导致的错误
     let  url =   fabricObject.toDataURL()

    source.setAttribute("src",url)




}

关键点:

  • 使用 fabric.Matrixinvert() 实现坐标变换。
  • 此方法具有更高的精确度,能处理复杂的变换情况。
  • 代码逻辑清晰易懂。

安全建议

  • 透视变换是一种相对复杂的计算过程,请确保传入控制点的参数合法,避免造成错误的显示效果或导致页面性能下降。
  • 为了更好地管理代码逻辑,可以将透视变换操作封装成一个独立函数或模块,方便复用。
  • 考虑对用户输入的控制点进行边界检查,避免用户输入超出预期的参数范围。

总结

通过上述方法, 可以解决 Fabric.js 对象在旋转后使用 Perspective.js 进行透视变换产生形变的问题。核心在于利用 Fabric.js 的变换矩阵和反向变换能力,实现透视变换坐标在不同坐标系下的准确转换。使用以上方法能够实现 Fabric.js 对象旋转后依旧保持准确的透视变形效果。 请选择适合你的项目的解决方案,并根据你的实际情况进行相应的修改。