Получить границы не повернутого повернутого прямоугольника

У меня есть прямоугольник, к которому уже применен поворот. Я хочу получить не повернутые размеры (x, y, ширина, высота).

Вот размеры элемента в настоящее время:

Bounds at a 90 rotation: {
 height     30
 width      0
 x          25
 y          10
}

Вот размеры после поворота, равные none:

Bounds at rotation 0 {
 height     0
 width      30
 x          10
 y          25
}

Раньше я мог установить вращение на 0, а затем прочитать обновленные границы. Однако в одной из функций, которые я использовал, есть ошибка, поэтому теперь я должен сделать это вручную.

Есть ли простая формула, чтобы получить границы при вращении 0, используя информацию, которая у меня уже есть?

Обновление: объект вращается вокруг центра объекта.

ОБНОВИТЬ:
Что мне нужно, это что-то вроде функции ниже:

function getRectangleAtRotation(rect, rotation) {
    var rotatedRectangle = {}
    rotatedRectangle.x = Math.rotation(rect.x * rotation);
    rotatedRectangle.y = Math.rotation(rect.y * rotation);
    rotatedRectangle.width = Math.rotation(rect.width * rotation);
    rotatedRectangle.height = Math.rotation(rect.height * rotation);
    return rotatedRectangle;
}

var rectangle = {x: 25, y: 10, height: 30, width: 0 };
var rect2 = getRectangleAtRotation(rect, -90); // {x:10, y:25, height:0, width:30 }

Я нашел подобный вопрос здесь.

ОБНОВЛЕНИЕ 2
Вот код, который у меня есть. Он пытается получить центральную точку линии, а затем x, y, width и height:

var centerPoint = getCenterPoint(line);
var lineBounds = {};
var halfSize;

halfSize = Math.max(Math.abs(line.end.x-line.start.x)/2, Math.abs(line.end.y-line.start.y)/2);
lineBounds.x = centerPoint.x-halfSize;
lineBounds.y = centerPoint.y;
lineBounds.width = line.end.x;
lineBounds.height = line.end.y;

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

Я знаю, что в моем примере используется прямой угол, и вы можете поменять местами x и y, но вращение может быть любым.

ОБНОВЛЕНИЕ 3

Мне нужна функция, которая возвращает необращенные границы прямоугольника. У меня уже есть границы для определенного поворота.

function getUnrotatedRectangleBounds(rect, currentRotation) {
    // magic
    return unrotatedRectangleBounds;
}

Ответ 1

Я думаю, что могу справиться с вычислением размера границ без особых усилий (несколько уравнений). Вместо этого я не уверен, как бы вы хотели, чтобы x и y обрабатывались.

Во-первых, давайте правильно назовем вещи:

enter image description here

Теперь мы хотим повернуть его на некоторый угол alpha (в радианах):

enter image description here

Для вычисления зеленых сторон ясно, что он состоит из двух повторяющихся прямоугольников-треугольников следующим образом:

enter image description here

Итак, сначала решая углы, мы знаем, что:

  1. сумма углов треугольника равна PI/2 или 180 °;
  2. вращение alpha;
  3. один угол гамма равен PI/4 или 90 °;
  4. последний угол, бета, является gamma - alpha;

Теперь, зная все углы и стороны, мы можем использовать закон синусов для вычисления других сторон.

Вкратце, закон синусов говорит нам, что существует соотношение между отношением длины стороны и ее противоположного угла. Более подробная информация здесь: https://en.wikipedia.org/wiki/Law_of_sines

В нашем случае для верхнего левого треугольника (и нижнего правого) мы имеем:

enter image description here

Помните, что AD - наша первоначальная высота.

Учитывая, что sin(gamma) равен 1, и мы также знаем значение AD, мы можем написать уравнения:

enter image description here

enter image description here

Для верхнего правого треугольника (и нижнего левого) мы имеем:

enter image description here

enter image description here

enter image description here

Имея все необходимые стороны, мы можем легко рассчитать ширину и высоту:

width = EA + AF
height = ED + FB

На данный момент мы можем написать довольно простой метод, который, учитывая прямоугольник и угол поворота в радианах, может возвращать новые границы:

function rotate(rectangle, alpha) {
  const { width: AB, height: AD } = rectangle
  const gamma = Math.PI / 4,
        beta = gamma - alpha,
        EA = AD * Math.sin(alpha),
        ED = AD * Math.sin(beta),
        FB = AB * Math.sin(alpha),
        AF = AB * Math.sin(beta)

  return {
    width: EA + EF,
    height: ED + FB
  }
}

Этот метод может быть использован как:

const rect = { width: 30, height: 50 }
const rotation = Math.PI / 4.2 // this is a random value it put here
const bounds = rotate(rect, rotation)

Надеюсь, что нет опечаток...

Ответ 2

Я думаю, что мог бы найти решение, но для безопасности я предпочитаю предварительно повторить то, что у нас есть и что нам нужно, чтобы быть уверенным, что я все правильно понял. Как я сказал в комментарии, английский не является моим родным языком, и я уже написал неправильный ответ из-за моего непонимания проблемы :)

Что мы имеем

what we have

Мы знаем, что в x и y есть прямоугольник границ (зеленый) размера w и h который содержит другой прямоугольник (серый с точками), повернутый на alpha градусы.

Мы знаем, что ось y перевернута относительно декартовой, и это делает угол, который нужно рассматривать по часовой стрелке, а не против часовой стрелки.

То, что нам нужно

what we need at first

Сначала нам нужно найти 4 вершины внутреннего прямоугольника (A, B, C и D) и, зная положение вершин, размер внутреннего прямоугольника (W и H).

В качестве второго шага нам нужно повернуть внутренний прямоугольник на 0 градусов и найти его положения X и Y

what we need at the end

Найти вершины

Вообще говоря, для каждой вершины мы знаем только одну координату, x или y. Другой "скользит" вдоль боковой граничной рамки по отношению к углу альфа.

Давайте начнем с A: мы знаем Ay, нам нужен Ax.

Мы знаем, что Ax лежит между x и x + w по отношению к углу alpha.

Когда alpha равна 0 °, Ax это x + 0. Когда alpha равна 90 °, Ax это x + w. Когда альфа 45 °, Ax это x + w/2.

По сути, Ax растет по отношению к греху (альфа), давая нам:

computing Ax

Имея Ax, мы можем легко вычислить Cx:

computing Cx

Таким же образом мы можем вычислить By и затем Dy:

computing By

computing Dy

Написание кода:

// bounds is a POJO with shape: { x, y, w, h }, update if needed
// alpha is the rotation IN RADIANS
const vertices = (bounds, alpha) => {
  const { x, y, w, h } = bounds,
        A = { x: x + w * Math.sin(alpha), y },
        B = { x, y: y + h * Math.sin(alpha) },
        C = { x: x + w - w * Math.sin(alpha), y },
        D = { x, y: y + h - h * Math.sin(alpha) }
  return { A, B, C, D }
}

Нахождение сторон

Теперь, когда у нас есть все вершины, мы можем легко вычислить стороны внутреннего прямоугольника, нам нужно определить еще пару точек E и F для ясности объяснения:

additional points

Хорошо видно, что мы можем использовать теорему Питагорея для вычисления W и H с помощью:

compute H

compute W

где:

compute EA

compute ED

compute AF

compute FB

В коде:

// bounds is a POJO with shape: { x, y, w, h }, update if needed
// vertices is a POJO with shape: { A, B, C, D }, as returned by the 'vertices' method
const sides = (bounds, vertices) => {
  const { x, y, w, h } = bounds,
        { A, B, C, D } = vertices,
        EA = A.x - x,
        ED = D.y - y,
        AF = w - EA,
        FB = h - ED,
        H = Math.sqrt(EA * EA + ED * ED),
        W = Math.sqrt(AF * AF + FB * FB
  return { h: H, w: W }
}

Нахождение положения вращающегося в противоположную сторону внутреннего прямоугольника

Прежде всего, мы должны найти углы (beta и gamma) диагоналей внутреннего прямоугольника.

compute diagonals angles

Давайте немного увеличим масштаб и добавим несколько дополнительных букв для большей ясности:

add some letters to compute beta

Мы можем использовать закон синусов, чтобы получить уравнения для вычисления beta:

law of sines

Чтобы сделать некоторые расчеты, мы имеем:

compute GI

compute IC

delta

sin delta

Сначала нам нужно вычислить GC, чтобы хотя бы одна сторона уравнения была полностью известна. GC - радиус окружности, в которую вписан внутренний прямоугольник, а также половина диагонали внутреннего прямоугольника.

Имея две стороны внутреннего прямоугольника, мы снова можем использовать теорему Питагорея:

compute GC

С помощью GC мы можем решить закон синусов на beta:

compute beta 1

мы знаем, что sin(delta) равен 1

compute beta 2

compute beta 3

compute beta 4

compute beta 5

Теперь beta - это угол вершины C относительно не повернутой оси x.

C vertex angle is beta

Глядя снова на это изображение, мы можем легко получить углы всех остальных вершин:

all vertices angles

D angle

A angle

B angle

Теперь, когда у нас есть почти все, мы можем вычислить новые координаты вершины A:

compute A position

compute final A_x

compute final A_y

Отсюда нам нужно перевести и Ax и Ay потому что они связаны с центром окружности, который равен x + w/2 и y + h/2:

compute translated A_x

compute translated A_y

Итак, написание последнего куска кода:

// bounds is a POJO with shape: { x, y, w, h }, update if needed
// sides is a POJO with shape: { w, h }, as returned by the 'sides' method
const origin = (bounds, sides) => {
  const { x, y, w, h } = bounds
  const { w: W, h: H } = sides
  const GC = r = Math.sqrt(W * W + H * H) / 2,
        IC = H / 2,
        beta = Math.asin(IC / GC),
        angleA = Math.PI + beta,
        Ax = x + w / 2 + r * Math.cos(angleA),
        Ay = y + h / 2 + r * Math.sin(angleA)
  return { x: Ax, y: Ay }
}

Собираем все вместе...

// bounds is a POJO with shape: { x, y, w, h }, update if needed
// rotations is... the rotation of the inner rectangle IN RADIANS
const unrotate = (bounds, rotation) => {
  const points = vertices(bounds, rotation),
        dimensions = sides(bounds, points)
  const { x, y } = origin(bounds, dimensions)
  return { ...dimensions, x, y }
}

Я действительно надеюсь, что это решит вашу проблему и что нет опечаток. Это был очень, очень забавный способ провести мои выходные: D

// bounds is a POJO with shape: { x, y, w, h }, update if needed
// alpha is the rotation IN RADIANS
const vertices = (bounds, alpha) => {
	const { x, y, w, h } = bounds,
		  A = { x: x + w * Math.sin(alpha), y },
		  B = { x, y: y + h * Math.sin(alpha) },
		  C = { x: x + w - w * Math.sin(alpha), y },
		  D = { x, y: y + h - h * Math.sin(alpha) }
	return { A, B, C, D }
 }
  
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// vertices is a POJO with shape: { A, B, C, D }, as returned by the 'vertices' method
const sides = (bounds, vertices) => {
  const { x, y, w, h } = bounds,
      { A, B, C, D } = vertices,
      EA = A.x - x,
      ED = D.y - y,
      AF = w - EA,
      FB = h - ED,
      H = Math.sqrt(EA * EA + ED * ED),
      W = Math.sqrt(AF * AF + FB * FB)
  return { h: H, w: W }
}

// bounds is a POJO with shape: { x, y, w, h }, update if needed
// sides is a POJO with shape: { w, h }, as returned by the 'sides' method
const originPoint = (bounds, sides) => {
  const { x, y, w, h } = bounds
  const { w: W, h: H } = sides
  const GC = Math.sqrt(W * W + H * H) / 2,
      r = Math.sqrt(W * W + H * H) / 2,
      IC = H / 2,
      beta = Math.asin(IC / GC),
      angleA = Math.PI + beta,
      Ax = x + w / 2 + r * Math.cos(angleA),
      Ay = y + h / 2 + r * Math.sin(angleA)
  return { x: Ax, y: Ay }
}
  
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// rotations is... the rotation of the inner rectangle IN RADIANS
const unrotate = (bounds, rotation) => {
  const points = vertices(bounds, rotation)
  const dimensions = sides(bounds, points)
  const { x, y } = originPoint(bounds, dimensions)
  return { ...dimensions, x, y }
}

function shortNumber(value) {
  var places = 2;
	value = Math.round(value * Math.pow(10, places)) / Math.pow(10, places);
	return value;
}

function getInputtedBounds() {
  var rectangle = {};
  rectangle.x = parseFloat(app.xInput.value);
  rectangle.y = parseFloat(app.yInput.value);
  rectangle.w = parseFloat(app.widthInput.value);
  rectangle.h = parseFloat(app.heightInput.value);
  return rectangle;
}

function rotationSliderHandler() {
  var rotation = app.rotationSlider.value;
  app.rotationOutput.value = rotation;
  rotate(rotation);
}

function rotationInputHandler() {
  var rotation = app.rotationInput.value;
  app.rotationSlider.value = rotation;
  app.rotationOutput.value = rotation;
  rotate(rotation);
}

function unrotateButtonHandler() {
  var rotation = app.rotationInput.value;
  app.rotationSlider.value = 0;
  app.rotationOutput.value = 0;
  var outerBounds = getInputtedBounds();
  var radians = Math.PI / 180 * rotation;
  var unrotatedBounds = unrotate(outerBounds, radians);
  updateOutput(unrotatedBounds);
}

function rotate(value) {
  var outerBounds = getInputtedBounds();
  var radians = Math.PI / 180 * value;
  var bounds = unrotate(outerBounds, radians);
  updateOutput(bounds);
}

function updateOutput(bounds) {
  app.xOutput.value = shortNumber(bounds.x);
  app.yOutput.value = shortNumber(bounds.y);
  app.widthOutput.value = shortNumber(bounds.w);
  app.heightOutput.value = shortNumber(bounds.h);
}

function onload() {
  app.xInput = document.getElementById("x");
  app.yInput = document.getElementById("y");
  app.widthInput = document.getElementById("w");
  app.heightInput = document.getElementById("h");
  app.rotationInput = document.getElementById("r");
  
  app.xOutput = document.getElementById("x2");
  app.yOutput = document.getElementById("y2");
  app.widthOutput = document.getElementById("w2");
  app.heightOutput = document.getElementById("h2");
  app.rotationOutput = document.getElementById("r2");
  app.rotationSlider = document.getElementById("rotationSlider");
  app.unrotateButton = document.getElementById("unrotateButton");
  
  app.unrotateButton.addEventListener("click", unrotateButtonHandler);
  app.rotationSlider.addEventListener("input", rotationSliderHandler);
  app.rotationInput.addEventListener("change", rotationInputHandler);
  app.rotationInput.addEventListener("input", rotationInputHandler);
  app.rotationInput.addEventListener("keyup", (e) => {if (e.keyCode==13) rotationInputHandler() });
  
  app.rotationSlider.value = app.rotationInput.value;
}

var app = {};
window.addEventListener("load", onload);
* {
  font-family: sans-serif;
  font-size: 12px;
  outline: 0px dashed red;
}

granola {
  display: flex;
  align-items: top;
}

flan {
  width: 90px;
  display: inline-block;
}

hamburger {
  display: flex:
  align-items: center;
}

spagetti {
  display: inline-block;
  font-size: 11px;
  font-weight: bold;
  letter-spacing: 1.5px;
}

fish {
  display: inline-block;
  padding-right: 40px;
  position: relative;
}

input[type=text] {
  width: 50px;
}

input[type=range] {
  padding-top: 10px;
  width: 140px;
  padding-left: 0;
  margin-left: 0;
}

button {
  padding-top: 3px;
  padding-bottom:1px;
  margin-top: 10px;
}
<granola>
  <fish>
    <spagetti>Bounds of Rectangle</spagetti><br><br>
    <flan>x: </flan><input id="x" type="text" value="14.39"><br>
    <flan>y: </flan><input id="y" type="text" value="14.39"><br>
    <flan>width: </flan><input id="w" type="text" value="21.2"><br>
    <flan>height: </flan><input id="h" type="text" value="21.2"><br>
    <flan>rotation:</flan><input id="r" type="text" value="90"><br>
    <button id="unrotateButton">Unrotate</button>    
  </fish>

  <fish>
    <spagetti>Computed Bounds</spagetti><br><br>
    <flan>x: </flan><input id="x2" type="text" disabled="true"><br>
    <flan>y: </flan><input id="y2" type="text"disabled="true"><br>
    <flan>width: </flan><input id="w2" type="text" disabled="true"><br>
    <flan>height: </flan><input id="h2" type="text" disabled="true"><br>
    <flan>rotation:</flan><input id="r2" type="text" disabled="true"><br>
    <input id="rotationSlider" type="range" min="-360" max="360" step="5"><br>
  </fish>
</granola>

Ответ 3

Как это работает?

Расчет с использованием ширины, высоты, х и у

Радианы и углы

Используя градусы, рассчитайте радианы и рассчитайте углы sin и cos:

function calculateRadiansAndAngles(){
  const rotation = this.value;
  const dr = Math.PI / 180;
  const s = Math.sin(rotation * dr);
  const c = Math.cos(rotation * dr);
  console.log(rotation, s, c);
}

document.getElementById("range").oninput = calculateRadiansAndAngles;
<input type="range" min="-360" max="360" id="range"/>

Ответ 4

Это основной код для вращения прямоугольника (Unrotating - это то же самое, только с отрицательным углом) вокруг его центра.

function getUnrotatedRectangleBounds(rect, currentRotation) {

    //Convert deg to radians
    var rot = currentRotation / 180 * Math.PI;
    var hyp = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
    return {
       x: rect.x + rect.width / 2 - hyp * Math.abs(Math.cos(rot)) / 2,
       y: rect.y + rect.height / 2 - hyp * Math.abs(Math.sin(rot)) / 2,
       width: hyp * Math.abs(Math.cos(rot)),
       height: hyp * Math.abs(Math.sin(rot))
       } 
}

Вектор, начинающийся в начале координат (0,0) и заканчивающийся в (ширине, высоте), проецируется на единичный вектор для целевого угла (cos rot, sin rot) * hyp.

Абсолютные значения гарантируют, что ширина и высота являются положительными.

Координаты проекции - это ширина и высота, соответственно, нового прямоугольника.

Для значений x и y возьмите исходные значения в центре (x + rect.x) и переместите его обратно (- 1/2 * NewWidth), чтобы он центрировал новый прямоугольник.

пример

function getUnrotatedRectangleBounds(rect, currentRotation) {
    //Convert deg to radians
    var rot = currentRotation / 180 * Math.PI;
    var hyp = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
    return {
       x: rect.x + rect.width / 2 - hyp * Math.abs(Math.cos(rot)) / 2,
       y: rect.y + rect.height / 2 - hyp * Math.abs(Math.sin(rot)) / 2,
       width: hyp * Math.abs(Math.cos(rot)),
       height: hyp * Math.abs(Math.sin(rot))
    }
}

var originalRectangle = {x:10, y:25, width:30, height:0};
var rotatedRectangle = {x:14.39, y:14.39, width:21.2, height:21.2};
var rotation = 45;
var unrotatedRectangle = getUnrotatedRectangleBounds(rotatedRectangle, rotation);

var boundsLabel = document.getElementById("boundsLabel");
boundsLabel.innerHTML = JSON.stringify(unrotatedRectangle);
<span id="boundsLabel"></span>