nudoru
natural media
paths
Creating Interesting Paths

Creating Interesting Paths

written by nudoru

05 Jan 2024500 EDITIONS
1 TEZ

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;
};

stay ahead with our newsletter

receive news on exclusive drops, releases, product updates, and more

feedback