最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Standard method of getting containing box? - Stack Overflow

programmeradmin1浏览0评论

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

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:

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:

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/ajax/libs/fabric.js/1.7.6/fabric.js"></script>
<script src=".1.1/jquery.min.js"></script>
<canvas id="c" width="800" height="600"></canvas><br/>

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

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:

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:

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./ajax/libs/fabric.js/1.7.6/fabric.js"></script>
<script src="https://ajax.googleapis./ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="c" width="800" height="600"></canvas><br/>

Share Improve this question edited Mar 15, 2017 at 23:57 Lee Taylor asked Mar 15, 2017 at 2:36 Lee TaylorLee Taylor 7,99416 gold badges37 silver badges53 bronze badges 1
  • Are the actual shapes svg? – KevBot Commented Mar 15, 2017 at 2:40
Add a ment  | 

3 Answers 3

Reset to default 4

So the getBoundingBox is a method of the Object class of fabricjs. Nothing stops you from rewriting this method for each shape of which you can think of.

I ll start with circle and triangle, i'll let you imagine polygon. It gets harder and harder when shapes are paths or when circle is scaled as an ellipse.

Circle is the hardest. I sampled the circle at 30, 60, 90 degrees for all the quadrants. still is not perfect. You may need to increase sampling or find a better formula (maybe sample every 15 degrees will make the trick ).

Triangle is the easier since it has 3 points of interest.

Polygon is derived from triangle, nothing difficult here.

fabric.Circle.prototype.getBoundingRect = function() {
  var matrix = this.calcTransformMatrix();
  var points = [{x:-this.width/2, y:0}, {x:this.width/2, y:0}, {x:0, y: -this.height/2}, {x: 0, y: this.height/2}, {x: 0, y: -this.height/2}, {x: 0.433 * this.width, y: this.height/4}, {x: -0.433 * this.width, y: this.height/4}, {y: 0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: -this.width/4}, {y: 0.433 * this.height, x: -this.width/4}, {x: 0.433 * this.width, y: -this.height/4}, {x: -0.433 * this.width, y: -this.height/4}];
  points = points.map(function(p) {
     return fabric.util.transformPoint(p, matrix);
  });
  return fabric.util.makeBoundingBoxFromPoints(points);
}

fabric.Triangle.prototype.getBoundingRect = function() {
  var matrix = this.calcTransformMatrix();
  var points = [{x:-this.width/2, y:this.height/2}, {x:this.width/2, y:this.height/2}, {x:0, y: -this.height/2}, {x: 0, y: 0}];
  points = points.map(function(p) {
     return fabric.util.transformPoint(p, matrix);
  });
  return fabric.util.makeBoundingBoxFromPoints(points);
}

fabric.Polygon.prototype.getBoundingRect = function() {
  var matrix = this.calcTransformMatrix();
  var points = this.points;
  var offsetX = this.pathOffset.x;
  var offsetY = this.pathOffset.y;
      points = points.map(function(p) {
         return fabric.util.transformPoint({x: p.x - offsetX , y: p.y -
 offsetY}, matrix);
      });
      return fabric.util.makeBoundingBoxFromPoints(points);
    }

$(function () 
{
    fabric.util.makeBoundingBoxFromPoints = function(points) {
      var minX = fabric.util.array.min(points, 'x'),
          maxX = fabric.util.array.max(points, 'x'),
          width = Math.abs(minX - maxX),
          minY = fabric.util.array.min(points, 'y'),
          maxY = fabric.util.array.max(points, 'y'),
          height = Math.abs(minY - maxY);

      return {
        left: minX,
        top: minY,
        width: width,
        height: height
      };
    };

    fabric.Circle.prototype.getBoundingRect = function() {
      var matrix = this.calcTransformMatrix();
      var points = [{x:-this.width/2, y:0}, {x:this.width/2, y:0}, {x:0, y: -this.height/2}, {x: 0, y: this.height/2}, {x: 0, y: -this.height/2}, {x: 0.433 * this.width, y: this.height/4}, {x: -0.433 * this.width, y: this.height/4}, {y: 0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: -this.width/4}, {y: 0.433 * this.height, x: -this.width/4}, {x: 0.433 * this.width, y: -this.height/4}, {x: -0.433 * this.width, y: -this.height/4}];
      points = points.map(function(p) {
         return fabric.util.transformPoint(p, matrix);
      });
      return fabric.util.makeBoundingBoxFromPoints(points);
    }
    
    fabric.Triangle.prototype.getBoundingRect = function() {
      var matrix = this.calcTransformMatrix();
      var points = [{x:-this.width/2, y:this.height/2}, {x:this.width/2, y:this.height/2}, {x:0, y: -this.height/2}, {x: 0, y: 0}];
      points = points.map(function(p) {
         return fabric.util.transformPoint(p, matrix);
      });
      return fabric.util.makeBoundingBoxFromPoints(points);
    }

    fabric.Polygon.prototype.getBoundingRect = function() {
      var matrix = this.calcTransformMatrix();
      var points = this.points;
      var offsetX = this.pathOffset.x;
      var offsetY = this.pathOffset.y;
      points = points.map(function(p) {
         return fabric.util.transformPoint({x: p.x - offsetX , y: p.y -
 offsetY}, matrix);
      });
      return fabric.util.makeBoundingBoxFromPoints(points);
    }

    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();
            if(bound)
            {
                canvas.contextContainer.strokeRect(
                    bound.left + 0.5,
                    bound.top + 0.5,
                    bound.width,
                    bound.height
                );
            }                
        });
    });

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

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/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./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>

I wrote a blog post about this - too long to reproduce here. Essence is to grab the pixel data and walk in from the edges until color is detected. There are various tactics to make this run quickly.

发布评论

评论列表(0)

  1. 暂无评论