nylki / lindenmayer

Feature complete classic L-System library (branching, context sensitive, parametric) & multi-purpose modern L-System/LSystem implementation that can take javascript functions as productions. It is not opinionated about how you do visualisations.
MIT License
182 stars 14 forks source link
fractal l-systems lindenmayer lsystem turtle-graphics

Lindenmayer Build Status minified gzip size

Lindenmayer is a L-System javascript library with focus on a concise syntax. The idea is to have a powerful but simple base functionality, that can handle most use-cases by simply allowing anonymous functions as productions, which makes it very flexible in comparison to classic L-Systems.

The library can also parse to some extent classic L-System syntax as defined in Aristid Lindenmayers original work Algorithmic Beauty of Plants from 1990. For example branches: [] or context sensitive productions: <>.

If you simply want to work with L-Systems in 3D and VR without defining your own draw methods, you can check out the accompanying aframe-lsystem-component.

Full API doc | Getting Started | A-Frame (VR) L-System component

3D Hilbert Curve rendered in Interactive L-System builder.

Examples

Install

Direct download

<script src="https://github.com/nylki/lindenmayer/raw/main/lindenmayer.browser.js"></script>

npm

If you would like to use npm instead of directly downloading:

npm install --save lindenmayer

Then in your Node.js script:

var LSystem = require('lindenmayer')

or via import syntax:

import LSystem from 'lindenmayer'

Or in your index.html:

<script src="https://github.com/nylki/lindenmayer/raw/main/node_modules/lindenmayer/dist/lindenmayer.browser.js"></script>

See releases for change logs.

Quick Intro

// Initializing a L-System that produces the Koch-curve
let kochcurve = new LSystem({
      axiom: 'F++F++F',
      productions: {'F': 'F-F++F-F'}
})
// Iterate the L-System two times and log the result.
let result = kochcurve.iterate(2)
console.log(result)
//'F-F++F-F-F-F++F-F++F-F++F-F-F-F++F-F++F-F++F-F-F-F++F-F++F
//-F++F-F-F-F++F-F++F-F++F-F-F-F++F-F++F-F++F-F-F-F++F-F'

There are multiple ways to set productions, including javascript functions:

// Directly when initializing a new L-System object:
let lsystem = new LSystem({
  axiom: 'ABC',
  productions: { 'B': 'BB' }
})

// After initialization:
lsystem.setProduction('B', 'F+F')

// Stochastic L-System:
lsystem.setProduction('B', {
  successors: [
  {weight: 50, successor: 'X'}, // 50% probability
  {weight: 25, successor: 'XB'},// 25% probability
  {weight: 25, successor: 'X+B'}// 25% probability
]})

// Context Sensitive:
lsystem.setProduction('B', {leftCtx: 'A', successor: 'B', rightCtx: 'C'})

// or if you prefer the concise *classic* syntax for context sensitive productions:
lsystem.setProduction('A<B>C', 'Z')

// You can also use ES6 arrow functions. Here a Simple (custom) stochastic production, producing `F` with 10% probability, `G` with 90%
lsystem.setProduction('B', () => (Math.random() < 0.1) ? 'F' : 'G')

//Or make use of additional info fed into production functions on runtime.
// Here: return 'B-' if 'B' is in first half of word/axiom, otherwise 'B+'
lsystem.setProduction('B', (info) => (info.currentAxiom.length / 2) <= info.index ? 'B-' : 'B+')

Documentation

The following section is a quick overview. Please refer to the full documentation for a detailed usage reference.

Initialization

let lsystem = new LSystem(options)

options may contain:

advanced options (see API docs for details):

Most often you will find yourself only setting axiom, productions and finals.

Setting an Axiom

As seen in the first section you can simply set your axiom when you init your L-System.

let lsystem = new LSystem({
      axiom: 'F++F++F'
})

You can also set an axiom after initialization:

let lsystem = new LSystem({
      axiom: 'F++F++F'
})
lsystem.setAxiom('F-F-F')

Setting Productions

Productions define how the symbols of an axiom get transformed. For example, if you want all As to be replaced by B in your axiom, you could construct the following production:

let lsystem = new LSystem({
  axiom: 'ABC',
  productions: {'A': 'B'}
})
//lsystem.iterate() === 'BBC'

You can set as many productions on initialization as you like:

let lsystem = new LSystem({
      axiom: 'ABC',
      productions: {
        'A': 'A+',
        'B': 'BA',
        'C': 'ABC'
      }
})
// lsystem.iterate() === 'A+BAABC'

You could also start with an empty L-System object, and use setAxiom() and setProduction() to edit the L-System later:

let lsystem = new LSystem()
lsystem.setAxiom('ABC')
lsystem.setProduction('A', 'AAB')
lsystem.setProduction('B', 'CB')

This can be useful if you want to dynamically generate and edit L-Systems. For example, you might have a UI, where the user can add new production via a text box.

A major feature of this library is the possibility to use functions as productions, which could be used for stochastic L-Systems:

// This L-System produces `F+` with a 70% probability and `F-` with 30% probability
let lsystem = new LSystem({
      axiom: 'F++F++F',
      productions: {'F': () => (Math.random() <= 0.7) ? 'F+' : 'F-'}
})

// Productions can also be changed later:
lsys.setProduction('F', () => (Math.random() < 0.2) ? 'F-F++F-F' : 'F+F')

If you are using functions as productions, your function can make use of a number of additional parameters that are passed as an info object to the function (see full docs for more details):

lsys.setAxiom('FFFFF')
lsys.setProduction('F', (info) => {
  // Use the `index` to determine where inside the current axiom, the function is applied on.
  if(info.index === 2) return 'X';
})
// lsys.iterate() === FFXFF

The info object includes:

For a shorter notation you could use the ES6 feature of object destructuring (has support in most modern browsers):

lsys.setProduction('F', ({index}) => index === 2  ? 'X' : false);

If undefined or false is returned in a production function, as above, the initiating symbol or symbol object is returned (in aboves example, that would be'F').

Getting Results

Now that we have set up our L-System set, we want to generate new axioms with iterate():

// Iterate once
lsystem.iterate();

// Iterate n-times
lsystem.iterate(5);

iterate() conveniently returns the resulting string:

console.log(lsystem.iterate())

If you want to fetch the result later, use getString():

lsystem.iterate()
console.log(lsystem.getString())

Putting it all together

Final functions: Visualization and other post processing

Most likely you want to visualize or post-process your L-Systems output in some way. You could iterate and parse the result yourself, however lindemayer already offers an easy way to define such postprocessing: final functions. In those final functions you can define what should be done for each literal/character. The classic way to use L-Systems is to visualize axioms with turtle graphics. The standard rules, found in Aristid Lindenmayer's and Przemyslaw Prusinkiewicz's classic work Algorithmic Beauty of Plants can be easily implented this way, to output the fractals onto a Canvas.

You can fiddle with the following example in this codepen!

<body>
    <canvas id="canvas" width="1000" height="1000"></canvas>
</body>
var canvas = document.getElementById('canvas')
var ctx = canvas.getContext("2d")

// translate to center of canvas
ctx.translate(canvas.width / 2, canvas.height / 4)

// initialize a koch curve L-System that uses final functions
// to draw the fractal onto a Canvas element.
// F: draw a line with length relative to the current iteration (half the previous length for each step)
//    and translates the current position to the end of the line
// +: rotates the canvas 60 degree
// -: rotates the canvas -60 degree

var koch = new LSystem({
  axiom: 'F++F++F',
  productions: {'F': 'F-F++F-F'},
  finals: {
    '+': () => { ctx.rotate((Math.PI/180) * 60) },
    '-': () => { ctx.rotate((Math.PI/180) * -60) },
    'F': () => {
      ctx.beginPath()
      ctx.moveTo(0,0)
      ctx.lineTo(0, 40/(koch.iterations + 1))
      ctx.stroke()
      ctx.translate(0, 40/(koch.iterations + 1))}
   }
})

koch.iterate(3)
koch.final()

And the result:

Resulting image

As this library is not opinionated about what your results should be like, you can write your own finals. Therefore you can draw 2D turtle graphics as seen above, but also 3D ones with WebGL/three.js, or even do other things like creating sound!

Advanced Usage

Parametric L-Systems

When defining axioms you may also use an Array of Objects instead of basic Strings. This makes your L-System very flexible because you can inject custom parameters into your symbols. Eg. a symbol like a A may contain a food variable to simulate organic growth:

let parametricLsystem = new lsys.LSystem({
  axiom: [
    {symbol: 'A', food:0.5},
    {symbol: 'B'},
    {symbol: 'A', , food:0.1},
    {symbol: 'C'}
  ],
  // And then do stuff with those custom parameters in productions:
  productions: {
    'A': ({part, index}) => {
      // split A into one A and a new B if it ate enough:
      if(part.food >= 1.0) {
        return [{symbol: 'A', food:0}, {symbol: 'B', food:0}]
      } else {
        // otherwise eat a random amount of food
        part.food += Math.random() * 0.1;
        return part;
      }
    }
  }
});

// parametricLsystem.iterate(60);
// Depending on randomness:
// parametricLsystem.getString() ~= 'ABBBBBABBBC';
// The first part of B's has more B's because the first A got more initial food which in the end made a small difference, as you can see.

As you can see above, you need to explicitly define the symbol value, so the correct production can be applied.

Full Documentation