Working with paths

Path Time

A path is a continuous curve made up of anchors. We can use path.anchors to get information about the path at the anchor points, but what about in-between the anchors?

That’s where path “time” comes in. For an open path, time is a number that goes from 0 to anchors.length - 1. On closed paths, time can go up to anchors.length (completing the loop). You can think of time as an animation from the start of the path when time is 0, to the end.

Note: Time is an integer at anchors, so path.positionAtTime(1) will return the position of the path’s second anchor.

Let’s use path time to get a continuous position along the path:

// For each path in the input... input.allPaths().forEach((path) => { console.log(path.anchors.length); const position = path.positionAtTime(0.0); console.geometry(Anchor(position)); }); return input;

Example: Repeating a shape along a Path

Now that we can get the position on a path at any time, let’s use it to place some shapes.

// Create a shape. const triangle = Polygon({ sides: 3 }).transform({ scale: 0.25, }); // Give our shape a style. triangle.copyStyle(input); // Accumulate some output. const output = []; input.allPaths().forEach((path) => { // Step time at a constant rate. range(0, path.anchors.length - 1, 0.25).forEach((time) => { // Get a position from the path. const position = path.positionAtTime(time); // Push a triangle at that position to the output. output.push(triangle.clone().transform({ position })); }); }); return output;

Time at Distance

You might have noticed that our shapes are not evenly spaced in the previous example. That’s because Bezier paths are non-linear, meaning the “position” on the path might speed up or slow down depending on the anchor handles.

If we want our shapes spaced evenly, we need to step along the path by “distance” rather than time. We can use the path.timeAtDistance() to turn a distance into a time, and then use the time to get a position.

Example: Repeating along a Path (Distance)

Instead of stepping along the path at a fixed time interval, we’ll step at a fixed distance up to the length of the path.

// Create a shape. const triangle = Polygon({ sides: 3 }).transform({ scale: 0.25, }); // Give our shape a style. triangle.copyStyle(input); // Accumulate some output. const output = []; input.allPaths().forEach((path) => { // Get the total arc length of the path. const length = path.length(); // Step distance at a constant rate. range(0, length, 0.25).forEach((distance) => { // Convert distance to time. const time = path.timeAtDistance(distance); // Get a position from the path. const position = path.positionAtTime(time); // Push a triangle at that position to the output. output.push(triangle.clone().transform({ position })); }); }); return output;

Now our triangles are spaced more evenly! You can see that as the length of the path changes, the number of triangles also changes.

Tangents and Normals

Now we can place shapes at positions along a path, but what if we want to orient the shapes to the face in the direction that the path is heading?

For this, we can use the path “tangent”. A tangent is like an anchor handle. It’s a unit vector that faces in the direction of the path, and we can get one for any time along the path.

Let’s write a simple modifier that logs tangents at fixed distances along the path.

// For each path in the input... input.allPaths().forEach((path) => { // Get the total arc length of the path. const length = path.length(); // Step distance at a constant rate. range(0, length, 0.25).forEach((distance) => { // Convert distance to time. const time = path.timeAtDistance(distance); // Get a position from the path. const position = path.positionAtTime(time); // Get a tangent from the path. const tangent = path.tangentAtTime(time); // Show the tangent. console.geometry(LineSegment(position, position + tangent)); }); }); return input;

We can show normals, too. A path “normal” is like a tangent, but oriented perpendicular to the path. A normal vector is actually just a tangent vector rotated by 90°.

// For each path in the input... input.allPaths().forEach((path) => { // Get the total arc length of the path. const length = path.length(); // Step distance at a constant rate. range(0, length, 0.25).forEach((distance) => { // Convert distance to time. const time = path.timeAtDistance(distance); // Get a position from the path. const position = path.positionAtTime(time); // Get a normal from the path. const normal = path.normalAtTime(time); // Show the normal. console.geometry(LineSegment(position, position + normal * 0.5)); }); }); return input;

Example: Putting it all together!

We can use tangent.angle() to rotate our triangles to face in the direction of the path.

const spacing = 0.3; // A small, rightwards-facing triangle. const triangle = Polygon({ sides: 3, }) .transform({ rotation: 90, scale: Vec(0.1, 0.2), }) .copyStyle(input); // We'll accumulate some output. const output = []; // For each path in the input... input.allPaths().forEach((path) => { // Get the total arc length of the path. const length = path.length(); // Step along the length of the path by `spacing`. range(0, length, spacing).forEach((distance) => { // Convert distance to time. const time = path.timeAtDistance(distance); // Get a position from the path. const position = path.positionAtTime(time); // We can also get the tangent direction (a unit-lengthed Vec). const tangent = path.tangentAtTime(time); // Taking the angle of the tangent Vec. const angle = tangent.angle(); // Push a triangle at that position and angle to the output. output.push( triangle.clone().transform({ position, rotation: angle, }) ); }); }); return output;