【问题标题】:Standard method of getting containing box?获取包含框的标准方法?
【发布时间】:2020-05-21 01:30:27
【问题描述】:

好的,如果我有以下形状被旋转然后被选中,您将看到它们的边界框:

我正在尝试编写一些代码来使对象相互对齐。所以我想得到每个对象的“包含框”。

我知道getBoundingRect 但是,对于上述形状,这给了我以下信息:

因此,这些盒子对我来说不是那么有用。是否有一种标准方法可以获取所有形状的“包含框”?例如,我希望能够将以下盒子退回给我:

因此,对于任何给定的形状,我都希望能够获得红色边界矩形(没有旋转)。

显然,我可以在 fabricJS 中为每个可能的形状编写一个例程,但我不想重新发明轮子!有什么想法吗?

编辑这是一个显示当前边界框(红色)的交互式 sn-p:

$(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/>

【问题讨论】:

  • 实际的形状是 svg 吗?

标签: javascript fabricjs


【解决方案1】:

所以getBoundingBox是fabricjs的Object类的一个方法。 没有什么能阻止你为你能想到的每个形状重写这个方法。

我将从圆形和三角形开始,我会让你想象多边形。当形状是路径或圆形被缩放为椭圆时,它会变得越来越难。

圆是最难的。 我在所有象限的 30、60、90 度处对圆进行了采样。仍然不完美。您可能需要增加采样或找到更好的公式(也许每 15 度采样一次就可以了)。

三角形比较容易,因为它有 3 个兴趣点。

多边形来源于三角形,这里没什么难的。

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

【讨论】:

  • 谢谢。我已将它修改为运行,因为它在尝试为没有 getBoundingRect 主体的形状渲染红色框时抱怨。我有点希望我刚刚忽略了一些东西并且这段代码已经存在了。也许对未来有很好的包容性?
  • 我需要完成答案。倾斜的圆圈是最难的情况。我需要一些时间,我现在正在工作。您拥有的点数越少,创建当前框就越难。理想情况下,我们应该将圆圈分成 4 个 beizers 和一些复杂的东西。它超出了我在答案中所能做的,但我会尽量留下有意义的提示
  • 添加了三角形@LeeTaylor
  • 三角形可以很好地响应缩放和倾斜
  • 对 makeBoundingBoxFromPoints 的修复我认为它应该放在库中
【解决方案2】:

我遇到了同样的问题,我找到了解决方法。我从活动对象创建了一个临时 SVG 并将其放置在视口之外。然后我使用原生的getBBox()函数测量了真实的边界框。

更新

显然,上面的解决方案只适用于 Firefox (76),所以我想出了一个不同的解决方案。由于我找不到正常工作的原生函数来获取形状的真实边界框,因此我决定扫描像素并从那里检索边界。

小提琴: https://jsfiddle.net/divpusher/2m7c61gw/118/

工作原理:

  • 导出选定的织物对象toDataURL()
  • 将其放置在隐藏的画布中并使用getImageData() 获取像素
  • 扫描像素以检索形状的 x1、x2、y1、y2 边缘坐标

下面的演示

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

【讨论】:

  • 不确定这里对齐的是什么,或者红色方块。
  • 我为你创建了一个小提琴并用绿色绘制了真正的边界框:jsfiddle.net/1xh6bk80/2
  • 抱歉,我可能遗漏了一些东西,但这些盒子都不是我想要的。请参阅我原始问题中最后一组图像中的红色框...
  • 诶!它似乎在 Chrome 中无法正常工作,仅在 Firefox 中!对不起!我会努力解决这个问题,稍后再回复您!
  • 更新有效(toDataURL() 方法),但是如果在每次修改画布后调用它会不会出现性能问题?基本上我想动态地向用户报告实际尺寸。
猜你喜欢
  • 1970-01-01
  • 2011-11-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-02
  • 1970-01-01
  • 2011-05-30
相关资源
最近更新 更多