获取未旋转的矩形的边界

时间:2019-01-07 10:11:32

标签: javascript canvas ecmascript-6 transform rectangles

我有一个已经对其应用旋转的矩形。我想获得未旋转的尺寸(x,y,宽度,高度)。

以下是当前元素的尺寸:

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

以下是将旋转设置为无后的尺寸:

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

过去,我能够将旋转度设置为0,然后读取更新的bounds。但是,我正在使用的其中一个功能存在一个错误,因此现在我必须手动进行操作。

是否有一个简单的公式可以使用我已有的信息获取旋转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 }

我发现了类似的问题here

更新2
这是我的代码。它将尝试获取线的中心点,然后获取x,y,宽度和高度:

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;
}

4 个答案:

答案 0 :(得分:13)

我想我可以毫不费力地(很少的方程式)来处理边界大小的计算,但是我不确定如何处理xy

首先,让我们正确命名:

enter image description here

现在,我们要将其旋转某个角度alpha(以弧度为单位):

enter image description here

要计算绿色边,很明显,它是由两个重复的矩形三角形组成的,如下所示:

enter image description here

所以,首先解决角度,我们知道:

  1. 三角形的角度之和为PI / 2,即180°;
  2. 轮换为alpha;
  3. 一个角度伽玛为PI / 4或90°;
  4. 最后一个角度beta是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)

希望没有错别字...

答案 1 :(得分:11)

我认为我可能会找到解决方案,但是为了安全起见,我宁愿事先重复我们拥有的和需要确保我正确理解所有内容的内容。正如我在评论中说的那样,英语不是我的母语,并且由于我对问题的理解不够,我已经写错了答案:)

我们有什么

what we have

我们知道在xy处有一个边界矩形(绿色),其大小为wh,其中包含另一个旋转的矩形(灰色虚线) alpha度。

我们知道y轴相对于笛卡尔坐标轴是相对翻转的,因此可以将角度视为顺时针,而不是逆时针。

我们需要什么

what we need at first

首先,我们需要找到内部矩形的4个顶点(ABCD),并知道顶点的位置,即内部矩形的大小(WH)。

第二步,我们需要将内部矩形反向旋转到0度,然后找到其位置XY

what we need at the end

找到顶点

通常来说,对于每个顶点,我们只知道一个坐标,即x或y。另一个相对于角度α沿边界框的侧面“滑动”。

让我们从A开始:我们知道Ay,我们需要Ax

我们知道Ax相对于角度x位于x + walpha之间。

alpha为0°时,Axx + 0。当alpha为90°时,Axx + w。当alpha为45°时,Axx + w / 2

基本上,Ax与sin(alpha)的关系增长,给我们:

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 }
}

发现双方

现在我们有了所有顶点,我们可以轻松计算内部矩形的边,为清晰起见,我们需要定义另外两个点EF

additional points

很明显,我们可以使用Pitagorean定理来计算WH

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 }
}

找到反向旋转的内部矩形的位置

首先,我们必须找到内部矩形对角线的角度(betagamma)。

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

在这里,我们需要同时翻译AxAy,因为它们与圆周中心有关,即x + w / 2y + 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>

答案 2 :(得分:5)

这是如何工作的?

使用宽度,高度,x和y的计算

弧度和角度

使用度数计算弧度并计算sincos角:

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分

我们假设矩形的原点是中心,位置为0,0

double for循环将为ij创建以下值对:(-1,-1),(-1,1),(1,-1)和(1 ,1)

使用每对,我们可以计算4个平方向量之一。

(即(-1,1),i = -1, j = 1

const px = w*i/2; //-> 30 * -1/2 = -15
const py = h*j/2; //-> 50 * 1/2  = 25
//[-15,25]

一旦有了一个点,就可以通过包含旋转来计算该点的新位置。

const nx = (px*c) - (py*s);
const ny = (px*s) + (py*c);

解决方案

一旦所有点都是根据旋转计算出来的,我们就可以重新绘制正方形。

在绘制调用之前,使用translate将光标定位在矩形的xy上。这就是为什么我能够假定矩形的中心和原点为0,0的原因。

const canvas = document.getElementById("canvas");
const range = document.getElementById("range");
const rotat = document.getElementById("rotat");

range.addEventListener("input", function(e) {
  rotat.innerText = this.value;
  handleRotation(this.value);
})

const context = canvas.getContext("2d");
const container = document.getElementById("container");

const rect = {
  x: 50,
  y: 75,
  w: 30,
  h: 50
}

function handleRotation(rotation) {
  const { w, h, x, y } = rect;
  
  const dr = Math.PI / 180;
  const s = Math.sin(rotation * dr);
  const c = Math.cos(rotation * dr);
  
  const points = [];
  for(let i = -1; i < 2; i+=2){
    for(let j = -1; j < 2; j+=2){
      
      const px = w*i/2;
      const py = h*j/2;
      
      const nx = (px*c) - (py*s);
      const ny = (px*s) + (py*c);
      points.push([nx, ny]);
    }
  }
  //console.log(points);
  draw(points);
  
}

function draw(points) {
  context.clearRect(0,0,canvas.width, canvas.height);
  context.save();
  context.translate(rect.x+(rect.w/2), rect.y + (rect.h/2))
  context.beginPath();
  context.moveTo(...points.shift());
  [...points.splice(0,1), ...points.reverse()]
  .forEach(p=>{
    context.lineTo(...p);
  })
  context.fill();
  context.restore();
}

window.onload = () => handleRotation(0);
div {
  display: flex;
  background-color: lightgrey;
  padding: 0 5px;
}

div>p {
  padding: 0px 10px;
}

div>input {
  flex-grow: 1;
}

canvas {
  border: 1px solid black;
}
<div>
  <p id="rotat">0</p>
  <input type="range" id="range" min="-360" max="360" value="0" step="5" />
</div>
<canvas id="canvas"></canvas>

答案 3 :(得分:1)

这是围绕矩形中心旋转的基本代码(仅在负角度下,不旋转是相同的事情。

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)开始到(width,height)结束的向量被投影到目标角度(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>