Search code examples
javascriptcanvashtml5-canvasfabricjsclipping

Fabric.js dynamic clipping regions affecting images and SVG/shapes differently


I have a Fabric.js canvas that allows for dynamic clipping based on the region an object occupies. However, the clipping regions treat SVG/shapes differently than images, making them offset slightly and "breaking through" the clipping area. To recreate:

  1. Go to https://jsfiddle.net/mikewagz/hncqbhah/
  2. Drag the logo to the boundary of the clipping region - this clips correctly, being clipped to whatever region its center occupies without breaking the boundary.
  3. Now grab a shape and drag it to the clipping region. You can see a small amount of it beneath the boundary.
  4. Scaling up the shape exacerbates the problem, showing an increasing amount of pixels below the clipping area. Note that it also offsets the clipping area in the same direction on the top and left.

enter image description here

How can this be corrected? Any help is very appreciated, thank you!

Code for clipping function:

var clipByName = function (ctx) {
  this.setCoords();
  var clipObj = findByClipName(this.clipName);
  var scaleXTo1 = (1 / this.scaleX);
  var scaleYTo1 = (1 / this.scaleY);
  ctx.save();

  var ctxLeft = -( this.width / 2 ) + clipObj.strokeWidth;
  var ctxTop = -( this.height / 2 ) + clipObj.strokeWidth;
  var ctxWidth = clipObj.width - clipObj.strokeWidth;
  var ctxHeight = clipObj.height - clipObj.strokeWidth;

  ctx.translate( ctxLeft, ctxTop );
  ctx.scale(scaleXTo1, scaleYTo1);
  ctx.rotate(degToRad(this.angle * -1));

  ctx.beginPath();

  var isPolygon = clipObj instanceof fabric.Polygon;
  // polygon cliping area
  if(isPolygon)
  {
      // prepare points of polygon
      var points = [];
      for(i in clipObj.points)
          points.push({
              x: (clipObj.left + clipObj.width / 2) + clipObj.points[i].x - this.oCoords.tl.x,
              y: (clipObj.top + clipObj.height / 2) + clipObj.points[i].y - this.oCoords.tl.y
          });

      ctx.moveTo(points[0].x, points[0].y);
      for(i=1; i<points.length; ++i)
      {
        ctx.lineTo(points[i].x, points[i].y);
      }
      ctx.lineTo(points[0].x, points[0].y);
  }
  // rectangle cliping area
  else
  {
      ctx.rect(
          clipObj.left - this.oCoords.tl.x,
          clipObj.top - this.oCoords.tl.y,
          clipObj.width,
          clipObj.height
    );
  }

  ctx.closePath();

  ctx.restore();

}


Solution

  • You should specify that this happens when the paths are scaled. The offset comes from the use of strokeWidth in calculating ctxLeft and ctxTop.

    This is a simpler clipByName function that does not need to do lot of math to draw the clipping area and remove the need of calling .setCoords() and using .oCoords

    https://jsfiddle.net/hncqbhah/2/

    var clipByName = function (ctx) {
    
    var clipObj = findByClipName(this.clipName);
    
    ctx.save();
    ctx.setTransform(1,0,0,1,0,0);
    
    ctx.beginPath();
    
    var isPolygon = clipObj instanceof fabric.Polygon;
    // polygon cliping area
    if(isPolygon)
    {
        // prepare points of polygon
        var points = [];
        for(i in clipObj.points)
            points.push({
                x: (clipObj.left + clipObj.width / 2) + clipObj.points[i].x ,
                y: (clipObj.top + clipObj.height / 2) + clipObj.points[i].y
            });
    
        ctx.moveTo(points[0].x, points[0].y);
        for(i=1; i<points.length; ++i)
        {
            ctx.lineTo(points[i].x, points[i].y);
        }
        ctx.lineTo(points[0].x, points[0].y);
    }
    // rectangle cliping area
    else
    {
        ctx.rect(
            clipObj.left,
            clipObj.top,
            clipObj.width,
            clipObj.height
        );
    }
    
    ctx.closePath();
    
    ctx.restore();
    

    }

    Also to bind clipTo function there is no need to io loDash:

      obj.set({
        clipName: 'clip2',
        clipTo: clipByName
      }
    

    This is enough.