Fabricjs - selection only via border

Fabric.js uses Object.containsPoint() to determine whether a mouse event should target the object. This method, in turn, calculates the object's edges via Object._getImageLines() and checks how many times the projection of a mouse pointer crossed those lines.

The solution below calculates additional inner edges based on the coordinates of each corner, therefore object scale and rotation are taken care of automatically.

const canvas = new fabric.Canvas('c', {
  enableRetinaScaling: true
})

const rect = new fabric.Rect({
  left: 0,
  top: 0,
  width: 100,
  height: 100,
  dragBorderWidth: 15, // this is the custom attribute we've introduced
})

function innerCornerPoint(start, end, offset) {
  // vector length
  const l = start.distanceFrom(end)
  // unit vector
  const uv = new fabric.Point((end.x - start.x) / l, (end.y - start.y) / l)
  // point on the vector at a given offset but no further than side length
  const p = start.add(uv.multiply(Math.min(offset, l)))
  // rotate point
  return fabric.util.rotatePoint(p, start, fabric.util.degreesToRadians(45))
}

rect._getInnerBorderLines = function(c) {
  // the actual offset from outer corner is the length of a hypotenuse of a right triangle with border widths as 2 sides
  const offset = Math.sqrt(2 * (this.dragBorderWidth ** 2))
  // find 4 inner corners as offsets rotated 45 degrees CW
  const newCoords = {
    tl: innerCornerPoint(c.tl, c.tr, offset),
    tr: innerCornerPoint(c.tr, c.br, offset),
    br: innerCornerPoint(c.br, c.bl, offset),
    bl: innerCornerPoint(c.bl, c.tl, offset),
  }
  return this._getImageLines(newCoords)
}

rect.containsPoint = function(point, lines, absolute, calculate) {
  const coords = calculate ? this.calcCoords(absolute) : absolute ? this.aCoords : this.oCoords
  lines = lines || this._getImageLines(coords)
  const innerRectPoints = this._findCrossPoints(point, lines);
  const innerBorderPoints = this._findCrossPoints(point, this._getInnerBorderLines(coords))
  // calculate intersections
  return innerRectPoints === 1 && innerBorderPoints !== 1
}

canvas.add(rect)
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<canvas id="c" width="400" height="300"></canvas>


here is my approach, when rect is clicked I am calculating where it is clicked and if it is not clicked on border I have to set canvas.discardActiveObject , see comments on code

var canvas = new fabric.Canvas('c', {
  selection: false
});
var rect = new fabric.Rect({
  left: 50,
  top: 50,
  width: 100,
  height: 100,
  strokeWidth: 10,
  stroke: 'red',
  selectable: false,
  evented: true,
  hasBorders: true,
  lockMovementY: true,
  lockMovementX: true

})
canvas.on("mouse:move", function(e) {
  if (!e.target || e.target.type != 'rect') return;
  // when selected event is fired get the click position.
  var pointer = canvas.getPointer(e.e);
  // calculate the click distance from object to be exact
  var distanceX = pointer.x - rect.left;
  var distanceY = pointer.y - rect.top;

  // check if click distanceX/Y are less than 10 (strokeWidth) or greater than 90 ( rect width = 100)


  if ((distanceX <= rect.strokeWidth || distanceX >= (rect.width - rect.strokeWidth)) || (distanceY <= rect.strokeWidth || distanceY >= (rect.height - rect.strokeWidth))) {
    rect.set({
      hoverCursor: 'move',
      selectable: true,
      lockMovementY: false,
      lockMovementX: false
    });
    document.getElementById('result').innerHTML = 'on border';
  } else {
    canvas.discardActiveObject();
    document.getElementById('result').innerHTML = 'not  on border';
    rect.set({
      hoverCursor: 'default',
      selectable: false,
      lockMovementY: true,
      lockMovementX: true
    });
  }

});

canvas.add(rect);
canvas.renderAll();
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<div id="result" style="width: 100%; "></div>
<canvas id="c" width="600" height="200"></canvas>
<pre>
</pre>

ps: you can also set the rect property to selectable: false and call canvas.setActiveObject(this); to make it selection inside if statement.


This approach overrides the _checkTarget method within FabricJS to reject clicks that are more than a specified distance from the border (defined by the clickableMargin variable).

//sets the width of clickable area
var clickableMargin = 15;

var canvas = new fabric.Canvas("canvas");

canvas.add(new fabric.Rect({
  width: 150,
  height: 150,
  left: 25,
  top: 25,
  fill: 'green',
  strokeWidth: 0
}));

//overrides the _checkTarget method to add check if point is close to the border
fabric.Canvas.prototype._checkTarget = function(pointer, obj, globalPointer) {
  if (obj &&
      obj.visible &&
      obj.evented &&
      this.containsPoint(null, obj, pointer)){
    if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) {
      var isTransparent = this.isTargetTransparent(obj, globalPointer.x, globalPointer.y);
      if (!isTransparent) {
        return true;
      }
    }
    else {
    	var isInsideBorder = this.isInsideBorder(obj);
    	if(!isInsideBorder) {
      	return true;
      }
    }
  }
}

fabric.Canvas.prototype.isInsideBorder = function(target) {
   var pointerCoords = target.getLocalPointer();
   if(pointerCoords.x > clickableMargin && 
     pointerCoords.x < target.getScaledWidth() - clickableMargin && 
     pointerCoords.y > clickableMargin && 
     pointerCoords.y < target.getScaledHeight() - clickableMargin) {
     return true;
   }
 }
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<canvas id="canvas" height="300" width="400"></canvas>

Tags:

Fabricjs