Tutorial 193: Sierpinski

Joe Clay | Sep 13, 2019

In building something for a client last week, I came up with an interesting movement using our generative paths from Tutorial 190: Cicada. At the same time I was doing more experiments with L-systems and so I coded a sort of Logo or Turtle graphics interpreter expression inside of After Effects. So this is the result of that.

The base premise is that we take the total number of points and fit them into a Trim Paths operation—or a pointOnPath() expression—by fitting the total number of points into 100. So we divide 100 by our total number of points, and set keys to move the end of our trim by that tiny increment. Then using loopOut('offset'), we have that tiny path move down the whole path. Setting our trim's start to be just a tiny bit away from the end effectively lets us use strokes that extend past the end like round or projecting caps to make circular or square dots.

There's a lot of additional things that can be done. After recording, I even built a system to put layers at specific positions matching the points of these generated paths so that anything could follow the paths in this mechanical fashion. By adding multiple points together, you could even make interesting animated shapes with these, which we'll probably do for Patron files this month. Also, that position setup found it's way into the project files if you want to buy them below.

Have fun experimenting with this! It's a rabbit hole.

Expression Code

Make sure to add all of the slider controls necessary as well as an angle control and rename them. We need Iterations, Distance, and the angle control is called Angle. The expression below should be applied to a path in a shape layer.

cmd would be the axiom or start. And the rules go in the map object. And the replace method within the first for loop. This first one makes the Dragon curve.

pts = [];
var x = y = 0;
var a = -90;
var cmd = 'FX';
var iterations = thisComp.layer("Controller").effect("Iterations")("Slider");
var d = thisComp.layer("Controller").effect("Distance")("Slider");
var angle = thisComp.layer("Controller").effect("Angle")("Angle");
var map = { X:"X+YF+", Y:"-FX-Y"};
for(var i = 0; i < iterations; i++) {
    cmd = cmd.replace(/X|Y/g, function(matched) {
        return map[matched];
    })
}
pts.push([x,y]);
for(var i = 0; i < cmd.length; i++) {
    switch(cmd.charAt(i)) {
        case 'F':
            x += Math.cos(degreesToRadians(a)) * d;
            y += Math.sin(degreesToRadians(a)) * d;
            pts.push([x,y]);
            break;
        case '+':
            a += angle;
            break;
        case '-':
            a -= angle;
            break;
    }
}
createPath(pts, [], [], false);

This one creates the Sierpinski curve.

pts = [];
var x = y = 0;
var a = -60;
var cmd = 'A';
var iterations = thisComp.layer("Controller").effect("Iterations")("Slider");
var d = thisComp.layer("Controller").effect("Distance")("Slider");
var angle = thisComp.layer("Controller").effect("Angle")("Angle");
var map = { A:"B-A-B", B:"A+B+A"};
for(var i = 0; i < iterations; i++) {
    cmd = cmd.replace(/A|B/gi, function(matched) {
        return map[matched];
    })
}
pts.push([x,y]);
for(var i = 0; i < cmd.length; i++) {
    switch(cmd.charAt(i)) {
        case 'B':
        case 'A':
            x += Math.cos(degreesToRadians(a)) * d;
            y += Math.sin(degreesToRadians(a)) * d;
            pts.push([x,y]);
            break;
        case '+':
            a += angle;
            break;
        case '-':
            a -= angle;
            break;
    }
}
createPath(pts, [], [], false);

And this one makes my...uh...Cauliflower curve. Good luck with it.

pts = [];
var x = y = bufferX = bufferY = 0;
var a = bufferA = -60;
var cmd = 'X';
var iterations = thisComp.layer("Controller").effect("Iterations")("Slider");
var d = thisComp.layer("Controller").effect("Distance")("Slider");
var angle = thisComp.layer("Controller").effect("Angle")("Angle");
var map = { X:"F+[[X]-X]-F[-FX]+X", F:"FF"};
for(var i = 0; i < iterations; i++) {
    cmd = cmd.replace(/X|F/gi, function(matched) {
        return map[matched];
    })
}
pts.push([x,y]);
for(var i = 0; i < cmd.length; i++) {
    switch(cmd.charAt(i)) {
        case 'F':
            x += Math.cos(degreesToRadians(a)) * d;
            y += Math.sin(degreesToRadians(a)) * d;
            pts.push([x,y]);
            break;
        case '+':
            a += angle;
            break;
        case '-':
            a -= angle;
            break;
        case '[':
            bufferX = x;
            bufferY = y;
            bufferA = a;
            break;
        case ']':
            x = bufferX;
            y = bufferY;
            a = bufferA;
            break;
    }
}
createPath(pts, [], [], false);

Grab the Project Files

The best way to get our project files is to become a patron on Patreon. For $5 a month, you get access to all of the tutorial project files we've made available as well as other monthly projects, rigs, R&D, elements, early product previews, and BTS content not available anywhere else! You can also purchase just this project file on our Gumroad Store if you would rather do that.

Get access to all of our project files on Patreon or Get this single project file on Gumroad