Lindenmayer is a L-System library using modern (ES6) JavaScript with focus on a concise syntax. The idea is to have a very 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.
Better docs and more examples are coming soon :)
- codepen collection (editable!)
- Interactive L-System builder (2D turtle graphics)
- Interactive L-System builder (3D turtle graphics)
- Download
lindenmayer.js
: - Then in your
index.html
:
<script src="lindenmayer.js"></script>
npm install --save lindenmayer
Then in your Node.js/browserify/… script:
var LSystem = require('lindenmayer');
Or in your index.html
:
<script src="node_modules/lindenmayer/dist/lindenmayer.js"></script>
// 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:
let myLsys = new LSystem({
axiom: 'ABC',
/* Directly when initializing a new L-System object: */
productions: { 'B': 'BB' }
})
// After initialization:
myLsys.setProduction('B', 'F+F')
// You can also use ES6 arrow functions (same result as above):
myLsys.setProduction('B', () => 'F+F')
// Simple stochastic production, producing `F` with 10% probability, `B` with 90%
myLSys.setProduction('B', () => (Math.random() < 0.1) ? 'F' : 'B')
// Simple context sensitive production rule, replacing `B` with `Z` if previous character is a A and next character is 'C'
myLsys.setProduction('B',
({index, currentAxiom}) => (currentAxiom[index-1] === 'A') && (currentAxiom[index+1] === 'C') ? 'Z' : 'B'
)
// or if you prefer the concise *classic* syntax for context sensitive productions:
myLsys.setProduction('A<B>C', 'Z')
You can init a L-System object with the new
keyword:
let myCoolLSystem = 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 callingiterate()
. A production can be either a String or a Function (see below.)finals
: Optional key-value Object to set Functions be executed for each symbol in sequential order. Useful for visualization. Used when callingfinal()
.
advanced options (see [API docs](not yet created) 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)classicParametricSyntax
: A Bool to enable experimental parsing of parametric L-Systems as defined in Lindenmayers book Algorithmic Beauty of Plants. (default:false
)
Most often you will find yourself only setting axiom
, productions
and finals
.
As seen in the first section you can simply set your axiom when you init your L-System.
let lsys = new LSystem({
axiom: 'F++F++F'
})
You can also set an axiom after initialization:
let lsys = new LSystem({
axiom: 'F++F++F'
})
lsys.setAxiom('F-F-F')
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 lsys = new LSystem()
lsys.setAxiom('ABC')
lsys.setProduction('A', 'AAB')
lsys.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 is especially useful for stochastic L-Systems:
// This L-System produces `F+` with a 70% probability and `F-` with 30% probability
let lsys = 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:
lsys.setAxiom('FFFFF')
lsys.setProduction('F', (parameters) => {
// Use the `index` to determine where inside the current axiom, the function is applied on.
if(parameters.index === 2) return 'X';
})
// lsys.iterate() === FFXFF
For a shorter notation you could use the ES6 feature of object destructuring (has support in most modern browsers):
lsys.setProduction('F', ({index, part}) => index === 2 ? 'X' : part);
parameters:
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.
Now that we have set up our L-System set, we want to generate new axioms with iterate()
:
// Iterate once
lsys.iterate();
// Iterate n-times
lsys.iterate(5);
iterate()
conveniently returns the resulting string:
console.log(lsys.iterate())
If you want to fetch the result later, use getString()
:
lsys.iterate()
console.log(lsys.getString())
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:
When defining axioms you may also use an Array of Objects instead of basic Strings. This makes the library very flexible because you can insert custom parameters into your symbols. Eg. a symbol like a A
may contain a food
variable to simulate organic growth when combined with a random() function:
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.
they loook like A -> A(1,2)B(5,2)
.
Are planned, but not yet fully implemented. Stay tuned!