Creating Interesting Paths
written by nudoru
Note: The code samples use my own private framework. If you have some basic experience with JavaScript and p5js, it should be easy to adapt them. The variable c represents my personal drawing library, and the methods should translate to p5js almost identically. I also use several random number convenience methods, and what they do should be clear from the function name.
First, we’ll create two starting points, an array to hold them, and then a function to draw them.
Below is the output,
We need to connect them with a line. We’ll modify the code in the forEach loop to get the next point and draw a line between the current point and the one after it.
Let's write a function to insert additional points between these two.
Now we have a perfectly plain line between the start and endpoints. That’s no fun! Let's add code to move each point around before we add it to the array.
You can go crazy with this, and if you do, you’ll see some interesting behavior soon.
To make things easier from here on, let’s extract the point drawing code into a new function.
What if we wanted to further refine and smooth out this line? We need an algorithm to take in all of our rough points and smooth out the corners by adding in new points. The Chaikin Algorithm is the perfect solution for this! Matt DesLauriers has a JavaScript implementation of it that we can use.
Adding the code to our example project and inlining the vec2-copy import, we get this.
Then we run it over our points array and get this. The smoothed line is in green.
Looking nice! What if we want even more smoothing? Just iterate the smoothing function a few times. Let’s modify the chainkinSmooth
function to recursively iterate over the input points as many times as we specify.
Running this with 3 iterations returns a much smoother line.
And that's it! We’ve gone from a simple line from point A to point B and transformed it into a flowing path. Experiment with different values, even polygons, to see what you can create! The more starting points you have, the wilder the paths will be.
Paths like this are perfect for tracing with a natural media brush 😉. I hope this has given you new ideas and areas to explore in your work!
Please give me a follow on X/Twitter @nudoru and Instagram @hfaze! If you use this in a project, give me a shout-out, I’d love to see what you make!
Below is the full code for what I've created above. As stated above, this is for my own framework and will need to be adapted to p5js.
const chaikinSmooth = (points, itr = 1) => {
const smoothFn = (input, output = []) => {
const copy = (out, a) => {
out[0] = a[0];
out[1] = a[1];
return out;
};
if (input.length > 0) output.push(copy([0, 0], input[0]));
for (let i = 0; i < input.length - 1; i++) {
const p0 = input[i];
const p1 = input[i + 1];
const p0x = p0[0];
const p0y = p0[1];
const p1x = p1[0];
const p1y = p1[1];
const Q = [0.75 * p0x + 0.25 * p1x, 0.75 * p0y + 0.25 * p1y];
const R = [0.25 * p0x + 0.75 * p1x, 0.25 * p0y + 0.75 * p1y];
output.push(Q);
output.push(R);
}
if (input.length > 1) output.push(copy([0, 0], input[input.length - 1]));
return output;
};
if (itr === 0) return points;
const smoothed = smoothFn(points);
return itr === 1 ? smoothed : chaikinSmooth(smoothed, itr - 1);
};
const drawPoints = (pointsArray, pColor = 'red', lColor = 'blue') => {
// Loop over the array and pass in the point and the current index
pointsArray.forEach((point, idx) => {
c.noStroke();
c.fill(pColor);
c.circle(point[0], point[1], 5);
// Draw a line between the current points, and the next one
// If it's the last point, don't do anything
const next = idx < pointsArray.length - 1 ? pointsArray[idx + 1] : null;
if (next) {
const px1 = point[0];
const py1 = point[1];
const px2 = next[0];
const py2 = next[1];
c.stroke(lColor);
c.line(px1, py1, px2, py2);
}
});
};
const draw = () => {
const cw = c.width; // Width of the canvas
const ch = c.height; // Height of the canvas
const m = 200; // Margin
const x1 = m;
const x2 = cw - m;
const y1 = ch / 2 - 50;
const y2 = ch / 2 + 50;
const points = [];
const pointsToInsert = randomWholeBetween(10, 50); // Will insert 1 more than this
const xIncrement = (x2 - x1) / pointsToInsert;
const yIncrement = (y2 - y1) / pointsToInsert;
let currentX = x1;
let currentY = y1;
const minOffset = 50;
const maxOffset = 100;
for (let i = 0; i <= pointsToInsert; i++) {
// Only move around the middle points
if (i > 0 && i < pointsToInsert) {
// Random radius between the min and max
const rRadius = randomNumberBetween(minOffset, maxOffset);
// Random radians between 0 and 2PI, the full circle
const rRadians = randomNumberBetween(0, Math.PI * 2);
const offsetX = currentX + rRadius * Math.cos(rRadians);
const offsetY = currentY + rRadius * Math.sin(rRadians);
points.push([offsetX, offsetY]);
} else {
points.push([currentX, currentY]);
}
currentX += xIncrement;
currentY += yIncrement;
}
const smoothPoints = chaikinSmooth(points, 3);
drawPoints(points, 'red', 'blue');
drawPoints(smoothPoints, 'green', 'green');
return false;
};