Generative art is mostly loops. Solandra makes the common looping patterns first-class: instead of fiddling with indices and offsets you get positions, sizes and centers handed to you, already in canvas coordinates.
The workhorse. Divides the canvas into an n-column grid and calls you for each tile with (point, delta, center, index):
point — the top-left corner of the tiledelta — the [width, height] of the tilecenter — the center of the tileindex — a sequential counterOptions: margin (space around the edges), type: "square" (force square tiles; the default "proportionate" follows the canvas aspect ratio), and order ("columnFirst" or "rowFirst").
s.background(210, 25, 95)
s.forTiling(
{ n: 8, type: "square", margin: 0.05 },
([x, y], [dX], [cX, cY], i) => {
s.setFillColor(120 + i * 2, 60, 50)
s.fill(
new RegularPolygon({
at: [cX, cY],
n: 3 + (i % 5),
r: dX * 0.4,
a: i * 0.05,
})
)
}
)Divide the canvas into full-height columns or full-width rows. Same callback signature as forTiling.
s.background(0, 0, 15)
s.forHorizontal({ n: 12, margin: 0.1 }, ([x, y], [dX, dY], _c, i) => {
const h = 0.3 + 0.6 * Math.abs(Math.sin(i * 0.8))
s.setFillColor(20 + i * 10, 80, 60)
s.fill(
new Rect({ at: [x + dX * 0.15, y + dY * (1 - h)], w: dX * 0.7, h: dY * h })
)
})forMargin(margin, callback) is the degenerate but useful case: a single cell inset from the edges.
Iterates over integer coordinates, for algorithmic patterns on discrete grids (pair it with the hex, triangle and isometric transforms):
s.background(45, 40, 96)
s.forGrid({ minX: 1, maxX: 9, minY: 1, maxY: 9 }, ([x, y], i) => {
const on = (x * y) % 3 === 0
s.setFillColor(on ? 340 : 215, 70, 55)
s.fill(
new Circle({
at: [x * 0.1, y * 0.1],
r: on ? 0.04 : 0.015,
})
)
})Simple counting loops: times(n, cb) counts up from 0, downFrom(n, cb) counts down from n (great for painter's-algorithm layering), and range({ from, to, n }, cb) walks n (by default inclusive) steps across a numeric interval:
s.background(230, 40, 12)
s.downFrom(9, (n) => {
s.setFillColor(260 - n * 15, 70, 25 + n * 6)
s.fill(new Circle({ at: [0.5, 0.5], r: 0.05 * n }))
})
s.setStrokeColor(0, 0, 100, 0.6)
s.lineWidth = 0.002
s.range({ from: 0, to: Math.PI / 3, n: 12, inclusive: false }, (a) => {
s.draw(new RegularPolygon({ at: [0.5, 0.5], n: 3, r: 0.47, a }))
})Places n points evenly around a circle (at defaults to the canvas center, r to 0.25):
s.background(20, 30, 95)
s.aroundCircle({ n: 24, r: 0.35 }, ([x, y], i) => {
s.setFillColor(i * 15, 70, 55)
s.fill(new Star({ at: [x, y], n: 5, r: 0.045, a: i * 0.3 }))
})Poisson disk sampling gives random points that are never closer than minDist: random but evenly spread, like scattered seeds. Much nicer than uniform random points for organic textures.
s.background(210, 60, 12)
s.forPoissonDiskPoints({ minDist: 0.08 }, ([x, y], i) => {
s.setFillColor(180 + y * 100, 70, 60)
s.fill(new Circle({ at: [x, y], r: 0.025 }))
})Every iteration helper can be composed with two higher-order utilities.
build runs an iteration helper but collects the callback's return values into an array, so you can gather data first and draw later:
// collect tile centers, then connect them in a shuffled tour
s.background(0, 0, 96)
const centers = s.build(
s.forTiling,
{ n: 6, type: "square" },
(_pt, _d, c) => c
)
s.shuffle(centers)
s.lineWidth = 0.004
s.setStrokeColor(215, 60, 45)
centers.forEach((from, i) => {
const to = centers[(i + 1) % centers.length]
s.draw(new Line(from, to))
})withRandomOrder runs an iteration helper but executes the callbacks in shuffled order; essential when overlapping tiles should layer unpredictably:
s.background(40, 30, 95)
s.withRandomOrder(
s.forTiling,
{ n: 6, type: "square", margin: 0.1 },
([x, y], [dX], _c, i) => {
s.setFillColor(i * 4, 70, 55, 0.95)
s.fill(new Square({ at: [x, y], s: dX * 1.4 }))
}
)Two small helpers bridge iteration and randomness. doProportion(p, cb) runs the callback with probability p. proportionately picks one of several weighted branches (weights need not sum to anything in particular):
s.background(220, 30, 14)
s.forTiling({ n: 10, type: "square" }, ([x, y], [dX], [cX, cY]) => {
s.proportionately([
[
3,
() => {
s.setFillColor(45, 90, 60)
s.fill(new Circle({ at: [cX, cY], r: dX * 0.35 }))
},
],
[
2,
() => {
s.setFillColor(340, 80, 60)
s.fill(new Square({ at: [cX, cY], s: dX * 0.6, align: "center" }))
},
],
[1, () => {}], // sometimes do nothing
])
})Next up: Randomness and Noise, which powers all the sampling used above.
Solandra was made by James Porter.
Check out the GitHub page or install with npm i solandra