KonvaJS connect squares and correct line placement?
Asked Answered
I

2

2

So I am building a UML drawing tool with KonvaJS and KonvaReact, for that I need to connect shapes with lines. I saw the tutorial on the website on connected objects https://konvajs.org/docs/sandbox/Connected_Objects.html.

They use a function get_connecter_points that calculates the possition from the line based on the radians on the circle.

function getConnectorPoints(from, to) {
        const dx = to.x - from.x;
        const dy = to.y - from.y;
        let angle = Math.atan2(-dy, dx);

        const radius = 50;

        return [
          from.x + -radius * Math.cos(angle + Math.PI),
          from.y + radius * Math.sin(angle + Math.PI),
          to.x + -radius * Math.cos(angle),
          to.y + radius * Math.sin(angle)
        ];
      }

I am trying to comeup with a simular function, but can't comeup with a good solution or find a good example. As you can see in the immage I just returned the from x and y and to x and y in the function and so the lines will be placed in the left top corner of every square.

enter image description here

The goal of the function should be to place the lines halfway to the side of the square and on the correct side of the square. So when the to square is placed below it should appear on the bottom side.

So if someone has a solution, any help is appreciated.

Involute answered 31/3, 2020 at 15:17 Comment(1)
You should consider abstracting the drawing into a model. The model will have objects that represent the shapes, hold their connector points, store link relationships, and importantly have methods (functions) to wrap the calculations you will need to manage for redrawing on drag events etc. The shapes on the diagram become a property of the shape in your model. Shape drawing code then becomes more granular, simple and easier to debug and maintain. More about models here.Aerodynamics
S
6

For the rectangles, the math is a bit more complex than for circles.

First, you need to calculate the angle for connection line, between two objects:

function getCenter(node) {
  return {
    x: node.x() + node.width() / 2,
    y: node.y() + node.height() / 2
  }
}
const c1 = getCenter(object1);
const c2 = getCenter(object2;

const dx = c1.x - c2.x;
const dy = c1.y - c2.y;
const angle = Math.atan2(-dy, dx);

Second, when you know the angle, you need a function, that finds a point of the rectangle border that you can use to connect with another object.

function getRectangleBorderPoint(radians, size, sideOffset = 0) {
  const width = size.width + sideOffset * 2;

  const height = size.height + sideOffset * 2;

  radians %= 2 * Math.PI;
  if (radians < 0) {
    radians += Math.PI * 2;
  }

  const phi = Math.atan(height / width);

  let x, y;
  if (
    (radians >= 2 * Math.PI - phi && radians <= 2 * Math.PI) ||
    (radians >= 0 && radians <= phi)
  ) {
    x = width / 2;
    y = Math.tan(radians) * x;
  } else if (radians >= phi && radians <= Math.PI - phi) {
    y = height / 2;
    x = y / Math.tan(radians);
  } else if (radians >= Math.PI - phi && radians <= Math.PI + phi) {
    x = -width / 2;
    y = Math.tan(radians) * x;
  } else if (radians >= Math.PI + phi && radians <= 2 * Math.PI - phi) {
    y = -height / 2;
    x = y / Math.tan(radians);
  }

  return {
    x: -Math.round(x),
    y: Math.round(y)
  };
}

Now, you just need to generate points for line shape:

function getPoints(r1, r2) {
  const c1 = getCenter(r1);
  const c2 = getCenter(r2);

  const dx = c1.x - c2.x;
  const dy = c1.y - c2.y;
  const angle = Math.atan2(-dy, dx);

  const startOffset = getRectangleBorderPoint(angle + Math.PI, r1.size());
  const endOffset = getRectangleBorderPoint(angle, r2.size());

  const start = {
    x: c1.x - startOffset.x,
    y: c1.y - startOffset.y
  };

  const end = {
    x: c2.x - endOffset.x,
    y: c2.y - endOffset.y
  };

  return [start.x, start.y, end.x, end.y]
}

function updateLine() {
  const points = getPoints(rect1, rect2);
  line.points(points);
}

All of this as a demo:

function getRectangleBorderPoint(radians, size, sideOffset = 0) {
  const width = size.width + sideOffset * 2;

  const height = size.height + sideOffset * 2;

  radians %= 2 * Math.PI;
  if (radians < 0) {
    radians += Math.PI * 2;
  }

  const phi = Math.atan(height / width);

  let x, y;
  if (
    (radians >= 2 * Math.PI - phi && radians <= 2 * Math.PI) ||
    (radians >= 0 && radians <= phi)
  ) {
    x = width / 2;
    y = Math.tan(radians) * x;
  } else if (radians >= phi && radians <= Math.PI - phi) {
    y = height / 2;
    x = y / Math.tan(radians);
  } else if (radians >= Math.PI - phi && radians <= Math.PI + phi) {
    x = -width / 2;
    y = Math.tan(radians) * x;
  } else if (radians >= Math.PI + phi && radians <= 2 * Math.PI - phi) {
    y = -height / 2;
    x = y / Math.tan(radians);
  }

  return {
    x: -Math.round(x),
    y: Math.round(y)
  };
}

const stage = new Konva.Stage({
  container: 'container',
  width: window.innerWidth,
  height: window.innerHeight
});

const layer = new Konva.Layer();
stage.add(layer);

const rect1 = new Konva.Rect({
  x: 20,
  y: 20,
  width: 50,
  height: 50,
  fill: 'green',
  draggable: true
});
layer.add(rect1);


const rect2 = new Konva.Rect({
  x: 220,
  y: 220,
  width: 50,
  height: 50,
  fill: 'red',
  draggable: true
});
layer.add(rect2);

const line = new Konva.Line({
  stroke: 'black'
});
layer.add(line);

function getCenter(node) {
  return {
    x: node.x() + node.width() / 2,
    y: node.y() + node.height() / 2
  }
}

function getPoints(r1, r2) {
  const c1 = getCenter(r1);
  const c2 = getCenter(r2);

  const dx = c1.x - c2.x;
  const dy = c1.y - c2.y;
  const angle = Math.atan2(-dy, dx);

  const startOffset = getRectangleBorderPoint(angle + Math.PI, rect1.size());
  const endOffset = getRectangleBorderPoint(angle, rect2.size());

  const start = {
    x: c1.x - startOffset.x,
    y: c1.y - startOffset.y
  };

  const end = {
    x: c2.x - endOffset.x,
    y: c2.y - endOffset.y
  };
  
  return [start.x, start.y, end.x, end.y]
}

function updateLine() {
  const points = getPoints(rect1, rect2);
  line.points(points);
}

updateLine();
layer.on('dragmove', updateLine);

layer.draw();
  <script src="https://unpkg.com/konva@^3/konva.min.js"></script>
  <div id="container"></div>
Smelter answered 2/4, 2020 at 15:29 Comment(2)
Wow, this way more complex then I thought it was thanks a lot.Involute
If anyone else was a little lost trying to understand that getRectangleBorderPoint function, here's a Desmos graph to visualize it a little bit, and part of an explanation of where the formulas come from.Dermatology
D
1

The easiest way to do this would probably just be to have two layers, one for lines that connect the centers of the rectangles, and a layer that draws the rectangles on top of the lines.

Other than that though, I thought of another way to solve the "point on a rectangle problem" that works out a bit nicely in my opinion.

Starting from this implicit formula for a rectangle (I got it from here, but I think you can also derive it by rotating the coordinate system of \left| x \right| + \left| y \right| = 1 and stretching the vertical and horizontal axes):

\left| \frac{x}{w} + \frac{y}{h} \right| + \left| \frac{x}{w} - \frac{y}{h} \right| = 1

We can convert to polar coordinates by substituting x = r \cos{\theta} and y = r \sin{\theta}. Rewriting as a function of the radius to get:

r = \frac{1}{\left| \frac{\cos{\theta}}{w} + \frac{\sin{\theta}}{h} \right| + \left| \frac{\cos{\theta}}{w} - \frac{\sin{\theta}}{h} \right|}

We can then convert that back into rectangular coordinates with the same substitution and get a parametric function of θ:

x(\theta) = \frac{\cos{\theta}}{\left| \frac{\cos{\theta}}{w} + \frac{\sin{\theta}}{h} \right| + \left| \frac{\cos{\theta}}{w} - \frac{\sin{\theta}}{h} \right|}

y(\theta) = \frac{\sin{\theta}}{\left| \frac{\cos{\theta}}{w} + \frac{\sin{\theta}}{h} \right| + \left| \frac{\cos{\theta}}{w} - \frac{\sin{\theta}}{h} \right|}

So why is this useful? Besides the fact that we don't have to worry about what range the angle falls in, working in polar coordinates makes rotation trivial - it's just a domain shift.

Using that polar function of r (for ease of notation) we can express that as (letting α be the angle we are rotating counterclockwise by):

x(\theta) = r(\theta - \alpha) \cos{\theta}}

y(\theta) = r(\theta - \alpha) \sin{\theta}}

Which you can play around with in this desmos visualization: https://www.desmos.com/calculator/zgi3jzb2eg

Putting that into practice (assuming rotation about the center), we get:

function degreesToRadians(degrees) {
    return Math.PI * degrees / 180;
}


function getPointOnRectangle(width, height, angle, rotation) {
    const rot_angle = angle - rotation;

    const radius = 1 / (
        Math.abs(Math.cos(rot_angle) / width + Math.sin(rot_angle) / height) + Math.abs(Math.cos(rot_angle) / width - Math.sin(rot_angle) / height)
    );

    return {x: radius * Math.cos(angle), y: radius * Math.sin(angle)};
}


function connectRects(rectA, rectB, line, margin=0) {
    const deltaX = rectB.x() - rectA.x();
    const deltaY = rectB.y() - rectA.y();

    const angleA = Math.atan2(deltaY, deltaX);
    const angleB = Math.PI + angleA;

    const rotA = Konva.angleDeg? degreesToRadians(rectA.rotation()) : rectA.rotation();
    const rotB = Konva.angleDeg? degreesToRadians(rectB.rotation()) : rectB.rotation();

    const relA = getPointOnRectangle(rectA.width() + margin, rectA.height() + margin, angleA, rotA);
    const relB = getPointOnRectangle(rectB.width() + margin, rectB.height() + margin, angleB, rotB);

    line.points([
        rectA.x() + relA.x,
        rectA.y() + relA.y,
        rectB.x() + relB.x,
        rectB.y() + relB.y,
    ]);
}

Codepen Demo (adapted from lavrton's demo) here: https://codepen.io/dsfbmmg/pen/WNEVjgW

Dermatology answered 26/11, 2021 at 5:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.