在给定角度上找到矩形上的点

21

我想在一个矩形对象中画一个渐变,给定一个角度(Theta),让渐变的两端触及矩形的周长。

Graph

我认为使用正切函数会起作用,但是我无法消除其中的错误。有没有易于实现的算法?

最终结果

因此,这将是(angle, RectX1, RectX2, RectY1, RectY2)的一个函数。我希望以[x1, x2, y1, y2]的形式返回,以便渐变跨越整个正方形。 在我的问题中,如果原点为0,则x2=-x1且y2=-y1。但不总是在原点上。


3
这张图片与问题有什么关系?线的一端(我假设这条线是斜边)只触及到边界。这条线会不会始终经过(或者如图片所示,从)原点? - aaronasterling
1
@aaronasterling,这是我对我所要实现的理解。我需要X和Y两者。三角形将根据角度而改变。 - bradlis7
9个回答

43

让我们称矩形的边为ab(x0,y0)是矩形中心的坐标。

您需要考虑四个区域:

alt text

    区域     起点            终点               位置
    ====================================================================
       1      -arctan(b/a)    +arctan(b/a)      右侧绿色三角形
       2      +arctan(b/a)    π-arctan(b/a)   上方黄色三角形
       3      π-arctan(b/a) π+arctan(b/a)   左侧绿色三角形
       4      π+arctan(b/a) -arctan(b/a)      下方黄色三角形

通过一些三角运算技巧,我们可以在每个区域中得出所需交点的坐标:

alt text

因此,当处于第1和第3区域时,Z0是交点的表达式。
当处于第2和第4区域时,Z1是交点的表达式。

所需的线从(X0,Y0)到Z0或Z1,具体取决于所处的区域。 因此,请记住Tan(φ)=Sin(φ)/Cos(φ):

    区域中的线段     起点                     终点
    ======================================================================
       1 和 3        (X0,Y0)      (X0 + a/2 , (a/2 * Tan(φ))+ Y0
       2 和 4        (X0,Y0)      (X0 + b/(2* Tan(φ)) , b/2 + Y0)

请注意每个象限中Tan(φ)的符号以及角度始终是从正X轴逆时针测量的。

希望对您有所帮助!


2
我不理解你的回答或其他回答中的两个角度φ和θ代表什么--问题不是只指定一个角度吗?在区域1中,相交/终点的x坐标应该与区域3中的不同(在区域2中相交点的y坐标也应该与区域4中的不同),对吗? - Victor Van Hee
1
@VictorVanHee:角度是从中心点开始的角度。他使用了两个字母来表示它们是不同的可能的代码路径,但在公式中,它只是“theta”(或者你使用的任何变量)。 - Olie
@belisarius: 你能解释一下关于“只需注意每个象限中 Tan(φ) 的符号即可”的最后一句话吗?我已经做得非常接近了,但我剩下的错误肯定与不理解如何根据 tan(φ)的符号修改公式有关。谢谢! - Olie
1
@Olie:区域2的结束位置X值低于X0(毕竟在其左侧),因此正确计算区域2中X的方法是x(end)=X0 - a/2(请注意减号而非加号)。同样地,在区域4中,Y值的计算方式为Y(end)=Y0 - b/2 - Felix Alcala

14

好的,呼!,我终于搞定了。

注意:我是基于belisarius棒极了的答案进行的。如果你喜欢这个,也请点赞他的回答。我所做的只是把他说的话转化成代码。

下面是Objective-C的样子。将它转换为你最喜欢的语言应该很简单。

+ (CGPoint) edgeOfView: (UIView*) view atAngle: (float) theta
{
    // Move theta to range -M_PI .. M_PI
    const double twoPI = M_PI * 2.;
    while (theta < -M_PI)
    {
        theta += twoPI;
    }

    while (theta > M_PI)
    {
        theta -= twoPI;
    }

    // find edge ofview
    // Ref: https://dev59.com/Tm855IYBdhLWcg3w-JMr
    float aa = view.bounds.size.width;                                          // "a" in the diagram
    float bb = view.bounds.size.height;                                         // "b"

    // Find our region (diagram)
    float rectAtan = atan2f(bb, aa);
    float tanTheta = tan(theta);

    int region;
    if ((theta > -rectAtan)
    &&  (theta <= rectAtan) )
    {
        region = 1;
    }
    else if ((theta >  rectAtan)
    &&       (theta <= (M_PI - rectAtan)) )
    {
        region = 2;
    }
    else if ((theta >   (M_PI - rectAtan))
    ||       (theta <= -(M_PI - rectAtan)) )
    {
        region = 3;
    }
    else
    {
        region = 4;
    }

    CGPoint edgePoint = view.center;
    float xFactor = 1;
    float yFactor = 1;

    switch (region)
    {
        case 1: yFactor = -1;       break;
        case 2: yFactor = -1;       break;
        case 3: xFactor = -1;       break;
        case 4: xFactor = -1;       break;
    }

    if ((region == 1)
    ||  (region == 3) )
    {
        edgePoint.x += xFactor * (aa / 2.);                                     // "Z0"
        edgePoint.y += yFactor * (aa / 2.) * tanTheta;
    }
    else                                                                        // region 2 or 4
    {
        edgePoint.x += xFactor * (bb / (2. * tanTheta));                        // "Z1"
        edgePoint.y += yFactor * (bb /  2.);
    }

    return edgePoint;
}

此外,我创建了一个小的测试视图来验证其工作情况。创建此视图并将其放在某个地方,它将使另一个小视图在边缘周围移动。

@interface DebugEdgeView()
{
    int degrees;
    UIView *dotView;
    NSTimer *timer;
}

@end

@implementation DebugEdgeView

- (void) dealloc
{
    [timer invalidate];
}


- (id) initWithFrame: (CGRect) frame
{
    self = [super initWithFrame: frame];
    if (self)
    {
        self.backgroundColor = [[UIColor magentaColor] colorWithAlphaComponent: 0.25];
        degrees = 0;
        self.clipsToBounds = NO;

        // create subview dot
        CGRect dotRect = CGRectMake(frame.size.width / 2., frame.size.height / 2., 20, 20);
        dotView = [[DotView alloc] initWithFrame: dotRect];
        dotView.backgroundColor = [UIColor magentaColor];
        [self addSubview: dotView];

        // move it around our edges
        timer = [NSTimer scheduledTimerWithTimeInterval: (5. / 360.)
                                                 target: self
                                               selector: @selector(timerFired:)
                                               userInfo: nil
                                                repeats: YES];
    }

    return self;
}


- (void) timerFired: (NSTimer*) timer
{
    float radians = ++degrees * M_PI / 180.;
    if (degrees > 360)
    {
        degrees -= 360;
    }

    dispatch_async(dispatch_get_main_queue(), ^{
        CGPoint edgePoint = [MFUtils edgeOfView: self atAngle: radians];
        edgePoint.x += (self.bounds.size.width  / 2.) - self.center.x;
        edgePoint.y += (self.bounds.size.height / 2.) - self.center.y;
        dotView.center = edgePoint;
    });
}

@end

太棒了!我刚在Java中实现了这个代码。我不得不在计算中交换第2和第4区域,并且我必须在第1和第3区域使用正y因子,但我认为这是因为在Cocoa/Objective-C中原点在左下角。太棒了!干得好! - Michael
你是将theta作为弧度还是“以东方为0度,顺时针0到180度,逆时针0到-180度”的角度? - MiltsInit

12

Javascript 版本:

function edgeOfView(rect, deg) {
  var twoPI = Math.PI*2;
  var theta = deg * Math.PI / 180;
  
  while (theta < -Math.PI) {
    theta += twoPI;
  }
  
  while (theta > Math.PI) {
    theta -= twoPI;
  }
  
  var rectAtan = Math.atan2(rect.height, rect.width);
  var tanTheta = Math.tan(theta);
  var region;
  
  if ((theta > -rectAtan) && (theta <= rectAtan)) {
      region = 1;
  } else if ((theta > rectAtan) && (theta <= (Math.PI - rectAtan))) {
      region = 2;
  } else if ((theta > (Math.PI - rectAtan)) || (theta <= -(Math.PI - rectAtan))) {
      region = 3;
  } else {
      region = 4;
  }
  
  var edgePoint = {x: rect.width/2, y: rect.height/2};
  var xFactor = 1;
  var yFactor = 1;
  
  switch (region) {
    case 1: yFactor = -1; break;
    case 2: yFactor = -1; break;
    case 3: xFactor = -1; break;
    case 4: xFactor = -1; break;
  }
  
  if ((region === 1) || (region === 3)) {
    edgePoint.x += xFactor * (rect.width / 2.);                                     // "Z0"
    edgePoint.y += yFactor * (rect.width / 2.) * tanTheta;
  } else {
    edgePoint.x += xFactor * (rect.height / (2. * tanTheta));                        // "Z1"
    edgePoint.y += yFactor * (rect.height /  2.);
  }
  
  return edgePoint;
};


谢谢你!我刚开始写代码,结果发现已经有人做过了 :) - Tomáš Zato
它为我节省了很多时间。谢谢,伙计。 - Mayur Kukadiya

4
根据您的图片,我假设矩形居中于(0,0),右上角为(w,h)。那么连接(0,0)和(w,h)的线段与X轴成角度φ,其中tan(φ)=h/w。
如果假设θ>φ,我们要找到您所绘制的线段与矩形顶部边缘相交的点(x,y)。然后y/x=tan(θ)。我们知道y=h,因此解出x,得到x=h/tan(θ)。
如果θ<φ,这条线段将与矩形的右侧相交于点(x,y)。这次我们知道x=w,因此y=tan(θ)*w。

1

这个问题有一个很好的(更加编程化的iOS/Objective-C)答案,可以在Find the CGPoint on a UIView rectangle intersected by a straight line at a given angle from the center point找到,其中包括以下步骤:

  1. 假设角度大于等于0且小于2*π,逆时针从0(东)开始。
  2. 获取与矩形右边缘相交的y坐标[tan(angle)*width/2]。
  3. 检查此y坐标是否在矩形框架内(绝对值小于或等于高度的一半)。
  4. 如果y交点在矩形内,则如果角度小于π/2或大于3π/2,则选择右边缘(宽度/2,-y coord)。否则选择左边缘(-宽度/2,y coord)。
  5. 如果右边缘交点的y坐标越界,则计算与底部边缘的交点的x坐标[height/2/tan(angle)]。
  6. 接下来确定您想要顶部边缘还是底部边缘。如果角度小于π,则我们想要底部边缘(x,-height/2)。否则,我们想要顶部边缘(-x coord,height/2)。
  7. 然后(如果框架中心不是0,0),通过实际框架中心偏移点。

0

虚幻引擎4(UE4)C++版本。

注意:这是基于Olie的代码。基于Belisarius的答案。如果这对你有帮助,请给这些人点赞。

更改:使用UE4语法和函数,并且角度被取反。

头文件

UFUNCTION(BlueprintCallable, meta = (DisplayName = "Project To Rectangle Edge (Radians)"), Category = "Math|Geometry")
static void ProjectToRectangleEdgeRadians(FVector2D Extents, float Angle, FVector2D & EdgeLocation);

代码

void UFunctionLibrary::ProjectToRectangleEdgeRadians(FVector2D Extents, float Angle, FVector2D & EdgeLocation)
{
    // Move theta to range -M_PI .. M_PI. Also negate the angle to work as expected.
    float theta = FMath::UnwindRadians(-Angle);

    // Ref: https://dev59.com/Tm855IYBdhLWcg3w-JMr
    float a = Extents.X; // "a" in the diagram | Width
    float b = Extents.Y; // "b"                | Height

    // Find our region (diagram)
    float rectAtan = FMath::Atan2(b, a);
    float tanTheta = FMath::Tan(theta);

    int region;
    if ((theta > -rectAtan) && (theta <= rectAtan))
    {
        region = 1;
    }
    else if ((theta > rectAtan) && (theta <= (PI - rectAtan)))
    {
        region = 2;
    }
    else if ((theta > (PI - rectAtan)) || (theta <= -(PI - rectAtan)))
    {
        region = 3;
    }
    else
    {
        region = 4;
    }

    float xFactor = 1.f;
    float yFactor = 1.f;

    switch (region)
    {
        case 1: yFactor = -1; break;
        case 2: yFactor = -1; break;
        case 3: xFactor = -1; break;
        case 4: xFactor = -1; break;
    }

    EdgeLocation = FVector2D(0.f, 0.f); // This rese is nessesary, UE might re-use otherwise. 

    if (region == 1 || region == 3)
    {
        EdgeLocation.X += xFactor * (a / 2.f);              // "Z0"
        EdgeLocation.Y += yFactor * (a / 2.f) * tanTheta;
    }
    else // region 2 or 4
    {
        EdgeLocation.X += xFactor * (b / (2.f * tanTheta)); // "Z1"
        EdgeLocation.Y += yFactor * (b / 2.f);
    }
}

0

对于Java,LibGDX。我已经将角度设置为double类型以增加精度。

public static Vector2 projectToRectEdge(double angle, float width, float height, Vector2 out)
{
    return projectToRectEdgeRad(Math.toRadians(angle), width, height, out);
}

public static Vector2 projectToRectEdgeRad(double angle, float width, float height, Vector2 out)
{
    float theta = negMod((float)angle + MathUtils.PI, MathUtils.PI2) - MathUtils.PI;

    float diag = MathUtils.atan2(height, width);
    float tangent = (float)Math.tan(angle);

    if (theta > -diag && theta <= diag)
    {
        out.x = width / 2f;
        out.y = width / 2f * tangent;
    }
    else if(theta > diag && theta <= MathUtils.PI - diag)
    {
        out.x = height / 2f / tangent;
        out.y = height / 2f;
    }
    else if(theta > MathUtils.PI - diag && theta <= MathUtils.PI + diag)
    {
        out.x = -width / 2f;
        out.y = -width / 2f * tangent;
    }
    else
    {
        out.x = -height / 2f / tangent;
        out.y = -height / 2f;
    }

    return out;
}

0

PYTHON

import math
import matplotlib.pyplot as plt

twoPI = math.pi * 2.0
PI = math.pi

def get_points(width, height, theta):
    theta %= twoPI

    aa = width
    bb = height

    rectAtan = math.atan2(bb,aa)
    tanTheta = math.tan(theta)

    xFactor = 1
    yFactor = 1
    
    # determine regions
    if theta > twoPI-rectAtan or theta <= rectAtan:
        region = 1
    elif theta > rectAtan and theta <= PI-rectAtan:
        region = 2

    elif theta > PI - rectAtan and theta <= PI + rectAtan:
        region = 3
        xFactor = -1
        yFactor = -1
    elif theta > PI + rectAtan and theta < twoPI - rectAtan:
        region = 4
        xFactor = -1
        yFactor = -1
    else:
        print(f"region assign failed : {theta}")
        raise
    
    # print(region, xFactor, yFactor)
    edgePoint = [0,0]
    ## calculate points
    if (region == 1) or (region == 3):
        edgePoint[0] += xFactor * (aa / 2.)
        edgePoint[1] += yFactor * (aa / 2.) * tanTheta
    else:
        edgePoint[0] += xFactor * (bb / (2. * tanTheta))
        edgePoint[1] += yFactor * (bb /  2.)

    return region, edgePoint

l_x = []
l_y = []
theta = 0
for _ in range(10000):
    r, (x, y) = get_points(600,300, theta)
    l_x.append(x)
    l_y.append(y)
    theta += (0.01 / PI)

    if _ % 100 == 0:
        print(r, x,y)

plt.plot(l_x, l_y)
plt.show()

0

Unity C#(从Winter的Java代码转换而来)

    public Vector2 DetermineRectangleEdge(float aDegrees, float aWidth, float aHeight) {

        if (aDegrees < -90)
            aDegrees += 360f;

        float ANGLE = Mathf.Deg2Rad * aDegrees;
        float diag = Mathf.Atan2(aHeight, aWidth);
        float tangent = Mathf.Tan(ANGLE);

        Vector2 OUT = Vector2.zero;

        if (ANGLE > -diag && ANGLE <= diag)
        {
            OUT.x = aWidth / 2f;
            OUT.y = aWidth / 2f * tangent;
            _ObjectRectTransform.sizeDelta = _VerticalSize;
        }
        else if(ANGLE > diag && ANGLE <= Mathf.PI - diag)
        {
            OUT.x = aHeight / 2f / tangent;
            OUT.y = aHeight / 2f;
            _ObjectRectTransform.sizeDelta = _HorizontalSize;
        }
        else if(ANGLE > Mathf.PI - diag && ANGLE <= Mathf.PI + diag)
        {
            OUT.x = -aWidth / 2f;
            OUT.y = -aWidth / 2f * tangent;
            _ObjectRectTransform.sizeDelta = _VerticalSize;
        }
        else
        {
            OUT.x = -aHeight / 2f / tangent;
            OUT.y = -aHeight / 2f;
            _ObjectRectTransform.sizeDelta = _HorizontalSize;
        }

        return OUT;
        
    }  

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接