Michael Wilson
Michael Wilson

Reputation: 860

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?

Upvotes: 1

Views: 1484

Answers (3)

herrstrietzel
herrstrietzel

Reputation: 17165

Here's an attempt to create a versatile helper function – most importantly also calculating points on an ellipse at an "absolute"/parametric angle:

parametric angles
Angle -60 deg – left: absolute angle; right: geometric angle value

Example 1: calculating points on a rotated ellipse

In your case you only need to calculate the 4 points (top:-90/270, right: 0, bottom: 90, left: 180 degree) – all calculations expect radians (you can convert them by angleDegree * Math.PI / 180 )

// Calculate the point on the ellipse without rotation
let x = cx + rx * Math.cos(angle);
let y = cy + ry * Math.sin(angle);

and then rotate these points as described by Alex

// Rotate the calculated point by the specified angle
let rotatedX = cx + (x - cx) * Math.cos(ellipseAngle) - (y - cy) * sin(ellipseAngle);
let rotatedY = cy + (x - cx) * Math.sin(ellipseAngle) + (y - cy) * cos(ellipseAngle);

also set the absolute argument to false and pass only the desired angles (e.g. for top, right, bottom, left) and ellipse rotation like so:

// top 
let pt = getPointOnEllipse(cx, cy, rx, ry, -90, rotation, false)

// right 
let pt1 = getPointOnEllipse(cx, cy, rx, ry, 0, rotation, false)

// bottom 
let pt2 = getPointOnEllipse(cx, cy, rx, ry, 90, rotation, false)

// left 
let pt3 = getPointOnEllipse(cx, cy, rx, ry, -180, rotation, false)

function getPointOnEllipse(cx, cy, rx, ry, angle, ellipseRotation = 0, parametricAngle = true, degrees = true) {
  let {
    cos,
    sin,
    PI,
    atan,
    tan
  } = Math;

  // Convert degrees to radians
  angle = degrees ? (angle * PI) / 180 : angle;
  ellipseRotation = degrees ? (ellipseRotation * PI) / 180 : ellipseRotation;
  // reset rotation for circles or 360 degree 
  ellipseRotation = rx !== ry ? (ellipseRotation !== PI * 2 ? ellipseRotation : 0) : 0;

  // is ellipse
  if (parametricAngle && rx !== ry) {

    // adjust angle for ellipse rotation
    angle = ellipseRotation ? angle - ellipseRotation : angle;

    // Get the parametric angle for the ellipse
    let angleParametric = atan(tan(angle) * (rx / ry));

    // Ensure the parametric angle is in the correct quadrant
    angle = cos(angle) < 0 ? angleParametric + PI : angleParametric;

  }

  // Calculate the point on the ellipse without rotation
  let x = cx + rx * cos(angle),
    y = cy + ry * sin(angle);
  let pt = {
    x: x,
    y: y
  }

  if (ellipseRotation) {
    pt.x = cx + (x - cx) * cos(ellipseRotation) - (y - cy) * sin(ellipseRotation)
    pt.y = cy + (x - cx) * sin(ellipseRotation) + (y - cy) * cos(ellipseRotation)
  }

  return pt
}
body {
  font-family: sans-serif;
}

svg {
  outline: 1px solid #ccc;
  overflow: visible;
  width: auto;
  max-height: 60vh;
  display: block;
  margin: 0 auto;
}

.inp-wrp-outer {
  margin-bottom: 1rem;
}

label {
  font-weight: 700;
}
<div class="inp-wrp-outer">
  <div class="inp-wrp">
    <label>Ellipse rotation</label> <input type="range" class="inputs" id="inputRot" value="120" min="-360" max="360" step="30"> (<span id="rot">0</span> deg)
  </div>
</div>

<!-- SVG illustration -->
<svg id="svg" viewBox="0 0 100 100">
  <ellipse id="e" cx="50" cy="50" rx="50" ry="25" fill="none" stroke="#ccc" transform="rotate(0 50 50)" />

  <!-- markers -->
  <circle id="m1" r="2" />
  <circle id="m2" r="2" />
  <circle id="m3" r="2" />
  <circle id="m4" r="2" />


</svg>

<!-- only for illustration -->
<script>
  //get default values from inputs
  let cx = 50
  let cy = 50
  let rx = 50
  let ry = 25
  let rotation = +inputRot.value

  window.addEventListener('DOMContentLoaded', (e) => {
    // display current values
    rot.textContent = rotation;
    // init
    updateMarkers(rotation);
  });

  // update input values
  let inputs = document.querySelectorAll('.inputs')
  inputs.forEach(inp => {
    inp.addEventListener('input', e => {
      rotation = +inputRot.value
      rot.textContent = rotation;
      // recalculate points
      updateMarkers(rotation)
    })
  })

  function updateMarkers(rotation) {
    e.setAttribute('transform', `rotate(${rotation} 50 50)`)

    // top 
    let pt = getPointOnEllipse(cx, cy, rx, ry, -90, rotation, false)
    m1.setAttribute('cx', `${pt.x}`)
    m1.setAttribute('cy', `${pt.y}`)

    // right 
    let pt1 = getPointOnEllipse(cx, cy, rx, ry, 0, rotation, false)
    m2.setAttribute('cx', `${pt1.x}`)
    m2.setAttribute('cy', `${pt1.y}`)


    // bottom 
    let pt2 = getPointOnEllipse(cx, cy, rx, ry, 90, rotation, false)
    m3.setAttribute('cx', `${pt2.x}`)
    m3.setAttribute('cy', `${pt2.y}`)


    // left 
    let pt3 = getPointOnEllipse(cx, cy, rx, ry, -180, rotation, false)
    m4.setAttribute('cx', `${pt3.x}`)
    m4.setAttribute('cy', `${pt3.y}`)

  }
</script>

Example 2: get absolute angles

function getPointOnEllipse(cx, cy, rx, ry, angle, ellipseRotation = 0, parametricAngle = true, degrees = true) {
  let {
    cos,
    sin,
    PI,
    atan,
    tan
  } = Math;

  // Convert degrees to radians
  angle = degrees ? (angle * PI) / 180 : angle;
  ellipseRotation = degrees ? (ellipseRotation * PI) / 180 : ellipseRotation;
  // reset rotation for circles or 360 degree 
  ellipseRotation = rx !== ry ? (ellipseRotation !== PI * 2 ? ellipseRotation : 0) : 0;

  // is ellipse
  if (parametricAngle && rx !== ry) {

    // adjust angle for ellipse rotation
    angle = ellipseRotation ? angle - ellipseRotation : angle;

    // Get the parametric angle for the ellipse
    let angleParametric = atan(tan(angle) * (rx / ry));

    // Ensure the parametric angle is in the correct quadrant
    angle = cos(angle) < 0 ? angleParametric + PI : angleParametric;

  }

  // Calculate the point on the ellipse without rotation
  let x = cx + rx * cos(angle),
    y = cy + ry * sin(angle);
  let pt = {
    x: x,
    y: y
  }

  if (ellipseRotation) {
    pt.x = cx + (x - cx) * cos(ellipseRotation) - (y - cy) * sin(ellipseRotation)
    pt.y = cy + (x - cx) * sin(ellipseRotation) + (y - cy) * cos(ellipseRotation)
  }

  return pt
}
body {
  font-family: sans-serif;
}

svg {
  outline: 1px solid #ccc;
  overflow: visible;
  width: auto;
  max-height: 60vh;
  display: block;
  margin: 0 auto;
}

.inp-wrp-outer{
margin-bottom:1rem;
}

label {
  font-weight: 700;
}
<div class="inp-wrp-outer">
  <div class="inp-wrp">
    <label>Angle</label> <input type="range" class="inputs" id="inputAngle" value="-150" min="-360" max="360" step="30"> (<span id="val">0</span> deg)
  </div>
  <div class="inp-wrp">
    <label>Ellipse rotation</label> <input type="range" class="inputs" id="inputRot" value="120" min="-360" max="360" step="30"> (<span id="rot">0</span> deg)
  </div>
  <div class="inp-wrp">
    <label><input class="inputs" id="inputAbsolute" type="checkbox" checked> Absolute angle</label>
  </div>
  <div class="inp-wrp">
    <label>rx</label> <input type="range" class="inputs" id="inputRx" value="50" min="10" max="100">(<span id="rX">50</span>)
    <label>ry</label> <input type="range" class="inputs" id="inputRy" value="25" min="10" max="100">(<span id="rY">25</span>)
  </div>
</div>

<!-- SVG illustration -->
<svg id="svg" viewBox="0 0 100 100">
  <circle id="c" cx="50" cy="50" r="50" fill="none" stroke="#ccc" />
  <ellipse id="e" cx="50" cy="50" rx="50" ry="25" fill="none" stroke="#ccc" transform="rotate(0 50 50)" />

  <path id="path" stroke="red" fill="none" />

  <!-- lines -->
  <line id="l1" stroke="#ccc" />
  <line id="l2" stroke="#ccc" />

  <!-- markers -->
  <circle id="markerCircle" fill="orange" r="2" />
  <circle id="markerEllipse" fill="green" r="2" />
  <circle id="cc" cx="50" cy="50" r="2" fill="red" />

</svg>

<!-- only for illustration -->
<script>
  //get default values from inputs
  let angle = +inputAngle.value
  let cx = 50
  let cy = 50
  let rx = +inputRx.value
  let ry = +inputRy.value
  let rotation = +inputRot.value
  let absolute = inputAbsolute.checked
  window.addEventListener('DOMContentLoaded', (e) => {
    // display current values
    val.textContent = angle;
    rot.textContent = rotation;
    rX.textContent = rx;
    rY.textContent = ry;
    // init
    updateMarkers(angle, rotation);
  });
  // update input values
  let inputs = document.querySelectorAll('.inputs')
  inputs.forEach(inp => {
    inp.addEventListener('input', e => {
      rx = +inputRx.value
      ry = +inputRy.value
      angle = +inputAngle.value
      absolute = inputAbsolute.checked;
      rotation = +inputRot.value
      val.textContent = angle;
      rot.textContent = rotation;
      rX.textContent = rx;
      rY.textContent = ry;
      // recalculate points
      updateMarkers(angle, rotation, absolute)
    })
  })

  function updateMarkers(angle, rotation, absolute) {
    c.setAttribute('r', `${rx}`)
    e.setAttribute('rx', `${rx}`)
    e.setAttribute('ry', `${ry}`)
    e.setAttribute('transform', `rotate(${rotation} 50 50)`)
    // circle
    let pt = getPointOnEllipse(cx, cy, rx, rx, angle, 0, absolute)
    markerCircle.setAttribute('cx', `${pt.x}`)
    markerCircle.setAttribute('cy', `${pt.y}`)
    // ellipse
    let pt1 = getPointOnEllipse(cx, cy, rx, ry, angle, rotation, absolute)
    markerEllipse.setAttribute('cx', `${pt1.x}`)
    markerEllipse.setAttribute('cy', `${pt1.y}`)
    // illustrate point relations
    l1.setAttribute('x1', pt.x)
    l1.setAttribute('y1', pt.y)
    l1.setAttribute('x2', pt1.x)
    l1.setAttribute('y2', pt1.y)
    l2.setAttribute('x1', pt1.x)
    l2.setAttribute('y1', pt1.y)
    l2.setAttribute('x2', cx)
    l2.setAttribute('y2', cy)
  }
  // render points
  function renderPoint(
    svg,
    coords,
    fill = "red",
    r = "2",
    opacity = "1",
    id = "",
    className = ""
  ) {
    //console.log(coords);
    if (Array.isArray(coords)) {
      coords = {
        x: coords[0],
        y: coords[1]
      };
    }
    let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
  <title>${coords.x} ${coords.y}</title></circle>`;
    svg.insertAdjacentHTML("beforeend", marker);
  }
</script>

How it works

  • if rx!==ry we have an ellipse and apply the parametric angle calculation: we get the point-at-angle as measured between cx/cy (ellipse centerpoint) and starting point via
let angleParametric = atan(tan(angle) * (rx / ry));
  • we need to adjust the angle conditionally
// Ensure the parametric angle is in the correct quadrant
angle = cos(angle) < 0 ? angleParametric + PI : angleParametric;
  • if the ellipse is rotated (e.g in SVG arc via x-axis-rotation) we prepend an angle correction by subtracting this ellipse-rotation angle from the input angle
angle = ellipseRotation ? angle - ellipseRotation : angle;

Feel free to modify this helper by forking/modifying this codepen testbed.

Further reading

Upvotes: 1

Kaiido
Kaiido

Reputation: 136648

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>

Upvotes: 2

Alex
Alex

Reputation: 1849

Just rotate point you got with your function around center of an ellipse:

function rotatePoint(x, y, originX, originY, rotation) {
  const cosA = Math.cos(rotation);
  const sinA = Math.sin(rotation);
  const dx = x - originX;
  const dy = y - originY;
  return {
    x: originX + dx * cosA - dy * sinA,
    y: originY + dx * sinA + dy * cosA
  }
}

Please note that your function getPointOnEllipse does not return point corresponding to central angle.

Upvotes: 1

Related Questions