Creating Interesting Paths

Starting from a very simple path, let’s see how easy it can be to turn it into something interesting! The approach below is the basic method I used on my recent fx(hash) release, Crisis Worlds.

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.

image

Below is the output.

image

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.

image
image

Let’s pull in some code from my last tutorial on generative brushes. But instead of drawing random circles at each step along the line, we’ll use it to build the array of points between the start and end.

image
image

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.

image
image
image

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.

image

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.

image

Then we run it over our points array and get this. The smoothed line is in green.

image
image

Looking nice! What if we want even more smoothing? Just iterate the smoothing function a few times. Let’s modify the chainkinSmooth function to iterate over the input points as many times as we specify.

image

Running this with 3 iterations returns a much smoother line.

image

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.

Below are some examples with increasing amounts of starting points (pointsToInsert).

image
image
image

Paths like this are perfect for tracing with a natural media brush 😉. I hope this has given you new ideas and new areas to explore in your work!

Please give me a follow on 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;
};

Leave a Reply