Creating Interactive Diagrams with Fabric.js: A Guide to Ports and Connector Lines

Dinesh Rawat
calendar_month
May 28, 2024
timer
5 min
read time

Welcome to the second part of our Fabric.js “Mastering Fabric.js: Tips and Tricks for Object Coordinates and Connecting Lines” series where we dive deeper into the world of interactive diagrams.

In our previous blog post, "Finding the Middle Coordinates of the Vertices of a Polygon in Fabric.js", we explored how to add middle points to a polygon and create a parallelogram with circles located at its vertices. Now, we will take our interactive diagrams to the next level by adding ports and connector lines. In this blog post, "Creating Interactive Diagrams with Fabric.js: A Guide to Ports and Connector Lines", we will guide you through the steps to create a function that modifies the placement of the circles in real-time as the parallelogram undergoes movement, resizing, skewing, or rotation.

Finding the Middle Coordinates of the Vertices of a Polygon in Fabric.js

With this new knowledge, you can create interactive diagrams that are not only informative but also engaging for your audience. So let's get started!

Ports and connectors are essential elements in interactive diagrams created with Fabric.js because they enable the creation of a dynamic and interactive user interface. Ports represent the points of connection on an object or diagram, while connectors are the lines or paths that connect these points. By creating ports and connectors, you can allow users to interact with your diagrams by dragging and dropping objects, changing their size or position, and visually connecting them through the use of connectors.

Ports and connectors are commonly used in a variety of applications, including flowcharts, diagrams, and network topologies. They allow users to create visual representations of complex systems, making it easier to understand and visualize information. Furthermore, they enable users to simulate different scenarios, test hypotheses, and explore different solutions to problems.

Overall, ports and connectors are powerful tools that can enhance the usability and interactivity of your Fabric.js diagrams, making them more engaging, informative, and useful for your users.

As you go further, you'll find a series of steps to create interactive diagrams using Fabric.js:

  1. Firstly, we will create a main polygon, which you can refer to in our previous blog.
  2. Next, we will add target points, also known as ports, to the main shape. You can refer to our previous blog for more information on this step.
  3. We will update the polygon and its ports when the object is scaled, skewed, rotated, or moved. These icons are placed on every middle edge point and allow you to clone and add the current group/shape to the canvas.


const CONTROL_FLOW_ICON =
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='8px' viewBox='0 0 24 24' width='8px' fill='%23573DF4'%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2z'/%3E%3C/svg%3E";
const TOLERANCE_VALUE = 20;

function renderIcon(icon) {
  return function renderIcon(ctx, left, top, styleOverride, fabricObject) {
    const size = this.cornerSize;
    ctx.save();
    ctx.translate(left, top);
    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
    ctx.drawImage(icon, -size / 2, -size / 2, size, size);
    ctx.restore();
  };
}

function createTopControlFlowObject(e) {
  const object = canvas.getActiveObject();

  const pointer: any = canvas.getPointer(e);
  const origY = pointer.y;

  const prop = {
    top: origY - object.height - 2 * TOLERANCE_VALUE
  };

  setClonedObjectPosition(object, prop, pointer);
}

function createRightControlFlowObject(e) {
  const object = canvas.getActiveObject();
  const pointer: any = canvas.getPointer(e);
  const origX = pointer.x;
  const prop = {
    left: origX + 2 * TOLERANCE_VALUE
  };

  setClonedObjectPosition(object, prop, pointer);
}

function createLeftControlFlowObject(e, target) {
  const object = canvas.getActiveObject();
  const pointer: any = canvas.getPointer(e);
  const origX = pointer.x;
  const prop = {
    left: origX - object.width - 2 * TOLERANCE_VALUE
  };

  setClonedObjectPosition(object, prop, pointer);
}

function createBottomControlFlowObject(e) {
  const object = canvas.getActiveObject();
  const pointer: any = canvas.getPointer(e);
  const origY = pointer.y;
  const prop = {
    top: origY + 2 * TOLERANCE_VALUE
  };

  setClonedObjectPosition(object, prop, pointer);
}

function addFlowPoints(object: any): any {
  const controlFlowIcon = CONTROL_FLOW_ICON;
  const controlFlowImg = document.createElement("img");
  controlFlowImg.src = controlFlowIcon;

  const bottomFlowControl = {
    key: "bottom",
    x: object.controls.mb.x,
    y: object.controls.mb.y,
    offsetY: 1.5 * TOLERANCE_VALUE,
    mouseUpHandler: createBottomControlFlowObject
  };

  const topFlowControl = {
    key: "top",
    x: object.controls.mt.x,
    y: object.controls.mt.y,
    offsetY: -1.5 * TOLERANCE_VALUE,
    mouseUpHandler: createTopControlFlowObject
  };

  const leftFlowControl = {
    key: "left",
    x: object.controls.ml.x,
    y: object.controls.ml.y,
    offsetX: -1.5 * TOLERANCE_VALUE,
    mouseUpHandler: createLeftControlFlowObject
  };

  const rightFlowControl = {
    key: "right",
    x: object.controls.mr.x,
    y: object.controls.mr.y,
    offsetX: 1.5 * TOLERANCE_VALUE,
    mouseUpHandler: createRightControlFlowObject
  };

  [
    bottomFlowControl,
    topFlowControl,
    leftFlowControl,
    rightFlowControl
  ].forEach(
    (contol) =>
      (fabric.Object.prototype.controls[
        `${[contol.key]}ControlFlowControl`
      ] = new fabric.Control({
        cursorStyle: "crosshair",
        //@ts-ignore
        render: renderIcon(controlFlowImg),
        //@ts-ignore
        cornerSize: 8,
        ...contol
      }))
  );
  if (object && object.item && object.item(0)) {
    addPortsToPolygon(object.item(0));
  }
  canvas.requestRenderAll();
}

Ref.: https://github.com/dinesh-rawat-dev/fabricjs-port-connector-library/blob/main/src/index.ts#L332

4. In this step, we will clone the current shape, along with its control points and ports, and then point the clone shape to the ports of the original shape.

To accomplish this, we will first create a function that clones the original shape and its control points using the clone() method in Fabric.js. Next, we will loop through each port of the original shape and create a corresponding port on the clone shape using the addPort() method.


function createClonedObjectAndAddConnector(object: any, prop, controlClicked) {
  const direction = object.__corner;
  const allHintCirclesForPort1 = canvas
    .getObjects()
    .filter(
      (object) =>
        object &&
        object.isType("circle") &&
        //@ts-ignore
        object.className === "control_flow_points"
    )
    .map((object) => [object.left, object.top]);

  //@ts-ignore
  let currentShapeCoords = allHintCirclesForPort1.slice();
  const sourcePoint: any = getClosestPoint(
    [controlClicked.x, controlClicked.y],
    currentShapeCoords
  );

  object.clone((cloned: any) => {
    cloned.set(prop);
    cloned.id = Date.now();
    cloned.parent = [];
    cloned.children = [];
    const textbox = cloned.item(1);
    textbox.text = `${cloned.id}`;
    canvas.add(cloned);
    canvas.discardActiveObject();
    canvas.setActiveObject(cloned);
    canvas.requestRenderAll();
    addFlowPoints(cloned);
    addPortsToPolygon(cloned.item(0));
    setTimeout((_) => {
      addConnector(canvas, object, direction, cloned, sourcePoint);
    });
  });
}

5. Create a connector line between the main shape and its cloned shape by locating the ports of each shape and adding a line between them. To accomplish this, we will first use the addConnector() method to find the coordinates of the ports on each shape. Next, we will use the drawLine() method to draw a line between the two port coordinates.

Note: Determine which port the connector line will be connected to using the Euclidean formula. This formula calculates the distance between two points in a two-dimensional plane. I will provide an example array of coordinates to demonstrate how this formula works. By using this formula, we can accurately place the connector line between the two shapes based on the user's selection.


function getDistance(point1, point2) {
  const xDiff = point2[0] - point1[0];
  const yDiff = point2[1] - point1[1];
  return Math.sqrt(xDiff * xDiff + yDiff * yDiff);
}

function getClosestPoint(point, points) {
  let closestPoint = null;
  let closestDistance = Infinity;
  for (let i = 0; i < points.length; i++) {
    const distance = getDistance(point, points[i]);
    if (distance < closestDistance) {
      closestPoint = points[i];
      closestDistance = distance;
    }
  }

  return closestPoint;
}

function drawLine(
  canvas,
  fromPoint,
  toPoint,
  mainShape,
  clonedShape,
  direction
) {
  const line: any = new fabric.Line([...fromPoint, ...toPoint], {
    stroke: "black",
    strokeWidth: 2,
    centeredRotation: true,
    centeredScaling: true
  });

  line.id = Date.now();
  canvas.add(line);
  line.sendToBack();

  mainShape.children = mainShape.children || [];
  mainShape.children.push({
    connector: line.id,
    id: clonedShape.id
  });
  const mainShapeProp = {
    children: mainShape.children
  };

  addCustomPropertyToFabric(mainShape, mainShapeProp);
  clonedShape.parent = clonedShape.parent || [];
  clonedShape.parent.push({
    connector: line.id,
    id: mainShape.id
  });
  const cloneShapeProp = {
    parent: clonedShape.parent
  };
  addCustomPropertyToFabric(clonedShape, cloneShapeProp);

  // Port 1
  const port1MiddleCornerName = currentCornerMetrix[direction];

  // Port 2
  const port2MiddleCornerName = oppositeCornerMetrix[direction];

  line.bounds = line.bounds || {};

  const lineProp = {
    bounds: {
      [port1MiddleCornerName]: [
        (line.bounds[port1MiddleCornerName] || []).push(clonedShape.id)
      ],
      [port2MiddleCornerName]: [
        (line.bounds[port2MiddleCornerName] || []).push(mainShape.id)
      ]
    }
  };

  line.set(lineProp);
  addCustomPropertyToFabric(line, {
    lineProp
  });

  canvas.requestRenderAll();
}

6. Lastly, we need to adjust the lines when the shapes/group is moving:


canvas.on("object:moving", (e: any) => {
  const object = e.target;
  if (object.children && object.children.length) {
    adjustAssociatedNodes(object.children, "x1", "y1");
  }
  if (object.parent && object.parent.length) {
    adjustAssociatedNodes(object.parent, "x2", "y2");
    console.log(object.parent.length);
  }
});

In this blog, we have explored

  1. How to create interactive diagrams with Fabric.js, specifically focusing on ports and connector lines.
  2. Building upon the previous blog "Finding the Middle Coordinates of the Vertices of a Polygon in Fabric.js", we have learned how to add ports to a polygon and update their position in real time as the shape is manipulated.
  3. Additionally, we have explored how to clone a shape and add a connector line between the original shape and its clone by using the Euclidean formula to determine the placement of the connector line.

Overall, Fabric.js provides a powerful toolset for creating interactive diagrams, and the techniques explored in this blog can be used to create a wide range of diagrams and visualizations. By following the steps outlined in this blog, readers can create their own interactive diagrams and explore the full capabilities of Fabric.js.

Codebase:

https://github.com/dinesh-rawat-dev/fabricjs-port-connector-library

Demo: https://codesandbox.io/s/github/dinesh-rawat-dev/fabricjs-port-connector-library

This article answers:

https://stackoverflow.com/questions/53796053/how-to-connect-fabric-js-objects-programmatically-with-a-connector

https://github.com/fabricjs/fabric.js/discussions/8732

https://github.com/fabricjs/fabric.js/discussions/8718

https://github.com/fabricjs/fabric.js/issues/2779

https://stackoverflow.com/questions/75541658/how-to-create-a-connecting-line-between-two-asymmetrical-shapes-in-fabric-js

https://stackoverflow.com/questions/74441682/in-fabricjs-how-to-give-multiple-function-in-fabric-controlsutils-actionhandler

https://stackoverflow.com/questions/75541563/obtaining-object-coordinates-in-fabric-js-actual-coordinates-vs-bounding-rectan

https://stackoverflow.com/questions/58063289/fabricjs-function-to-join-2-objects-with-line

https://stackoverflow.com/questions/17347216/how-to-use-fabric-js-to-do-visio-like-drawings-with-connections

https://stackoverflow.com/questions/56800134/unable-to-connect-endpoints-by-line-in-fabric-js

https://stackoverflow.com/questions/20418153/connecting-two-canvas-objects-in-fabric-js