s.times(25, () => {
  s.strokedPath((attr) =>
    attr.fill(
      s.sample([0, 20, 40]), 90, 50, 0.2)
  )
    .moveTo(s.randomPoint())
    .arcTo(s.randomPoint())
})

Solandra-SVG

A little, zero dependency, library for drawing in SVG, with a fluent TypeScript (many things use chained methods). This style is a little unconventional but has some interesting benefits with fewer imports or things you have to remember.

There are four key parts

SolandraSvg class

This is the main class. It is the thing that creates SVGs, but it also offers many convenient functions and handles pseudo-randomness. In most examples an instance of this is accessed via s.

Path class

The main thing that allows you to assemble drawings. Get started with s.path or another more specific call.

Attributes class and Transform class

These set up things like strokes and fills and transforms. You can create a new instance of each with s.T or s.A and start using chained calls to configure.

Why this library?

Because you want to create SVGs with TypeScript.

I initially made this to create things with an AxiDraw plotter. My first generated drawings for a 2D plotter. And my Second collection. And a Third collection. And a Fourth collection. I've been using it more recently for creating cut and fold sculptures/pop ups with a Cricut machine.

Try it

Try out

A ready to play with CodeSandbox.

GitHub

Full source code for the library and this site

Install
npm install solandra-svg
pnpm add solandra-svg
yarn add solandra-svg

Solandra SVG API Highlights

Here are a few highlights from the Solandra SVG API.

New

Support for OkLCH colours.

Easy Regular Polygons.

Use with AI

Add a prompt like LLM.md to help generate code for Solandra SVG.

Tiling

Quickly create graphics that tile the canvas without having to worry about all the low level details.

s.forTiling(
  { n: 5, type: "square", margin: 0.1 },
  ([x, y], [dX]) => {
    const path = s
      .strokedPath((attr) =>
        attr.stroke(355, 10, 10, 0.9).fill(340, 90, 70, 0.2)
      )
      .moveTo([0.5, 0.5])

    s.times(10, () => {
      const pt = s.randomPoint()
      path.lineTo([x + pt[0] * dX, y + pt[1] * dX])
    })
  }
)

Curves

Curves are easy and fun to draw with an API from Solandra that actually makes sense.

s.times(15, () => {
  let start = [s.random(), bottom] as Point2D
  let end = [s.random(), bottom] as Point2D
  s.strokedPath((attr) => attr.stroke(20, 90, 60, 0.5))
    .moveTo(start)
    .curveTo(end, { curveSize: 3 })

  start = [s.random(), 0] as Point2D
  end = [s.random(), 0] as Point2D
  s.strokedPath((attr) => attr.stroke(0, 90, 60, 0.5))
    .moveTo(start)
    .curveTo(end, { polarity: -1, curveSize: 3 })
})

Rectangles

Rectangles are easy to draw and the framework takes care of alignment.

s.times(25, () => {
  s.strokedPath((attr) => attr.fill(220, 90, 50, 0.2)).rect(
    s.randomPoint(),
    s.gaussian({ sd: 0.05, mean: 0.2 }),
    s.gaussian({ sd: 0.1, mean: 0.3 })
  )
})

Ellipses

Ellipses are easy to draw and the framework takes care of alignment.

s.times(35, () => {
  const size = s.gaussian({ sd: 0.2, mean: 0.25 })
  s.strokedPath((attr) =>
    attr.fill(s.sample([130, 200, 210]), 90, 40, 0.2)
  ).ellipse(s.randomPoint(), size, size / 1.25)
})

Chaikin

An elegant algorithm for smooth a path of lines. Repeatedly cut the corners. In solandra-svg this is only applied to lines (as it doesn't make sense for other path segments).

const { bottom } = s.meta
s.times(4, (n) => {
  const path = s
    .strokedPath((attr) =>
      attr.strokeOpacity(0.2 + n * 0.1).stroke(15, 90, 60)
    )
    .moveTo([0.1, bottom * 0.4])
  for (let i = 0.1; i <= 0.9; i += 0.2) {
    path.lineTo([i, bottom * 0.4 + 0.3 * Math.cos(i * 10)])
  }

  path
    .map((el) => {
      if (el.kind === "line" || el.kind === "move") {
        return { ...el, to: v.add(el.to, [0, 0.1 * n]) }
      } else {
        return el
      }
    })
    .chaikin(n + 1)
})

Transforms

s.strokedPath((attr) =>
  attr
    .fill(210, 90, 20, 0.5)
    .transform(s.T.rotate(Math.PI / 8))
).rect([0.3, 0.3], 0.2, 0.3)

Clone

const path = s.strokedPath().ellipse([0, 0], 0.3, 0.4)

s.times(20, (n) => {
  s.clonePath(path).configureAttributes((attr) =>
    attr
      .transform(s.T.scale(n / 2, n / 2))
      .stroke(n * 5, 90, 40)
  )
})

Groups

solandra-svg offers a closure based API for building svg groups. You use the same fluent Attributes api to set up their attributes.

const { center } = s.meta
s.times(8, (n) => {
  s.group(
    Attributes.stroked.transform(
      s.T.translate(center).scale((4 + n) / 14)
    ),
    () => {
      s.path(s.A.opacity((8 - n) / 10)).rect([0, 0], 1, 1)

      s.path(
        s.A.stroke(n * 4, 90, 50).transform(s.T.rotate(n))
      ).ellipse([0, 0], 1, 0.8)
    }
  )
})

Solandra SVG is made by James Porter. The full code for all these examples is available on GitHub.