Points in Solandra are plain [x, y] tuples; no wrapper classes to construct or unwrap. All the geometry you need to manipulate them lives in the v namespace, alongside a set of numeric and grid utilities.
import { v } from "solandra"
v.add([1, 2], [3, 4]) // [4, 6]
v.subtract([5, 7], [2, 3]) // [3, 4]
v.scale([2, 3], 2) // [4, 6]
v.magnitude([3, 4]) // 5
v.distance([0, 0], [3, 4]) // 5
v.normalize([3, 4]) // [0.6, 0.8]
v.dot([1, 2], [3, 4]) // 11
v.cross([1, 0], [0, 1]) // 1 (orientation tests)
v.heading([0, 1]) // π/2 (angle of a vector)
v.rotate([1, 0], Math.PI / 2) // rotate about the origin
v.rotateAround([0.5, 0.5], pt, a) // rotate about any point
v.pointAlong(a, b, 0.25) // 25% of the way from a to b
v.polarToCartesian([0.5, 0.5], r, a) // point at radius r, angle apolarToCartesian and pointAlong do a lot of work in practice; here they build a web between two rings of points:
s.background(230, 40, 12)
s.lineWidth = 0.002
const { center } = s.meta
const n = 36
s.times(n, (i) => {
const a = (i * Math.PI * 2) / n
const outer = v.polarToCartesian(center, 0.45, a)
const inner = v.polarToCartesian(center, 0.2, a * 3)
s.setStrokeColor(160 + i * 3, 70, 60, 0.8)
s.draw(new Line(inner, outer))
s.setFillColor(45, 90, 70)
s.fill(new Circle({ at: v.pointAlong(inner, outer, 0.5), r: 0.005 }))
})import { clamp, lerp, scaler, scaler2d, centroid } from "solandra"
clamp({ from: 0, to: 1 }, 1.5) // 1
lerp({ from: 10, to: 20 }, 0.25) // 12.5
// map between numeric ranges (e.g. data to canvas coordinates)
const toCanvas = scaler({
minDomain: 0,
maxDomain: 100,
minRange: 0.1,
maxRange: 0.9,
})
toCanvas(50) // 0.5
// same, for 2D points, with independent x/y configuration
const project = scaler2d(
{ minDomain: 0, maxDomain: 10, minRange: 0, maxRange: 1 },
{ minDomain: 0, maxDomain: 10, minRange: 0, maxRange: 1 }
)
centroid([
[0, 0],
[1, 0],
[1, 1],
[0, 1],
]) // [0.5, 0.5]Available individually or under the c namespace:
import { c, pairWise, tripleWise, zip2, sum, arrayOf } from "solandra"
pairWise([1, 2, 3, 4]) // [[1, 2], [2, 3], [3, 4]]
tripleWise([1, 2, 3, 4]) // [[1, 2, 3], [2, 3, 4]]
zip2([1, 2], ["a", "b"]) // [[1, "a"], [2, "b"]]
sum([1, 2, 3, 4]) // 10
arrayOf(3, () => s.randomPoint()) // 3 fresh random pointspairWise turns a list of points into segments; handy for styling each edge of a path separately:
s.background(40, 30, 95)
s.lineStyle = { cap: "round" }
const points = s.build(s.range, { from: 0.1, to: 0.9, n: 14 }, (x) => [
x,
0.5 + 0.3 * Math.sin(x * 9),
])
pairWise(points).forEach(([from, to], i) => {
s.setStrokeColor(i * 12, 80, 55)
s.lineWidth = 0.005 + 0.02 * Math.abs(Math.sin(i * 0.7))
s.draw(new Line(from, to))
})hexTransform({ r, vertical }) maps integer grid coordinates to hexagon centers in a proper hexagonal tiling, matched to the Hexagon shape. Combine with forGrid and a translation to the canvas center:
import { hexTransform, Hexagon } from "solandra"
s.background(35, 30, 10)
const r = 0.09
const hex = hexTransform({ r })
s.withTranslation(s.meta.center, () => {
s.forGrid({ minX: -3, maxX: 3, minY: -3, maxY: 3 }, (gp, i) => {
s.setFillColor(20 + i * 4, 70, 55)
s.fill(new Hexagon({ at: hex(gp), r: r * 0.92 }))
})
})triTransform({ s }) does the same for equilateral triangle tilings; it returns both a position and whether the triangle should be flipped, spreading exactly onto the EquilateralTriangle configuration:
import { triTransform, EquilateralTriangle } from "solandra"
s.background(215, 40, 30)
const side = 0.14
const tri = triTransform({ s: side })
s.withTranslation(s.meta.center, () => {
s.forGrid({ minX: -6, maxX: 6, minY: -3, maxY: 3 }, (gp, i) => {
s.setFillColor(190 + (i % 7) * 8, 60, 55, 0.9)
s.fill(new EquilateralTriangle({ ...tri(gp), s: side * 0.9 }))
})
})isoTransform(height) returns a function from 3D coordinates [x, y, z] (with y up) to 2D isometric screen coordinates. Draw faces back-to-front (use downFrom) and you have instant axonometric worlds:
import { isoTransform, clamp } from "solandra"
s.background(30, 20, 85)
s.lineWidth = 0.005
s.withTranslation([0.5, 0.9], () => {
const iso = isoTransform(0.05)
s.downFrom(8, (x) => {
s.downFrom(8, (z) => {
const h = clamp({ from: 1, to: 5 }, s.poisson(2))
// top face
s.setFillColor(10 + h * 25, 80, 65)
s.fill(
SimplePath.withPoints([
iso([x, h, z]),
iso([x + 1, h, z]),
iso([x + 1, h, z + 1]),
iso([x, h, z + 1]),
]).close()
)
// left face
s.setFillColor(10 + h * 25, 60, 45)
s.fill(
SimplePath.withPoints([
iso([x, h, z + 1]),
iso([x, 0, z + 1]),
iso([x, 0, z]),
iso([x, h, z]),
]).close()
)
// right face
s.setFillColor(10 + h * 25, 70, 30)
s.fill(
SimplePath.withPoints([
iso([x, h, z + 1]),
iso([x + 1, h, z + 1]),
iso([x + 1, 0, z + 1]),
iso([x, 0, z + 1]),
]).close()
)
})
})
})Solandra was made by James Porter.
Check out the GitHub page or install with npm i solandra