javascriptcanvasellipse

How to calculate a point on a rotated ellipse


I have working code to find the x,y at a given angle on an ellipse :

getPointOnEllipse(origin_x, origin_y, radius_x, radius_y, angle) {

  var r_end_angle = end_angle / 360 * 2 * Math.PI;
  var x        = origin_x + Math.cos(r_end_angle) * radius_x;
  var y        = origin_y + Math.sin(r_end_angle) * radius_y;

  return {x: x, y: y}

My question is how do I do this calculation if the ellipse is rotated at angle r, as if created by:

ellipse(origin_x, origin_y, radius_x, radius_y, r);

Update: I tried the suggestion below, and it didn't quite work. Here's the original (0) rotation:

Original Orientation

And here's after approximately 90-degree rotation:

After approximately 90-degree rotation

Here's the code I tried:

/* 
* origin_x - Center of the Ellipse X
* origin_y - Center of the Ellipse Y
* radius_x - Radius X
* radius_y - Radius Y
* angle    - angle along the ellipse on which to place the handle
* rotation - Angle which the ellipse is rotated
*/
getPointOnEllipse(origin_x, origin_y, radius_x, radius_y, angle, rotation) {
 var r_end_angle = (angle + rotation) / 360 * 2 * Math.PI;
 var endX        = origin_x + Math.cos(r_end_angle) * radius_x;
 var endY        = origin_y + Math.sin(r_end_angle) * radius_y;

 var cosA  = Math.cos(rotation);
 var sinA  = Math.sin(rotation);
 var dx    = endX - origin_x;
 var dy    = endY - origin_y;

 rotated_x = origin_x + dx * cosA - dy * sinA;
 rotated_y = origin_y + dx * sinA + dy * cosA;

Here's some logging:

X 369, Y 233, radiusX 104, radiusY 17, end_angle 0, rotation 0, endX 473, endY 233, cosA 1, sinA 0, dx 104, dy 0, rotated_x 473, rotated_y 233

X 369, Y 233, radiusX 104, radiusY 17, end_angle 90, rotation 0, endX 369, endY 250, cosA 1, sinA 0, dx 0, dy 17, rotated_x 369, rotated_y 250

X 369, Y 233, radiusX 104, radiusY 17, end_angle 180, rotation 0, endX 265, endY 233, cosA 1, sinA 0, dx -104, dy 0, rotated_x 265, rotated_y 233

X 369, Y 233, radiusX 104, radiusY 17, end_angle 270, rotation 0, endX 369, endY 216, cosA 1, sinA 0, dx 0, dy -17, rotated_x 369, rotated_y 216

Here after a 90-degree rotation the points don't seem to end up on the ellipse:

X 369, Y 233, radiusX 104, radiusY 17, end_angle 0, rotation 96.40608527543233, endX 357.396254311691, endY 249.89385326910204, cosA -0.5542897094655916, sinA 0.8323238059676955, dx -11.603745688309004, dy 16.89385326910204, rotated_x 361.3706805758866, rotated_y 213.97783720494053

X 369, Y 233, radiusX 104, radiusY 17, end_angle 90, rotation 96.40608527543233, endX 265.6493682360816, endY 231.10323387787258, cosA -0.5542897094655916, sinA 0.8323238059676955, dx -103.35063176391839, dy -1.896766122127417, rotated_x 427.86491525130737, rotated_y 148.03016676384783

X 369, Y 233, radiusX 104, radiusY 17, end_angle 180, rotation 96.40608527543233, endX 380.603745688309, endY 216.10614673089796, cosA -0.5542897094655916, sinA 0.8323238059676955, dx 11.603745688309004, dy -16.89385326910204, rotated_x 376.6293194241134, rotated_y 252.02216279505947

X 369, Y 233, radiusX 104, radiusY 17, end_angle 270, rotation 96.40608527543233, endX 472.35063176391833, endY 234.89676612212745, cosA -0.5542897094655916, sinA 0.8323238059676955, dx 103.35063176391833, dy 1.8967661221274454, rotated_x 310.1350847486927, rotated_y 317.969833236

I'm sure I got something wrong here - any ideas?


Solution

  • You actually may not need to calculate this position.

    The canvas API offers means to control the current transformation matrix of your context.
    In many cases, this is way more convenient to embrace this than to calculate everything yourself.

    For instance, your example places the four squares relatively to the ellipse own transformation. So what you need to do, is to first set your transformation matrix to this ellipse's position, and then to only move it by the relative position of each squares.

    Here is a fast written example:

    const ctx = canvas.getContext('2d');
    
    class Shape {
      constructor(cx, cy, parent) {
        this.cx = cx;
        this.cy = cy;
        this.parent = parent;
        this.path = new Path2D();
        this.angle = 0;
        this.color = "black";
      }
      applyTransform() {
        if (this.parent) { // recursively apply all the transforms
          this.parent.applyTransform();
        }
        ctx.transform(1, 0, 0, 1, this.cx, this.cy);
        ctx.rotate(this.angle);
      }
    }
    
    const rad_x = (canvas.width / 1.3) / 2;
    const rad_y = (canvas.height / 1.3) / 2;
    
    class Rect extends Shape {
      constructor(dx, dy, parent) {
        super(rad_x * dx, rad_y * dy, parent);
        this.path.rect(-5, -5, 10, 10);
        Object.defineProperty(this, 'angle', {
          get() {
            // so the squares are not rotated
            return parent.angle * -1;
          }
        })
      }
    }
    
    const ellipse = new Shape(canvas.width / 2, canvas.height / 2);
    ellipse.path.ellipse(0, 0, rad_x, rad_y, 0, 0, Math.PI * 2);
    
    const shapes = [ellipse].concat(
      [
        new Rect(0, -1, ellipse),
        new Rect(1, 0, ellipse),
        new Rect(0, 1, ellipse),
        new Rect(-1, 0, ellipse)
      ]
    );
    
    const mouse = {x:0, y:0};
    canvas.onmousemove = ({offsetX, offsetY}) => {
      mouse.x = offsetX;
      mouse.y = offsetY;
    };
    
    draw();
    
    function clearTransform() {
      ctx.setTransform(1, 0, 0, 1, 0, 0);
    }
    
    function draw() {
      // update ellipse's angle
      ellipse.angle = (ellipse.angle + Math.PI / 180) % (Math.PI * 2);
      
      // clear
      clearTransform();
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      // draw the shapes
      shapes.forEach(shape => {
        clearTransform(); // clear the transform matrix completely
        shape.applyTransform(); // will apply their parent's transform too
    
         // check if we are hovering this shape
         shape.color = ctx.isPointInPath(shape.path, mouse.x, mouse.y) ? 'red' : 'black';
    
        ctx.strokeStyle = shape.color;
        ctx.stroke(shape.path);
      });
    
      // do it again
      requestAnimationFrame(draw);
    }
    <canvas id="canvas"></canvas>