SolandraExamplesDocsSlidesDownload Book

Vectors and Utilities

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.

Vector operations

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 a

polarToCartesian 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 }))
})

Numeric helpers

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]

Collection helpers

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 points

pairWise 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))
})

Hexagonal grids

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 }))
  })
})

Triangular grids

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 }))
  })
})

Isometric projection

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