Generative Icon Design, a Solandra tutorial

This tutorial introduces the Solandra framework for algorithmic art via the practical task of designing a modern looking App Icon for a mobile App.

In Solandra drawing is usually done with a function, which takes a Solandra Canvas as its only argument. This Canvas is built on top of HTML Canvas, but with nicer APIs and a bunch of my opinions about how things should be done. It is TypeScript-first for great autocompletition and elimination of silly mistakes.

Okay let's draw something. An App icon is usually a rounded rectangle. Let's include a dark background too. The code is really simple. The background is a one line call (setting the hue, saturation and lightness: how colour always works in Solandra (actually opacity is optional fourth argument)). We then set the fill colour to something light and call fill on a path. Solandra comes with a bunch of built in paths including a rounded rectangle.

Points are always [x,y]. I've tried all the obvious alternatives and this seems to work best in context of Javascript/TypeScript. Most Solandra APIs take an object with concise keys, many of which are optional. While this is friendlier than confusing(x,y,w,h,rX,rY) or whatever APIs it could be argued to be less clear than something more explicit like draw({ width: 0.5, height: 0.4}). Solandra takes the opinion that verbosity itself sacrifices some clarity and that remembering that h is height isn't too hard. Anyway enough on this, let's focus on practical stuff.

(s: SCanvas) => {
  s.background(215, 40, 15)
  s.setFillColour(45, 50, 95)
  s.fill(
    new RoundedRect({ at: [0.1, 0.1], w: 0.8, h: 0.8, r: 0.15 })
  )
}

Let's make it look like a real icon. Add a circle with these two new lines.

s.setFillColour(30, 95, 65)
s.fill(new Circle({ at: [0.5, 0.5], r: 0.3 }))

Okay we are kind of done. Some pretty successful Apps have Icons that are pretty similar to this. But let's make it a bit more interesting. Solandra comes several built in control flow functions. The least interesting do things like iterate over a range of values. Let's do something with that.

s.range({ from: 0.3, to: 0.1, n: 3 }, r => {
  s.setFillColour(175 + r * 100, 95, 65)
  s.fill(new Circle({ at: [0.5, 0.5], r }))
})

The canvas in Solandra is always of width 1. As these examples are square the height is also 1. We don't worry about pixels, so if we ever want a larger or smaller size we don't have to change anything.

One of the aewesome things about generative drawing is that we can easily create an infinite amount of variation. Let's generate a bunch of alternatives based on the sample above. One of Solandra's more interesting control flow functions allows for tiling. Let's start by drawing the above icon many, many times.

That wasn't hard, but let's actually generate a new variation for each tile. Let's mess around with both the colours and circle sizes.

We added several sources of random variation: the base hue of the central part, the size of the circles and the direction of the colour gradient from centre outwards. Maybe you see an icon you like more than our hand coded one? But these are still a bit boring.

Moving around the circles has revealed a problem... they are now overlapping in a nasty way. This is actually trivial to solve in Solandra. It allows you to clip (restrict drawing to) with any of the paths you might draw.

(s: SCanvas) => {
  s.background(215, 40, 15)
  s.forTiling(
    { n: 6, type: 'square', margin: 0.05 },
    ([x, y], [dX], c, i) => {
      const rr = new RoundedRect({
        at: [x + dX * 0.1, y + dX * 0.1],
        w: dX * 0.8,
        h: dX * 0.8,
        r: dX * 0.15,
      })

      const baseHue = s.uniformRandomInt({ from: 0, to: 360 })

      s.setFillColour(baseHue, 50, 90)
      s.fill(rr)

      s.withClipping(rr, () => {
        const polarity = s.randomPolarity()
        const size = s.gaussian({ mean: 0.1, sd: 0.05 })
        const centre = v.add(
          c,
          v.scale([0, s.random() - 0.5], 0.15)
        )

        s.range(
          {
            from: 0.3 + size,
            to: 0.1 + size,
            n: s.uniformRandomInt({ from: 2, to: 6 }),
          },
          r => {
            s.setFillColour(baseHue + polarity * r * 100, 95, 65)
            s.fill(new Circle({ at: centre, r: r * dX }))
          }
        )
      })
    }
  )
}}

Let's amp up the variation in position and add a bit more transparency.

Let's make things a bit sharper by swapping out the circles for triangles.

There seem to be some nice ideas in there. Let's implement something based on one of them. We'll manually position a few triangles and add a gradient background.

(s: SCanvas) => {
    s.background(215, 40, 15)
    s.setFillGradient(
      new LinearGradient({
        from: [0, 0],
        to: [0, 1],
        colours: [
          [0, { h: 45, s: 50, l: 95 }],
            [1, { h: 345, s: 60, l: 75 }],
        ],
      })
    )
    s.fill(
      new RoundedRect({ at: [0.1, 0.1], w: 0.8, h: 0.8, r: 0.15 })
    )

    s.setFillColour(345, 95, 65, 0.7)
    s.fill(new RegularPolygon({ at: [0.65, 0.45], r: 0.2, n: 3 }))
    s.setFillColour(15, 95, 65, 0.8)
    s.fill(new RegularPolygon({ at: [0.4, 0.45], r: 0.25, n: 3 }))
    s.setFillColour(0, 95, 65, 0.8)
    s.fill(new RegularPolygon({ at: [0.5, 0.65], r: 0.2, n: 3 }))
  }

We started out with a very simple minimalist icon and have, with the help of Solandra quickly iterated into something more interesting.