将对象的边缘彼此对齐并防止重叠

时间:2014-03-23 14:06:39

标签: javascript html canvas fabricjs

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

想象一下两个矩形,其中包含位置和大小的信息,您可以在画布内拖放任何矩形。

如果矩形A足够接近矩形B,则矩形A的位置应该捕捉到矩形B的边缘。这应该适用于矩形B的任何边缘。顶点不必匹配,导致矩形的大小矩形是可变的。

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

我最好的jsfiddle尝试

jsfiddle

但我需要它来处理两个维度上的矩形。我很确定,我的代码不足以管理它。

可能有用的代码段:

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

对齐应适用于无限量的对象(不仅适用于两个矩形)。

4 个答案:

答案 0 :(得分:12)

我自己解决了这个问题。 见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;
    }
});

工作非常合法!干杯!

答案 1 :(得分:10)

这是基于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);
            }
        }
    });
});

答案 2 :(得分:2)

我以this fiddle为基础关闭@Anna Phillips&#39;和@ gco的例子。它包括:

  • 角落对齐
  • Edge snapping
  • 对象可以重叠
  • 对象完全包含在画布中
  • 对象的大小不能超过画布区域

以下是代码:

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;
        }
    });
});

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

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

答案 3 :(得分:0)

我需要捕捉大小不等的区域。 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);