SolandraExamplesDocsSlidesDownload Book

Colour, Gradients and Palettes

Solandra standardises on HSLA colours: hue (0–360), saturation (0–100), lightness (0–100), alpha (0–1). HSLA is a great fit for generative work because meaningful variations are simple arithmetic: rotate the hue for a new colour scheme, nudge lightness for depth, drop alpha for layering.

s.setFillColor(210, 70, 50) // h, s, l
s.setStrokeColor(210, 70, 50, 0.5) // ... and alpha
s.background(45, 20, 95)
 
// or with a { h, s, l, a } object (a ColorSpec)
s.setFillColorFromSpec({ h: 210, s: 70, l: 50 })
s.backgroundFromSpec({ h: 45, s: 20, l: 95 })

Hue arithmetic in action; each ring shifts hue and lightness slightly:

s.background(230, 40, 12)
s.aroundCircle({ n: 12, r: 0.3 }, ([x, y], i) => {
  s.times(5, (n) => {
    s.setFillColor(200 + i * 12, 70, 65 - n * 8, 0.85)
    s.fill(new Circle({ at: [x, y], r: 0.11 - n * 0.02 }))
  })
})

Canvas gradients

LinearGradient and RadialGradient produce real canvas gradients. Both take colour stops as [position, ColorSpec] tuples and can be used for fills, strokes or backgrounds via setFillGradient, setStrokeGradient and backgroundGradient.

import { LinearGradient, RadialGradient } from "solandra"
 
s.backgroundGradient(
  new LinearGradient({
    from: [0, 0],
    to: [0, 1],
    colors: [
      [0, { h: 215, s: 80, l: 25 }],
      [0.6, { h: 340, s: 70, l: 40 }],
      [1, { h: 30, s: 90, l: 60 }],
    ],
  })
)
s.setFillGradient(
  new RadialGradient({
    start: [0.5, 0.42],
    end: [0.5, 0.42],
    rStart: 0,
    rEnd: 0.35,
    colors: [
      [0, { h: 50, s: 100, l: 80 }],
      [1, { h: 50, s: 100, l: 80, a: 0 }],
    ],
  })
)
s.fill(new Circle({ at: [0.5, 0.42], r: 0.35 }))

Colour themes (step-wise gradients)

Sometimes you want a discrete sequence of colours rather than a canvas gradient: one colour per tile, per layer, per particle. The theme helpers each return a function from a step number to a ColorSpec:

  • simpleLinearGradient(a, b, steps) — interpolate every channel between two colours
  • hueRange({ h1, h2, s, l, steps }) — vary only hue
  • saturationRange({ h, s1, s2, l, steps }) — vary only saturation
  • lightnessRange({ h, s, l1, l2, steps }) — vary only lightness
  • alphaRange({ h, s, l, a1, a2, steps }) — vary only alpha
import { hueRange, lightnessRange } from "solandra"
 
s.background(0, 0, 96)
const warm = hueRange({ h1: 0, h2: 60, s: 80, l: 55, steps: 12 })
const fade = lightnessRange({ h: 215, s: 70, l1: 25, l2: 80, steps: 12 })
s.forHorizontal({ n: 12, margin: 0.06 }, ([x, y], [dX, dY], _c, i) => {
  s.setFillColorFromSpec(warm(i))
  s.fill(new Rect({ at: [x, y], w: dX * 0.85, h: dY * 0.45 }))
  s.setFillColorFromSpec(fade(i))
  s.fill(new Rect({ at: [x, y + dY * 0.55], w: dX * 0.85, h: dY * 0.45 }))
})

Generative palettes

Based on Inigo Quilez's cosine palette technique, Solandra can generate whole colour palettes procedurally. palettePreset(name, steps) gives you a curated palette as an array of [h, s, l] tuples; presets are "rainbow", "warmth", "rusty", "autumnal", "natural", "neon" and "subtle".

import { palettePreset } from "solandra"
 
s.background(0, 0, 10)
const presets = ["rainbow", "warmth", "natural", "neon", "subtle"]
s.forVertical({ n: 5, margin: 0.05 }, ([x, y], [dX, dY], _c, i) => {
  const colors = palettePreset(presets[i], 10)
  colors.forEach(([h, sat, l], j) => {
    s.setFillColor(h, sat, l)
    s.fill(
      new Rect({
        at: [x + (j * dX) / 10, y + dY * 0.1],
        w: dX / 10 - 0.005,
        h: dY * 0.8,
      })
    )
  })
})

For full control use palette({ a, b, c, d, steps }) with your own cosine coefficients (each a [r, g, b] triple): color(t) = a + b * cos(2π(c * t + d)).

import { palette } from "solandra"
 
s.background(230, 30, 10)
const colors = palette({
  a: [0.6, 0.4, 0.5],
  b: [0.4, 0.4, 0.4],
  c: [1.0, 1.0, 1.0],
  d: [0.1, 0.25, 0.55],
  steps: 64,
})
s.forTiling(
  { n: 8, type: "square", margin: 0.05 },
  (_pt, [dX], [cX, cY], i) => {
    const [h, sat, l] = colors[i % colors.length]
    s.setFillColor(h, sat, l)
    s.fill(new Star({ at: [cX, cY], n: 6, r: dX * 0.42, a: i * 0.1 }))
  }
)

Transparency and layering

Because alpha is a first-class argument everywhere, translucent layering (a staple of generative texture) is easy; combine it with withBlendMode from Transforms for stronger effects:

s.background(45, 30, 96)
s.times(60, () => {
  s.setFillColor(s.sample([200, 215, 230]), 70, 55, 0.15)
  s.fill(
    new Circle({
      at: s.perturb({ at: [0.5, 0.5], magnitude: 0.6 }),
      r: 0.08 + s.random() * 0.15,
    })
  )
})

Solandra was made by James Porter.

Check out the GitHub page or install with npm i solandra