SolandraExamplesDocsSlidesDownload Book

Randomness and Noise

Randomness is the raw material of generative art, and Solandra builds it in properly: every SCanvas has a seeded pseudo-random number generator. The same seed always produces exactly the same picture, which means you can reproduce, iterate on, and export the exact variation you like. Change the seed, get a sibling artwork.

All the methods below live on SCanvas and draw from that seeded generator.

The basics

s.random() // uniform number in [0, 1)
s.uniformRandomInt({ from: 3, to: 6 }) // integer: 3, 4, 5 or 6
s.uniformRandomInt({ to: 6, inclusive: false }) // 0..5
s.randomPoint() // a random point on the canvas
s.randomAngle() // uniform in [0, 2π)
s.randomPolarity() // -1 or 1
s.uniformGridPoint({ minX: 0, maxX: 4, minY: 0, maxY: 4 }) // random integer point
s.background(220, 25, 12)
s.times(150, () => {
  s.setFillColor(s.uniformRandomInt({ from: 150, to: 220 }), 80, 60, 0.8)
  s.fill(
    new Star({
      at: s.randomPoint(),
      r: 0.01 + 0.04 * s.random(),
      n: s.uniformRandomInt({ from: 4, to: 7 }),
      a: s.randomAngle(),
    })
  )
})

Working with collections

sample picks one element from an array, samples picks n (with replacement), and shuffle reorders an array in place:

const palette = [200, 215, 340, 45]
s.sample(palette) // e.g. 340
s.samples(3, palette) // e.g. [45, 200, 45]
s.shuffle([...palette]) // e.g. [215, 45, 200, 340]
s.background(0, 0, 96)
const hues = [200, 215, 340, 45]
s.forTiling({ n: 9, type: "square", margin: 0.05 }, ([x, y], [dX]) => {
  s.setFillColor(s.sample(hues), 70, s.sample([45, 55, 65]))
  s.fill(new Square({ at: [x + dX * 0.05, y + dX * 0.05], s: dX * 0.9 }))
})

perturb

perturb({ at, magnitude }) nudges a point by a uniform random offset up to magnitude / 2 in each direction; the quickest way to make something regular feel hand-made:

s.background(35, 40, 94)
s.lineWidth = 0.005
s.setStrokeColor(25, 60, 30)
s.forTiling({ n: 12, type: "square" }, ([x, y], [dX, dY]) => {
  s.draw(
    SimplePath.withPoints([
      [x, y],
      [x + dX, y],
      [x + dX, y + dY],
      [x, y + dY],
    ])
      .close()
      .transformLoopedPoints((pt) => s.perturb({ at: pt, magnitude: 0.02 }))
  )
})

Other distributions

Uniform randomness often looks too random. Solandra also offers:

  • gaussian({ mean, sd }) — normally distributed values, which cluster around the mean
  • poisson(lambda) — non-negative integers with mean (and variance) lambda, good for "how many things here?" decisions
s.background(0, 0, 12)
s.times(400, () => {
  const x = s.gaussian({ mean: 0.5, sd: 0.12 })
  const y = s.gaussian({ mean: 0.5, sd: 0.12 })
  const d = v.distance([x, y], [0.5, 0.5])
  s.setFillColor(200 + d * 300, 80, 60, 0.7)
  s.fill(new Circle({ at: [x, y], r: 0.012 }))
})

Also see proportionately and doProportion on the Iteration page for weighted random choices, and forPoissonDiskPoints for evenly spread random points.

Perlin noise

Random numbers are independent of each other; noise is randomness with continuity: nearby inputs give nearby outputs. Solandra exports a 2D Perlin noise function, perlin2(x, y), returning values in roughly [-1, 1]. Scale your inputs to control the frequency (zoomed-in inputs change slowly, zoomed-out ones quickly).

import { perlin2 } from "solandra"
 
s.background(0, 0, 96)
s.forTiling({ n: 30, type: "square" }, ([x, y], [dX], [cX, cY]) => {
  const n = perlin2(x * 3, y * 3)
  s.setFillColor(190 + n * 60, 70, 50)
  s.withTranslation([cX, cY], () => {
    s.withRotation(n * Math.PI, () => {
      s.fill(
        new Rect({ at: [0, 0], w: dX * 0.9, h: dX * 0.18, align: "center" })
      )
    })
  })
})

A classic use is a flow field: use noise as an angle everywhere and let particles follow it:

s.background(220, 40, 14)
s.lineWidth = 0.003
s.times(120, (i) => {
  let pt = s.randomPoint()
  const path = SimplePath.startAt(pt)
  s.times(30, () => {
    const a = perlin2(pt[0] * 2.5, pt[1] * 2.5) * Math.PI * 2
    pt = v.add(pt, [0.008 * Math.cos(a), 0.008 * Math.sin(a)])
    path.addPoint(pt)
  })
  s.setStrokeColor(170 + i, 70, 60, 0.7)
  s.draw(path.chaiken({ n: 2 }))
})

Seeds and reproducibility

The seed is supplied when the SCanvas is created (in this site's canvases there's a refresh control for it; with the raw API it's a constructor argument). Within a sketch you can also rewind determinism explicitly:

s.resetRandomNumberGenerator(42) // restart the sequence from a known seed

If you need seeded randomness outside a sketch, the underlying generator is exported directly:

import { RNG } from "solandra"
 
const rng = new RNG(42)
rng.number() // deterministic sequence of numbers in [0, 1)

Next: Colour, Gradients and Palettes.

Solandra was made by James Porter.

Check out the GitHub page or install with npm i solandra