Search code examples
javascriptfabricjs

Standard method of getting containing box?


OK, if I have the following shapes that are rotated and then selected you will see their bounding boxes: enter image description here

I am trying to write some code to align objects with respect to each other. So I would like to get each object's "containing box".

I am aware of getBoundingRect but, for the above shapes this gives me the following: enter image description here

As such, these boxes are not that useful to me. Is there a standard method of getting what I would call the "containing boxes" for all shapes? For example, I would like to be able to have the following boxes returned to me: enter image description here

So, for any given shape I would like to be able get the red bounding rectangle (with no rotation).

Obviously, I could write a routine for each possible shape within fabricJS but I would prefer not to reinvent the wheel! Any ideas?

Edit Here's an interactive snippet that shows the current bounding boxes (in red):

$(function () 
{
    canvas = new fabric.Canvas('c');

    canvas.add(new fabric.Triangle({
      left: 50,
      top: 50,
      fill: '#FF0000',
      width: 50,
      height: 50,
      angle : 30
    }));

    canvas.add(new fabric.Circle({
      left: 250,
      top: 50,
      fill: '#00ff00',
      radius: 50,
      angle : 30
    }));

    canvas.add(new fabric.Polygon([
      {x: 185, y: 0},
      {x: 250, y: 100},
      {x: 385, y: 170},
      {x: 0, y: 245} ], {
        left: 450,
        top: 50,
        fill: '#0000ff',
        angle : 30
      }));

    canvas.on("after:render", function(opt) 
    { 
        canvas.contextContainer.strokeStyle = '#FF0000';
        canvas.forEachObject(function(obj) 
        {
            var bound = obj.getBoundingRect();

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

    canvas.renderAll();
});
<script src="//cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.6/fabric.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="c" width="800" height="600"></canvas><br/>


Solution

  • I had the same issue and I found a workaround. I created a temporary SVG from the active object and placed it outside the viewport. Then I measured the real bounding box using the native getBBox() function.

    UPDATE

    Apparently, the solution above works only in Firefox (76), so I came up with a different solution. Since I couldn't find a properly working, native function to get the real bounding box of a shape, I decided to scan the pixels and retrieve the boundaries from there.

    Fiddle: https://jsfiddle.net/divpusher/2m7c61gw/118/

    How it works:

    • export the selected fabric object toDataURL()
    • place it in a hidden canvas and get the pixels with getImageData()
    • scan the pixels to retrieve x1, x2, y1, y2 edge coords of the shape

    Demo below

    // ---------------------------
    // the magic
    
    var tempCanv, ctx, w, h;
    
    function getImageData(dataUrl) { 
      // we need to use a temp canvas to get imagedata
      if (tempCanv == null) {
        tempCanv = document.createElement('canvas');
        tempCanv.style.border = '1px solid blue';
        tempCanv.style.visibility = 'hidden';
        ctx = tempCanv.getContext('2d');
      	document.body.appendChild(tempCanv);
    	}
        
    	return new Promise(function(resolve, reject) {
      	if (dataUrl == null) return reject();    
        
        var image = new Image();
        image.addEventListener('load', function() {
          w = image.width;
          h = image.height;
          tempCanv.width = w;
          tempCanv.height = h;
          ctx.drawImage(image, 0, 0, w, h);
          var imageData = ctx.getImageData(0, 0, w, h).data.buffer;         
    			resolve(imageData, false);      
        });
        image.src = dataUrl;
    	});
      
    }
    
    function scanPixels(imageData) {
    	var data = new Uint32Array(imageData),
          len = data.length,
          x, y, y1, y2, x1 = w, x2 = 0;
      
      // y1
      for(y = 0; y < h; y++) {
        for(x = 0; x < w; x++) {
          if (data[y * w + x] & 0xff000000) {
            y1 = y;
            y = h;
            break;
          }
        }
      }
      
      // y2
      for(y = h - 1; y > y1; y--) {
        for(x = 0; x < w; x++) {
          if (data[y * w + x] & 0xff000000) {
            y2 = y;
            y = 0;
            break;
          }
        }
      }
    
      // x1
      for(y = y1; y < y2; y++) {
        for(x = 0; x < w; x++) {
          if (x < x1 && data[y * w + x] & 0xff000000) {
            x1 = x;
            break;
          }
        }
      }
    
      // x2
      for(y = y1; y < y2; y++) {
        for(x = w - 1; x > x1; x--) {
          if (x > x2 && data[y * w + x] & 0xff000000) {
            x2 = x;
            break;
          }
        }
      }
      
      return {
      	x1: x1,
        x2: x2,
        y1: y1,
        y2: y2
      }
    }
    
    
    
    // ---------------------------
    // align buttons
    
    function alignLeft(){
    	var obj = canvas.getActiveObject();
      obj.set('left', 0);
      obj.setCoords();
      canvas.renderAll();
    }
    
    
    function alignLeftbyBoundRect(){
    	var obj = canvas.getActiveObject();
      var bound = obj.getBoundingRect();
      obj.set('left', (obj.left - bound.left));
      obj.setCoords();
      canvas.renderAll();
    }
    
    function alignRealLeft(){
    	var obj = canvas.getActiveObject();
      getImageData(obj.toDataURL())
      	.then(function(data) {    
        	var bound = obj.getBoundingRect();
        	var realBound = scanPixels(data);  
          obj.set('left', (obj.left - bound.left - realBound.x1));      
          obj.setCoords();
          canvas.renderAll(); 
        });
    }
    
    
    // ---------------------------
    // set up canvas
    
    var canvas = new fabric.Canvas('c');
    
    var path = new fabric.Path('M 0 0 L 150 50 L 120 150 z');
    path.set({
      left: 170,
      top: 30,
      fill: 'rgba(0, 128, 0, 0.5)',
      stroke: '#000',
      strokeWidth: 4,
      strokeLineCap: 'square',
      angle: 65
    });
    canvas.add(path);
    canvas.setActiveObject(path);
    
    var circle = new fabric.Circle({
      left: 370,
      top: 30,
      radius: 45,
      fill: 'blue',
      scaleX: 1.5,
      angle: 30
    });
    canvas.add(circle);
    
    
    canvas.forEachObject(function(obj) {
      var setCoords = obj.setCoords.bind(obj);
      obj.on({
        moving: setCoords,
        scaling: setCoords,
        rotating: setCoords
      });
    });
    
    
    canvas.on('after:render', function() {
    	canvas.contextContainer.strokeStyle = 'red';
      
    	canvas.forEachObject(function(obj) {
          
    		getImageData(obj.toDataURL())
      		.then(function(data) {    	
            var boundRect = obj.getBoundingRect();
            var realBound = scanPixels(data);                
            canvas.contextContainer.strokeRect(
    					boundRect.left + realBound.x1, 
            	boundRect.top + realBound.y1, 
            	realBound.x2 - realBound.x1, 
            	realBound.y2 - realBound.y1
            );    
        });           
    
    	});
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.3/fabric.min.js"></script>
    
    <p>&nbsp;</p>
    <button onclick="alignLeft()">align left (default)</button>&nbsp;&nbsp;&nbsp;
    <button onclick="alignLeftbyBoundRect()">align left (by bounding rect)</button>&nbsp;&nbsp;&nbsp;
    <button onclick="alignRealLeft()">align REAL left (by pixel)</button>&nbsp;&nbsp;&nbsp;
    <p></p>
    <canvas id="c" width="600" height="250" style="border: 1px solid rgb(204, 204, 204); touch-action: none; user-select: none;" class="lower-canvas"></canvas>
    <p></p>