SBB
SBB

Reputation: 8970

Javascript SVG controlling X Y based on path direction

I have some javascript code that is generating SVG paths to connect HTML elements on a page. This show which box is connected to another box.

The paths are using markers so that I can have an arrow on the end to show its direction.

Overall, this is working well. I have been trying to add a circle to this setup now that is directly in the center of the starting and ending element.

My goal is that the path stops just short of this circle so that the arrow is pointing to it, not on top of it.

Example Screenshot: enter image description here

Code Sample:

//helper functions, it turned out chrome doesn't support Math.sgn() 
function signum(x) {
  return (x < 0) ? -1 : 1;
}

function absolute(x) {
  return (x < 0) ? -x : x;
}

/**
 * Get the offsets of page elements
 * @param el
 */
function getOffset(el) {
  const rect = el.getBoundingClientRect();
  return {
    left: rect.left + window.pageXOffset,
    top: rect.top + window.pageYOffset,
    bottom: rect.bottom - window.pageYOffset,
    width: rect.width || el.offsetWidth,
    height: rect.height || el.offsetHeight
  };
}

/**
 * Draw the path on the SVG using proided coords
 * @param svg
 * @param path
 * @param startX
 * @param startY
 * @param endX
 * @param endY
 */
function drawPath(svg, path, startX, startY, endX, endY, circle) {

  // Get the path's stroke width (if one wanted to be  really precize, one could use half the stroke size)
  const style = getComputedStyle(path);
  const stroke = parseFloat(style.strokeWidth);

  // Check if the svg is big enough to draw the path, if not, set height/width
  if (svg.getAttribute("height") < startY) {
    svg.setAttribute("height", startY + 20);
  }

  if (svg.getAttribute("width") < startX + stroke) {
    svg.setAttribute("width", startX + stroke + 20);
  }
  if (svg.getAttribute("width") < endX + stroke) {
    svg.setAttribute("width", endX + stroke + 20);
  }

  /**
            M = moveto
            L = lineto
            H = horizontal lineto
            V = vertical lineto
            C = curveto
            S = smooth curveto
            Q = quadratic Bézier curve
            T = smooth quadratic Bézier curveto
            A = elliptical Arc
            Z = closepath
         */

  // Straight line from XY Start to XY End
  path.setAttribute(
    "d",
    "M" + startX + " " + startY + " L" + endX + " " + endY
  );

  // Show the starting and ending circle
  if (circle) {
    circle.setAttribute("cx", startX);
    circle.setAttribute("cy", startY);
    circle.setAttribute("cx", endX);
    circle.setAttribute("cy", endY);
  }
}

/**
 * Calculate the coords for where the line will be drawn
 * @param svg
 * @param path
 * @param startElem
 * @param endElem
 * @param type
 */
function connectElements(svg, path, startElem, endElem, circle) {

  // Define our container
  const svgContainer = document.getElementById("svgContainer"),
    svgTop = getOffset(svgContainer).top,
    svgLeft = getOffset(svgContainer).left,
    startCoord = startElem,
    endCoord = endElem;

  let startX, startY, endX, endY;

  // Calculate path's start (x,y)  coords
  // We want the x coordinate to visually result in the element's mid point
  startX =
    getOffset(startCoord).left +
    getOffset(startCoord).width / 2 -
    svgLeft;
  startY =
    getOffset(startCoord).top +
    getOffset(startCoord).height / 2 -
    svgTop;

  // Calculate path's start (x,y)  coords
  // We want the x coordinate to visually result in the element's mid point
  endX =
    getOffset(endCoord).left +
    0.5 * getOffset(endCoord).width -
    svgLeft;
  endY =
    getOffset(endCoord).top +
    getOffset(endCoord).height / 2 -
    svgTop;

  // Call function for drawing the path
  drawPath(svg, path, startX, startY, endX, endY, circle);
}

function connectAll() {

  // Loop over our destinations
  for (let i = 0; i < dest.length; i++) {

    // Define
    const marker = document.createElementNS(
      "http://www.w3.org/2000/svg",
      "marker"
    );
    const path = document.createElementNS(
      "http://www.w3.org/2000/svg",
      "path"
    );
    const markerPath = document.createElementNS(
      "http://www.w3.org/2000/svg",
      "path"
    );
    const defs = document.createElementNS(
      "http://www.w3.org/2000/svg",
      "defs"
    );

    const circle = document.createElementNS(
      "http://www.w3.org/2000/svg",
      "circle"
    );

    // Set definitions attribute
    defs.setAttribute("id", "defs");

    // Create our center circle
    circle.setAttribute("id", "circle_" + dest[i].linkID + "_" + dest[i].boxID);
    circle.setAttribute("cx", "0");
    circle.setAttribute("cy", "0");
    circle.setAttribute("r", "15");
    circle.setAttribute("fill", "red");

    // Append our circle
    document.getElementById('svg1').appendChild(circle);

    // Set up marker (Arrow)
    marker.setAttribute("id", "Triangle");
    marker.setAttribute("viewBox", "0 0 10 10");
    marker.setAttribute("refX", "0");
    marker.setAttribute("refY", "5");
    marker.setAttribute("markerUnits", "strokeWidth");
    marker.setAttribute("markerWidth", "4");
    marker.setAttribute("markerHeight", "3");
    marker.setAttribute("orient", "auto");

    // Append our marker (Arrow)
    marker.appendChild(markerPath);
    markerPath.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
    markerPath.setAttribute("fill", "#428bca");

    // Create our main path
    path.setAttribute(
      "id",
      "path_" + dest[i].linkID + "_" + dest[i].boxID
    );
    path.setAttribute("class", "path");
    path.setAttribute(
      "marker-end",
      "url(" + window.location + "#Triangle)"
    );

    // Only create one set of definitions
    if (i === 0) {
      document.getElementById('svg1').appendChild(defs);
      document.getElementById('defs').appendChild(marker);
    }

    // Append our path to the SVG
    document.getElementById('svg1').appendChild(path);

    const svg = document.getElementById("svg1"),
      p = document.getElementById(
        "path_" + dest[i].linkID + "_" + dest[i].boxID
      ),
      startingBox = document.getElementById("box_" + dest[i].boxID),
      destinationBox = document.getElementById(
        "box_" + dest[i].destinationBoxID
      );

    // Connect paths
    connectElements(
      svg,
      p,
      startingBox,
      destinationBox,
      circle
    );
  }


}

// Define our boxes to connect
var dest = [{
    "boxID": "16",
    "destinationBoxID": "5",
    "linkID": "1"
  },
  {
    "boxID": "18",
    "destinationBoxID": "1",
    "linkID": "9"
  },
  {
    "boxID": "2",
    "destinationBoxID": "5",
    "linkID": "8"
  }
]

// Run
connectAll()

My Attempt:

One of the things I tried was adjusting the final X coordinate for the path like so:

// Straight line from XY Start to XY End
path.setAttribute(
  "d",
  "M" + startX + " " + startY + " L" + (endX-30) + " " + endY
);

By telling it to end 30 short, I expected the arrow to not quite go all the way to the mid-point.

Well, this works just fine for horizontal lines but when I have a diagonal, it is putting it -30 to the left of the mid-point as its told to do, when really it needs to account for the direction and adjust the Y as well.

The horizontal line is fine with the -30 but the diagonal one is not where I want it to be (understanding its where I TOLD it to be).

enter image description here

Here is an example of where I would expect to see the diagonal line ending for the top left box:

enter image description here

How can I go about adjusting the XY based on the direction of the path or is there more to it than that?

JS Fiddle: http://jsfiddle.net/m4fupk7g/5/

Upvotes: 1

Views: 800

Answers (1)

aptriangle
aptriangle

Reputation: 1428

You need to convert your lines to angles and distances, reduce the distances and then convert them back. For example:

let dx = endX - startX;
let dy = endY - startY;

let angle = Math.atan2(dy, dx);
let distance = Math.sqrt(dx * dx + dy * dy) - 20;

let offsetX = Math.cos(angle) * distance + startX;
let offsetY = Math.sin(angle) * distance + startY;

Upvotes: 2

Related Questions