Finding points on a rectangle at a given angle
Asked Answered
M

10

21

I'm trying to draw a gradient in a rectangle object, with a given angle (Theta), where the ends of the gradient are touching the perimeter of the rectangle.

Graph

I thought that using tangent would work, but I'm having trouble getting the kinks out. Is there an easy algorithm that I am just missing?

End Result

So, this is going to be a function of (angle, RectX1, RectX2, RectY1, RectY2). I want it returned in the form of [x1, x2, y1, y2], so that the gradient will draw across the square. In my problem, if the origin is 0, then x2 = -x1 and y2 = -y1. But it's not always going to be on the origin.

Maud answered 31/10, 2010 at 2:28 Comment(3)
what does the picture have to do with the problem? Only one end of the line (I'm assuming the line is the hypotenuse in this case) touches the boundary. will the line always pass through (or, as pictured, start at) the origin?Penetrance
@aaronasterling, It's my understanding of what I am trying to achieve. I need both X and Y. The triangle will change based on the angle.Maud
I've been meaning to post an answer here for ages but haven't ever gotten to it :( For an option that doesn't involve bounds checking and supports rotation, see here.Stamper
D
42

Let's call a and b your rectangle sides, and (x0,y0) the coordinates of your rectangle center.

You have four regions to consider:

alt text

    Region    from               to                 Where
    ====================================================================
       1      -arctan(b/a)       +arctan(b/a)       Right green triangle
       2      +arctan(b/a)        π-arctan(b/a)     Upper yellow triangle
       3       π-arctan(b/a)      π+arctan(b/a)     Left green triangle
       4       π+arctan(b/a)     -arctan(b/a)       Lower yellow triangle

With a little of trigonometry-fu, we can get the coordinates for your desired intersection in each region.

alt text

So Z0 is the expression for the intersection point for regions 1 and 3
And Z1 is the expression for the intersection point for regions 2 and 4

The desired lines pass from (X0,Y0) to Z0 or Z1 depending the region. So remembering that Tan(φ)=Sin(φ)/Cos(φ)


    Lines in regions      Start                   End
    ======================================================================
       1 and 3           (X0,Y0)      (X0 + a/2 , (a/2 * Tan(φ))+ Y0
       2 and 4           (X0,Y0)      (X0 + b/(2* Tan(φ)) , b/2 + Y0)

Just be aware of the signs of Tan(φ) in each quadrant, and that the angle is always measured from THE POSITIVE x axis ANTICLOCKWISE.

HTH!

Dereliction answered 31/10, 2010 at 9:8 Comment(4)
I don't understand what the two angles φ and θ represent in your answer or the other answer -- doesn't the question only specify one angle? And shouldn't there be a different x coordinate for the intersecting / end point in region 1 compared to 3 (and a different y coordinate for the intersecting point in region 2 compared to 4)?Flunky
@VictorVanHee: the angles are the angle from the center point. He used 2 letters to indicate that they are different possible code-paths but, in the formulas, it's just "theta" (or whatever variable you use.)Pottage
@belisarius: could you explain a bit the sentence near the end about "Just be aware of the signs of Tan(φ) in each quadrant"? I've got this so close, but my remaining bugs I'm pretty sure have to do with not understanding how to alter the formulae based on the sign of tan(φ). Thanks!Pottage
@Olie: The end position in region 2 has an X value lower than X0 (it's left of it, after all), making x(end)=X0 - a/2 (note the minus sign instead of the plus) the proper way to calculate X in region 2. Similarly, in region 4, the Y value is calculated by Y(end)=Y0 - b/2.Selenium
P
14

Ok, whew!, I finally got this one.

NOTE: I based this off of belisarius's awesome answer. If you like this, please like his, too. All I did was turn what he said into code.

Here's what it looks like in Objective-C. It should be simple enough to convert to whatever your favorite language is.

+ (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://mcmap.net/q/593482/-finding-points-on-a-rectangle-at-a-given-angle
    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;
}

In addition, here's a little test-view I created to verify that it works. Create this view and put it somewhere, it will make another little view scoot around the edge.

@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
Pottage answered 31/10, 2014 at 22:6 Comment(2)
Fantastic code! I just implemented this in Java. I had to swap regions 2 and 4 in the calculations and I had to use a positive yFactor in regions 1 and 3, but I think that is because in Cocoa/Objective-C the origin is bottom left. Bravo! Nice work!Tailpipe
are you using theta as radians or "degrees from East" (in range 0 to 180 clockwise, and 0 to -180 anticlockwise)?Balladmonger
H
12

Javascript version:

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;
};
Heredes answered 7/8, 2015 at 21:10 Comment(3)
Man thank you so much, I just started writing it, then found somebody already did it :)Scenarist
It saves MY lot's of time. Thanks, Buddy.Septuagenarian
thank you for your code, works very well! I just want it to work clockwise instead of counter clockwise, what should change?Gannon
C
4

Following your picture, I'm going to assume that the rectangle is centered at (0,0), and that the upper right corner is (w,h). Then the line connecting (0,0) to (w,h) forms an angle φ with the X axis, where tan(φ) = h/w.

Assuming that θ > φ, we are looking for the point (x,y) where the line that you have drawn intersects the top edge of the rectangle. Then y/x = tan(θ). We know that y=h so, solving for x, we get x = h/tan(θ).

If θ < φ, the line intersects with the right edge of the rectangle at (x,y). This time, we know that x=w, so y = tan(θ)*w.

Contravention answered 31/10, 2010 at 2:59 Comment(0)
F
1

There's a good (more programmatic iOS / Objective-C) answer to this question at Find the CGPoint on a UIView rectangle intersected by a straight line at a given angle from the center point involving the following steps:

  1. Assume that the angle is greater than or equal to 0 and less than 2*π, going counterclockwise from 0 (East).
  2. Get the y coordinate of the intersection with the right edge of the rectangle [tan(angle)*width/2].
  3. Check whether this y coordinate is in the rectangle frame (absolute value less than or equal to half the height).
  4. If the y intersection is in the rectangle, then if the angle is less than π/2 or greater than 3π/2 choose the right edge (width/2, -y coord). Otherwise choose the left edge (-width/2, y coord).
  5. If the y coordinate of the right edge intersection was out-of-bounds, calculate the x coordinate of the intersection with the bottom edge [half the height/tan(angle)].
  6. Next determine whether you want the top edge or the bottom edge. If the angle is less than π, we want the bottom edge (x, -half the height). Otherwise, we want the top edge (-x coord, half the height).
  7. Then (if the center of the frame is not 0,0), offset the point by the actual center of the frame.
Flunky answered 2/3, 2012 at 11:37 Comment(0)
K
0

For Java, LibGDX. I've let the angle be a double to increase precision.

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;
}
Koontz answered 6/1, 2019 at 17:58 Comment(0)
L
0

Unreal Engine 4 (UE4) C++ Version.

Note: This is based off of Olie's Code. Based on Belisarius's Answer. Give those guys upvotes if this helps you.

Changes: Uses UE4 syntax and functions, and Angle is negated.

Header

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

Code

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://mcmap.net/q/593482/-finding-points-on-a-rectangle-at-a-given-angle
    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);
    }
}
Levant answered 7/4, 2020 at 16:16 Comment(0)
N
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()
Neumark answered 30/12, 2020 at 7:23 Comment(0)
U
0

Unity C# (Converted from Winter's Java code)

    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;
        
    }  
Unnecessary answered 9/2, 2023 at 2:58 Comment(0)
W
0

const toDegrees = (angle) => angle * (180 / Math.PI);
const toRadians = (angle) => angle * (Math.PI / 180);

 class Theta {
  degrees;
  radians;
  constructor({ degrees, radians }) {
    if (degrees === undefined && radians === undefined)
      throw new Error("degrees or radians must be provided");
    if (degrees !== undefined) {
      this.degrees = degrees;
      this.radians = toRadians(degrees);
    }
    if (radians !== undefined) {
      this.radians = radians;
      this.degrees = toDegrees(radians);
    }
  }

  add(deg) {
    return new Theta({ degrees: this.degrees + deg });
  }

  sub(deg) {
    return new Theta({ degrees: this.degrees - deg });
  }

  static Degrees(deg) {
    return new Theta({ degrees: deg });
  }

  static Radians(rad) {
    return new Theta({ radians: rad });
  }
}
/**
 @link https://zh.wikipedia.org/wiki/%E4%B8%89%E8%A7%92%E5%87%BD%E6%95%B0#%E4%BB%A5%E7%9B%B4%E8%A7%92%E5%9D%90%E6%A0%87%E7%B3%BB%E4%BE%86%E5%AE%9A%E4%B9%89
 *
 * */
function getRX(theta, x) {
  return Math.abs(x / Math.cos(theta.radians));
}
function getRY(theta, y) {
  return Math.abs(y / Math.sin(theta.radians));
}

 function cartesian2Polar(x, y) {
  const r = Math.sqrt(x * x + y * y);
  const theta = Theta.Radians(Math.atan(y / x));
  return { r, theta };
}

 function polar2Cartesian(r, theta) {
  const radians = theta.radians;
  const x = r * Math.cos(radians);
  const y = r * Math.sin(radians);
  return { x, y };
}

 function getCrossPoint(rect, theta) {
  const x = rect.width / 2;
  const y = rect.height / 2;

  const r1 = getRX(theta, x);
  const r2 = getRY(theta, y);
  const r = Math.min(r1, r2);
  const point = polar2Cartesian(r, theta);

  // move back to real coordinate
  point.x += rect.width / 2 + rect.x;
  point.y += rect.height / 2 + rect.y;


  return {
    point,
    radius: r,
  };
}

 function getCross(rect, theta) {
  const head = getCrossPoint(rect, theta).point;
  const tail = getCrossPoint(rect, Theta.Degrees(theta.degrees + 180)).point;

  return {
    head,
    tail,
  };
}



function main() {

    const width = 300 
    const height = 300 


  function drawCanvas() {
    const canvas = document.getElementById('canvas');
    canvas.width = width
    canvas.height = height
    const ctx = canvas.getContext('2d');
    const offset = -270
    const rect = {
        x: 10,
        y: 50,
        width: 280,
        height: 200,
    }
    const toRad = (deg) => toRadians(deg + offset)


    return {
        draw(angle) {
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            ctx.fillText(`radians: ${toRad(angle, 3)}`, 10, 10)
            ctx.fillText(`degrees: ${angle}`, 10, 30)
            ctx.strokeRect(rect.x, rect.y, rect.width, rect.height)


            ctx.save()
            const theta = Theta.Degrees(angle + offset)
            const {head, tail} = getCross(rect, theta)

            ctx.moveTo(head.x, head.y)
            ctx.lineTo(tail.x, tail.y)
            ctx.stroke()
            ctx.restore()

            ctx.save()
            ctx.translate(head.x, head.y)
            const ro = Theta.Degrees(135).add(theta.degrees)
            console.log(ro.degrees)
            ctx.rotate(ro.radians)
            ctx.beginPath()
            ctx.moveTo(-5, -5)
            ctx.lineTo(5, -5)
            ctx.lineTo(-5, 5)
            ctx.closePath()
            ctx.fill()
            ctx.stroke()

            ctx.beginPath();

            ctx.restore()
        }
    }
}
    
    
 const canvas = drawCanvas()
 canvas.draw(0)

 document.getElementById('degrees').oninput = e => {
        const value = Number(e.target.value)
        console.log('degrees', value)
        document.querySelector('[for="degrees"]').innerText = `degrees ${value}`
            canvas.draw(value)
        
    }

}

main()
        [for="degrees"] {
            display: inline-block;
            width: 130px;
        }
<div>
  <label for="degrees">degrees 0</label>
  <input type="range" value="0" min="0" max="360" id="degrees"/>
</div>
<canvas id="canvas" width="300" height="300"></canvas>

extending the four sides of a rectangle.
A ray emanating from the center will inevitably intersect with two sides, one intersection point on the rectangle and one on the extended line. Moreover, the point on the rectangle is always closer to the center than the one on the extended line. rectangle with two cross point

Our clues are the ray's angle and the rectangle's size. By using the angle with x and the angle with y, we calculate the distances from the center to the two points. We take the shorter distance as R. At this point, we have the ray's angle and its length, which exactly form a polar coordinate. Converting this polar coordinate to a Cartesian coordinate gives us the exact coordinates of the intersection point.

You can check it in codepen for linearGradient example

Weems answered 9/7 at 10:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.