9

I have been trying to keep an object (constructed in fabric js over a canvas) inside the boundaries at all the times. It has been achieved at moving and rotating it. I took help from Move object within canvas boundary limit for achieving this. But when I start to scale the object, it simply keeps on going out of boundary. I do not understand what has to be done to keep it inside the boundary only, even while scaling. Please help me with a code to prevent this behavior. It would be great if you can attach a demo too.

    <html>
<head>
    <title>Basic usage</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.3/fabric.min.js"></script>

</head>
<body>
<canvas id="canvas" style= "border: 1px solid black" height= 480 width = 360></canvas>
<script>
 var canvas = new fabric.Canvas('canvas');
  canvas.add(new fabric.Circle({ radius: 30, fill: '#f55', top: 100, left: 100 }));

  canvas.item(0).set({
    borderColor: 'gray',
    cornerColor: 'black',
    cornerSize: 12,
    transparentCorners: true
  });
  canvas.setActiveObject(canvas.item(0));
  canvas.renderAll();


  canvas.on('object:moving', function (e) {
        var obj = e.target;
         // if object is too big ignore
        if(obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width){
            return;
        }        
        obj.setCoords();        
        // top-left  corner
        if(obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0){
            obj.top = Math.max(obj.top, obj.top-obj.getBoundingRect().top);
            obj.left = Math.max(obj.left, obj.left-obj.getBoundingRect().left);
        }
        // bot-right corner
        if(obj.getBoundingRect().top+obj.getBoundingRect().height  > obj.canvas.height || obj.getBoundingRect().left+obj.getBoundingRect().width  > obj.canvas.width){
            obj.top = Math.min(obj.top, obj.canvas.height-obj.getBoundingRect().height+obj.top-obj.getBoundingRect().top);
            obj.left = Math.min(obj.left, obj.canvas.width-obj.getBoundingRect().width+obj.left-obj.getBoundingRect().left);
        }
});

</script>
</body>
</html>

My demo is attached here. : https://jsfiddle.net/3v0cLaLk/

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Ankit Joshi
  • 183
  • 1
  • 3
  • 9
  • Did you look at the solution below "Move object within canvas boundary limit", http://stackoverflow.com/a/36011859/3389046? – Tim Harker Mar 16 '17 at 11:59
  • Yes Tim, I tried this solution but this doesn't seem to work. Once your object is stretched out of the boundaries, it goes out of control. – Ankit Joshi Mar 16 '17 at 12:28
  • Can you explain further what you mean by, "it goes out of control"? Do you mean it goes out of the canvas??? – Tim Harker Mar 16 '17 at 13:04
  • Thank you Tim for your replies. Yes. It just keeps on extending and then if you leave the mouse, you can't see the end points to get it back in. Try that in the fiddle. Keep one side touching the boundary and increase the size from other end. Let it go out of boundary. Probably then you will understand what happens. – Ankit Joshi Mar 17 '17 at 04:49
  • @AnkitJoshi why do you need `Math.max`? Isn't for example: `obj.top = obj.top - obj.getBoudingRect().top` enough. I could not find a case, where `Math.max(obj.top, obj.top-obj.getBoundingRect().top);` obj.top would be taken as `obj.getBoundingRect().top is always negative` – broadband Feb 28 '20 at 13:11
  • @broaband obj.top would give me the top most point of the shape. On the other hand obj.getBoundingRect().top would always give me the coordinates of the point below the rotating handle. Imagine a case where I rotate the shape full 180 degrees. Then the boundingRect.top would come below. Hence, I am checking the max of both of these. – Ankit Joshi Mar 04 '20 at 06:25

6 Answers6

20

I was able to solve the problem as follows:

var canvas = new fabric.Canvas('canvas');
  canvas.add(new fabric.Circle({ radius: 30, fill: '#f55', top: 100, left: 100 }));

  canvas.item(0).set({
    borderColor: 'gray',
    cornerColor: 'black',
    cornerSize: 12,
    transparentCorners: true
  });
  canvas.setActiveObject(canvas.item(0));
  canvas.renderAll();


  canvas.on('object:moving', function (e) {
        var obj = e.target;
         // if object is too big ignore
        if(obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width){
            return;
        }        
        obj.setCoords();        
        // top-left  corner
        if(obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0){
            obj.top = Math.max(obj.top, obj.top-obj.getBoundingRect().top);
            obj.left = Math.max(obj.left, obj.left-obj.getBoundingRect().left);
        }
        // bot-right corner
        if(obj.getBoundingRect().top+obj.getBoundingRect().height  > obj.canvas.height || obj.getBoundingRect().left+obj.getBoundingRect().width  > obj.canvas.width){
            obj.top = Math.min(obj.top, obj.canvas.height-obj.getBoundingRect().height+obj.top-obj.getBoundingRect().top);
            obj.left = Math.min(obj.left, obj.canvas.width-obj.getBoundingRect().width+obj.left-obj.getBoundingRect().left);
        }
});

    var left1 = 0;
    var top1 = 0 ;
    var scale1x = 0 ;    
    var scale1y = 0 ;    
    var width1 = 0 ;    
    var height1 = 0 ;
  canvas.on('object:scaling', function (e){
    var obj = e.target;
    obj.setCoords();
    var brNew = obj.getBoundingRect();
    
    if (((brNew.width+brNew.left)>=obj.canvas.width) || ((brNew.height+brNew.top)>=obj.canvas.height) || ((brNew.left<0) || (brNew.top<0))) {
    obj.left = left1;
    obj.top=top1;
    obj.scaleX=scale1x;
    obj.scaleY=scale1y;
    obj.width=width1;
    obj.height=height1;
  }
    else{    
      left1 =obj.left;
      top1 =obj.top;
      scale1x = obj.scaleX;
      scale1y=obj.scaleY;
      width1=obj.width;
      height1=obj.height;
    }
 });
<html>
<head>
    <title>Basic usage</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.3/fabric.min.js"></script>

</head>
<body>
<canvas id="canvas" style= "border: 1px solid black" height= 480 width = 360></canvas>
</body>
</html>
Pedro Paulo
  • 201
  • 2
  • 3
19

You can set on object modified listener and check if object is out of bounds. If so, then restore it to its original state.

this.canvas.on('object:modified', function (options: any) {
    let obj = options.target;
    let boundingRect = obj.getBoundingRect(true);
    if (boundingRect.left < 0
        || boundingRect.top < 0
        || boundingRect.left + boundingRect.width > scope.canvas.getWidth()
        || boundingRect.top + boundingRect.height > scope.canvas.getHeight()) {
        obj.top = obj._stateProperties.top;
        obj.left = obj._stateProperties.left;
        obj.angle = obj._stateProperties.angle;
        obj.scaleX = obj._stateProperties.scaleX;
        obj.scaleY = obj._stateProperties.scaleY;
        obj.setCoords();
        obj.saveState();
    }
});
Milan Hlinák
  • 4,260
  • 1
  • 30
  • 41
  • Thanks. This worked with a few minute changes for my use. I appreciate your help. Thanks man, – Ankit Joshi Mar 27 '17 at 19:24
  • 1
    Should be noticed that the canvas should be build with stateful = true option, otherwise there won't be _stateProperties property in object – Victor Apr 26 '18 at 20:30
  • I wish to do it at runtime instead of checking it at the last how do I achieve this? with the existing code of yours as mentioned in the answer? – Sundeep Pidugu Oct 09 '19 at 12:23
7

If you want to perform a real time prevention, you should use object:scaling event, as object:modified is only triggered at the end of the transformation.

1) Add event handler to canvas:

this.canvas.on('object:scaling', (e) => this._handleScaling(e));

2) In the handler function, get the old and the new object's bounding rect:

_handleScaling(e) {
  var obj = e.target;
  var brOld = obj.getBoundingRect();
  obj.setCoords();
  var brNew = obj.getBoundingRect();

3) For each border, check if object has scaled beyond the canvas boundaries and compute its left, top and scale properties:

  // left border
  // 1. compute the scale that sets obj.left equal 0
  // 2. compute height if the same scale is applied to Y (we do not allow non-uniform scaling)
  // 3. compute obj.top based on new height
  if(brOld.left >= 0 && brNew.left < 0) {
    let scale = (brOld.width + brOld.left) / obj.width;
    let height = obj.height * scale;
    let top = ((brNew.top - brOld.top) / (brNew.height - brOld.height) *
      (height - brOld.height)) + brOld.top;
    this._setScalingProperties(0, top, scale);
  } 

4) Similar code for the other borders:

  // top border
  if(brOld.top >= 0 && brNew.top < 0) {
    let scale = (brOld.height + brOld.top) / obj.height;
    let width = obj.width * scale;
    let left = ((brNew.left - brOld.left) / (brNew.width - brOld.width) * 
      (width - brOld.width)) + brOld.left;
    this._setScalingProperties(left, 0, scale);
  }
  // right border
  if(brOld.left + brOld.width <= obj.canvas.width 
  && brNew.left + brNew.width > obj.canvas.width) {
    let scale = (obj.canvas.width - brOld.left) / obj.width;
    let height = obj.height * scale;
    let top = ((brNew.top - brOld.top) / (brNew.height - brOld.height) * 
      (height - brOld.height)) + brOld.top;
    this._setScalingProperties(brNew.left, top, scale);
  }
  // bottom border
  if(brOld.top + brOld.height <= obj.canvas.height 
  && brNew.top + brNew.height > obj.canvas.height) {
    let scale = (obj.canvas.height - brOld.top) / obj.height;
    let width = obj.width * scale;
    let left = ((brNew.left - brOld.left) / (brNew.width - brOld.width) * 
      (width - brOld.width)) + brOld.left;
    this._setScalingProperties(left, brNew.top, scale);
  }

5) If object's BoundingRect has crossed canvas boundaries, fix its position and scale:

  if(brNew.left < 0
  || brNew.top < 0
  || brNew.left + brNew.width > obj.canvas.width
  || brNew.top + brNew.height > obj.canvas.height) {
    obj.left = this.scalingProperties['left'];
    obj.top = this.scalingProperties['top'];
    obj.scaleX = this.scalingProperties['scale'];
    obj.scaleY = this.scalingProperties['scale'];
    obj.setCoords();
  } else {
    this.scalingProperties = null;
  }
}

6) Finally, when setting the scaling properties, we have to stick with the smallest scale in case the object has crossed more than one border:

_setScalingProperties(left, top, scale) {
  if(this.scalingProperties == null 
  || this.scalingProperties['scale'] > scale) {
    this.scalingProperties = {
      'left': left,
      'top': top,
      'scale': scale
    };
  }
}
William Dias
  • 339
  • 2
  • 6
  • Long but gold. Thank you! – KitKit Jun 04 '18 at 11:56
  • when i implemented it so it generate error "Property 'scalingProperties' does not exis" @William Dias – kunal shaktawat Mar 07 '19 at 10:09
  • @kunalshaktawat, the API may have changed since I posted the answer. Check the documentation to use the correct methods and properties. – William Dias Mar 08 '19 at 19:32
  • When object rotate at that time not working with this code otherwise it's working fine. Can you please help with rotate object scaling restriction. @WilliamDias – Aman Gojariya May 02 '19 at 06:43
  • 1
    @AmanGojariya, when rotating, you'll have to create a handler for the rotating event. Probably, the event data is different from the one provided by the scaling event. You have to adapt the code to your needs. – William Dias May 06 '19 at 20:18
  • @Marcel, this code was not tested on newer versions of Fabric.js. Maybe that's the reason why you are facing this problem. – William Dias May 06 '21 at 17:13
  • Does this apply to irregular outer shape and prevent inner shape from going outside or only for outer shape's bounding box only? – Shashank Bhatt Jun 05 '23 at 22:03
0

Below is the code for blocking the coordinates of any object outside the canvas area from all directions

canvas.on('object:modified', function (data) {
var currentObject = data.target;
var tempObject = angular.copy(data.target);
var canvasMaxWidth = canvas.width - 20,
    canvasMaxHeight = canvas.height - 20;
    var actualWidth = currentObject.getBoundingRect().width,
    actualHeight = currentObject.getBoundingRect().height;
if (actualHeight > canvasMaxHeight) {
    currentObject.scaleToHeight(canvasMaxHeight);
    currentObject.setCoords();
    canvas.renderAll();
    if (tempObject.scaleX < currentObject.scaleX) {
        currentObject.scaleX = tempObject.scaleX;
        currentObject.setCoords();
        canvas.renderAll();
    }
    if (tempObject.scaleY < currentObject.scaleY) {
        currentObject.scaleY = tempObject.scaleY;
        currentObject.setCoords();
        canvas.renderAll();
    }
        if (currentObject.getBoundingRectHeight() < canvasMaxHeight - 50) {
            currentObject.scaleX = (currentObject.scaleX * canvasMaxHeight) / (currentObject.scaleX * currentObject.width);
            currentObject.setCoords();
            canvas.renderAll();
        }

}
if (actualWidth > canvasMaxWidth) {
    currentObject.scaleToWidth(canvasMaxWidth);
    obj.setCoords();
    canvas.renderAll();
    if (tempObject.scaleX < currentObject.scaleX) {
        currentObject.scaleX = tempObject.scaleX;
        currentObject.setCoords();
        canvas.renderAll();
    }
    if (tempObject.scaleY < currentObject.scaleY) {
        currentObject.scaleY = tempObject.scaleY;
        currentObject.setCoords();
        canvas.renderAll();
    }
}
obj.setCoords();
canvas.renderAll();
});
0

I was able to block movement outside of boundaries using the Bounding box in the following way using the last version of Fabric ("fabric": "^4.6.0") & Typescript:

private boundingBox: fabric.Rect = null;

this.setBoundingBox(width, height);

private setBoundingBox(width: number, height: number) {
        this.boundingBox = new fabric.Rect({
            name: OBJECT_TYPE.BOUNDING_BOX,
            fill: DEFINITIONS.BG_COLOR,
            width: width,
            height: height,
            hasBorders: false,
            hasControls: false,
            lockMovementX: true,
            lockMovementY: true,
            selectable: false,
            evented: false,
            stroke: 'red',
        });
        this._canvas.add(this.boundingBox);
    }

this._canvas.on('object:moving', (e) => {
            console.log('object:moving');
            this._avoidObjectMovingOutsideOfBoundaries(e);
        });

private _avoidObjectMovingOutsideOfBoundaries(e: IEvent) {
        let obj = e.target;
        const top = obj.top;
        const bottom = top + obj.height;
        const left = obj.left;
        const right = left + obj.width;

        const topBound = this.boundingBox.top;
        const bottomBound = topBound + this.boundingBox.height;
        const leftBound = this.boundingBox.left;
        const rightBound = leftBound + this.boundingBox.width;

        obj.left = Math.min(Math.max(left, leftBound), rightBound - obj.width);
        obj.top = Math.min(Math.max(top, topBound), bottomBound - obj.height);

        return obj;
    }

Any additional extensions for Scaling objects are welcome.

redrom
  • 11,502
  • 31
  • 157
  • 264
0
 canvas.on('object:scaling', function (e) {
      var obj = e.target;
      obj.setCoords();
      let top = obj.getBoundingRect().top;
      let left = obj.getBoundingRect().left;
      let height = obj.getBoundingRect().height;
      let width = obj.getBoundingRect().width;

      // restrict scaling below bottom of canvas
      if (top + height > CANVAS_HEIGHT) {
        obj.scaleY = 1;
        obj.setCoords();
        let h = obj.getScaledHeight();

        obj.scaleY = (CANVAS_HEIGHT - top) / h;
        obj.setCoords();
        canvas.renderAll();

        obj.lockScalingX = true;
        obj.lockScalingY = true;
        obj.lockMovementX = true;
        obj.lockMovementY = true;
      }

      // restrict scaling above top of canvas
      if (top < 0) {
        obj.scaleY = 1;
        obj.setCoords();
        let h = obj.getScaledHeight();
        obj.scaleY = (height + top) / h;
        obj.top = 0;
        obj.setCoords();
        canvas.renderAll();

        obj.lockScalingX = true;
        obj.lockScalingY = true;
        obj.lockMovementX = true;
        obj.lockMovementY = true;
      }

      // restrict scaling over right of canvas
      if (left + width > CANVAS_WIDTH) {
        obj.scaleX = 1;
        obj.setCoords();
        let w = obj.getScaledWidth();

        obj.scaleX = (CANVAS_WIDTH - left) / w;
        obj.setCoords();
        canvas.renderAll();

        obj.lockScalingX = true;
        obj.lockScalingY = true;
        obj.lockMovementX = true;
        obj.lockMovementY = true;
      }

      // restrict scaling over left of canvas
      if (left < 0) {
        obj.scaleX = 1;
        obj.setCoords();
        let w = obj.getScaledWidth();
        obj.scaleX = (width + left) / w;
        obj.left = 0;
        obj.setCoords();
        canvas.renderAll();
        obj.lockScalingX = true;
        obj.lockScalingY = true;
        obj.lockMovementX = true;
        obj.lockMovementY = true;
      }
    });

 canvas.on('object:modified', function (event) {
      // after text object is done with modifing e.g. resizing or moving
      if (!!event.target) {
        event.target.lockScalingX = false;
        event.target.lockScalingY = false;
        event.target.lockMovementX = false;
        event.target.lockMovementY = false;
      }
 })