Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fabricjs - selection only via border

Tags:

fabricjs

I'm using Fabric.js to draw some rectangles on a canvas. The default behavior is that clicking inside a rectangle selects it. How can I change the behavior such that it is only selected when clicking on the border of the rectangle?

Clicking inside the rectangle but not on the border should do nothing.

You can see this behavior by drawing a rectangle on a TradingView.com chart

It there an option for this in fabric, and if not how could I go around implementing it?

like image 513
parliament Avatar asked Feb 10 '20 03:02

parliament


3 Answers

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>
like image 191
melchiar Avatar answered Oct 13 '22 13:10

melchiar


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>
like image 42
shkaper Avatar answered Oct 13 '22 14:10

shkaper


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.

like image 3
user969068 Avatar answered Oct 13 '22 12:10

user969068