Source - tate.org.uk. Sol LeWitt's Square Art

Recreating Sol LeWitt's Famous Square Art in JavaScript

I've been keeping myself a little busy lately to understand basic maths when I came across one of the artworks of Sol LeWitt.

The goal of this post is to understand how to recreate

Sol LeWitt's
A Square Divided Horizontally and Vertically into Four Equal Parts artwork in JavaScript. We will use
p5js
, a popular tool for creative coding. Let's get started!

Please note that basic familiarity with p5js is necessary to understand some of the code. You can visit their documentation or checkout

The Coding Train
youtube channel for an introduction.

If you observe carefully the original art is fairly easy to recreate. It has an outer rectangle (actually square) split into four parts that are painted with horizontal, vertical and diagonal lines.

To draw rectangle and lines between the edges, I had to revisit some of the basic concepts of

linear equations
that I learnt back in school. It felt quite good to understand the basics required to draw a line on a graph and actually use it to create this art.

We will eventually understand how to draw a rectangle on canvas and more specifically how to draw lines between two edges of a rectangle.

But before that let's understand what is a point in 2D space or a plane.

Point

A Point is a precise location on a surface or a plane, usually designated by a dot. If we consider our 2D plane represented by X & Y axis then in our code we can use array of size 2 to denote a point, for example, const point = [x, y].

Line

A Line is formed when two points located on a 2D plane joined by a line segment. A straight line is the shortest distance between two points.

P5 has a handy method line(x1, y1, x2, y2) to draw a line between two points on a 2D plane.

We will create a wrapper function that will eventually call p5's line method.

function drawLine(pointA, pointB) {
  p5.line(pointA[0], pointA[1], pointB[0], pointB[1]);
}

Quadrilateral

Quadrilateral literally means 4 sides. A more exact definition from wikipedia explains it further --  "a quadrilateral is a polygon with four edges (or sides) and four vertices or corners". Some examples include Rectangle, Square, Rhombus, etc.

So what makes Square, Rectangle and other Quadrilaterals different from each other? It's the interior angle & length of the sides compared with each other. For example, all sides of a Rectangle are joined at an angle of 90 degrees making two parallel sides (width & height) equal, while for square all sides are equal and the interior angle is always 90 degree.

Square is a special case of Rectangle, and therefore even though we will be drawing square we will call it a rectangle.

Drawing a rectangle on canvas in JavaScript

Let's first setup our canvas and draw a basic rectangle with p5's rect(x, y, width, height) method.

export default function Sketch(p5) {
  p5.setup = () => {
    p5.createCanvas(300, 300);
    p5.noFill();
    p5.noLoop();
  };
  p5.draw = () => {
    p5.background(0);
    p5.stroke(255);
    // leave 10% margin on all sides
    const margin = 0.1;
    const x = p5.width * margin;
    const y = p5.height * margin;
    // adjust width & height based on margin
    let w = p5.width - 2 * x;
    let h = p5.height - 2 * y;
    p5.rect(x, y, w, h);
  };
  function drawLine(pointA, pointB) {
    p5.line(pointA[0], pointA[1], pointB[0], pointB[1]);
  }
}

This code will produce a canvas of size 300 x 300 and rectangle (actually square) adjusted by margins.

 

If we think about it, every edge is a straight line between two points on a plane, and each edge has a start and end coordinate, or corner vertices.

Let's create a function called Rectangle that can hold values of all the coordinates of vertices of the rectangle. We will represent vertices as points a, b, c, d. Each vertex is a Point hence can be represented as const a = [x, y];

export default function Sketch(p5) {
...
  function Rectangle(x, y, width, height) {
    return {
      coordinates,
      draw,
    }
    function coordinates() {
      // adjust width & height as our scale might not be at zero
      const w = x + width;
      const h = y + height;
      return {
        a: [x, y],
        b: [w, y],
        c: [w, h],
        d: [x, h],
      };
    }
    function draw() {
      p5.rect(x, y, width, height);
    }
  }
...
}

Point at a K distance

Our job can be made easier if we can just locate an arbitrary point on each edge of the rectangle and draw a line between the two points. Actually there's a convenient mathematical formula to find coordinates of a point located exactly at k distance on a line segment, and we can use that as we now know coordinates of the vertices of our rectangle.

P(x,y)=(x1 + k(x2 - x1), y1 + k(y2 - y1))

where k is a fractional part of distance.

So to find coordinates of a point located exactly at the midpoint of an edge AB (vertices a & b makes edge AB), we would use k as 1/2 or 0.5.

We can write equivalent Javascript as -

// find coordinates of a point on line at distance k
function pointCoordinatesOnLine(pointA, pointB, k) {
  const x = pointA[0] + k * (pointB[0] - pointA[0]);
  const y = pointA[1] + k * (pointB[1] - pointA[1]);
  return [x, y];
}

Now using this method we can add another helper method in our Rectangle function to find center coordinates of all the edges of the rectangle.

function Rectangle(x, y, width, height) {
  return {
    centerCoordinates,
    ...
  }
  ...
  function centerCoordinates(midpoints = {}) {
    // represent center coordinates of a particular side
    const midpointMapping = {
      ab: 'p',
      bc: 'q',
      cd: 'r',
      da: 's',
      ba: 'p',
      cb: 'q',
      dc: 'r',
      ad: 's',
    };
    const calculateMidpoint = (point1, point2) => {
      let [kA, pointA] = point1;
      let [kB, pointB] = point2;
      const edge = `${kA}${kB}`;
      midpoints[midpointMapping[edge]] = pointCoordinatesOnLine(
        pointA,
        pointB,
        0.5
      );
      return point2;
    };
    const coordinatesArray = Object.entries(coordinates());
    const sideDA = [
      coordinatesArray[0],
      coordinatesArray[coordinatesArray.length - 1],
    ];
    [...coordinatesArray, ...sideDA].reduce((point1, point2) =>
      calculateMidpoint(point1, point2)
    );
    return midpoints;
  }
  ...
}

Let's recreate our rectangle on canvas but let's use the Rectangle.draw method instead.

 

Next we add lines to the midpoints using the co-ordinates we calculated, and divide our rectangle into four parts. Midpoint vertices are represented as p, q, r, s, o, here o simply means the center vertex of the rectangle.

p5.draw = () => {
...
    const { a, b, c, d } = rectangle.coordinates();
    const { p, q, r, s } = rectangle.centerCoordinates();
    const o = [x + w / 2, y + h / 2];
    drawLine(p, r);
    drawLine(q, s);
...
}

If we run above code we can see our rectangle perfectly divided into four parts.

Connecting edges

One final step we need to figure out is how to draw horizontal, vertical and diagonal lines between the edges. Actually, we already have everything in place to draw these lines. We have all the coordinates of the rectangle and we can use our method pointCoordinatesOnLine to find coordinates on each side at a certain distance on our rectangle. So let's create a new function that will help draw multiple lines between two edges.

p5.draw = () => {
  ...
  drawLines([a, p], [s, o], 12);
}
...
// helper function to draw lines between two
// edges every k distance
function drawLines(edge1, edge2, k) {
  const [pointA, pointB] = edge1;
  const [pointC, pointD] = edge2;
  for (let i = 0; i < k; ++i) {
    const [x1, y1] = pointCoordinatesOnLine(pointA, pointB, i / k);
    const [x2, y2] = pointCoordinatesOnLine(pointC, pointD, i / k);
    drawLine([x1, y1], [x2, y2]);
  }
}
...

And our sketch looks pretty close. We will need to increase the number of lines to match the original artwork.

Awesome! We are getting there. Let's draw the rest of the lines and finish our sketch.

p5.draw = () => {
  ...
  drawLines([a, p], [s, o], 18);
  drawLines([p, o], [b, q], 18);
  drawLines([s, o], [s, d], 13);
  drawLines([d, r], [o, r], 13);
  drawLines([q, c], [q, o], 13);
  drawLines([c, r], [o, r], 13);
}

Great! Here's our final output. It looks pretty close to the original work.

 

Let's add some background color and change our stroke color to match the original painting.

 

So that's it! This was a fun exercise for me. The final code is available on

github
. If you have any suggestions to improve feel free to reach out or open a pull request on github.

Namaskar 🙏🏾

© 2020 Varun Maliwal

LinkedIn logoGithub logoTwitter logo