skip to Main Content

Say I have a pentagon (and we number the sides, like moving around a clock):

enter image description here

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):

  1. At the vertex between sides 2 and 3 (this is the max distance from the center).
  2. At the midpoint of 4 (this is the min distance from the center).
  3. At a point 2/3 across side 3, moving clockwise (randomly chosen distance from the center).

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.

2

Answers


  1. Chosen as BEST ANSWER

    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.

    enter image description here


  2. The problem with your trigonometrical computation is that the circumscribed circle is too big. You can see it if you add

    <circle cx="«centerX»" cy="«centerY»" r="«R»"/>
    

    to your diagram:
    circumscribed circle too big

    But you need not do the computation yourself, you can let SVG do it for you with the path.getPointAtLength(length) function. In your cases,

    1. the vertex between sides 2 and 3 is at length=2
    2. the midpoint of 4 is at length=3.5
    3. a point 2/3 across side 3, moving clockwise, is at length=2.67.
    var path = document.querySelector("polygon");
    var circle = document.querySelector("circle");
    var control = document.querySelector("input[type=range]");
    
    function init(n) {
      control.setAttribute("max", n);
      var d = [];
      for (var i = 0; i < n; i++) {
        var alpha = 2 * i * Math.PI / n;
        d.push(`${Math.sin(alpha)},${-Math.cos(alpha)}`);
      }
      path.setAttribute("points", d.join(" "));
      control.value = 0;
      move();
    }
    
    function move() {
      var p = path.getPointAtLength(path.getTotalLength() * control.value / control.getAttribute("max"));
      circle.setAttribute("cx", p.x);
      circle.setAttribute("cy", p.y);
      control.nextElementSibling.textContent = control.value;
    }
    
    init(5);
    polygon {
      fill: yellow;
      stroke: black;
      stroke-width: 0.01;
    }
    
    circle {
      fill: red;
    }
    <input type="number" value="5" onchange="init(this.value)" />
    <svg viewBox="-1 -1 2 2">
        <polygon/>
        <circle r="0.1"/>
    </svg>
    <input type="range" step="0.01" onchange="move()" /><span></span>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search