【问题标题】:Snap edges of objects to each other and prevent overlap将对象的边缘相互对齐并防止重叠
【发布时间】:2014-04-30 18:40:36
【问题描述】:

我的目标是防止两个或多个矩形在我的 FabricJS 画布内重叠。

想象两个具有位置和大小信息的矩形,您可以在画布内拖放任何矩形。

如果矩形 A 与矩形 B 足够接近,矩形 A 的位置应该对齐矩形 B 的边缘。这应该适用于矩形 B 的任何边缘。顶点不必匹配,因为矩形是可变的。

我有一个在一维(x 轴)上捕捉的工作示例。

我最好的 jsfiddle 尝试

jsfiddle

但我需要它在两个维度上围绕矩形工作。我很确定,我的代码不足以管理这个。

可能有帮助的代码-sn-ps:

object.oCoords.tl.x //top-left corner x position. similar goes for top-right (tr), bottom-left (bl), bottom-right (br) and .y for y-position
mouse_pos = canvas.getPointer(e.e);
mouse_pos.x //pointer x.position
mouse_pos.y //pointer y.position
object.intersectsWithObject(targ) // object = dragged rectangle, targ = targeted rectangle

捕捉应该适用于无限数量的对象(不仅适用于两个矩形)。

【问题讨论】:

    标签: javascript html canvas fabricjs


    【解决方案1】:

    我自己解决了这个问题。 见 jsfiddle:http://jsfiddle.net/gcollect/FD53A/

    这是代码:

    this.canvas.on('object:moving', function (e) {
    var obj = e.target;
    obj.setCoords(); //Sets corner position coordinates based on current angle, width and height
    canvas.forEachObject(function (targ) {
        var objects = this.canvas.getObjects(),
            i = objects.length;
        activeObject = canvas.getActiveObject();
    
        if (targ === activeObject) return;
    
    
        if (Math.abs(activeObject.oCoords.tr.x - targ.oCoords.tl.x) < edgedetection) {
            activeObject.left = targ.left - activeObject.currentWidth;
        }
        if (Math.abs(activeObject.oCoords.tl.x - targ.oCoords.tr.x) < edgedetection) {
            activeObject.left = targ.left + targ.currentWidth;
        }
        if (Math.abs(activeObject.oCoords.br.y - targ.oCoords.tr.y) < edgedetection) {
            activeObject.top = targ.top - activeObject.currentHeight;
        }
        if (Math.abs(targ.oCoords.br.y - activeObject.oCoords.tr.y) < edgedetection) {
            activeObject.top = targ.top + targ.currentHeight;
        }
        if (activeObject.intersectsWithObject(targ) && targ.intersectsWithObject(activeObject)) {
            targ.strokeWidth = 10;
            targ.stroke = 'red';
        } else {
            targ.strokeWidth = 0;
            targ.stroke = false;
        }
        if (!activeObject.intersectsWithObject(targ)) {
            activeObject.strokeWidth = 0;
            activeObject.stroke = false;
        }
    });
    

    工作非常合法!干杯!

    【讨论】:

    • 我真的很喜欢你在这里所做的。这里的一些小错误是,一旦对象可拖动,它往往会被锁定在网格上。此外,您仍然可以在对象中移动。试图想出与您所做的类似的事情,只是在关闭时才捕捉并防止重叠。
    • @ryan 非常感谢。有一阵子了。在我的用例中,我提出了解决方案,最好允许相交对象。因为否则用户必须绕过一个对象才能到达所需的目的地。也许它也适合您的用例。由于您可以检查对象何时相交,因此您可以提醒用户或禁用保存。
    • 谁会在 2017 年阅读这篇文章,从 Fabric 1.5 开始,你应该使用 .getWidth().getHeight() 而不是 .currentWidth.currentHeight
    • 也适用于 FabricJS 版本 4.3.1
    【解决方案2】:

    这是基于 gco 的回答,更新后可与 FabricJS 1.5.0 一起使用,并进行了以下改进:

    • 形状不重叠。
    • 捕捉反应更快。
    • 形状包含在画布中。

    JS 小提琴:https://jsfiddle.net/aphillips8/31qbr0vn/1/

    var canvas = new fabric.Canvas('canvas'),
    canvasWidth = document.getElementById('canvas').width,
    canvasHeight = document.getElementById('canvas').height,
    counter = 0,
    rectLeft = 0,
    snap = 20; //Pixels to snap
    
    canvas.selection = false;
    plusrect();
    plusrect();
    plusrect();
    
    function plusrect(top, left, width, height, fill) {
        var rect = new fabric.Rect({
            top: 300,
            name: 'rectangle ' + counter,
            left: 0 + rectLeft,
            width: 100,
            height: 100,
            fill: 'rgba(' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ', 0.75)',
            lockRotation: true,
            originX: 'left',
            originY: 'top',
            cornerSize: 15,
            hasRotatingPoint: false,
            perPixelTargetFind: true,
            minScaleLimit: 1,
            maxWidth: canvasWidth,
            maxHeight: canvasHeight
        });
    
        rect.custom = {};
        rect.custom.counter = counter;
    
        canvas.add(rect);
        counter++;
        rectLeft += 200;
    }
    
    function findNewPos(distX, distY, target, obj) {
        // See whether to focus on X or Y axis
        if(Math.abs(distX) > Math.abs(distY)) {
            if (distX > 0) {
                target.setLeft(obj.getLeft() - target.getWidth());
            } else {
                target.setLeft(obj.getLeft() + obj.getWidth());
            }
        } else {
            if (distY > 0) {
                target.setTop(obj.getTop() - target.getHeight());
            } else {
                target.setTop(obj.getTop() + obj.getHeight());
            }
        }
    }
    
    canvas.on('object:moving', function (options) {
        // Sets corner position coordinates based on current angle, width and height
        options.target.setCoords();
    
        // Don't allow objects off the canvas
        if(options.target.getLeft() < snap) {
            options.target.setLeft(0);
        }
    
        if(options.target.getTop() < snap) {
            options.target.setTop(0);
        }
    
        if((options.target.getWidth() + options.target.getLeft()) > (canvasWidth - snap)) {
            options.target.setLeft(canvasWidth - options.target.getWidth());
        }
    
        if((options.target.getHeight() + options.target.getTop()) > (canvasHeight - snap)) {
            options.target.setTop(canvasHeight - options.target.getHeight());
        }
    
        // Loop through objects
        canvas.forEachObject(function (obj) {
            if (obj === options.target) return;
    
            // If objects intersect
            if (options.target.isContainedWithinObject(obj) || options.target.intersectsWithObject(obj) || obj.isContainedWithinObject(options.target)) {
    
                var distX = ((obj.getLeft() + obj.getWidth()) / 2) - ((options.target.getLeft() + options.target.getWidth()) / 2);
                var distY = ((obj.getTop() + obj.getHeight()) / 2) - ((options.target.getTop() + options.target.getHeight()) / 2);
    
                // Set new position
                findNewPos(distX, distY, options.target, obj);
            }
    
            // Snap objects to each other horizontally
    
            // If bottom points are on same Y axis
            if(Math.abs((options.target.getTop() + options.target.getHeight()) - (obj.getTop() + obj.getHeight())) < snap) {
                // Snap target BL to object BR
                if(Math.abs(options.target.getLeft() - (obj.getLeft() + obj.getWidth())) < snap) {
                    options.target.setLeft(obj.getLeft() + obj.getWidth());
                    options.target.setTop(obj.getTop() + obj.getHeight() - options.target.getHeight());
                }
    
                // Snap target BR to object BL
                if(Math.abs((options.target.getLeft() + options.target.getWidth()) - obj.getLeft()) < snap) {
                    options.target.setLeft(obj.getLeft() - options.target.getWidth());
                    options.target.setTop(obj.getTop() + obj.getHeight() - options.target.getHeight());
                }
            }
    
            // If top points are on same Y axis
            if(Math.abs(options.target.getTop() - obj.getTop()) < snap) {
                // Snap target TL to object TR
                if(Math.abs(options.target.getLeft() - (obj.getLeft() + obj.getWidth())) < snap) {
                    options.target.setLeft(obj.getLeft() + obj.getWidth());
                    options.target.setTop(obj.getTop());
                }
    
                // Snap target TR to object TL
                if(Math.abs((options.target.getLeft() + options.target.getWidth()) - obj.getLeft()) < snap) {
                    options.target.setLeft(obj.getLeft() - options.target.getWidth());
                    options.target.setTop(obj.getTop());
                }
            }
    
            // Snap objects to each other vertically
    
            // If right points are on same X axis
            if(Math.abs((options.target.getLeft() + options.target.getWidth()) - (obj.getLeft() + obj.getWidth())) < snap) {
                // Snap target TR to object BR
                if(Math.abs(options.target.getTop() - (obj.getTop() + obj.getHeight())) < snap) {
                    options.target.setLeft(obj.getLeft() + obj.getWidth() - options.target.getWidth());
                    options.target.setTop(obj.getTop() + obj.getHeight());
                }
    
                // Snap target BR to object TR
                if(Math.abs((options.target.getTop() + options.target.getHeight()) - obj.getTop()) < snap) {
                    options.target.setLeft(obj.getLeft() + obj.getWidth() - options.target.getWidth());
                    options.target.setTop(obj.getTop() - options.target.getHeight());
                }
            }
    
            // If left points are on same X axis
            if(Math.abs(options.target.getLeft() - obj.getLeft()) < snap) {
                // Snap target TL to object BL
                if(Math.abs(options.target.getTop() - (obj.getTop() + obj.getHeight())) < snap) {
                    options.target.setLeft(obj.getLeft());
                    options.target.setTop(obj.getTop() + obj.getHeight());
                }
    
                // Snap target BL to object TL
                if(Math.abs((options.target.getTop() + options.target.getHeight()) - obj.getTop()) < snap) {
                    options.target.setLeft(obj.getLeft());
                    options.target.setTop(obj.getTop() - options.target.getHeight());
                }
            }
        });
    
        options.target.setCoords();
    
        // If objects still overlap
    
        var outerAreaLeft = null,
        outerAreaTop = null,
        outerAreaRight = null,
        outerAreaBottom = null;
    
        canvas.forEachObject(function (obj) {
            if (obj === options.target) return;
    
            if (options.target.isContainedWithinObject(obj) || options.target.intersectsWithObject(obj) || obj.isContainedWithinObject(options.target)) {
    
                var intersectLeft = null,
                intersectTop = null,
                intersectWidth = null,
                intersectHeight = null,
                intersectSize = null,
                targetLeft = options.target.getLeft(),
                targetRight = targetLeft + options.target.getWidth(),
                targetTop = options.target.getTop(),
                targetBottom = targetTop + options.target.getHeight(),
                objectLeft = obj.getLeft(),
                objectRight = objectLeft + obj.getWidth(),
                objectTop = obj.getTop(),
                objectBottom = objectTop + obj.getHeight();
    
                // Find intersect information for X axis
                if(targetLeft >= objectLeft && targetLeft <= objectRight) {
                    intersectLeft = targetLeft;
                    intersectWidth = obj.getWidth() - (intersectLeft - objectLeft);
    
                } else if(objectLeft >= targetLeft && objectLeft <= targetRight) {
                    intersectLeft = objectLeft;
                    intersectWidth = options.target.getWidth() - (intersectLeft - targetLeft);
                }
    
                // Find intersect information for Y axis
                if(targetTop >= objectTop && targetTop <= objectBottom) {
                    intersectTop = targetTop;
                    intersectHeight = obj.getHeight() - (intersectTop - objectTop);
    
                } else if(objectTop >= targetTop && objectTop <= targetBottom) {
                    intersectTop = objectTop;
                    intersectHeight = options.target.getHeight() - (intersectTop - targetTop);
                }
    
                // Find intersect size (this will be 0 if objects are touching but not overlapping)
                if(intersectWidth > 0 && intersectHeight > 0) {
                    intersectSize = intersectWidth * intersectHeight;
                }
    
                // Set outer snapping area
                if(obj.getLeft() < outerAreaLeft || outerAreaLeft == null) {
                    outerAreaLeft = obj.getLeft();
                }
    
                if(obj.getTop() < outerAreaTop || outerAreaTop == null) {
                    outerAreaTop = obj.getTop();
                }
    
                if((obj.getLeft() + obj.getWidth()) > outerAreaRight || outerAreaRight == null) {
                    outerAreaRight = obj.getLeft() + obj.getWidth();
                }
    
                if((obj.getTop() + obj.getHeight()) > outerAreaBottom || outerAreaBottom == null) {
                    outerAreaBottom = obj.getTop() + obj.getHeight();
                }
    
                // If objects are intersecting, reposition outside all shapes which touch
                if(intersectSize) {
                    var distX = (outerAreaRight / 2) - ((options.target.getLeft() + options.target.getWidth()) / 2);
                    var distY = (outerAreaBottom / 2) - ((options.target.getTop() + options.target.getHeight()) / 2);
    
                    // Set new position
                    findNewPos(distX, distY, options.target, obj);
                }
            }
        });
    });
    

    【讨论】:

    • 在某些情况下,形状可能会重叠。 imgur.com/a/42gWy 不完全确定为什么,但很好但可以重现。 1)调整中间形状。 2)将第一个形状放在右上角。 3) 逆时针将第三个形状拖到该对周围和上方。不过不错。
    【解决方案3】:

    我基于this fiddle @Anna Phillips' 和 @gco 的示例。它包括:

    • 拐角捕捉
    • 边缘捕捉
    • 对象可以重叠
    • 对象完全包含在画布中
    • 对象的大小不能大于画布区域

    代码如下:

    window.canvas = new fabric.Canvas('fabriccanvas');
    window.counter = 0;
    var newleft = 0,
        edgedetection = 20, //pixels to snap
        canvasWidth = document.getElementById('fabriccanvas').width,
        canvasHeight = document.getElementById('fabriccanvas').height;
    
    canvas.selection = false;
    plusrect();
    plusrect();
    plusrect();
    
    function plusrect(top, left, width, height, fill) {
        window.canvas.add(new fabric.Rect({
            top: 300,
            name: 'rectangle ' + window.counter,
            left: 0 + newleft,
            width: 100,
            height: 100,
            fill: 'rgba(' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ', 0.75)',
            lockRotation: true,
            originX: 'left',
            originY: 'top',
            cornerSize: 15,
            hasRotatingPoint: false,
            perPixelTargetFind: true,
            minScaleLimit: 1,
            maxHeight: document.getElementById("fabriccanvas").height,
            maxWidth: document.getElementById("fabriccanvas").width,
        }));
        window.counter++;
        newleft += 200;
    }
    this.canvas.on('object:moving', function (e) {
        var obj = e.target;
        obj.setCoords(); //Sets corner position coordinates based on current angle, width and height
    
        if(obj.getLeft() < edgedetection) {
            obj.setLeft(0);
        }
    
        if(obj.getTop() < edgedetection) {
            obj.setTop(0);
        }
    
        if((obj.getWidth() + obj.getLeft()) > (canvasWidth - edgedetection)) {
            obj.setLeft(canvasWidth - obj.getWidth());
        }
    
        if((obj.getHeight() + obj.getTop()) > (canvasHeight - edgedetection)) {
            obj.setTop(canvasHeight - obj.getHeight());
        }
    
        canvas.forEachObject(function (targ) {
            activeObject = canvas.getActiveObject();
    
            if (targ === activeObject) return;
    
    
            if (Math.abs(activeObject.oCoords.tr.x - targ.oCoords.tl.x) < edgedetection) {
                activeObject.left = targ.left - activeObject.currentWidth;
            }
            if (Math.abs(activeObject.oCoords.tl.x - targ.oCoords.tr.x) < edgedetection) {
                activeObject.left = targ.left + targ.currentWidth;
            }
            if (Math.abs(activeObject.oCoords.br.y - targ.oCoords.tr.y) < edgedetection) {
                activeObject.top = targ.top - activeObject.currentHeight;
            }
            if (Math.abs(targ.oCoords.br.y - activeObject.oCoords.tr.y) < edgedetection) {
                activeObject.top = targ.top + targ.currentHeight;
            }
            if (activeObject.intersectsWithObject(targ) && targ.intersectsWithObject(activeObject)) {
                targ.strokeWidth = 10;
                targ.stroke = 'red';
            } else {
                targ.strokeWidth = 0;
                targ.stroke = false;
            }
            if (!activeObject.intersectsWithObject(targ)) {
                activeObject.strokeWidth = 0;
                activeObject.stroke = false;
            }
        });
    });
    

    我想知道的是是否可以扩展它以添加以下功能:

    • 动态捕捉。在初始捕捉后继续拖动对象将暂时禁用捕捉,直到对象停止移动。例如,如果我将一个盒子拖到另一个盒子旁边,一旦它们在范围内,它们就会吸附在一起。但是,如果我继续移动第一个框,我可以将其“放置”在捕捉范围内但未与另一个框对齐的位置。
    • 显示指南行当所选对象范围内的另一个对象范围内。目前我们在目标对象周围添加了一个边框,但最好显示向外延伸(可能到画布边缘)的准则,以便更轻松地可视化目标对象的边界。
    • 平行捕捉。移动已捕捉到目标对象的对象时,所选对象应捕捉到目标对象,使两个对象的顶部、底部或侧面平行。例如,假设选定的正方形对齐到目标正方形的左侧,并且选定正方形的顶部低于目标正方形的顶部。向上移动所选方块应使其顶部在范围内与目标顶部对齐。向下移动时,或者如果所选对象高于/低于目标并水平移动,则应应用相同的逻辑。

    【讨论】:

      【解决方案4】:

      我需要捕捉大小不等的区域。 jsfiddle

      var canvas = new fabric.Canvas('c');
      canvas.setDimensions({width:window.innerWidth});
      
      var edge_detection_external = 21;
      var corner_detection = 5;
      
      canvas.selection = false;
      
      canvas.on('object:moving', function (e) {
      
          var obj = e.target;
          obj.setCoords();
      
          function update_position(obj){
              return function(targ){
                  if(targ === obj) return;                   
      
                  // Check overlap case https://www.geeksforgeeks.org/find-two-rectangles-overlap/ 
                  if(!(function(targ,obj){                        
                      if(obj.aCoords.tl.x > targ.aCoords.br.x || targ.aCoords.tl.x > obj.aCoords.br.x)
                          return false;
                      if(targ.aCoords.tl.y > obj.aCoords.br.y || obj.aCoords.tl.y > targ.aCoords.br.y)
                          return false;
                      return true;
                  })(targ,obj)){
                      // is on RIGHT or LEFT? 
                      if((obj.top > targ.top && obj.top < targ.top + targ.height)
                          || (targ.top > obj.top && targ.top < obj.top + obj.height)){
      
                          // Object is to the RIGHT and Edge detection 
                          if(obj.aCoords.tl.x > targ.aCoords.br.x
                              && obj.aCoords.tl.x - targ.aCoords.br.x < edge_detection_external){
                                  obj.set({left:targ.aCoords.br.x});
      
                                  // Corner detection
                                  obj.setCoords();
                                  if(Math.abs(targ.aCoords.tr.y - obj.aCoords.tl.y) < corner_detection)
                                      obj.set({top:targ.top});
                                  else if(Math.abs(targ.aCoords.br.y - obj.aCoords.bl.y) < corner_detection)
                                      obj.set({top:targ.top + targ.height - obj.height});                    
                          }
      
                          // LEFT
                          if(targ.aCoords.tl.x > obj.aCoords.br.x
                              && targ.aCoords.tl.x - obj.aCoords.br.x  < edge_detection_external){
                                  obj.set({left:targ.aCoords.tl.x - obj.width});
      
                                  obj.setCoords();
                                  if(Math.abs(targ.aCoords.tl.y - obj.aCoords.tr.y) < corner_detection)
                                      obj.set({top:targ.top});
                                  else if(Math.abs(targ.aCoords.bl.y - obj.aCoords.br.y) < corner_detection)
                                      obj.set({top:targ.top + targ.height - obj.height});  
                          }
                      }       
      
                      // is on TOP or BOTTOM?
                      if((obj.left > targ.left && obj.left < targ.left + targ.width) 
                          || (targ.left > obj.left && targ.left < obj.left + obj.width)){
      
                          // TOP 
                          if(targ.aCoords.tl.y > obj.aCoords.br.y  
                              && targ.aCoords.tl.y - obj.aCoords.br.y < edge_detection_external){
                                  obj.set({top:targ.aCoords.tl.y - obj.height});
      
                                  obj.setCoords();
                                  if(Math.abs(targ.aCoords.tl.x - obj.aCoords.bl.x) < corner_detection)
                                      obj.set({left:targ.left});
                                  else if(Math.abs(targ.aCoords.tr.x - obj.aCoords.br.x) < corner_detection)
                                      obj.set({left:targ.left + targ.width - obj.width});
                          }
      
                          // BOTTOM
                          if(obj.aCoords.tl.y > targ.aCoords.br.y
                              && obj.aCoords.tl.y - targ.aCoords.br.y < edge_detection_external){
                                  obj.set({top:targ.aCoords.br.y});
      
                                  obj.setCoords();
                                  if(Math.abs(targ.aCoords.bl.x - obj.aCoords.tl.x) < corner_detection)
                                      obj.set({left:targ.left});
                                  else if(Math.abs(targ.aCoords.br.x - obj.aCoords.tr.x) < corner_detection)
                                      obj.set({left:targ.left + targ.width - obj.width});
                          }
                      }
      
                  }
      
              }
          }
      
          canvas.getObjects('group').some(update_position(obj));          
      });
      
      String.prototype.to_inches = function(){
          return this.split('-').map(function(value,index){
              value = Number(value);
              if(index == 0)
                  return value * 12
              else
                  return value
          }).reduce(function(total,current){
              return total + current;
          });
      }
      
      Array.prototype.to_object_list = function(){
          const preserved = [...this];
          var header = this.splice(0,1)[0];           
      
         for(var i = 0;i < this.length; i++){
             var obj = {};
             for(var j = 0;j < header.length; j++){
                 obj[header[j].toLowerCase()] = this[i][j];
             }
             this[i] = obj;
         }
      
         return preserved;
      }
      
      function draw_areas(){
          var offset = 0;
      
          return function(area_params,index){
              if(area_params.area.indexOf('>') === -1){
      
                  var area = new fabric.Rect({            
                      fill: 'red',
                      width:area_params.width.to_inches(),
                      height:area_params.length.to_inches(),
                  });
      
                  var text = new fabric.Text(area_params.area + '\n' + area_params.width + ' x ' + area_params.length,{
                      fontSize:12,
                      fill:"white"
                  });
      
                  if(text.width - area.width > 0){
                      text.set('width',area.width);                    
                  }    
      
                  if(text.height - area.height > 0){
                      text.set('height',area.height);
                  }
      
                  var group_name = 'group_' + area_params.area.split(' ').join('-');
                  var group = new fabric.Group([area,text],{
                      name: group_name,
                      left: 5,
                      top: 5*(index++) + offset,                   
                  });
      
                  canvas.add(group);
      
                  offset = area_params.length.to_inches() + offset;
                  canvas.setDimensions({height:5*(index++) + offset});  
              }                       
          }
      }
      
      function handler_get_data(data){
          data = JSON.parse(data);
          data.to_object_list();                                           
          data.forEach(draw_areas());
      }        
      
      var d = '[["Area","Width","Length"],["Bedroom 1","19-5.5","14"],["Kitchen","14","16-3"],["Bedroom 2","13-6","12-9"]]';
      handler_get_data(d);           
      
      
      

      【讨论】:

        猜你喜欢
        • 2011-04-27
        • 1970-01-01
        • 2017-05-14
        • 2016-04-16
        • 1970-01-01
        • 2017-02-25
        • 1970-01-01
        • 2018-02-12
        • 2018-09-26
        相关资源
        最近更新 更多