Search code examples
javascripthtml5-canvasfabricjs

Complex shape in JavaScript by combining intersecting shapes


How can I create a new complex shape in JavaScript by combining intersecting shapes? I am currently using FabricJS, but I am finding it difficult to achieve the desired result.

shapes

Essentially, I have multiple simple shapes that intersect, and when I select them all I want to create a new complex shape with outer lines that correspond to the outer lines of the intersecting shapes.

Is it possible to achieve this?


Solution

  • I'm not entirely sure about FabricJS specifically, but the problem you are describing has a more general name - Constructive Solid Geometry. It is usually talked about in 3D context, but the idea and the concept is the same in 2D as well.

    Here is a jsfiddle example where I:

    • Construct rectangle, triangle and circle
    • Draw the ordinary shapes
    • Turn the shapes into a "CSG object" for csg2d.js
    • Perform a CSG union between rectangle and triangle
    • Draw the union
    • Turn the union of the rectangle and triangle into a "CSG object"
    • Perform a CSG union between rectangle-triangle union and circle
    • Perform a CSG subtraction between rectangle-triangle union and circle
    • Draw the end results

    I use csg2d.js library to perform the CSG operations.

    So the takeaway here is that unless Fabric.js supports CSG operations, you will most likely have to implement the constructions and conversions by hand from the Fabric.js objects, implement the CSG operations yourself or with the help of a library such as csg2d.js, and implement the conversions back to Fabric.js format as well.

    Not too big of a deal, but that is how I would approach this because these CSG operations are not Fabric.js specific, but instead a more general problem to solve.

    By treating the CSG operations as a "separate, different beast" from Fabric.js, you will also enjoy the benefits of finding a lot of helpful tutorials and documentation than if you'd search for "Fabric.js CSG" help specifically.

    JSFiddle code:

    const canvas = document.querySelector('#canvas');
    const ctx = canvas.getContext('2d');
    
    // Helper function to display polygons
    function drawPolygon(polygon, color, x, y) {
        const offX = x > 0 ? x : 0;
      const offY = y > 0 ? y : 0;
        const firstPoint = polygon.shift();
      
      // Skip last point
      polygon.pop();
      
      ctx.fillStyle = color;
        ctx.beginPath();
      ctx.moveTo(offX + firstPoint[0], offY + firstPoint[1]);
    
      for(let point of polygon) {
        ctx.lineTo(offX + point[0], offY + point[1]);
      }
      
      ctx.closePath();
        ctx.fill();
    }
    
    // Helper function to create polygons
    // Most importantly this closes the polygon for CSG2D.js by appending the first point to the end of the point list
    function makePolygon(points) {
        const closed = points;
      closed.push(points[0]);
      return closed;
    }
    
    // Helper function to convert the CSG2D.js polygon to our array format
    function xyPolygonToArrayPolygon(xyPolygon) {
        return xyPolygon[0].map(p => {
        return [p.x, p.y]
      });
    }
    
    // Define rectangle by its points
    const rectangle = makePolygon([[10, 10], [100, 10], [100, 100], [10, 100], [10, 10]]);
    
    // Define triangle by its points
    const triangle = makePolygon([[60, 60], [85, 125], [35, 125], [60, 60]]);
    
    // Define circle
    let circle = [];
    const circleX = 100;
    const circleY = 50;
    const circleRad = 25;
    
    // Generate points for the circle
    for(let d = 0; d < 360; d += 36){
        var rad = d * Math.PI / 180;
        var x = circleX + circleRad * Math.cos(rad);
        var y = circleY + circleRad * Math.sin(rad);
        circle.push([x, y]);
    }
    
    circle = makePolygon(circle);
    
    // Draw the simple polygons
    drawPolygon(rectangle, '#FF0000');
    drawPolygon(triangle, '#0000FF');
    drawPolygon(circle, '#00FF00');
    
    // Construct a more complex polygons by using CSG2D.js
    
    const rectangleCSG =  CSG.fromPolygons([rectangle]);
    const triangleCSG =  CSG.fromPolygons([triangle]);
    const circleCSG = CSG.fromPolygons([circle]);
    
    const rectangleAndTriangle = xyPolygonToArrayPolygon(rectangleCSG.union(triangleCSG).toPolygons());
    const rectangleAndTriangleCSG = CSG.fromPolygons([rectangleAndTriangle]);
    const rectangleAndTriangleAndCircle = xyPolygonToArrayPolygon(rectangleAndTriangleCSG.union(circleCSG).toPolygons());
    const rectangleAndTriangleAndSubtractCircle = xyPolygonToArrayPolygon(rectangleAndTriangleCSG.subtract(circleCSG).toPolygons());
    
    drawPolygon(rectangleAndTriangle, '#FFFF00', 100, 100);
    drawPolygon(rectangleAndTriangleAndCircle, '#00FFFF', 200, 200);
    drawPolygon(rectangleAndTriangleAndSubtractCircle, '#505000', 300, 300);
    

    Note: The subtraction didn't originally work properly until I defined the circle with 10 points, instead of the original 360 points. So it seems like there is some kind of vertex limit with the csg2d.js library, or it could be that I just did something wrong.