Solandra provides loops for grids, rows, circles and paths. Callbacks receive positions, sizes and centres in canvas coordinates.
forTiling divides the canvas into an n-column grid. Each callback receives (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) gives a single cell inset from the canvas edges.
forGrid iterates over integer coordinates. Use it with the hexagonal, triangular 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,
})
)
})times(n, cb) counts from 0 to n − 1. downFrom(n, cb) counts from n to 1. range({ from, to, n }, cb) visits n values across an interval, including both ends by default:
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 }))
})forRadialTiling divides concentric rings into n sectors. Each callback receives a HollowArc, its centre point, its bounds (r, r2, a, a2, ring, sector) and a sequential index.
s.background(0, 0, 8)
s.forRadialTiling(
{ n: 20, rings: 5, r: 0.45 },
(_at, cell, { ring, sector }) => {
s.setFillColor(
(sector % 2 === 0 ? 8 : 22) + ring * 4,
sector % 2 === 0 ? 75 : 45,
(ring + sector) % 2 === 0 ? 48 : 24
)
s.fill(cell)
}
)Set innerRadius to leave a hole in the centre, or from and to to draw a partial circle. Use the midpoint of the cell's angles to orient shapes outwards:
s.background(235, 25, 8)
s.forRadialTiling(
{ n: 12, rings: 3, r: 0.46, innerRadius: 0.09 },
(at, cell, { ring, a, a2 }) => {
s.setFillColor(210 + ring * 40, 60, 25 + ring * 8, 0.9)
s.fill(cell)
s.withTranslation(at, () => {
s.withRotation((a + a2) / 2, () => {
s.setFillColor(40 - ring * 10, 80, 60, 0.85)
s.fill(new Ellipse({ at: [0, 0], w: 0.1 + ring * 0.03, h: 0.035 }))
})
})
}
)order: "ringFirst" visits each ring in turn. Use "sectorFirst" to visit cells outwards along each sector.
Poisson disk sampling produces random points at least minDist apart. Use it to scatter shapes without tight clusters.
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 }))
})alongPath visits n points spaced evenly by distance along a path. The callback receives each point and the path's angle there, so you can position and rotate shapes along the outline.
Pass a SimplePath or an object with a .path, such as Line, Rect, RegularPolygon, Star or Spiral. Set inclusive: false on closed paths to avoid repeating the first point.
s.background(215, 40, 15)
const star = new Star({ at: s.meta.center, n: 5, r: 0.28, r2: 0.13 })
s.alongPath({ path: star, n: 60, inclusive: false }, (at, angle, i) => {
s.setFillColor(45 + i * 3, 85, 60)
s.withTranslation(at, () => {
s.withRotation(angle, () => {
s.fill(
new Rect({
at: [0, 0],
w: 0.006,
h: 0.03 + 0.03 * (i % 3),
align: "center",
})
)
})
})
})For measurements without a drawing callback, use SimplePath.length, pointAt, tangentAt or pointsAlong; see Paths and Curves.
Use these helpers with the iteration methods above.
build collects the callback's return values into an array:
// 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 the callbacks in shuffled order, changing how overlapping shapes are layered:
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 }))
}
)doProportion(p, cb) runs the callback with probability p. proportionately chooses a callback by weight; the weights do not need to sum to 1:
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
])
})See Randomness and Noise for more ways to choose values and positions.
Solandra was made by James Porter.
Check out the GitHub page or install with npm i solandra