Iterating on a theme

Sun Jul 20 2025

Articles in the On Grids series

In the last post, we discussed using a grid to draw diagonal lines in one direction or another, which leads to some reasonably complex outputs. See this codepen. This pen divides space into cells, defined by grid x and y position, and draws lines from the top-centre to the right-centre and bottom-centre to the left-centre, or vice-versa, depending on discrete rules.

In this post, we'll iterate on this basic setup in a couple of ways that will produce much more complex outcomes.

Recap

Recall this piece of code from the last post:

const topEdgePoint = {
  x: lerp(
    cell.points[0].x, 
    cell.points[1].x, 
    0.5
  ),
  y: cell.points[0].y,
};

This code finds a point between the top two points that make up a cell.

Showing a cell made up of 4 points, with an additional point 50% of the way between the top two points.

This image demonstrates this functionality and shows you exactly what the function is finding.

Iterating

You might remember that the third parameter of the lerp function describes a ratio between the two edges. Think of it like this:

lerp(
  start value, // The value at 0
  end value,   // The value at 1
  ratio        // The point between the two
)

What this realization may open up is the possibility to use a number other than .5 (50%). In fact, we can introduce a number, representing the number of lines we want per side, and loop to produce some complexity. Our new code might look something like this:

const numLines = 5;
for(let i = 0; i < numLines; i++) {
  const interpolant = i / numLines;
  const topEdgePoint = {
    x: lerp(
      cell.points[0].x, 
      cell.points[1].x, 
      interpolant
    ),
    y: cell.points[0].y,
  };
}

Now you can imagine we have something like this.

An illustration of lerping between points 0 and 1 5 times using a for loop, producing an array of ratios amounting to 0=0, 1=.2, 2=.4, 3=.6, 4=.8

Here you can see that the loop produces a series of 5 ratios from 0 to .8. If we repeat this on the right or left side, we'll get another series of points that will provide a way to draw lines.

const numLines = 5;
for(let i = 0; i < numLines; i++) {
  const interpolant = i / numLines;
  const rightEdgePoint = {
    y: lerp(
      cell.points[2].y, 
      cell.points[1].y, 
      interpolant
    ),
    x: cell.points[1].x,
  };
  const topEdgePoint = {
    x: lerp(
      cell.points[0].x, 
      cell.points[1].x, 
      interpolant
    ),
    y: cell.points[0].y,
  };
}

Crucial to understanding the way this works is understanding the order of the points we're lerping between. Notice in our code above, we're lerping between point 0 and 1, but then we're lerping between 2 and 1. The following illustration shows why this order change is important.

Repeating the iteration on the right edge gives us a second set of points we can use to draw a series of lines from the top edge, to the right.

As you can see, because we're lerping using ratios between 0 and 5, excluding 5, we never get a ratio of 1. This is necessary as drawing a line between point 1 and 1 would be meaningless.

Putting it into practice

All the code from the previous demo remains essentially unchanged, with the exception of the loop. If you just want to jump straight to the codepen example, click here.

Here's the updated code within the cell loop:

for(let i = 0; i < splits; i++) {
  const e = i/splits;

  const topEdgePoint = {
    x: lerp(
      cell.points[0].x, 
      cell.points[1].x, 
      e + topEdge.r),
    y: cell.points[0].y,
  };
  const leftEdgePoint = {
    x: cell.points[0].x,
    y: lerp(
      cell.points[3].y, 
      cell.points[0].y, 
      e + leftEdge.r),
  };
  const rightEdgePoint = {
    x: cell.points[1].x,
    y: lerp(
      cell.points[2].y, 
      cell.points[1].y, 
      e + rightEdge.r),
  };

  let a,b,c,d;
  if (edgeDirection === 1) {
    a = topEdgePoint;
    b = rightEdgePoint;
  } else {
    const topEdgePoint = {
      x: lerp(
        cell.points[1].x, 
        cell.points[0].x, 
        e + topEdge.r),
      y: cell.points[0].y,
    };
    a = topEdgePoint;
    b = leftEdgePoint;
  }

  toy.jump(a.x * cellWidth, a.y * cellHeight);
  toy.goto(b.x * cellWidth, b.y * cellHeight);
}

Here you can see exactly what we've implemented previously, with a small update to the logic depending on whether the edge direction is left or right. This is necessary for the reason explained in the last section - if left and right are iterating from the lower point to the upper, we need the top points to reflect a coincidental arrangement.

The image shows a black and white pattern. It's made up of many rows and columns of the same basic shape. That shape looks like a letter "V" or an open angle, created by several parallel black lines.

Putting it all together

Extending this to the other side of the cell we have both the top edge and the bottom edge extending to the left or right, depending on direction.

for(let i = 0; i < splits; i++) {
  const e = i/splits;

  const topEdgePoint = {
    x: lerp(
      cell.points[0].x, 
      cell.points[1].x, 
      e + topEdge.r),
    y: cell.points[0].y,
  };
  const bottomEdgePoint = {
    x: lerp(
      cell.points[3].x, 
      cell.points[2].x, 
      e + bottomEdge.r),
    y: cell.points[2].y,
  };
  const leftEdgePoint = {
    x: cell.points[0].x,
    y: lerp(
      cell.points[3].y, 
      cell.points[0].y, 
      e + rightEdge.r),
  };
  const rightEdgePoint = {
    x: cell.points[1].x,
    y: lerp(
      cell.points[2].y, 
      cell.points[1].y, 
      e + rightEdge.r),
  };

  let a,b,c,d;
  if (edgeDirection === 1) {
    a = topEdgePoint;
    b = rightEdgePoint;
    c = bottomEdgePoint;
    d = leftEdgePoint;
  } else {
    const topEdgePoint = {
      x: lerp(
        cell.points[1].x, 
        cell.points[0].x, 
        e + topEdge.r),
      y: cell.points[0].y,
    };
    const bottomEdgePoint = {
      x: lerp(
        cell.points[2].x, 
        cell.points[3].x, 
        e + bottomEdge.r),
      y: cell.points[2].y,
    };
    a = topEdgePoint;
    b = leftEdgePoint;
    c = bottomEdgePoint;
    d = rightEdgePoint;
  }

  toy.jump(a.x * cellWidth, a.y * cellHeight);
  toy.goto(b.x * cellWidth, b.y * cellHeight);
  if(i==0) continue;
  toy.jump(c.x * cellWidth, c.y * cellHeight);
  toy.goto(d.x * cellWidth, d.y * cellHeight);
}

You may notice that if(i==0) continue; in there. That just avoids a case where the points between top and bottom would produce the same line.

Click here to play around with the codepen for this post.

Wrapping up

We'll extend this idea even further in the next article, building out the system further with both irregular cells and variable width lines. But for now, consider the following code:

export const lerpPoint = (p1, p2, t) => ({
  x: lerp(p1.x, p2.x, t),
  y: lerp(p1.y, p2.y, t),
});

This is an extension of our lerp function that takes two points and returns a lerp between them. This will be very useful in the next post.

Iterating on a theme