Say I have a pentagon (and we number the sides, like moving around a clock):
Starting from the center of the polygon, how do you compute the position of a point in these spots (along the line of the edge of the polygon):
Knowing how to compute the x/y coordinates relative to the center will mean I can plot points along the straight line-segments of an arbitrary polygon (ranging from say 3 to 20 sides). I am having a really time conceptualizing how to do this, let alone getting it to work in code. Doesn't matter what language it is in, but preferably JavaScript/TypeScript, or else Python or C (or self-explanatory pseudocode).
Here is a combination of what I conjured up. The polygon layout works properly, but the point positioning isn't working. How do you layout these 3 points?
const ANGLE = -Math.PI / 2 // Start the first vertex at the top center
function computePolygonPoints({
width,
height,
sides,
strokeWidth = 0,
rotation = 0,
}) {
const centerX = width / 2 + strokeWidth / 2
const centerY = height / 2 + strokeWidth / 2
const radiusX = width / 2 - strokeWidth / 2
const radiusY = height / 2 - strokeWidth / 2
const offsetX = strokeWidth / 2
const offsetY = strokeWidth / 2
const rotationRad = (rotation * Math.PI) / 180
const points = Array.from({ length: sides }, (_, i) => {
const angle = (i * 2 * Math.PI) / sides + ANGLE
const x = centerX + radiusX * Math.cos(angle)
const y = centerY + radiusY * Math.sin(angle)
// Apply rotation around the center
const rotatedX =
centerX +
(x - centerX) * Math.cos(rotationRad) -
(y - centerY) * Math.sin(rotationRad)
const rotatedY =
centerY +
(x - centerX) * Math.sin(rotationRad) +
(y - centerY) * Math.cos(rotationRad)
return { x: rotatedX, y: rotatedY }
})
const minX = Math.min(...points.map(p => p.x))
const minY = Math.min(...points.map(p => p.y))
const adjustedPoints = points.map(p => ({
x: offsetX + p.x - minX,
y: offsetY + p.y - minY,
}))
return adjustedPoints
}
function vertexCoordinates(n, R, vertexIndex) {
const angle = 2 * Math.PI * vertexIndex / n - Math.PI / 2; // Adjusting to start from the top
return {
x: R * Math.cos(angle),
y: R * Math.sin(angle),
}
}
function midpointCoordinates(x1, y1, x2) {
return {
x: (x1 + x2.x) / 2,
y: (y1 + x2.y) / 2,
}
}
function fractionalPoint(x1, y1, x2, fraction) {
return {
x: x1 + fraction * (x2.x - x1),
y: y1 + fraction * (x2.y - y1),
}
}
const pentagonPoints = computePolygonPoints({ width: 300, height: 300, sides: 5 })
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", 300)
svg.setAttribute("height", 300);
const pentagon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
pentagon.setAttribute('fill', 'cyan')
pentagon.setAttribute('points', pentagonPoints
.map((p) => `${p.x},${p.y}`)
.join(" "))
svg.appendChild(pentagon)
document.body.appendChild(svg);
const n = 5 // Number of sides for a pentagon
const width = 300; // Width of the pentagon
const R = width / (2 * Math.cos(Math.PI / n)); // Radius of the circumscribed circle
const centerX = 150; // Center of the canvas
const centerY = 150;
// Vertex between sides 2 and 3
const vertex23 = vertexCoordinates(n, R, 2)
const vertex23Adjusted = {
x: centerX + vertex23.x, // subtract radius too?
y: centerY + vertex23.y
};
console.log('Vertex between sides 2 and 3:', vertex23Adjusted)
const circle23 = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle23.setAttribute('fill', 'magenta')
circle23.setAttribute('r', 16)
circle23.setAttribute('cx', vertex23Adjusted.x)
circle23.setAttribute('cy', vertex23Adjusted.y)
svg.appendChild(circle23)
// Midpoint of side 4
const vertex4_1 = vertexCoordinates(n, R, 3)
const vertex4_2 = vertexCoordinates(n, R, 4)
const mid4 = midpointCoordinates(vertex4_1.x, vertex4_1.y, vertex4_2)
console.log('Midpoint of side 4:', mid4)
const mid4Circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
mid4Circle.setAttribute('fill', 'magenta')
mid4Circle.setAttribute('r', 16)
mid4Circle.setAttribute('cx', mid4.x)
mid4Circle.setAttribute('cy', mid4.y)
svg.appendChild(mid4Circle)
// Point 2/3 across side 3, moving clockwise
const vertex3_1 = vertexCoordinates(n, R, 2)
const vertex3_2 = vertexCoordinates(n, R, 3)
const frac3 = fractionalPoint(
vertex3_1.x,
vertex3_1.y,
vertex3_2,
2 / 3,
)
console.log('Point 2/3 across side 3:', frac3)
const frac3Circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
frac3Circle.setAttribute('fill', 'magenta')
frac3Circle.setAttribute('r', 16)
frac3Circle.setAttribute('cx', frac3.x)
frac3Circle.setAttribute('cy', frac3.y)
svg.appendChild(frac3Circle)
I'd like to be able to solve this for any polygon from 3 to 20 sides, not just for the pentagon.
Here is the answer, I finally figured it out.
export function calculatePolygonDotPosition({
polygonRadius,
polygonSideCount,
polygonEdgeNumber,
polygonEdgePositionRatio, // from 0 to 1
gap = 0,
dotRadius,
rotation = 0,
offset = 0,
}: {
polygonRadius: number
polygonSideCount: number
polygonEdgeNumber: number
polygonEdgePositionRatio: number
gap?: number
dotRadius: number
rotation?: number
offset?: number
}) {
const n = polygonSideCount
const R = polygonRadius
const e = polygonEdgeNumber
const t = polygonEdgePositionRatio
const o = gap
const rotationAngle = (rotation * Math.PI) / 180
const V1 = rotatePoint(getPolygonVertex(e - 1, n, R), rotationAngle)
const V2 = rotatePoint(getPolygonVertex(e % n, n, R), rotationAngle) // Wrap around using modulo
// Interpolate position along the edge
const P = {
x: (1 - t) * V1.x + t * V2.x,
y: (1 - t) * V1.y + t * V2.y,
}
// Calculate the edge vector and the normal vector
const dx = V2.x - V1.x
const dy = V2.y - V1.y
const edgeLength = Math.sqrt(dx * dx + dy * dy)
// Unit normal vector (rotate by 90 degrees counter-clockwise)
const normal = {
x: -dy / edgeLength,
y: dx / edgeLength,
}
// Offset the point by the gap distance
const P_offset = {
x: P.x + (-o - dotRadius + offset) * normal.x,
y: P.y + (-o - dotRadius + offset) * normal.y,
}
return {
x: P_offset.x + R,
y: -P_offset.y + R,
}
}
// Calculate vertex positions
export function getPolygonVertex(i: number, n: number, R: number) {
const angle = (2 * Math.PI * i) / n + Math.PI / 2
return { x: R * Math.cos(angle), y: R * Math.sin(angle) }
}
function rotatePoint(
{ x, y }: { x: number; y: number },
angle: number,
) {
const cos = Math.cos(angle)
const sin = Math.sin(angle)
return {
x: x * cos - y * sin,
y: x * sin + y * cos,
}
}
Usage:
calculatePolygonDotPosition({
polygonRadius: 300,
polygonSideCount: 5,
polygonEdgeNumber: 2,
polygonEdgePositionRatio: 0.5,
gap: 4,
dotRadius: 3,
offset: 4, // strokeWidth
})
Here are 4 points laid out just outside the outer edge.