Solandra ships with a collection of shape classes. They all implement the Traceable interface, so anything on this page can be passed to s.draw(...) (stroke the outline) or s.fill(...) (fill the interior).
All are configured with a single object, using short, consistent names: at for position, r for radius, w/h for width/height, a for angle (radians), n for a count.
import {
Circle,
Ellipse,
Rect,
Square,
RoundedRect,
RegularPolygon,
Hexagon,
EquilateralTriangle,
Star,
Arc,
HollowArc,
Spiral,
Hatching,
Line,
} from "solandra"
s.background(210, 25, 95)
s.setFillColor(215, 60, 45)
const shapes = [
new Circle({ at: [0, 0], r: 0.11 }),
new Ellipse({ at: [0, 0], w: 0.22, h: 0.14 }),
new Rect({ at: [0, 0], w: 0.2, h: 0.14, align: "center" }),
new Square({ at: [0, 0], s: 0.18, align: "center" }),
new RoundedRect({ at: [0, 0], w: 0.2, h: 0.15, r: 0.04, align: "center" }),
new RegularPolygon({ at: [0, 0], n: 5, r: 0.11 }),
new Hexagon({ at: [0, 0], r: 0.11 }),
new EquilateralTriangle({ at: [0, 0], s: 0.2 }),
new Star({ at: [0, 0], n: 5, r: 0.12 }),
new Star({ at: [0, 0], n: 7, r: 0.12, r2: 0.09 }),
new Arc({ at: [0, 0], r: 0.11, a: 0, a2: (Math.PI * 3) / 2 }),
new HollowArc({ at: [0, 0], r: 0.11, r2: 0.06, a: 0, a2: Math.PI * 1.5 }),
]
s.forTiling(
{ n: 4, type: "square", margin: 0.05 },
(_pt, [dX], [cX, cY], i) => {
if (!shapes[i]) return
s.withTranslation([cX, cY], () => {
s.withScale([dX * 4, dX * 4], () => {
s.fill(shapes[i])
})
})
}
)Rect is positioned by its top-left corner by default; pass align: "center" to position by center. Square is a Rect with a single size s, and RoundedRect adds a corner radius r.
Rect also has a lovely split method that subdivides it, either in half, at a proportion, or into weighted pieces:
s.background(40, 30, 94)
const rect = new Rect({ at: [0.1, 0.1], w: 0.8, h: 0.8 })
const columns = rect.split({ orientation: "horizontal", split: [2, 1, 3, 1] })
columns.forEach((column, i) => {
const rows = column.split({
orientation: "vertical",
split: s.samples(3 + i, [1, 2, 3]),
})
rows.forEach((r, j) => {
s.setFillColor(20 + i * 30, 60, 50 + j * 5)
s.fill(
new Rect({
at: [r.at[0] + 0.01, r.at[1] + 0.01],
w: r.w - 0.02,
h: r.h - 0.02,
})
)
})
})Circle is simply an Ellipse with w = h = 2r. Both are drawn with cubic Bézier approximations, and are positioned by center by default (align: "topLeft" is also available).
Arc is a filled wedge (pie slice) from angle a to a2; HollowArc is a ring segment with outer radius r and inner radius r2. Angles are in radians, with 0 pointing right and increasing clockwise on screen.
s.background(230, 30, 15)
const { center } = s.meta
s.times(12, (n) => {
const a = (n * Math.PI) / 6
s.setFillColor(n * 30, 70, 60, 0.9)
s.fill(
new HollowArc({
at: center,
a,
a2: a + Math.PI / 7,
r: 0.2 + 0.02 * n,
r2: 0.1 + 0.01 * n,
})
)
})RegularPolygon takes a number of sides n, radius r and optional rotation a (shapes start pointing "up"). Hexagon and EquilateralTriangle are convenient subclasses that work nicely with the hex and triangle grid helpers. Star takes points n, outer radius r and optional inner radius r2 (defaults to r / 2).
s.background(45, 40, 94)
s.forHorizontal({ n: 4, margin: 0.05 }, (_pt, [dX], [cX, cY], i) => {
s.setFillColor(200 + i * 25, 65, 50)
s.fill(new RegularPolygon({ at: [cX, cY - 0.15], n: 3 + i, r: dX * 0.4 }))
s.setFillColor(20 + i * 25, 80, 55)
s.fill(
new Star({
at: [cX, cY + 0.15],
n: 4 + i,
r: dX * 0.4,
r2: dX * (0.15 + 0.05 * i),
})
)
})Line is the simplest shape: two points. Spiral unwinds from a point with segment length l, segment count n and a growth rate. Hatching fills a circular area with parallel lines at angle a, spaced delta apart; layering two at right angles gives cross-hatching.
s.background(0, 0, 96)
s.lineWidth = 0.003
s.setStrokeColor(340, 70, 40)
s.draw(new Hatching({ at: [0.3, 0.3], r: 0.2, a: Math.PI / 4, delta: 0.015 }))
s.draw(new Hatching({ at: [0.3, 0.3], r: 0.2, a: -Math.PI / 4, delta: 0.03 }))
s.setStrokeColor(215, 70, 40)
s.draw(new Spiral({ at: [0.7, 0.65], l: 0.02, n: 500, rate: 0.02 }))CompoundPath combines several traceables into one path. When filled, the non-zero winding rule means an inner shape traced in the opposite direction cuts a hole:
s.background(190, 40, 90)
s.setFillColor(215, 65, 35)
s.fill(
CompoundPath.withPaths(
new Circle({ at: [0.5, 0.5], r: 0.35 }),
new RegularPolygon({ at: [0.5, 0.5], n: 6, r: 0.22 }).path.reversed
)
)Many shapes can be converted to a SimplePath (a list of points), unlocking all the path transformations:
Rect, RegularPolygon, Star and Line have a .path getter (exact)Circle, Ellipse, Arc, HollowArc and RoundedRect have .toPath(detail), where detail controls how many points approximate the curvess.background(160, 25, 12)
s.setStrokeColor(160, 60, 70)
s.lineWidth = 0.004
const circle = new Circle({ at: [0.5, 0.5], r: 0.35 })
s.times(8, (n) => {
const path = circle
.toPath(20)
.transformLooped((pt) => s.perturb({ at: pt, magnitude: 0.03 + n * 0.01 }))
s.draw(path.chaiken({ n: 3, looped: true }))
})Next: build your own shapes with Paths and Curves.
Solandra was made by James Porter.
Check out the GitHub page or install with npm i solandra