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 }))
})
})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 }))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 colourshueRange({ h1, h2, s, l, steps }) — vary only huesaturationRange({ h, s1, s2, l, steps }) — vary only saturationlightnessRange({ h, s, l1, l2, steps }) — vary only lightnessalphaRange({ h, s, l, a1, a2, steps }) — vary only alphaimport { 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 }))
})harmony builds a colour scheme from a base colour:
complementary (the default) — the base hue and its oppositeanalogous — neighbouring huestriadic — three hues evenly spaced round the circletetradic — four hues, two complementary pairssplitComplementary — the base hue and two hues either side of its oppositemonochrome — one hue at a range of lightnessesSchemes 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 }))
})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.
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 }))
}
)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