Skip to content
Eric Yancey Dauenhauer edited this page Feb 5, 2024 · 10 revisions

Getting Started

Welcome to SalamiVG! This guide should get you going to generate your first sketch.

Pre-requisites

Install Node.js, or some other JS runtime if you're feeling frisky. Officially, SalamiVG supports Node 16+.

Installation

npm i --save @salamivg/core

Your first sketch!

Create a new file called salamivg-1.js.

Open the file in your favorite text editor or IDE and add the following code:

import { renderSvg } from '@salamivg/core'

const config = {
  loopCount: 1,
  openEveryFrame: false
}

renderSvg(config, (svg) => {
  // magic coming soon!
})

Now run the file, and inspect the output!

my_first_svg=$(node salamivg-1.js)
cat $my_first_svg

You should see something similar to this printed to your console:

<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100"></svg>

Understanding what we just did

  • import { renderSvg } from '@salamivg/core' imports the renderSvg function from the lib. This function is the most common entrypoint to all sketches.
  • const config = { ... } defines our sketch's configuration. This can include more properties which we'll cover later, but for now it is important to know the following:
    • loopCount controls how many times the sketch runs. Running more than once is probably only necessary if you are either:
      1. iteratively building up some global state which doesn't reset between renders, or
      2. using some type of random generator which will produce slightly different results between each sketch. In this case, multiple renders can be fun to generate all at once and compare the output.
    • openEveryFrame controls whether or not SalamiVG attempts to open the SVG it just generated, using your system's open command. This will typically open the SVG in a browser, but can be configured on a per-system basis.
  • renderSvg(config, (svg) => { ... } does all of the following:
    1. builds an SVG using the builder callback
    2. renders the SVG to a string
    3. writes the SVG string to a file

By default, the written filename is logged to the console, which allows us to capture it to a shell variable and then use cat to print it out.

Congratulations, you've written your first SVG with SalamiVG! Now let's do something more interesting.

Drawing basic shapes

Let's draw some randomly-placed circles. This is not an exciting result, but it demonstrates several key ideas behind how to use SalamiVG for your own sketches.

This time we'll build the script piecemeal so we can discuss the relevant parts as we go.

Imports

import { renderSvg, randomSeed, randomInt, random, createRng } from '@salamivg/core'

We'll need a few base components for our "random circles" sketch:

  • renderSvg, discussed above
  • randomSeed, a function which generates an integer string which can be used to see an Rng (a pseudo-random number generator (do not use for cryptographic purposes))
  • randomInt, a function which returns a random integer in the specified range
  • random, a function which returns a random float in the specified range
  • createRng, a function which takes seed value and returns an Rng function. Rngs should return a pseudo-random number in the range [0, 1]

Config

const config = {
  width: 100,
  height: 100,
  scale: 2,
  loopCount: 1,
  openEveryFrame: true
}

Most of these values are the default values, but to review:

  • width: the width of our sketch. In SVG terminology, this becomes the width property on the viewBox attribute
  • height: the height of our sketch. Similar to width, this will actually be used in the viewBox
  • scale: if we would like to render a final SVG that is larger than our viewBox, we can do so with the scale property. In this case, using scale: 2 will give our SVG width="200" and height="200" attributes, while keeping our viewBox set to 0 0 100 100
  • loopCount: discussed above. The default value is 1 so this can safely be omitted if desired
  • openEveryFrame: discussed above. We're changing this to true so you can see the rendered SVG after generating it.

Rendering

Before we start the render loop, we will define a mutable seed variable. The value of mutability will be explained later.

let seed = randomSeed()

Next, we'll start our render loop and define some values

let seed = randomSeed()

renderSvg(config, (svg) => {
  svg.filenameMetadata = { seed }
  const rng = createRng(seed)
  const numberOfCircles = randomInt(4, 10, rng)
})
  • filenameMetadata is an optional object which can add descriptive information to the written filename. I like adding the seed to the filename, so if a particular render is particularly pleasing, I can use the same seed again to generate the same output.
  • createRng: we create an Rng using the seed value declared outside the loop, and assign to a variable
  • randomInt: we randomly decide how many circles to generate

Finally, we can draw our circles

let seed = randomSeed()

renderSvg(config, (svg) => {
  svg.filenameMetadata = { seed }
  const rng = createRng(seed)
  const numberOfCircles = randomInt(4, 10, rng)

  svg.fill = '#333'
  svg.stroke = '#ee00aa'
  for (let i = 0; i < numberOfCircles; i++) {
    svg.circle({
      x: random(0, svg.width, rng),
      y: random(0, svg.height, rng),
      radius: random(10, 20, rng),
    })
  }
})

If you run this script, you should get an output similar to this:

Example random circles output

Let's break down the additional elements:

  • svg.fill = '#333': sets the fill to the hex value #333. This is automatically inherited by any child components (in our example, circles)
  • svg.stroke = '#ee00aa': sets the stroke to the hext value #ee00aa. This is automatically inherited by any child components.
  • for ...: create our randomly-specified number of circles
  • svg.circle(): draw a circle at a randomly-generated (x, y) coordinate with a randomly-generated radius

Now you've drawn elements to your SVG: you're a pro!

Adding more color and variations on each iteration

Now that we have the basics, we can augment our sketch to add some more flair. Let's define two different color spectra (one for the fill, and one for the stroke) and then generate several variations of our sketch with different values automatically created each time.

Imports

Add the following imports to your top-level import statement:

  • ColorSequence

Config

Update your config to loopCount: 5. You can do more or less as you prefer.

Render

Define two color spectra anywhere in your script:

const fillSpectrum = ColorSequence.fromHexes([
  '#2E3532',
  '#7E9181',
  '#C7CEDB',
  '#A0AAB2',
  '#94849B',
])

const strokeSpectrum = ColorSequence.fromHexes([
  '#253031',
  '#315659',
  '#2978A0',
  '#BCAB79',
  '#C6E0FF',
])

The ColorSpectrum class creates a smooth gradient between all the colors provided, and allows us to index into the gradient with the at() method. ColorSpectrum.prototype.at() accepts a number in the range [0, 1]

Update your renderSvg callback to randomly select a color on each iteration. Also, add a post-loop callback to update the seed value.

renderSvg(config, (svg) => {
  svg.filenameMetadata = { seed }
  const rng = createRng(seed)
  const numberOfCircles = randomInt(4, 10, rng)

  for (let i = 0; i < numberOfCircles; i++) {
    svg.circle({
      x: random(0, svg.width, rng),
      y: random(0, svg.height, rng),
      radius: random(10, 20, rng),
      fill: fillSpectrum.at(random(0, 1, rng)),
      stroke: strokeSpectrum.at(random(0, 1, rng)),
    })
  }

  return () => {
    seed = randomSeed()
  }
})

Your full sketch should now look something like this:

import { renderSvg, randomSeed, randomInt, random, createRng, ColorSequence } from '@salamivg/core'

const config = {
  width: 100,
  height: 100,
  scale: 2,
  loopCount: 5,
  openEveryFrame: true
}

let seed = randomSeed()

const fillSpectrum = ColorSequence.fromHexes([
  '#2E3532',
  '#7E9181',
  '#C7CEDB',
  '#A0AAB2',
  '#94849B',
])

const strokeSpectrum = ColorSequence.fromHexes([
  '#253031',
  '#315659',
  '#2978A0',
  '#BCAB79',
  '#C6E0FF',
])

renderSvg(config, (svg) => {
  svg.filenameMetadata = { seed }
  const rng = createRng(seed)
  const numberOfCircles = randomInt(4, 10, rng)

  for (let i = 0; i < numberOfCircles; i++) {
    svg.circle({
      x: random(0, svg.width, rng),
      y: random(0, svg.height, rng),
      radius: random(10, 20, rng),
      fill: fillSpectrum.at(random(0, 1, rng)),
      stroke: strokeSpectrum.at(random(0, 1, rng)),
    })
  }

  return () => {
    seed = randomSeed()
  }
})

When you run this sketch, you should get 5 variations of the same sketch, each with a different output. An example output might look something like this

Example random circles output with random colors

There are a few important notes about our changes

  • fill and stroke were set directly on each circle when we drew the circle. When these values are not defined, it will inherit from the parent, but any explicit values will always take precedence.
  • We returned a "post-loop" callback at the end which set seed to a random value after each iteration. This is the beauty of using a mutable seed value. Inside each render loop, we create a seeded Rng from the seed value. This gives us predictable random values for the render loop. However, each iteration should have a different seed in order to be visually interesting and unique. This is accomplished by randomizing the seed after each loop.
    • If desired, additional side-effects can be added to this post-loop callback
    • The primary benefit of encapsulating this in a callback is so we can ensure that any state mutation doesn't impact the script output until after the SVG has been rendered.

Next steps

Take a look at the guide to learn more about some concepts behind SalamiVG.

Check out some examples in my personal generative art repo to see more ways that SalamiVG can be used for creative coding!

Please

  • Open a GitHub Issue if you find a bug, have a question, or have a suggestion for the docs
  • Have fun! The only point of creative coding is to enjoy yourself, so try to do that!