SolandraExamplesDocsSlidesDownload Book

Canvas Basics

Everything in Solandra happens through an SCanvas (by convention called s or p). Your sketch is just a function that takes an SCanvas and draws with it:

import { SCanvas } from "solandra"
 
const sketch = (s: SCanvas) => {
  s.background(45, 30, 95)
  s.setFillColor(220, 70, 40)
  s.fill(new Circle({ at: s.meta.center, r: 0.3 }))
}

The coordinate system

Solandra uses a normalized coordinate system: the width of the canvas is always 1, whatever the actual pixel size. The height is 1 / aspectRatio. This means your sketches are resolution independent by default: the same code renders a thumbnail or a poster.

The meta property tells you about the drawing area:

s.meta.top // 0
s.meta.left // 0
s.meta.right // 1
s.meta.bottom // 1 / aspectRatio (1 for a square canvas)
s.meta.center // [0.5, 0.5 / aspectRatio]
s.meta.aspectRatio // width / height

Points are just tuples of numbers, [x, y], with x increasing rightwards and y increasing downwards (as is standard for canvases, if not mathematics).

A quick tour of the drawing area; here we mark the corners, edges and center:

const { center, bottom, right } = s.meta
s.background(210, 30, 20)
s.lineWidth = 0.005
s.setStrokeColor(0, 0, 90)
s.draw(new Line([0, 0], [right, bottom]))
s.draw(new Line([right, 0], [0, bottom]))
s.setFillColor(20, 90, 60)
s.fill(new Circle({ at: center, r: 0.05 }))
s.setFillColor(50, 90, 60)
const corners = [
  [0, 0],
  [right, 0],
  [right, bottom],
  [0, bottom],
]
corners.forEach((at) => {
  s.fill(new Circle({ at, r: 0.03 }))
})

Drawing and filling

Solandra separates what you draw from how you draw it. Shapes (anything implementing Traceable, see Shapes) are passed to one of two methods:

  • s.draw(shape) strokes the outline with the current stroke colour and line width
  • s.fill(shape) fills the interior with the current fill colour (or gradient)
s.background(0, 0, 95)
s.setStrokeColor(220, 80, 30)
s.lineWidth = 0.02
s.draw(new Rect({ at: [0.1, 0.1], w: 0.35, h: 0.35 }))
s.setFillColor(340, 80, 60)
s.fill(new Rect({ at: [0.55, 0.55], w: 0.35, h: 0.35 }))

Colours

Colours in Solandra are HSLA: hue (0–360), saturation (0–100), lightness (0–100) and optional alpha (0–1). This makes it easy to programmatically vary colours in ways that look good; see Colour for much more.

s.background(45, 20, 95) // hue, saturation, lightness
s.setFillColor(220, 70, 50, 0.5) // ...and optional alpha
s.setStrokeColor(0, 0, 20)

The state-based colour setters have FromSpec variants that take a { h, s, l, a } object, which is handy when passing colours around:

const brand = { h: 175, s: 80, l: 40 }
s.setFillColorFromSpec(brand)
s.backgroundFromSpec({ h: 45, s: 20, l: 95 })

Line style

Line width is also in canvas units (so 0.01 is 1% of the width of your canvas). You can also control caps, joins and dashes:

s.background(0, 0, 15)
s.setStrokeColor(45, 90, 70)
s.lineWidth = 0.02
s.lineStyle = { cap: "round" }
s.draw(new Line([0.1, 0.2], [0.9, 0.2]))
s.lineStyle = { cap: "butt" }
s.draw(new Line([0.1, 0.4], [0.9, 0.4]))
s.dash = { pattern: [0.04, 0.02] }
s.draw(new Line([0.1, 0.6], [0.9, 0.6]))
s.dash = { pattern: [0.005, 0.02] }
s.lineStyle = { cap: "round" }
s.draw(new Line([0.1, 0.8], [0.9, 0.8]))

The dash pattern is specified at the scale of the canvas (unlike raw HTML5 Canvas, where it would be in pixels).

Shadows

Shadows are configured with a single setter (again scaled to canvas size, unlike the raw Canvas API) and removed with clearShadow:

s.background(210, 20, 95)
s.shadow = {
  size: 0.03,
  dX: 0.01,
  dY: 0.02,
  color: { h: 210, s: 40, l: 30, a: 0.6 },
}
s.setFillColor(340, 70, 55)
s.fill(new Star({ at: [0.35, 0.4], r: 0.2, n: 5 }))
s.clearShadow()
s.setFillColor(220, 70, 55)
s.fill(new Circle({ at: [0.7, 0.65], r: 0.15 }))

Checking whether a point is visible

s.inDrawing(point) returns whether a point lies inside the canvas; useful for terminating random walks or culling offscreen work:

s.background(0, 0, 10)
s.lineWidth = 0.005
s.times(20, (n) => {
  s.setStrokeColor(180 + n * 6, 80, 60, 0.8)
  const path = SimplePath.startAt(s.meta.center)
  let pt = s.meta.center
  while (s.inDrawing(pt)) {
    pt = s.perturb({ at: pt, magnitude: 0.1 })
    path.addPoint(pt)
  }
  s.draw(path.chaiken({ n: 2 }))
})

Where next

  • Shapes for everything you can pass to draw and fill
  • Iteration for Solandra's signature looping helpers
  • Randomness for the seeded random number APIs used above

Solandra was made by James Porter.

Check out the GitHub page or install with npm i solandra