A27 ·
Polar Coordinates 🌀
Cartesian Coordinate System
A coordinate system allows us to use numbers to determine the position of points in a 2D or 3D space. The most popular coordinate system is probably the Cartesian coordinate system. It allows you to locate each point by a pair of numerical coordinates (x, y). There is a very good chance that you have used this system. It is everywhere — SVG, Canvas, WebGL and even Sketch & Illustrator.
Figure 1: Cartesian coordinate system
Polar Coordinate System
Polar coordinate system is a 2D coordinate system in which each point is determined by r & θ. Where r is the distance from the origin and θ is the angle from the x-axis.
Figure 2: Polar coordinate system
Converting Between Polar and Cartesian Coordinates
Trigonometry! Love it or hate, it is everywhere. You can convert a point from polar coordinates (r, θ) to Cartesian coordinates (x, y) using the equations below. These come in really handy since most tools only accept x & y as point locations.
const x = r * Math.cos(theta);
const y = r * Math.sin(theta);
Patterns
Below I have a few animations by Dave Whyte aka bees & bombs. They all have one thing in common, they use polar coordinates to generate a pattern.




Figure 3: Patterns generated using polar coordinates
Cartesian coordinates are a great choice for placing things evenly on a rectangular grid. However, if you want to distribute things evenly around a circle then polar coordinates are generally the better option. Keeping the radius constant we compute the angle for each point as angle = 360° * index / number of sides.
Figure 4: Placing items evenly around a circle
Below is the JavaScript implementation of this idea. The points function also accepts an optional offset argument to shift the points along the perimeter of the circle. For example, the outermost circle of points in Figure 4 can be generated using points(12, 200). The next one inwards using points(12, 175, 15). The one after that using points(12, 150, 30) and so on. My splash CodePen is was built using this.
function points(count, radius, offset = 0) {
const angle = 360 / count;
const vertexIndices = range(count);
return vertexIndices.map(index => {
return {
theta: offset + degreesToRadians(offset + angle * index),
r: radius,
};
});
}
// number => [0, 1, 2, ... number]
function range(count) {
return Array.from(Array(count).keys());
}
function degreesToRadians(angleInDegrees) {
return (Math.PI * angleInDegrees) / 180;
}
Polygon Generator
A regular polygon is a polygon that is equiangular i.e., all its angles are equal and all its sides have the same length. This means that all the vertices of a regular polygon are points evenly spaced on a circle. And isn’t it handy that we just created a function that generates exactly this!
Figure 5: Drawing a polygon by connecting polar coordinates
To generate an SVG polygon we will generate the list of of points using the points function and then simply connect the dots. With SVG we have two options for this: <polygon> or <path>. The example below generates the points attribute for a <polygon> element.
/**
* Usage with Vanilla JS/DOM:
* polygonEl.setAttribute('points', polygon((5, 64, 18)));
*
* Usage with React:
* <polygon points={polygon((5, 64, 18)} />
*
* Usage with View:
* <polygon :points="polygon((5, 64, 18)" />
*/
function polygon(noOfSides, circumradius, rotation) {
return points(noOfSides, circumradius, rotation)
.map(toCartesian)
.join(' ');
}
function toCartesian({ r, theta }) {
return [r * Math.cos(theta), r * Math.sin(theta)];
}
Seems simple but, you can do a lot of interesting things with it. Here are a couple of examples:

<div id="hex-a-portal"></div> body, html {
margin: 0;
padding: 0;
}
.input-range-rv {
vertical-align: middle;
background-color: transparent;
padding-top: 8px;
padding-top: .5rem;
padding-bottom: 8px;
padding-bottom: .5rem;
color: inherit;
background-color: transparent;
-webkit-appearance: none;
}
.input-range-rv::-webkit-slider-thumb {
position: relative;
width: 8px;
width: .5rem;
height: 20px;
height: 1.25rem;
cursor: pointer;
margin-top: -8px;
margin-top: -0.5rem;
border-radius: 3px;
background-color: #fff;
-webkit-appearance: none;
}
/* Touch screen friendly pseudo element */
.input-range-rv::-webkit-slider-thumb:before {
content: '';
display: block;
position: absolute;
top: -8px;
top: -0.5rem;
left: -14px;
left: -0.875rem;
width: 36px;
width: 2.25rem;
height: 36px;
height: 2.25rem;
opacity: 0;
}
.input-range-rv::-moz-range-thumb {
width: 8px;
width: .5rem;
height: 20px;
height: 1.25rem;
cursor: pointer;
border-radius: 3px;
border-color: transparent;
border-width: 0;
background-color: #fff;
}
.input-range-rv::-webkit-slider-runnable-track {
height: 4px;
height: 0.25rem;
cursor: pointer;
border-radius: 3px;
background-color: rgba(255, 255, 255, .25);
}
.input-range-rv::-moz-range-track {
height: 4px;
height: 0.25rem;
cursor: pointer;
border-radius: 3px;
background-color: rgba(255, 255, 255, .25);
}
.input-range-rv:focus {
outline: none;
} const degreesToRadians = angle => (Math.PI * angle) / 180;
const range = count => Array.from(Array(count).keys());
function polygon(sideCount, radius) {
const theta = 360 / sideCount;
const vertexIndices = range(sideCount);
return vertexIndices.map(idx => ({
theta: degreesToRadians(theta * idx),
r: radius,
}));
}
const pathDef = vertices =>
`M${vertices.map(({ theta, r }) => [
300 + (r * Math.cos(theta)),
300 + (r * Math.sin(theta)),
]).join('L')}Z`;
class HexAPortal extends React.Component {
constructor(props) {
super(props);
this.state = {
sideCount: 6,
radius: 150,
};
}
componentDidMount() {
const ease = Linear.easeNone;
const tl = new TimelineMax({
repeat: -1,
onUpdate: () => {
TweenMax.to([
this.chaser1,
this.chaser2,
this.chaser3,
this.chaser4,
this.chaser5,
], 1, {
strokeDashoffset: '-=450',
});
},
});
tl.to(this.canvas, 2, {
rotation: -360,
transformOrigin: '50% 50%',
ease,
});
tl.timeScale(1);
}
update = (prop, value) => {
this.setState({ [prop]: Number(value) });
};
render() {
const { sideCount, radius } = this.state;
return (
<div className="bg-black vh-100 ph4 flex flex-column">
<svg
className="mw7 center mb4 db flex-auto"
ref={(canvas) => { this.canvas = canvas; }}
viewBox="0 0 600 600"
fill="none"
>
<defs>
<filter id="glow" y="-50%" height="200%">
<feGaussianBlur stdDeviation="12 1" result="coloredBlur" />
<feGaussianBlur stdDeviation="12 1" result="coloredBlur" />
<feGaussianBlur stdDeviation="12 1" result="coloredBlur" />
<feMerge>
<feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<path
id="hexPath"
d={pathDef(polygon(sideCount, radius))}
fill="none"
strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"
strokeMiterlimit="10" strokeDasharray="112"
/>
</defs>
<g filter="url(#glow)" >
<use
xlinkHref="#hexPath"
ref={(p) => { this.chaser1 = p; }}
stroke="#FFFFFF" strokeDashoffset="10100"
/>
<use
xlinkHref="#hexPath"
ref={(p) => { this.chaser2 = p; }}
stroke="#0000FF" strokeDashoffset="10060"
/>
<use
xlinkHref="#hexPath"
ref={(p) => { this.chaser3 = p; }}
stroke="#00FF00" strokeDashoffset="10040"
/>
<use
xlinkHref="#hexPath"
ref={(p) => { this.chaser4 = p; }}
stroke="#FF0000" strokeDashoffset="10020"
/>
<use
xlinkHref="#hexPath"
ref={(p) => { this.chaser5 = p; }}
stroke="#1d1d1d" strokeDashoffset="10000"
/>
</g>
</svg>
<div className="flex mb4 mw7 center">
<label htmlFor="sideCount" className="f5 ttu tracked b w5 tr dib mr3 white">
Number of Sides <span className="gray">({ sideCount })</span>
</label>
<input
name="sideCount" type="range" className="input-range-rv flex-auto white"
min="3" max="16" step="1"
value={sideCount}
onChange={e => this.update('sideCount', e.target.value)}
/>
</div>
<div className="flex mb5 mw7 center">
<label htmlFor="radius" className="f5 ttu tracked b w5 tr dib mr3 white">
Radius <span className="gray">({ radius })</span>
</label>
<input
name="radius" type="range" className="input-range-rv flex-auto white"
min="100" max="300" step="1"
value={radius}
onChange={e => this.update('radius', e.target.value)}
/>
</div>
</div>
);
}
}
ReactDOM.render(
<HexAPortal />,
document.getElementById('hex-a-portal')
); Relative Polar Coordinates
When you define a point as (r, θ), by default, this is relative to the origin (0, 0). We can define points relative to other points by shifting the origin. This is often used for defining the position of curve handles relative to a vertex.
const x = cx + r * Math.cos(theta);
const y = cy + r * Math.sin(theta);
Figure 6: Relative polar coordinates
We can modify the polygon generator to allow us to draw a polygon centred at any location in the SVG canvas.
function polygon(noOfSides, circumradius, rotation, [cx = 0, cy = 0]) {
return points(noOfSides, circumradius, rotation)
.map(pt => toCartesian(pt, [cx, cy]))
.join(' ');
}
function toCartesian({ r, theta }, [cx, cy]) {
return [cx + r * Math.cos(theta), cy + r * Math.sin(theta)];
}
<main class="pa4 dark-gray mw7 center sans-serif min-vh-100 flex flex-column flex-row-ns items-center">
<form class="measure-wide mr4-ns mb4 mb0-ns f5">
<fieldset class="ba b--transparent pa0 mh0">
<legend class="f5 b ph0 mh0 ttu tracked bb bw2 pv2 db w-100">
Polygon Generator
</legend>
<div class="mt5 mb4">
<label class="f5 ttu tracked db b mb2" for="js-side-count">
Number of Sides
</label>
<input class="b pa2 input-reset ba bw2 bg-white b--dark-gray w-100"
type="number" id="js-side-count" value="8">
</div>
<div class="mv4">
<label class="f5 ttu tracked db b mb2" for="js-radius">
Radius
</label>
<input class="b pa2 input-reset ba bw2 bg-white b--dark-gray w-100"
type="number" id="js-radius" value="200">
</div>
<fieldset class="ba b--transparent pa0 mh0 mt4 mb5">
<legend class="f5 ttu tracked db b mb2">Center</legend>
<label class="f5 ttu tracked db b mb2 clip" for="js-cx">
Center X Coordinate
</label>
<label class="f5 ttu tracked db b mb2 clip" for="js-cy">
Center Y Coordinate
</label>
<div class="flex">
<input class="b pa2 input-reset ba bw2 bg-white b--dark-gray w-100 mr3"
type="number" id="js-cx" value="396">
<input class="b pa2 input-reset ba bw2 bg-white b--dark-gray w-100"
type="number" id="js-cy" value="418">
</div>
</fieldset>
</fieldset>
<div>
<input class="b pa3 input-reset ba b--dark-gray bw2 bg-white grow pointer f6 dib ttu tracked"
id="js-generate" type="button" value="Generate">
</div>
</form>
<div class="flex-auto flex items-center justify-center w-100">
<div class="w-100">
<svg class="w-100 white debug-grid mb4 db" viewBox="0 0 800 800"
stroke="#111" fill="none" stroke-width="4"
id="js-svg">
<polygon id="js-polygon" points="" />
</svg>
<pre id="js-result" class="code pa3 bg-near-white mb0 overflow-scroll"></pre>
</div>
</div>
</main> pre {
overflow-wrap: break-word;
} const sideCountEl = document.querySelector('#js-side-count');
const radiusEl = document.querySelector('#js-radius');
const cxEl = document.querySelector('#js-cx');
const cyEl = document.querySelector('#js-cy');
const generateEl = document.querySelector('#js-generate');
const polygonEl = document.querySelector('#js-polygon');
const resultEl = document.querySelector('#js-result');
const svgEl = document.querySelector('#js-svg');
function pts(sideCount, radius) {
const angle = 360 / sideCount;
const vertexIndices = range(sideCount);
const offsetDeg = 90 - ((180 - angle) / 2);
const offset = degreesToRadians(offsetDeg);
return vertexIndices.map((index) => {
return {
theta: offset + degreesToRadians(angle * index),
r: radius,
};
});
}
function range(count) {
return Array.from(Array(count).keys());
}
function degreesToRadians(angleInDegrees) {
return (Math.PI * angleInDegrees) / 180;
}
function polygon([cx, cy], sideCount, radius) {
return pts(sideCount, radius)
.map(({ r, theta }) => [
cx + r * Math.cos(theta),
cy + r * Math.sin(theta),
])
.join(' ');
}
function generatePolygon() {
const sideCount = +sideCountEl.value;
const radius = +radiusEl.value;
const cx = +cxEl.value;
const cy = +cyEl.value;
const s = 2 * radius + 50;
const res = polygon([cx, cy], sideCount, radius);
const viz = polygon([s / 2, s / 2], sideCount, radius);
svgEl.setAttribute('viewBox', `0 0 ${s} ${s}`);
polygonEl.setAttribute('points', viz);
resultEl.innerText = `<polygon points="${res}" />`;
}
generateEl.onclick = generatePolygon;
window.onload = generatePolygon; Rotation
Another common application of polar coordinates is to rotate things around a point. Here (cx, cy) is the point about which you want to rotate.
x = cx + r * Math.cos(theta);
y = cy + r * Math.sin(theta);
// somewhere in an animation loop
window.setInterval(() => {
theta++;
}, 1000 / 60);
Polar curves
We started by looking at individual points. Then we grouped a few points into a set to define shapes. For this we used the polygon generator function to compute the location of each vertex of the shape. We can write similar functions using other mathematical equations. Allowing us to generate more complex shapes and curves.
Two dimensional curves are described by equations of the type y = f(x). For example the equation of a circle is x2 + y2 = r2. We can generate the set of points, called locus, by iterating x and computing the corresponding y value or vice-versa. Therefore, each point will be of the form (x, f(x)) or (g(y), y).
With polar coordinates we can similarly draw polar curves. For example, the polar equation of a circle is r = 2 * cos(0). The points on a polar curve have the form (r(0), 0).
// examples of fn:
// circle : 2 * Math.cos(theta)
// blob thing : a * (1 - Math.cos(theta) * Math.sin(3 * theta))
const r = fn(theta);
const x = cx + r * Math.cos(theta);
const y = cy + r * Math.sin(theta);
<div class="vh-100 flex items-center justify-center">
<canvas id="curve"></canvas>
</div> const canvas = document.getElementById('curve');
const ctx = canvas.getContext('2d');
const w = 300; const h = 300;
const cx = w; const cy = h;
canvas.width = w * 2;
canvas.height = h * 2;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
ctx.clearRect(0, 0, w * 2, h * 2);
ctx.moveTo(cx, cy);
ctx.beginPath();
const fn = t =>
1 - Math.cos(t) * Math.sin(3 * t);
Array.from(Array(630).keys())
.forEach(idx => {
const t = idx * 0.01;
const r = w * 0.5 * fn(t);
const x = cx + r * Math.cos(t);
const y = cy + r * Math.sin(t);
ctx.lineTo(x, y);
});
ctx.strokeStyle = '#333';
ctx.lineWidth = 4;
ctx.lineJoin = 'round';
ctx.stroke(); Eukleides
All the diagrams in this post were created using a language called eukleides. It is a fantastic tool for making geometric drawings. Just look at this declarative API 😍
c = circle(point(3, 0), 3)
P = point(c, 170°)
M = point(0, 0)
N = point(6, 0)
draw (M.N.P)
label M, P, N right, 0.6