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.


Project maintained by nylki Hosted on GitHub Pages — Theme by mattgraham

Create LSystem Object

let myLsystem = new LSystem([options])

Options

Example Usage

Basic

let myLsystem = new LSystem({
	axiom: 'F',
	productions: {
		'F': 'F-A'
		'A': 'FF++FA'
	}
})

Context sensitive (using production objects, more below)

let myLsystem = new LSystem({
	axiom: 'ABCDE',
	productions: {
		'A': 'A+AAC',
		'B': {leftCtx: 'A', successor: 'A'},
		'C': {rightCtx: 'D', successor: 'ABCD'}
	}
})

Context sensitive (using classic ABOP-syntax)

This one is semantically equivalent to the previous example, but uses the classic syntax which is supported by default (support can be turned of by setting allowClassicSyntax:false).

let myLsystem = new LSystem({
	axiom: 'ABCDE',
	productions: {
		'A'	: 'A+AAC',
		'A<B'	: 'A',
		'C>DE'	: 'ABCD'
	}
})

Productions

You can set productions in two ways.

Multiple productions via constructor:

let myLsystem = new LSystem({
	productions: {
		[symbol]: [production],
		[symbol]: [production]
	}
})

Or via their setter-methods:

// Set single production
myLsystem.setProduction([symbol], [production])
// set multiple productions
myLsystem.setProductions({
	[symbol]: [production],
	[symbol]: [production]
})

Productions in lindenmayer.js come in different flavours suited for different situations:

String-Based Productions

The most basic production consists of a single String, representing the result of a production.

// Each F will be replacd with FF
myLsystem.setProduction('F', 'FF');

Array-Based Production

If you are reading about L-System in the classic ABOP, you may have stumbled upon parametric L-Systems. Those have optional parameters inside each symbol. To make this possible using Lindenmayer.js, you can use Arrays of Objects {symbol, [custom parameters]} besides basic Strings as production results (and axioms).

// Each F will be replaced with FF
myLsystem.setProduction('F', [{symbol: 'F'}, {symbol: 'F'}]);

// Or the same but with additional parameters per symbol.
myLsystem.setProduction('F', [
	{symbol: 'F', params: [5,6,1]},
	{symbol: 'F', params: [1,0,0]]}
]);

You can define any number of custom parameters (NOTE: Each symbol object must always posses a symbol property! ):

myLsystem.setAxiom([{symbol: 'F', food: 10, size: 4}])
myLsystem.setProduction('F', [
	{symbol: 'A', food: 5, size: 2, color: 'rgb(255, 0, 0)'},
	{symbol: '+'},
	{symbol: 'A', food: 5, size: 2, color: 'rgb(0, 255, 0)' }
]);

If you want to learn more about parametric L-Systems, the following chapters will give you some more details.

Object-Based Productions

myLsystem.setProduction( {successor/successors: [String, Array, Function]/[Array] , leftCtx: [String], rightCtx:[String], condition: [Function]} )

To allow even more flexibility than String or Array based productions, you can choose to use a wrapper Object in the following way to allow for stochastic, context-sensitive and conditional L-Systems. This object basically wraps around a regular Array, String or Function Production, which are now defined in the successor field. The additional functionality can be used via the leftCtx, rightCtx, successors and condition properties.

A barebone production using such a wrapper Object:

// Instead of:
myLsystem.setProduction('F', 'FF');

// You would write:
myLsystem.setProduction( 'F', {successor: 'FF'} );

// Or with an Array as successor/production result.
myLsystem.setProduction('F', { successor: [{symbol: 'F'}, {symbol: 'F'}] });

The above example do not yet make use of those extra functionality. To add eg. a context-sensitive check you could rewrite the second one to:

// You would write:
myLsystem.setProduction('F', { successor: 'FF', leftCtx: 'FB' });

Those extra properties are explained in more detail in the following short chapters.

Context-Sensitive

// Replace 'F' with FF only if left part is FX and the right part is 'X'
myLsystem.setProduction('F', {successor: 'FF', leftCtx: 'FX', rightCtx: 'X'});

See also the chapter on classic syntax to learn how to write more concise context sensitive productions.

Conditional

You may also define a condition which has to return a boolean:


// Replace 'F' with FF only if it is monday (yeah, probably less useful in practice ;), but that should illustrate what potential for creativity you have with.)

myLsystem.setProduction('F',
{successor: 'FF', condition: () => new Date().getDay() === 1});

Stochastic

Instead of a single successor, a stochastic L-System defines a successors array which includes multiple objects with their own successor. The weight property defines the probability of each successor to be choosen. If all successors have the same weight they have an equal chance to get choosen. If one successor has a higher weight than another, it is more likely to get choosen.

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

Function-Based Productions

Besides Strings, Arrays and (wrapping) Objects you can also define functions as productions for complete flexibilty. Each production function has also access to an info object.

myLsystem.setProduction([symbol], [Function(info)])

info object:

A production function returns a valid successor, like a String or Array. If nothing or false is returned, the symbol will not replaced.

Usage examples:

Replace ‘F’ with ‘A’ if it is at least at index 3 (4th position) inside the current axiom, otherwise return ‘B’:

myLsystem.setAxiom('FFFFFFF');
myLsystem.setProduction('F', ({index}) => index >= 3 ? 'A' : 'B');
myLsystem.iterate(); // FFFFFF results in -> BBBAAAA

Replace any occurrence of ‘F’ with a random amount (but max. 5) of ‘F’:

myLsystem.setProduction('F', () => {
	let result = '';
	let n = Math.ceil(Math.random() * 5);
	for (let i = 0; i < n; i++) result += 'F';
	return result;
})

Replace ‘F’ with ‘FM’ on mondays and with ‘FT’ on tuesdays. Otherwise nothing is returned, therefore ‘F’ stays ‘F’.

myLsystem.setProduction('F', () => {
	let day = new Date().getDay();
	if (day === 1) return 'FM';
	if (day === 2) return 'FT';
});

Parametric usage:

// Duplicate each F but reduce custom `size` parameter for new children by 50%.
myLsystem.setProduction('F', ({part}) =>
	[{symbol: 'F', food: part.size / 2},
	 {symbol: 'F', food: part.size / 2}
  ]
);

Apply Productions

To apply your productions onto the axiom you call iterate([n]) on your L-System object:

// iterate only once without arguments
myLsystem.iterate();
// iterate multiple times
mylsystem.iterate(5);

In each iteration step, all symbols of the axiom are replaced with new symbols based on your defined productions:

let myLsystem = new LSystem({
	axiom: 'F+X-X',
	ignoredSymbols: '+-',
	productions: {
		'F': 'G+H',
		'X': {leftCtx: 'F', successor: 'YZ'}
	}
});

let result = myLsystem.iterate();
console.log(result);
// result = 'G+H-YZ-X'
// Note that, because the production for 'X' is context-sensitive (leftCtx:F), only the first X is replacd by 'YZ'.

You can see more examples in the examples folder or take a look at the tests.

Retrieve Results

Basic usage:

myLsystem.iterate();
let result = myLsystem.getString();
// or:
let result = myLsystem.iterate();

When you call iterate(), the reduced string result/axiom of your L-System is returned. You can also get the string result/axiom via getString(). To retrieve the raw result/axiom you can use getRaw() or directly access the axiom property. To get the raw axiom may be useful if you are operating with arrays/objects in your axiom and productions. For string-based L-Systems it makes no difference.

To demonstrate the different behaviors, please take a look below:

String based L-System

let myLsystem = new LSystem({
	axiom: 'F',
	productions: {'F': 'F+F'}
});

// Before calling iterate()
let result = myLsystem.getString(); // result = 'F'
result = myLsystem.getRaw(); 	    // result = 'F'
result = myLsystem.axiom; 	    // result = 'F'

// Calling iterate()
result = myLsystem.iterate();       // result = 'F+F'

// Getting results after calling iterate()
result = myLsystem.getString();     // result = 'F+F'
result = myLsystem.getRaw(); 	    // result = 'F+F'
result = myLsystem.axiom; 	    // result = 'F+F'

Array based L-System

let myLsystem = new LSystem({
	axiom: [{symbol: 'F'}],
	productions: {'F': 'F+F'}
});

// Before calling iterate()
let result = myLsystem.getString(); // result = 'F'
result = myLsystem.getRaw(); 	    // result = [{symbol: 'F'}]
result = myLsystem.axiom; 	    // result = [{symbol: 'F'}]

// Calling iterate()
result = myLsystem.iterate(); 	    // result = 'F+F'

// Getting results after calling iterate()
result = myLsystem.getString();     // result = 'F+F'
result = myLsystem.getRaw(); 	    // result = [{symbol: 'F'}, {symbol: '+'}, {symbol: 'F'}]
result = myLsystem.axiom; 	    // result = [{symbol: 'F'}, {symbol: '+'}, {symbol: 'F'}]

Finals

To visualize or post-process your L-System you can define final functions for each symbol. They function similar to productions, but instead of replacing the existing axiom/word, finals are used to draw for example different lines for different symbols. All finals are executed by calling lsystem.final().

A very common application for finals would be the creation of turtle graphics. Below is an example on how to use finals to draw turtle graphics like the Koch Snowflake on the Canvas HTML element.

You can fiddle with the following example in this codepen!

<body>
	<canvas id="canvas" width="1000" height="1000"></canvas>
</body>

<script>
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()
</script>

Lindenmayer.js is not opinionated on what you do with your L-System, so you can draw 2D turtle graphics like above, but may also draw 3D ones or even do entirely different things, like creating sound and music, simply by defining your own final functions.

Classic Syntax Features

Currently supported:

myLsystem.setProduction(‘B’, {leftCtx: ‘A’, rightCtx: ‘C’, successor: ‘X’}

// you can write the following production if allowClassicSyntax is set to true:

myLsystem.setProduction(‘AC’, ‘X’)
```