Rayhan Hamada
Rayhan Hamada

Reputation: 33

Konva: get corners coordinate of a rotated rectangle

How can i get corners coordinate of a rotated rectangle (with center of rectangle as pivot) ?

i already tried all of the solution from the link below but seems haven't got any luck.

Rotating a point about another point (2D)

Find corners of a rotated rectangle given its center point and rotation

https://gamedev.stackexchange.com/questions/86755/how-to-calculate-corner-positions-marks-of-a-rotated-tilted-rectangle

here's the code

// make a rectangle with zero rotation
const rect1 = new Konva.Rect({
  x: 200,
  y: 200,
  width: 100,
  height: 50,
  fill: "#00D2FF",
  draggable: true,
  rotation: 0,
  name: "rect"
});

// convert degree to rad
const degToRad = (deg: number) => deg * (Math.PI / 180);

// here's the code i use to rotate it around its center (from https://konvajs.org/docs/posts/Position_vs_Offset.html)

const rotateAroundCenter = (node: Rect, rotation: number) => {
     const topLeft = {
    x: -node.width() / 2,
    y: -node.height() / 2
  };
  console.log(`current X: ${node.x()}, current Y: ${node.y()},`)

  const currentRotatePoint = rotatePoint(topLeft, degToRad(node.rotation()));
  const afterRotatePoint = rotatePoint(topLeft, degToRad(rotation));
  const dx = afterRotatePoint.x - currentRotatePoint.x;
  const dy = afterRotatePoint.y - currentRotatePoint.y;

  node.rotation(rotation);
  node.x(node.x() + dx);
  node.y(node.y() + dy);
  layer.draw();

console.log(`the actual position x: ${node.x()}, y: ${node.y()}`);
};

// the code that i expected to give me the corner point

const computeCornerPoint = (r:Rect) => {
  // for now we want to compute top left corner point(as it's the easiest corner to get)
  let corner = {
     x: r.x(),
     y: r.y()
  };

  // the coordinate of rectangle's center (in stage coordinate)
  const cx = r.x() + r.width() / 2;
  const cy = r.y();

  // sine and cosine of the rectangle's rotation
  const s = Math.sin(degToRad(r.rotation()));
  const c = Math.cos(degToRad(r.rotation()));

  // rotate the corner point
  let xnew = c * (corner.x - cx) - s * (corner.y - cy) + cx;
  let ynew = s * (corner.x - cx) + c * (corner.y - cy) + cy;

  console.log(`based on this function calculation: xnew : ${xnew}, ynew: ${ynew}`);
  return [xnew, ynew];
}

based on the code above, if the initial rotation is 0, and i rotate the rectangle 30 degree clockwise, then the actual position would be same as the value from computeCornerPoint, which is (219, 178) and if i rotate it again by 30 degree clockwise, the actual position would be (246, 169) while the value from computeCornerPoint would be (275, 175).

Upvotes: 3

Views: 4352

Answers (2)

Vanquished Wombat
Vanquished Wombat

Reputation: 9545

I recently learned of a new, easier way to achieve this using a built-in Konva function. The node.getTransform and its close relation node.getAbsoluteTransform methods will retrieve the transform applied to the node (shape). The absolute version gets the transform including the parent transform, while the plain getTransform gets the transform relative to the node's parent.

Both return a Konva.Trasform object, which itself has the point() method that will take a given {x, y} object and apply the transform to it.

Using the transform applied to the shape means that we do not have to be concerned with how to mimic the steps of that transform - we just ask for the same transform to be applied to our points.

Which means that we can do this...

// assuming we have a Konva rect already...
let rect = new Konva.Rect({
  x: 100,
  y: 80,
  width: 60,
  height: 20,
  fill: 'cyan'
  rotation: 45    
})

let corners = [],
    size = rect.size();

// Now get the 4 corner points
corners[0] = {x: 0, y: 0 }; // top left
corners[1] = {x: size.width, y: 0 }; // top right
corners[2] = {x: size.width, y: size.height }; // bottom right
corners[4] = {x: 0, y: size.height }; // bottom left

// And rotate the corners using the same transform as the rect.
for (let i = 0; i < 4; i++){
  // Here be the magic
  corners[i] = rect.getAbsoluteTransform().point(corners[i]); // top left
}

// At this point we have the rotated positions of the corners.

IMPORTANT NOTE

You will have seen that the corners of the rect in the above code are set relative to the origin and not the rect position. In other words the top-left corner is {x: 0, y: 0} and not {x: rect.x(), y: rect.y()}, and the bottom-right is {x: rect.width, y: rect.height}. This is because the rect's transform is:

  1. moveto(x, y)
  2. rotate(angle)

If we do not negate the moveto when deciding our unrotated corner points then they will appear to have experienced 2 times the moveto transform.

The moveto transform is not obvious - it is the effect of setting the shape.x() and shape.y() in the initial declaration of the shape.

Here is a working snippet using the getAbsoluteTransform() method.

// Function to rotate a point.
// node = the shape we are using to deterine the transform of the pt.
// pt = {x,y} of point to rotate, 
// returns {x, y} giving the new point.
function rotatePoint(node, pt){

  return node.getAbsoluteTransform().point(pt);
}

// This is just about drawing the circles at the corners.
function drawCorners(rect, angle){

  var rectPos = rect.position();
  
  var x = 0, y = 0;
  for (var i = 0; i < 4; i = i + 1){

  switch (i){
    
    case 0: 
      x = 0; y = 0;
      break;

    case 1: 
      x = rect.width(); y = 0;
      break;

    case 2: 
      x = rect.width(); y = rect.height();
      break;

    case 3: 
      x = 0; y = rect.height();
      break;

     }

    var pt = rotatePoint(rect, {x: x, y: y})
    circles[i].position(pt)

  }
 }


// rotate and redraw the rectangle
function rotateUnderMouse(){


  // Get the stage position of the mouse
  var mousePos = stage.getPointerPosition();

  // get the stage position of the mouse
  var shapePos = rect.position();

  // compute the vector for the difference
  var rel = {x: mousePos.x - shapePos.x, y: mousePos.y - shapePos.y} 

  // Now apply the rotation
  angle = angle + 90;

  circle.position({x: mousePos.x, y: mousePos.y});
  circle.show();
  
  // and reposition the shape to keep the same point in the shape under the mouse 
  var newPos = ({x: mousePos.x  + rel.y , y: mousePos.y - rel.x}) 

  rect.position(newPos);
  rect.rotation(angle);

  // re-calculate and draw the circle positions.
  drawCorners(rect, angle)

  stage.draw()
}





function setup() {

// Set up a stage and a shape
stage = new Konva.Stage({
  container: 'canvas-container',
  width: 650,
  height: 300
});


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

newPos = {x: 80, y: 100};
rect = new Konva.Rect({
   width: 140, height: 50, x: newPos.x, y: newPos.y, draggable: true, stroke: 'silver', fill: 'cyan'
  })

// not very dry, setting up the corner circles.
circle = new Konva.Circle({x: newPos.x, y: newPos.y, radius: 10, fill: 'magenta', listening: false}) 
circles[0] = circle.clone();
circles[0].fill('lime')
layer.add(circles[0]);
circles[1] = circle.clone();
circles[1].fill('gold')
layer.add(circles[1]);
circles[2] = circle.clone();
circles[2].fill('blue')
layer.add(circles[2]);
circles[3] = circle.clone();
circles[3].fill('darkviolet')

layer.add(circles[3]);

layer.add(rect);
layer.add(circle);
circle.hide()

drawCorners(rect, 0)

stage.draw()

rect.on('mousedown', function(){
  rotateUnderMouse()
})

}

var stage, layer, rect, circles = [], angle = 0;

setup()
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/4.0.13/konva.js"></script>

<p>Click the rectangle - it will rotate 90 degrees clockwise under the mouse and coloured circles will be drawn consistently at the corners. These circles have their position derived via the rect.transform - not calculated. NB: No dragging !</p>

<div id="canvas-container"></div>

Upvotes: 3

Vanquished Wombat
Vanquished Wombat

Reputation: 9545

Life is rectangular in the world of the canvas so all we need to do to predict the corner positions is know the shape top-left and the rotation angle, then apply some high-school math. The math to rotate a point is in function rotatePoint(). The rest of the snippet is setup for its use and illustration of the outcome.

Maybe better running the snippet in full screen mode.

// Function to rotate a point.
// pt = {x,y} of point to rotate, 
// o = {x, y} of rotation origin, 
// a = angle of rotation in degrees.
// returns {x, y} giving the new point.
function rotatePoint(pt, o, a){

  var angle = a * (Math.PI/180); // Convert to radians

  var rotatedX = Math.cos(angle) * (pt.x - o.x) - Math.sin(angle) * (pt.y - o.y) + o.x;

  var rotatedY = Math.sin(angle) * (pt.x - o.x) + Math.cos(angle) * (pt.y - o.y) + o.y;  

  return {x: rotatedX, y: rotatedY};

}

// This is just about drawing the circles at the corners.
function drawCorners(rect, angle){

  var rectPos = rect.position();
  
  var x = 0, y = 0;
  for (var i = 0; i < 4; i = i + 1){

  switch (i){
    
    case 0: 
      x = rectPos.x; y = rectPos.y;
      break;

    case 1: 
      x = rectPos.x + rect.width(); y = rectPos.y;
      break;

    case 2: 
      x = rectPos.x + rect.width(); y = rectPos.y + rect.height();
      break;

    case 3: 
      x = rectPos.x; y = rectPos.y + rect.height();
      break;

     }

    var pt = rotatePoint({x: x, y: y}, {x: rectPos.x, y: rectPos.y}, angle)
    circles[i].position(pt)

  }
 }


// rotate and redraw the rectangle
function rotateUnderMouse(){


  // Get the stage position of the mouse
  var mousePos = stage.getPointerPosition();

  // get the stage position of the mouse
  var shapePos = rect.position();

  // compute the vector for the difference
  var rel = {x: mousePos.x - shapePos.x, y: mousePos.y - shapePos.y} 

  // Now apply the rotation
  angle = angle + 90;

  circle.position({x: mousePos.x, y: mousePos.y});
  circle.show();
  
  // and reposition the shape to keep the same point in the shape under the mouse 
  var newPos = ({x: mousePos.x  + rel.y , y: mousePos.y - rel.x}) 

  rect.position(newPos);
  rect.rotation(angle);

  // re-calculate and draw the circle positions.
  drawCorners(rect, angle)

  stage.draw()
}





function setup() {

// Set up a stage and a shape
stage = new Konva.Stage({
  container: 'canvas-container',
  width: 650,
  height: 300
});


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

newPos = {x: 80, y: 100};
rect = new Konva.Rect({
   width: 140, height: 50, x: newPos.x, y: newPos.y, draggable: true, stroke: 'silver', fill: 'cyan'
  })

// not very dry, setting up the corner circles.
circle = new Konva.Circle({x: newPos.x, y: newPos.y, radius: 10, fill: 'magenta'}) 
circles[0] = circle.clone();
circles[0].fill('lime')
layer.add(circles[0]);
circles[1] = circle.clone();
circles[1].fill('gold')
layer.add(circles[1]);
circles[2] = circle.clone();
circles[2].fill('blue')
layer.add(circles[2]);
circles[3] = circle.clone();
circles[3].fill('darkviolet')

layer.add(circles[3]);

layer.add(rect);
layer.add(circle);
circle.hide()

drawCorners(rect, 0)

stage.draw()

rect.on('mousedown', function(){
  rotateUnderMouse()
})

}

var stage, layer, rect, circles = [], angle = 0;

setup()
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/4.0.13/konva.js"></script>

<p>Click the rectangle - it will rotate 90 degrees clockwise under the mouse and coloured circles will be drawn consistently at the corners. These circles have their position calculated rather than derived from the visual rectangle corner positions. NB: No dragging !</p>

<div id="canvas-container"></div>

Upvotes: 3

Related Questions