Exercises

Scatter Plots

In this exercise we'll implement scatter plots as in Figure generative:distributions. Experiment with different distributions (trying creating your own distributions by transforming ones defined on Random).

There are three main components of a scatter plot:

We tackle each task in turn.

Start by writing a method makePoint that will accept a Random[Double] for the x and y coordinates of a point and return a Random[Point]. It should have the following skeleton:

def makePoint(x: Random[Double], y: Random[Double]): Random[Point] =
  ???

Use a for comprehension in your implementation.

<div class="solution"> This is a nice example of composition of Randoms.

def makePoint(x: Random[Double], y: Random[Double]): Random[Point] =
  for {
    theX <- x
    theY <- y
  } yield Point.cartesian(theX, theY)

</div>

Now create, say, a thousand random points using the techniques we learned in the previous chapter on lists and a random distribution of your choice. You should end up with a List[Random[Point]].

<div class="solution"> Something like the following should work.

val normal = Random.normal(50, 15)
val normal2D = makePoint(normal, normal)

val data = (1 to 1000).toList.map(_ => normal2D)

</div>

Let's now transform our List[Random[Point]] into List[Random[Image]]. Do this in two steps: first write a method to convert a Point to an Image, then write code to convert List[Random[Point]] to List[Random[Image]].

<div class="solution"> We can convert a Point to an Image using a method point below. Note I've made each point on the scatterplot quite transparent---this makes it easier to see where a lot of points are grouped together.

def point(loc: Point): Image =
  Image.circle(2).fillColor(Color.cadetBlue.alpha(0.3.normalized)).noStroke.at(loc.toVec)

Converting between the lists is just a matter of calling map a few times.

val points = data.map(r => r.map(point _))

</div>

Now create a method that transforms a List[Random[Image]] to a Random[Image] by placing all the points on each other. This is the equivalent of the allOn method we've developed previously, but it now works with data wrapped in Random.

<div class="solution"> You might recognise this pattern. It's what we used in allOn with the addition of flatMap, which is exactly what randomConcentricCircles (and many other examples) use.

def allOn(points: List[Random[Image]]): Random[Image] =
  points match {
    case Nil => Random.always(Image.empty)
    case img :: imgs => 
      for {
        i  <- img
        is <- allOn(imgs)
      } yield (i on is)
  }

</div>

Now put it all together to make a scatter plot.

<div class="solution"> This is just calling methods and using values we've already defined.

val plot = allOn(points)

</div>

Parametric Noise

In this exercise we will combine parametric equations, from a previous chapter, with randomness.

Let's start by making a method perturb that adds random noise to a Point. The method should have skeleton

def perturb(point: Point): Random[Point] =
  ???

Choose whatever noise function you like.

<div class="solution"> Here's our solution. We've already seen very similar code in the scatter plot.

def perturb(point: Point): Random[Point] =
  for {
    x <- Random.normal(0, 10)
    y <- Random.normal(0, 10)
  } yield Point.cartesian(point.x + x, point.y + y) 

</div>

Now create a parametric function, like we did in a previous chapter. You could use the rose function (the function we explored previously) or you could create one of your own devising. Here's the definition of rose.

def rose(k: Int): Angle => Point =
  (angle: Angle) => {
    Point.cartesian((angle * k).cos * angle.cos, (angle * k).cos * angle.sin)
  }

We can combine our parametric function and perturb to create a method with type Angle => Random[Point]. You can write this easily using the andThen method on functions, or you can write this out the long way. Here's a quick example of andThen showing how we write the fourth power in terms of the square.

val square = (x: Double) => x * x
val quartic = square andThen square

<div class="solution"> Writing this with andThen is nice and neat.

def perturbedRose(k: Int): Angle => Random[Point] =
  rose(k) andThen perturb

</div>

Now using allOn create a picture that combines randomnes and structure. Be as creative as you like, perhaps adding color, transparency, and other features to your image.

<div class="solution"> Here's the code we used to create Figure generative:volcano. It's quite a bit larger than code we've seen up to this point, but you should understand all the components this code is built from.

object ParametricNoise {
  def rose(k: Int): Angle => Point =
    (angle: Angle) => {
      Point.cartesian((angle * k).cos * angle.cos, (angle * k).cos * angle.sin)
    }

  def scale(factor: Double): Point => Point =
    (pt: Point) => {
      Point.polar(pt.r * factor, pt.angle)
    }

  def perturb(point: Point): Random[Point] =
    for {
      x <- Random.normal(0, 10)
      y <- Random.normal(0, 10)
    } yield Point.cartesian(point.x + x, point.y + y) 

  def smoke(r: Normalized): Random[Image] = {
    val alpha = Random.normal(0.5, 0.1)
    val hue = Random.double.map(h => (h * 0.1).turns)
    val saturation = Random.double.map(s => s * 0.8)
    val lightness = Random.normal(0.4, 0.1)
    val color =
      for {
        h <- hue
        s <- saturation
        l <- lightness
        a <- alpha
      } yield Color.hsla(h, s, l, a)
    val c = Random.normal(5, 5) map (r => Image.circle(r))
    
    for {
      circle <- c
      line   <- color
    } yield circle.strokeColor(line).noFill
  }

  def point(
    position: Angle => Point,
    scale: Point => Point,
    perturb: Point => Random[Point],
    image: Normalized => Random[Image],
    rotation: Angle
  ): Angle => Random[Image] = {
    (angle: Angle) => {
      val pt = position(angle)
      val scaledPt = scale(pt)
      val perturbed = perturb(scaledPt)

      val r = pt.r.normalized
      val img = image(r)

      for {
        i  <- img
        pt <- perturbed
      } yield (i at pt.toVec.rotate(rotation))
    }
  }

  def iterate(step: Angle): (Angle => Random[Image]) => Random[Image] = {
    (point: Angle => Random[Image]) => {
      def iter(angle: Angle): Random[Image] = {
        if(angle > Angle.one)
          Random.always(Image.empty)
        else
          for {
            p  <- point(angle)
            ps <- iter(angle + step)
          } yield (p on ps)
      }

      iter(Angle.zero)
    }
  }

  val image: Random[Image] = {
    val pts =
      for(i <- 28 to 360 by 39) yield {
        iterate(1.degrees){
          point(
            rose(5),
            scale(i),
            perturb _,
            smoke _,
            i.degrees
          )
        }
      }
    val picture = pts.foldLeft(Random.always(Image.empty)){ (accum, img) =>
      for {
        a <- accum
        i <- img
      } yield (a on i)
    }
    val background = (Image.rectangle(650, 650).fillColor(Color.black))

    picture map { _ on background }
  }
}

</div>