Shapes

All shapes on this page implement Traceable. Pass them to s.draw(...) to stroke the outline or s.fill(...) to fill the interior.

Shape constructors take a configuration object. Common options are at (position), r (radius), w/h (width/height), a (angle in radians) and n (count).

The gallery

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

Rectangles

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.split divides a rectangle into two parts or a set of 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,
      })
    )
  })
})

Circles, ellipses and arcs

Circle is an Ellipse with w = h = 2r. Both use cubic Bézier approximations and are positioned by centre. Pass align: "topLeft" to position by the top-left corner.

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

Polygons and stars

RegularPolygon takes a side count n, radius r and optional rotation a. Its first vertex points up by default. Hexagon and EquilateralTriangle work with the grid helpers. Star takes a point count n, outer radius r and inner radius r2 (default 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),
    })
  )
})

Lines, spirals and hatching

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

Compound paths (shapes with holes)

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

Converting shapes to paths

Convert a shape to a SimplePath to use its 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 curves
s.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