How can you determine the location of a point on the perimeter of a regular polygon?

Imagine you have a pentagon, with numbered sides like moving around a clock:

https://i.sstatic.net/pWqdFtfg.png

Starting from the center of the polygon, how can you calculate the position of a point in these locations (along the edge of the polygon):

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

Understanding how to calculate the x/y coordinates relative to the center will enable me to plot points along the straight line segments of any polygon (ranging from 3 to 20 sides). I am struggling to conceptualize this, let alone make it work in code. It doesn't matter what programming language is used, but preferably JavaScript/TypeScript, or else Python or C (or self-explanatory pseudocode).

Here is a combination of my attempts so far. The polygon layout is correct, but the point positioning is not working. How would you position 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 would like to be able to solve this for any polygon from 3 to 20 sides, not just for the pentagon.

Answer №1

One issue with your trigonometrical calculation is that the circumscribed circle is oversized. This can be visualized by adding

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

to your diagram: https://i.sstatic.net/LRJzZKXd.png

However, you don't have to manually carry out the calculation, as you can use the path.getPointAtLength(length) function in SVG. In your specific scenarios,

  1. the vertex between sides 2 and 3 can be found at length=2
  2. the midpoint of side 4 is at length=3.5
  3. a point 2/3 along side 3 in a clockwise direction 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>

Answer №2

Finally cracked it! Here's the solution.

export function calculatePolygonDotPosition({
  polygonRadius,
  polygonSideCount,
  polygonEdgeNumber,
  polygonEdgePositionRatio, // Value ranges 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,
  }
}

How to use this:

calculatePolygonDotPosition({
  polygonRadius: 300,
  polygonSideCount: 5,
  polygonEdgeNumber: 2,
  polygonEdgePositionRatio: 0.5,
  gap: 4,
  dotRadius: 3,
  offset: 4, // strokeWidth
})

You'll see 4 points placed just beyond the outer edge.

https://i.sstatic.net/z1wrq8t5.png

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Can we integrate the Node.js API into Meteor?

Being new to Meteor.js, I've been wondering if it's possible to utilize the node API in my project. I've hit a roadblock and haven't been able to find any information on this topic despite spending a significant amount of time researchi ...

Is it possible to align the last item in a MUI stack to the bottom?

https://i.stack.imgur.com/IsutM.png Currently, I am working on a modal component using MUI's Grid. My goal is to position the Button at the bottom of the modal. const styles = { modalBox: { position: 'absolute', top: &ap ...

Step-by-step guide on utilizing jQuery to fade out all elements except the one that is selected

There are multiple li elements created in this way: echo '<ul>'; foreach ($test as $value){ echo '<li class="li_class">.$value['name'].</li>'; } echo '</ul>'; This code will generate the fol ...

Converting every item's values into keys

Currently, my goal is to export all object values as keys specifically for a tree-shakable import system in a plugin I'm currently developing. The approach involves dynamically importing modules from various directories and subfolders, consolidating t ...

Add the element to a fresh collection of objects using an associative array

Can you help me figure out what's causing an issue when attempting to add a new element to an associative array of objects? var storeData3 = [ { 'key1' : 'value1' }, { 'key2' : 'value2' }, { 'key3&ap ...

Optimal method for organizing individuals into teams using Google Apps Script

There are approximately 200 individuals in this particular department. Our goal is to form groups of 4, with each group consisting of members from different teams based in the same city. Each group must have one driver and three non-drivers, all sharing si ...

Configuring static files in Django for hosting a website in production mode

I have been attempting to serve my static files from the same production site in a different way. Here are the steps I took: First, I updated the settings.py file with the following: DEBUG = False ALLOWED_HOSTS = ['12.10.100.11', 'localhos ...

Having trouble with Next.js environment variables not being recognized in an axios patch request

Struggling with passing environment variables in Axios patch request const axios = require("axios"); export const handleSubmit = async (formValue, uniquePageName) => { await axios .patch(process.env.INTERNAL_RETAILER_CONFIG_UPDATE, formVal ...

Here is a method to display a specific string in the mat-datepicker input, while only sending the date in the backend

enter image description hereIn this code snippet, there is a date input field along with a Permanent button. The scenario is that when the Permanent button is clicked, it should display "Permanent" in the input UI (nativeElements value), but the value bein ...

The variable is unable to be accessed within the PHP function query

After passing a variable through ajax to a function within my php file "Cart_code.php", I encountered an issue where the variable was not accessible inside the function. Can you help me figure out why? Javascript $.ajax({ type: "POST", url: "incl ...

Utilizing $templateCache with ui-router and minifying in AngularJS 1.x

Posting this as a question-answer post. How can one effectively utilize the $templateCache in the templateProvider of a route within ui-router when attempting to refactor the code? Injection is ineffective, and Angular cannot inject by reference. For ins ...

Array scripts are removed once data has been extracted from the CSV file

Having trouble reading a CSV file using file reader in Javascript. I wrote a script that declares arrays and updates them as the file is read. However, once the reading is complete, all data from the arrays gets wiped out. I'm quite new to JS and can& ...

Error message received when calling a function within a Vue watcher states "function is not defined"

My goal is to trigger a function in a Vue component when a prop changes using a watcher: props: [ 'mediaUrl' ], watch: { mediaUrl: function() { this.attemptToLoadImage(); } }, medthods: { attemptToLoadImage: function() { console ...

Menu/navigation bar designed with floating lines and an array of color options

I'm currently working on implementing this specific menu into my Wordpress site. My main concern is figuring out how to customize the hover effect for each navigation item. Currently, the float line changes to red (background-color:#800; height:2px;) ...

Send a pair of values using a set of two parameters

My current code is currently passing only one parameter value. However, I would like to modify it to pass two values with two parameters. This is my current code: <script> function getfilter(str){ document.getElementById("result"). ...

Encountering issue while static generating project: Cannot find resolution for 'fs' module

I am encountering an issue in my Next.js app when trying to serve a static file. Each time I attempt to use import fs from 'fs';, an error is thrown. It seems strange that I have to yarn add fs in order to use it, as I thought it was not necessa ...

How to retrieve nested menu items within the scope by utilizing JSON and AngularJS

I have recently started working with angular and am facing difficulty in accessing the submenu items within my angular application. While I can successfully access the top level menu items, I am struggling to retrieve the second level items. Here is a sni ...

Angular UI Bootstrap Typeahead - Apply a class when the element is added to the document body

In my current project, I am utilizing angular bootstrap typeahead with multiple instances. Some of these instances are directly appended to the body using the "typeahead-append-to-body" option. Now, I have a specific instance where I need to customize the ...

Processing JSON data through parsing in JavaScript

To fulfill the requirement, the data must be extracted via JSON and supplied to a chart. The data should be in the format of: var dataArray = [{data:[]},{data:[]}]; Below is the code to retrieve JSON data on the client-side: $.ajax({ type: "POST", ...

Conflicts arising between smoothState.js and other plugins

After successfully implementing the smoothState.js plugin on my website, I encountered an issue with another simple jQuery plugin. The plugin begins with: $(document).ready() Unfortunately, this plugin does not work unless I refresh the page. I have gone ...