Colour, Gradients and Palettes

Solandra uses HSLA colours: hue (0–360), saturation (0–100), lightness (0–100) and alpha (0–1). Change hue to shift colour, lightness to make it lighter or darker, and alpha to adjust opacity.

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 })

Each ring below shifts the hue and lightness:

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 take colour stops as [position, ColorSpec] tuples. Apply them with setFillGradient, setStrokeGradient or 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)

For a separate colour per tile, layer or particle, use a theme helper. Each returns a function that maps 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 }))
})

Colour schemes

harmony builds a colour scheme from a base colour:

  • complementary (the default) — the base hue and its opposite
  • analogous — neighbouring hues
  • triadic — three hues evenly spaced round the circle
  • tetradic — four hues, two complementary pairs
  • splitComplementary — the base hue and two hues either side of its opposite
  • monochrome — one hue at a range of lightnesses

Schemes preserve the base saturation, lightness and alpha, except monochrome, which varies lightness. n sets the colour count for analogous and monochrome. spread sets hue spacing or the lightness range. All schemes start with the base colour except monochrome, which runs from dark to light.

import { harmony } from "solandra"
 
s.background(0, 0, 96)
const base = { h: 205, s: 70, l: 50 }
const schemes = [
  harmony(base, { type: "analogous", n: 5 }),
  harmony(base, { type: "triadic" }),
  harmony(base, { type: "splitComplementary" }),
  harmony(base, { type: "monochrome", n: 5 }),
]
 
s.forVertical({ n: 4, margin: 0.05 }, ([x, y], [dX, dY], _c, i) => {
  schemes[i].forEach((color, j) => {
    s.setFillColorFromSpec(color)
    const w = dX / schemes[i].length
    s.fill(new Rect({ at: [x + j * w, y], w: w * 0.94, h: dY * 0.8 }))
  })
})

A scheme is an array of ColorSpec objects. Use sample to pick a colour and setFillColorFromSpec to apply it:

s.background(30, 15, 92)
const scheme = harmony(
  { h: 15, s: 70, l: 55 },
  { type: "analogous", n: 5, spread: 18 }
)
s.forTiling({ n: 12, type: "square", margin: 0.05 }, (at, [w, h]) => {
  s.setFillColorFromSpec(s.sample(scheme))
  s.fill(new Rect({ at, w, h }))
})

Mixing colours

mixColors(a, b, proportion) blends colours along the shortest route around the hue circle. For example, hues 350 and 10 blend through 0, a span of 20 degrees.

import { mixColors } from "solandra"
 
const sunset = { h: 350, s: 80, l: 55 }
const gold = { h: 40, s: 85, l: 60 }
 
s.forHorizontal({ n: 24 }, (at, [dX, dY], _c, i) => {
  s.setFillColorFromSpec(mixColors(sunset, gold, i / 23))
  s.fill(new Rect({ at, w: dX, h: dY }))
})

Use mixColors with a harmony scheme to blend between its colours. Proportions outside 0–1 extrapolate beyond the two colours.

Generative palettes

palettePreset(name, steps) returns an array of [h, s, l] tuples using Inigo Quilez's cosine palette technique. 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

Set alpha below 1 to layer translucent shapes. Combine this with withBlendMode from Transforms:

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