4

How can an object be constrained to being resized/scaled only within the bounds of the canvas (or another object in my case) when using Fabric.js?

Currently, I'm handling the object:scaling event:

// Typescript being used; 

onObjectScaling = (event): boolean => {
    // Prevent scaling object outside of image

    var object = <fabric.IObject>event.target;
    var objectBounds: BoundsHelper;
    var imageBounds: BoundsHelper;

    object.setCoords();
    objectBounds = new BoundsHelper(object);
    imageBounds = new BoundsHelper(this.imageManipulatorContext.getImageObject());

    if (objectBounds.left < imageBounds.left || objectBounds.right > imageBounds.right) {
        console.log('horizontal bounds exceeded');
        object.scaleX = this.selectedObjectLastScaleX;
        object.lockScalingX = true;
        object.setCoords();
    } else {
        this.selectedObjectLastScaleX = object.scaleX;
    }

    if (objectBounds.top < imageBounds.top || objectBounds.bottom > imageBounds.bottom) {
        console.log('vertical bounds exceeded');
        object.scaleY = this.selectedObjectLastScaleY;
        object.lockScalingY = true;
        object.setCoords();
    } else {
        this.selectedObjectLastScaleY = object.scaleY;
    }

    return true;
}

**edit The BoundsHelper class is just a helper for wrapping up the math of calculating the right/bottom sides of the bounding box for an object.

import fabric = require('fabric');

class BoundsHelper {
    private assetRect: { left: number; top: number; width: number; height: number; };

    get left(): number { return this.assetRect.left; }
    get top(): number { return this.assetRect.top; }
    get right(): number { return this.assetRect.left + this.assetRect.width; }
    get bottom(): number { return this.assetRect.top + this.assetRect.height; }

    get height(): number { return this.assetRect.height; }
    get width(): number { return this.assetRect.width; }

    constructor(asset: fabric.IObject) {
        this.assetRect = asset.getBoundingRect();
    }
}

export = BoundsHelper;

I also make use of the onBeforeScaleRotate callback to disable the scaling lock added by the above:

onObjectBeforeScaleRotate = (targetObject: fabric.IObject): boolean => {
    targetObject.lockScalingX = targetObject.lockScalingY = false;

    return true;
}

The issue I observe is that it seems like Fabric doesn't redraw the object fast enough for me to accurately detect that scale is passing the image's boundary. In other words, if I scale slowly, then life is good; if I scale quickly, then I can scale outside of the image's bounds.

Kenneth K.
  • 2,987
  • 1
  • 23
  • 30
  • can you show your BoundsHelper code and can you explain how much is different your object from the canvas? more details are needed to understand where your approach (that looks correct) is failing – AndreaBogazzi Jan 07 '16 at 09:52
  • @AndreaBogazzi Added, but as I mentioned, all it does is wrap the calculation for determining `right` and `bottom`. – Kenneth K. Jan 07 '16 at 15:53
  • getBoundingRect is calling setCoords internally. So you can safely remove some of the calls. I would like to investigate this issue as i'm a fabricjs contributor, would it possible for you to make a fiddle? i add also, is the method object.isContainedWithinObject(otherObject) not enough for you? – AndreaBogazzi Jan 07 '16 at 16:36
  • I've created the following Fiddle that recreates the behavior. I've stripped out the Typescript bits, so it's only JS now: https://jsfiddle.net/84zovnek/ – Kenneth K. Jan 07 '16 at 23:38
  • @AndreaBogazzi Unless I misunderstand its use, `isContainedWithinObject` would only tell me that an object has left the bounds of another object, but I need to know which side(s) has left the bounds so that I can restrict the interior object from leaving the bounds on any given side. In other words, the outer object--an image in this case--will act like a bounding box for the interior object, no matter what kind of transform is applied. – Kenneth K. Jan 07 '16 at 23:40
  • Is the Fiddle sufficient in demonstrating the problem? – Kenneth K. Jan 19 '16 at 17:55
  • i spent some time over it, then i dropped. i update the answer. – AndreaBogazzi Jan 19 '16 at 20:49
  • @KennethK. The fiddle doesn't allow me to resize the black box at all, it seems it's locking right away. Is this the issue? From your question, the issue seemed to be something else. – otterslide Apr 13 '16 at 23:09
  • @otterslide Drag the box into the image, then try. – Kenneth K. Apr 14 '16 at 00:19
  • Wasted many time to looking the decision. And this helped me: [link] (https://jsfiddle.net/hq7gc0kd/) – Roman A. May 14 '16 at 17:14

1 Answers1

3

It is not about speed. Events are discrete sampling of something you are doing with the mouse. Even if you move pixel by pixel virtually, the mouse has its own sample rate and the javascript event do not fire every pixel you move when you go fast.

So if you limit your application to stop scaling when you overcome a limit, when you overcome this limit you will stop your scaling, some pixel after the limit, simply because the last check you were 1 pixel before bound, the event after you are 10 pixel after it.

I changed the code, that is not perfect at all, but gives you idea for dealing with the issue.

When you overcome the limit, instead of lock scaling, calculate the correct scaling to be inside the image and apply that scale.

This logic has problems when you are completely outside the image, so i placed the rect already in, just to demonstrate the different approach.

var BoundsHelper = (function () {
  function BoundsHelper(asset) {
    this.assetRect = asset.getBoundingRect();
  }
  Object.defineProperty(BoundsHelper.prototype, "left", {
    get: function () {
      return this.assetRect.left;
    },
    enumerable: true,
    configurable: true
  });
  Object.defineProperty(BoundsHelper.prototype, "top", {
    get: function () {
      return this.assetRect.top;
    },
    enumerable: true,
    configurable: true
  });
  Object.defineProperty(BoundsHelper.prototype, "right", {
    get: function () {
      return this.assetRect.left + this.assetRect.width;
    },
    enumerable: true,
    configurable: true
  });
  Object.defineProperty(BoundsHelper.prototype, "bottom", {
    get: function () {
      return this.assetRect.top + this.assetRect.height;
    },
    enumerable: true,
    configurable: true
  });

  Object.defineProperty(BoundsHelper.prototype, "height", {
    get: function () {
      return this.assetRect.height;
    },
    enumerable: true,
    configurable: true
  });
  Object.defineProperty(BoundsHelper.prototype, "width", {
    get: function () {
      return this.assetRect.width;
    },
    enumerable: true,
    configurable: true
  });
  return BoundsHelper;
})();

////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////

var canvas = new fabric.Canvas('c');
var rectangle = new fabric.Rect({
 fill: 'black',
  originX: 'left',
  originY: 'top',
  stroke: 'false',
  opacity: 1,
  left: 180,
  top: 180,
  height: 50,
  width: 50
});
var rectangleBounds = new BoundsHelper(rectangle);
var image = new fabric.Image(i, {
 selectable: false,
  borderColor: 'black',
  width: 200,
  height: 200
});

canvas.on("object:scaling", function (event) {
  var object = event.target;
  var objectBounds = null;
  var imageBounds = null;
  var desiredLength;
  object.setCoords();
  objectBounds = new BoundsHelper(object);
  imageBounds = new BoundsHelper(image);

  if (objectBounds.left < imageBounds.left) {
    object.lockScalingX = true;
    // now i have to calculate the right scaleX factor.
    desiredLength =objectBounds.right -  imageBounds.left;
    object.scaleX = desiredLength / object.width;
  }

  if (objectBounds.right > imageBounds.right) {
   object.lockScalingX = true;
    desiredLength = imageBounds.right - objectBounds.left;
    object.scaleX = desiredLength / object.width;
  }


  if (objectBounds.top < imageBounds.top) { 
   object.lockScalingY = true;
    desiredLength = objectBounds.bottom - imageBounds.top;
    object.scaleY = desiredLength / object.height;
  }

  if (objectBounds.bottom > imageBounds.bottom) {
    object.lockScalingY = true;
    desiredLength = imageBounds.bottom - objectBounds.top;
    object.scaleY = desiredLength / object.height;
  }

return true;
});

canvas.onBeforeScaleRotate = function (targetObject) {
  targetObject.lockScalingX = targetObject.lockScalingY = false;

  return true;
};

canvas.on('after:render', function() {
  canvas.contextContainer.strokeStyle = '#555';

  var bound = image.getBoundingRect();

  canvas.contextContainer.strokeRect(
    bound.left + 0.5,
    bound.top + 0.5,
    bound.width,
    bound.height
  );
});

canvas.add(image);
canvas.centerObject(image);
image.setCoords();
canvas.add(rectangle);
canvas.renderAll();
img {
  display: none;
}

canvas {
  border: solid black 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.4.12/fabric.min.js"></script>
<img id="i" src="http://fabricjs.com/assets/ladybug.png" />
<canvas id="c" width="500" height="500"></canvas>

https://jsfiddle.net/84zovnek/2/

  if (objectBounds.left < imageBounds.left) {
    //object.lockScalingX = true;
    // now i have to calculate the right scaleX factor.
    desiredLength = imageBounds.left - objectBounds.right;
    object.scaleX = desiredLength / object.width;
  }

Other that, you should update to latest version

AndreaBogazzi
  • 14,323
  • 3
  • 38
  • 63
  • It's not that responsiveness itself is slow. Rather, it seems like if I scale slowly, Fabric fires this event quick enough that when I approach an edge, the code limits the scale. If I scale very quickly, then I am able to scale past the edge I am trying to limit to. – Kenneth K. Jan 07 '16 at 15:59
  • @Kenneth K. I want to add background image. My post is http://stackoverflow.com/questions/34949933/restrict-area-for-draw-image-object-in-canvas-html5. – Varun Sharma Jan 23 '16 at 05:40
  • Try scaling this object by anything other than bottom & right scaling tools, and it doesn't work.... not usable. – Sean Haddy Jan 31 '16 at 21:08
  • is not usable for a copy paste in a project, but it explains where is the problem and how to costrain an object inside another. anyone can fix it. – AndreaBogazzi Feb 01 '16 at 00:00