Drawing imperfect circles
written by syntaks
Unlike using pen and paper, it’s trivial to draw a perfect circle using common code libraries. In p5.js, you’d do:
circle(x, y, diameter);
On the other hand, it’s difficult to draw an imperfect circle. Here, we’ll show how it can be done.
First, let’s see how we can draw points on a circle’s circumference:
function drawPoints() {
for (let i = 0; i < numVertices; i++) {
const rad = i * 2 * PI / numVertices;
const x = radius * cos(rad);
const y = radius * sin(rad);
circle(canvasSide / 2 + x, canvasSide / 2 - y, canvasSide * .05);
}
}
This renders:
The basic idea is to divide one revolution (2π radians) into a fixed number of steps and draw one point per step. Here, each step is 2 * PI / numVertices
radians. We can use trigonometry (see "Sine and cosine" on Wikipedia) to determine the corresponding points on the circumference.
We don’t actually want to render these points, but we can use them as vertices in a shape. curveVertex
helps us draw smooth curves between points:
function drawShape() {
beginShape();
for (let i = 0; i < numVertices; i++) {
const rad = i * 2 * PI / numVertices;
const x = radius * cos(rad);
const y = radius * sin(rad);
curveVertex(canvasSide / 2 + x, canvasSide / 2 - y);
}
endShape();
}
That's not quite what we wanted though. From drawPoints
above, we know the points are placed properly. But we also know they don’t form a closed loop as we didn’t include the first point twice. Further, per curveVertex
’s documentation:
The first and last points [...] guide the beginning and end of the curve.
This means our first and last points also need points to guide them. That is, we can draw the (closed) circle by duplicating a total three of the existing points. Lastly, we create the imperfection by randomly offsetting each point:
function drawImperfectCircle(noise = .1) {
let vertices = [];
for (let i = 0; i < numVertices; i++) {
const rad = i * 2 * PI / numVertices;
const x = radius * cos(rad) * random(1 - noise, 1 + noise);
const y = radius * sin(rad) * random(1 - noise, 1 + noise);
vertices.push({ x: canvasSide / 2 + x, y: canvasSide / 2 - y });
}
// Duplicate 3 vertices to close the loop and provide guides for the first and last points.
for (let i = 0; i < 3; i++) {
vertices.push(vertices[i]);
}
beginShape();
for (let i = 0; i < vertices.length; i++) {
curveVertex(vertices[i].x, vertices[i].y);
}
endShape();
}
And that’s it! Below follow a few simple sketches created using this algorithm.