npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@salamivg/core

v1.0.0

Published

A creative coding framework for generating SVGs

Downloads

18

Readme

SalamiVG ("Salami Vector Graphics")

A place to play with SVGs.

SalamiVG is a creative coding framework for JavaScript with a single render target: SVG.

Why?

I love OPENRNDR and wanted to see if I could make a generative art framework that ran in an interpretted language. I've never been a JVM guy, and even though I like Kotlin, it sounded appealing to me to be able to write generative art in a language I used every day: JavaScript.

Of course you may (reasonably) ask why I'm not just using p5.js, the dominant JavaScript framework for writing generative art. Well, I don't have a good answer to that. I suppose this is really "just for fun" ¯\_(ツ)_/¯. (There is a more detailed comparison with p5.js in the Wiki.)

Installation

npm i --save @salamivg/core

If you use yarn and you can't automatically convert the above to the correct yarn command, then that's on you 😏

Examples

There is a Gallery page in the Wiki with some example renders and links to the code used to create them.

If you're the clone-n-run type, you can use the examples from the /examples directory in this repo:

git clone [email protected]:ericyd/salamivg
cd salamivg
node examples/oscillator-noise.js

Here are some simple SVGs generated with SalamiVG

import { renderSvg, circle, hypot, vec2, map } from '@salamivg/core'

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

renderSvg(config, (svg) => {
  // set basic SVG props
  svg.setBackground('#fff')
  svg.fill = null
  svg.stroke = '#000'
  svg.numericPrecision = 3

  // draw circle in middle of viewport
  svg.circle(
    circle({
      x: svg.center.x,
      y: svg.center.y,
      radius: hypot(svg.width, svg.height) * 0.04,
      'stroke-width': 1,
    }),
  )

  // draw 14 concentric rings around the center. (14 is arbitrary)
  const nRings = 14
  for (let i = 1; i <= nRings; i++) {
    // use `map` to linearly interpolate the radius on a log scale
    const baseRadius = map(
      0,
      Math.log(nRings),
      hypot(svg.width, svg.height) * 0.09,
      hypot(svg.width, svg.height) * 0.3,
      Math.log(i),
    )

    // as the rings get further from the center,
    // the path is increasingly perturbated by the sine wave.
    const sineInfluence = map(
      0,
      Math.log(nRings),
      baseRadius * 0.01,
      baseRadius * 0.1,
      Math.log(i),
    )

    svg.path((p) => {
      // the stroke width gets thinner as the rings get closer to the edge
      p.strokeWidth = map(1, nRings, 0.8, 0.1, i)

      // the radius varies because the path is perturbated by a sine wave
      const radius = (angle) => baseRadius + Math.sin(angle * 6) * sineInfluence
      p.moveTo(
        vec2(Math.cos(0) * radius(0), Math.sin(0) * radius(0)).add(svg.center),
      )

      // move our way around a circle to draw a smooth path
      for (let angle = 0; angle <= Math.PI * 2; angle += 0.05) {
        p.lineTo(
          vec2(
            Math.cos(angle) * radius(angle),
            Math.sin(angle) * radius(angle),
          ).add(svg.center),
        )
      }
      p.close()
    })
  }
})

Concentric circles example. 14 concentric circles are drawn around the center of the image. As the circle radius increases, the circles becomes increasingly perturbated by a sine wave, making the circle somewhat wavy.

SalamiVG ships with a bespoke noise function called "oscillator noise".

import {
  renderSvg,
  map,
  vec2,
  randomSeed,
  createRng,
  Vector2,
  random,
  ColorRgb,
  PI,
  cos,
  sin,
  ColorSequence,
  shuffle,
  createOscNoise,
} from '@salamivg/core'

const config = {
  width: 100,
  height: 100,
  scale: 3,
  loopCount: 1,
}

const colors = ['#B2D0DE', '#E0A0A5', '#9BB3E7', '#F1D1B8', '#D9A9D6']

renderSvg(config, (svg) => {
  // filenameMetadata will be added to the filename that is written to disk;
  // this makes it easy to recall which seeds were used in a particular sketch
  svg.filenameMetadata = { seed }

  // a seeded pseudo-random number generator provides controlled randomness for our sketch
  const rng = createRng(seed)

  // black background 😎
  svg.setBackground('#000')

  // set some basic SVG props
  svg.fill = null
  svg.stroke = ColorRgb.Black
  svg.strokeWidth = 0.25
  svg.numericPrecision = 3

  // create a 2D noise function using the built-in "oscillator noise"
  const noiseFn = createOscNoise(seed)

  // create a bunch of random start points within the svg boundaries
  const nPoints = 200
  const points = new Array(nPoints)
    .fill(0)
    .map(() => Vector2.random(0, svg.width, 0, svg.height, rng))

  // define a color spectrum that can be indexed randomly for line colors
  const spectrum = ColorSequence.fromColors(shuffle(colors, rng))

  // noise functions usually require some type of scaling;
  // here we randomize slightly to get the amount of "flowiness" that we want.
  const scale = random(0.05, 0.13, rng)

  // each start point gets a line
  for (const point of points) {
    svg.path((path) => {
      // choose a random stroke color for the line
      path.stroke = spectrum.at(random(0, 1, rng))

      // move along the vector field defined by the 2D noise function.
      // the line length is "100", which is totally arbitrary.
      path.moveTo(point)
      for (let i = 0; i < 100; i++) {
        let noise = noiseFn(path.cursor.x * scale, path.cursor.y * scale)
        let angle = map(-1, 1, -PI, PI, noise)
        path.lineTo(path.cursor.add(vec2(cos(angle), sin(angle))))
      }
    })
  }

  // when loopCount > 1, this will randomize the seed on each iteration
  return () => {
    seed = randomSeed()
  }
})

Oscillator noise example. Wavy multi-colored lines defined by a noisy vector field weave through the canvas.

/*
Rules

1. Draw an equilateral triangle in the center of the viewBox
2. Subdivide the triangle into 4 equal-sized smaller triangles
3. If less than max depth and <chance>, continue recursively subdividing
4. Each triangle gets a different fun-colored fill, and a slightly-opacified stroke
*/
import {
  renderSvg,
  vec2,
  randomSeed,
  createRng,
  Vector2,
  random,
  randomInt,
  PI,
  ColorSequence,
  shuffle,
  TAU,
  ColorRgb,
} from '@salamivg/core'

const config = {
  width: 100,
  height: 100,
  scale: 3,
  loopCount: 1,
}

let seed = 8852037180828291 // or, randomSeed()

const colors = [
  '#974F7A',
  '#D093C2',
  '#6F9EB3',
  '#E5AD5A',
  '#EEDA76',
  '#B5CE8D',
  '#DAE7E8',
  '#2E4163',
]

const bg = '#2E4163'
const stroke = ColorRgb.fromHex('#DAE7E8')

renderSvg(config, (svg) => {
  const rng = createRng(seed)
  const maxDepth = randomInt(5, 7, rng)
  svg.filenameMetadata = { seed, maxDepth }
  svg.setBackground(bg)
  svg.numericPrecision = 3
  svg.fill = bg
  svg.stroke = stroke
  svg.strokeWidth = 0.25
  const spectrum = ColorSequence.fromColors(shuffle(colors, rng))

  function drawTriangle(a, b, c, depth = 0) {
    // always draw the first triangle; then, draw about half of the triangles
    if (depth === 0 || random(0, 1, rng) < 0.5) {
      // offset amount increases with depth
      const offsetAmount = depth / 2
      const offset = vec2(
        random(-offsetAmount, offsetAmount, rng),
        random(-offsetAmount, offsetAmount, rng),
      )
      // draw the triangle with some offset
      svg.polygon({
        points: [a.add(offset), b.add(offset), c.add(offset)],
        fill: spectrum.at(random(0, 1, rng)).opacify(0.4).toHex(),
        stroke: stroke.opacify(1 / (depth / 4 + 1)).toHex(),
      })
    }
    // recurse if we're above maxDepth and "lady chance allows it"
    if (depth < maxDepth && (depth < 2 || random(0, 1, rng) < 0.75)) {
      const ab = Vector2.mix(a, b, 0.5)
      const ac = Vector2.mix(a, c, 0.5)
      const bc = Vector2.mix(b, c, 0.5)
      drawTriangle(ab, ac, bc, depth + 1)
      drawTriangle(a, ab, ac, depth + 1)
      drawTriangle(b, bc, ab, depth + 1)
      drawTriangle(c, bc, ac, depth + 1)
    }
  }

  // construct an equilateral triangle from the center of the canvas with a random rotation
  const angle = random(0, TAU, rng)
  const a = svg.center.add(Vector2.fromAngle(angle).scale(45))
  const b = svg.center.add(Vector2.fromAngle(angle + (PI * 2) / 3).scale(45))
  const c = svg.center.add(Vector2.fromAngle(angle + (PI * 4) / 3).scale(45))
  drawTriangle(a, b, c)

  // when loopCount > 1, this will randomize the seed on each iteration
  return () => {
    seed = randomSeed()
  }
})

Recursive triangles example. A large equilateral triangle is drawn in the middle of the screen. The triangle is equally subdivided into 4 smaller triangles. Each triangle gets a random color. The subdivision continues for 6 iterations.

Getting Started, Documentation, and FAQ

Please see the project Wiki

Design Philosophy

  1. Imperative works better than declarative for art. Though declarative/functional programming is awesome for production apps, imperative patterns work better for generative art. This is because a lot of sketches build up designs iteratively, and being able to easily modify the logic makes it easier to iterate on ideas and create art quickly. SalamiVG is heavily inspired by OPENRNDR, and likewise utilizes the builder pattern extensively.
  2. Type hints lead to better developer experience. TypeScript is used for typechecking the library, and generating type declaration files for all classes and functions. This improves the developer experience by providing type hints as documentation. Note: although TypeScript is used for typechecking the lib, the actual code is written in JavaScript with JSDocs. This was inspired by the SvelteKit team (source 1, source 2) and it provides a lot of benefits such as reducing build tooling and speeding up test execution. In addition, shipping human-readable JS files means that it's much easier for a library user to modify the files locally, which can be a great way to lead to experimentation and PRs!
  3. Creative coding should be fun. Don't take yourself too seriously.

Internal Development

Install dependencies:

npm i

Before committing:

npm run check:all

Publishing

npm version minor
git push --tags && git push
./scripts/changelog.sh
npm login --registry https://registry.npmjs.org --scope=@salamivg
npm publish --access public

NodeJS Version Compatibility

SalamiVG was developed with Node 20 but it probably works back to Node 14 or so.

This library has been tested against

  • Node 20.8.0
  • Node 18.19.0
  • Node 16.20.2
  • Attempted to test against Node 14 but asdf wouldn't install it on our M1 Mac. Please open an issue if this is causing you problems.

Deno / Bun Support?

Both Deno and Bun work out of the box, with the exception of the renderSvg() function.

Please see the FAQ for a more detailed answer and examples of using SalamiVG with Deno and Bun.

ES Modules Only

SalamiVG ships ES Modules, and does not include CommonJS builds.

Is this a problem? Feel free to open an issue if you need CommonJS. It would probably be trivial to set up Rollup or similar to bundle into a CommonJS package and include it in the exports, but it isn't clear if it is necessary for anyone.