Reputation: 860
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:
And here's 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
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:
Angle -60 deg – left: absolute angle; right: geometric angle value
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>
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>
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 vialet angleParametric = atan(tan(angle) * (rx / ry));
// Ensure the parametric angle is in the correct quadrant
angle = cos(angle) < 0 ? angleParametric + PI : angleParametric;
arc
via x-axis-rotation
) we prepend an angle correction by subtracting this ellipse-rotation angle from the input angleangle = ellipseRotation ? angle - ellipseRotation : angle;
Feel free to modify this helper by forking/modifying this codepen testbed.
Upvotes: 1
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
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