Solving Sol with Solandra

This is an unconventional introduction to Solandra, an agile framework for algorithmic art, which teaches it by implementing solutions to some simple instructions from Sol LeWitt (an early algorithmic artist). It is based on Solving Sol's challenges. For a more conventional introduction, see the main Solandra website, which has many examples and a tutorial.

Wall Drawing #16 (1969) Bands of lines 12 inches (30 cm) wide, in three directions (vertical, horizontal, diagonal right) intersecting.

Okay, let's get going. Solandra sketches are functions on an SCanvas, where most functionality is exposed. This gives us nice autocompletion and doesn't pollute things with some kind of global context. Let's draw by filling in a background (our wall):

(s: SCanvas) => {
  s.background(30, 20, 90)
}

Now let's draw some lines. Solandra gives us access to meta data via meta. We use destructuring to pull out relevant things then draw 3 lines.

const {
  bottom,
  right,
  center: [cX, cY],
} = s.meta

s.drawLine([cX, 0], [cX, bottom])
s.drawLine([0, cY], [right, cY])
s.drawLine([0, 0], [right, bottom])

But we are meant to be drawing bands of lines. Let's draw 13, assuming each is '1 inch' apart.

(s: SCanvas) => {
  s.background(30, 20, 90)
  const {
    bottom,
    right,
    center: [cX, cY],
  } = s.meta

  s.lineWidth = 0.0025
  s.times(13, n => {
    const d = (n - 6) * 0.01
    const dSq2 = d / Math.sqrt(2)

    s.drawLine([cX + d, 0], [cX + d, bottom])
    s.drawLine([0, cY + d], [right, cY + d])
    s.drawLine([dSq2, -dSq2], [right + dSq2, bottom + -dSq2])
  })
}

We can add some colour to make things more interesting. Colour is always hue, saturation. lightness (and optionally opacity).

s.setStrokeColour(215, 90, 30)
s.drawLine([cX + d, 0], [cX + d, bottom])
s.setStrokeColour(15, 90, 30)
s.drawLine([0, cY + d], [right, cY + d])
s.setStrokeColour(45, 90, 30)
s.drawLine([dSq2, -dSq2], [right + dSq2, bottom + -dSq2])

Wall Drawing #17 (1969) Four-part drawing with a different line direction in each part.

Actually support for hatching is pretty much built in to Solandra. Let's see that working:

(s: SCanvas) => {
  s.background(215, 40, 95)
  s.lineWidth = 0.005
  s.forHorizontal({ n: 4, margin: 0.1 }, (at, [w, h], c, i) => {
    s.setStrokeColour(215, i * 20, 50)
    s.withClipping(new Rect({ at, w, h }), () => {
      s.draw(
        new Hatching({
          at: c,
          r: 1,
          a: (i * Math.PI) / 11,
          delta: 0.02,
        })
      )
    })
  })
}

Solandra supports hatching around a circle, but we can clip to a rectangular area.

Wall Drawing #91 (1971) A six-inch (15 cm) grid covering the wall. Within each square, not straight lines from side to side, using red, yellow and blue pencils. Each square contains at least one line of each color.

(s: SCanvas) => {
  const hues = [215, 350, 45]
  s.background(0, 0, 15)
  s.lineWidth = 0.005
  s.forTiling({ n: 12, type: 'square' }, (at, [w, h], c, i) => {
    const [x, y] = at
    s.withClipping(new Rect({ at, w, h }), () => {
      s.times(3, j => {
        const start: Point2D = [x, y + h * s.random()]
        const end: Point2D = [x + w, y + h * s.random()]
        const p = Path.startAt(start).addCurveTo(end, {
          curveSize: s.gaussian({ mean: 0.0, sd: 0.2 }),
        })
        s.setStrokeColour(hues[j], 90, 60, 0.95)
        s.draw(p)
      })
      s.times(s.poisson(3), () => {
        const start: Point2D = [x, y + h * s.random()]
        const end: Point2D = [x + w, y + h * s.random()]
        const p = Path.startAt(start).addCurveTo(end, {
          curveSize: s.gaussian({ mean: 0.0, sd: 0.2 }),
        })
        s.setStrokeColour(s.sample(hues), 90, 60, 0.95)
        s.draw(p)
      })
    })
  })
}

Wall Drawing #797 (1995) The first drafter has a black marker and makes an irregular horizontal line near the top of the wall. Then the second drafter tries to copy it (without touching it) using a red marker. The third drafter does the same, using a yellow marker. The fourth drafter does the same using a blue marker. Then the second drafter followed by the third and fourth copies the last line drawn until the bottom of the wall is reached.

(s: SCanvas) => {
  const N = 60
  const hues = [5, 45, 210]
  let hIdx = 0
  s.background(45, 35, 97)
  const { right, bottom } = s.meta
  const dV = 0.05 * bottom
  const sp = SimplePath.startAt([0, dV])
  let maxY = 0
  s.times(N, n => {
    const x = ((n + 1) * right) / N
    const y =
      dV + s.random() * 0.01 + s.gaussian({ mean: 0.04, sd: 0.03 })
    sp.addPoint([x, y])
  })
  s.draw(sp)
  while (maxY < bottom - dV) {
    s.setStrokeColour(hues[hIdx], 95, 45)
    sp.transformPoints(([x, y]) => {
      const nY = y + dV / 2 + (dV * s.poisson(3)) / 15
      if (nY > maxY) maxY = nY
      return [x, nY]
    })
    s.draw(sp)

    hIdx++
    if (hIdx > 2) hIdx = 0
  }
}