Lindenmayer
Lindenmayer is a L-System library using modern (ES6) JavaScript 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: <>
.
Most stuff should work. I am currently working on parametric L-System support.
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
Examples
- codepen collection (editable!)
- Examples for the accompanying A-Frame component of this library
- Interactive L-System builder (2D turtle graphics)
- Interactive L-System builder (3D turtle graphics)
- Basic Tree
- Basic Tree (rendered in three.js)
Install
Direct download
- Download latest
lindenmayer.browser.js
: - Then in your
index.html
:
<script src="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="node_modules/lindenmayer/dist/lindenmayer.browser.js"></script>
See releases for change logs.
Quick Intro
// Initializing a L-System that produces the Koch-curvelet 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:
axiom
: A String or an Array of Objects to set the initial axiom (sometimes called axiom, start or initiator).productions
: key-value Object to set the productions from one symbol to its axiom. Used when calling iterate(). A production can be either a String, Object or a Function.finals
: Optional key-value Object to set functions that should be executed each symbol in sequential order when calling final(). Useful for visualization.
advanced options (see API docs for details):
branchSymbols
: A String of two characters. Only used when working with classic context sensitive productions. The first symbol is treated as start of a branch, the last symbol as end of a branch. (default:"[]"
, but only when using classic CS syntax)ignoredSymbols
: A String of characters to ignore when using context sensitive productions. (default:"+-&^/|\\"
, but only when using classic CS syntax)
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 A
s 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% probabilitylet 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:
index
: the index inside the axiomcurrentAxiom
: the current full axiom/wordpart
: the current part (symbol or object) the production is applied on. This is especially useful if you are using parametric L-Systems (see last chapter) to have access to parameters of a symbol.
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 oncelsystem.iterate(); // Iterate n-timeslsystem.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 canvasctx.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:
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.