Skip to content Skip to sidebar Skip to footer

Prevent Collision Or Intersection Of Canvas Objects

I'm drawing n rectangles on a canvas. The rectangles are draggable and scalable. I want to prevent them from overlapping or intersecting. The best case is, if they just snap to eac

Solution 1:

This is based on gco's answer, updated to work with FabricJS 1.5.0, with the following improvements:

  • Shapes don't overlap.
  • Snapping is more responsive.
  • Shapes are contained within the canvas.

JS Fiddle: 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();

functionplusrect(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;
}

functionfindNewPos(distX, distY, target, obj) {
    // See whether to focus on X or Y axisif(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 canvasif(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 intersectif (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 positionfindNewPos(distX, distY, options.target, obj);
        }

        // Snap objects to each other horizontally// If bottom points are on same Y axisif(Math.abs((options.target.getTop() + options.target.getHeight()) - (obj.getTop() + obj.getHeight())) < snap) {
            // Snap target BL to object BRif(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 BLif(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 axisif(Math.abs(options.target.getTop() - obj.getTop()) < snap) {
            // Snap target TL to object TRif(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 TLif(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 axisif(Math.abs((options.target.getLeft() + options.target.getWidth()) - (obj.getLeft() + obj.getWidth())) < snap) {
            // Snap target TR to object BRif(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 TRif(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 axisif(Math.abs(options.target.getLeft() - obj.getLeft()) < snap) {
            // Snap target TL to object BLif(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 TLif(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 overlapvar 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 axisif(targetLeft >= objectLeft && targetLeft <= objectRight) {
                intersectLeft = targetLeft;
                intersectWidth = obj.getWidth() - (intersectLeft - objectLeft);

            } elseif(objectLeft >= targetLeft && objectLeft <= targetRight) {
                intersectLeft = objectLeft;
                intersectWidth = options.target.getWidth() - (intersectLeft - targetLeft);
            }

            // Find intersect information for Y axisif(targetTop >= objectTop && targetTop <= objectBottom) {
                intersectTop = targetTop;
                intersectHeight = obj.getHeight() - (intersectTop - objectTop);

            } elseif(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 areaif(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 touchif(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 positionfindNewPos(distX, distY, options.target, obj);
            }
        }
    });
});

Solution 2:

these solutions are pretty good, but non of them working with rotated objects. I came up with this. It's preventing intersecting of objects..Even rotated.

I made up this fiddle. Check it out.

[Fiddle](http://jsfiddle.net/m0jjc23v/9)

Explanation:

Workaround is that you have to save last non-intersectiong top & left position while moving object. When object is intersecting or isContained with another object, just use previously saved top & left position and reposition object. There's a lot of possible improvements. Feel free to use this code and make improvements.

Code:

//handle moving objectthis.canvas.on('object:moving', function(event) {
        var obj = event.target;
        intersectingCheck(obj);
});

functionintersectingCheck(activeObject) {
    activeObject.setCoords();
    if(typeof activeObject.refreshLast != 'boolean') {
        activeObject.refreshLast = true
    };

    //loop canvas objects
    activeObject.canvas.forEachObject(function (targ) {
        if (targ === activeObject) return; //bypass self//check intersections with every object in canvasif (activeObject.intersectsWithObject(targ) 
            || activeObject.isContainedWithinObject(targ) 
            || targ.isContainedWithinObject(activeObject)) {
                //objects are intersecting - deny saving last non-intersection position and break loopif(typeof activeObject.lastLeft == 'number') {
                    activeObject.left = activeObject.lastLeft;
                    activeObject.top = activeObject.lastTop;
                    activeObject.refreshLast = false;
                    return;
                }
       }
       else {
           activeObject.refreshLast = true;
       }
   });

   if(activeObject.refreshLast) {
       //save last non-intersecting position if possible
       activeObject.lastLeft = activeObject.left
       activeObject.lastTop = activeObject.top;
   }
}

Solution 3:

For those who are still interested in the solution: I solved it here: https://stackoverflow.com/a/22649022/3207478 See jsfiddle: http://jsfiddle.net/gcollect/FD53A/

Working with

.oCoords.tl .tr .bl. and .br solved it.

Solution 4:

I figured it out how to prevent the collision on the x-axis. By adding these lines:

canvas.forEachObject(function (obj) {
                if (obj === options.target) return;
                if (options.target.isContainedWithinObject(obj)||options.target.intersectsWithObject(obj)||obj.isContainedWithinObject(options.target)) {
                    var distx = ((obj.left + obj.width)/2) - ((options.target.left + options.target.width)/2);
                    var disty = ((obj.top + obj.height)/2) - ((options.target.top + options.target.height)/2);                  

                    if (distx > 0){
                        options.target.left = obj.left - options.target.width;
                    } else {
                        options.target.left = obj.left + obj.width;
                    }

See JSFiddle. Actually it's pretty cool and the objects snap to each other on the x-axis. Troublemaker is now the y-axis. An approach from top or below will move the object to the left or right edge... Will work on this issue.

Solution 5:

I've been contemplating a similar problem and I + the last fiddle here from @kangax. One could even use this type of mechanism to do collision detection for paths themselves if one has a path collision detection algorithm like the polygon intersection code found here: http://www.kevlindev.com/geometry/2D/intersections/index.htm.

But what I don't like about this 'sticky' solution is that to me the object should actually slide along the surface for my application instead of forcing the user to 'unstick' the object. For this kind of effect, I realized that a 2D physics engine would probably do a great job of enabling this type of functionality, and some of the example uses of http://brm.io/matter-js/ demonstrate solving the problem and the positioning and rotation should surely be able to map into fabric.js. However, if fixed rotation is required, it looks like the probability of success would be reduced.

Post a Comment for "Prevent Collision Or Intersection Of Canvas Objects"