获取容器的标准方法?

Standard method of getting containing box?

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

我正在尝试编写一些代码来使对象相互对齐。所以我想得到每个对象的 "containing box".

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

因此,这些盒子对我来说用处不大。是否有一种标准方法可以为所有形状获取我称之为 "containing boxes" 的东西?例如,我希望能够将以下箱子退还给我:

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

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

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

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

所以getBoundingBox是fabricjs的对象class的一个方法。 没有什么能阻止您为您能想到的每种形状重写此方法。

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

圆是最难的。 我对所有象限的 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/>

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

更新

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

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

工作原理:

  • 导出选定的织物对象toDataURL()
  • 将它放在一个隐藏的 canvas 中并获得具有 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>