Lindenmayer systems

A Lindemayer system or LSystem for short consists of an alphabet of symbols that can be used to make strings, a collection of production that expand each symbol into some larger string of symbols, an initial “axiom” string from which to begin construction, and a mechanism for translating the generated strings into geometric structures.

A few years back, I built a rudimentary website to extrapolate a few known LSystems in pure JS. However, presently I have been getting more comfortable with Rust 🦀 and explored rebuilding it with Rust and wasm-bindgen to get better performance for tougher iterations.

Website - vsekar.me/LSystems/

Repository - github.com/Spockuto/LSystems/

Now let's explore how an axiom, a set of rules, and an angle can be developed into this beautiful fern Barnsley Fern

Step 0 - Define the LSystem

For Barnsley Fern, the LSystem is defined as follows

Axiom : X
Rules : 
 - X -> F-[[X]+X]+F[+FX]-X
 - F -> FF
Angle : 22.5

This translates into the following for our Rust implementation.

#![allow(unused)]
fn main() {
lazy_static! {
    static ref BARNSLEY_FERN: LSystem = LSystem {
        variables: "XF",
        axiom: "X",
        rules: vec![
            ('X', "F-[[X]+X]+F[+FX]-X"), 
            ('F', "FF"),
        ]
        .into_iter()
        .collect(),
        angle: 22.5,
        max_rounds: 8,
    };
}
}
  • variables : These are the identifiers that will be considered during building the sequence. Others will be skipped.
  • axiom : The initial sequence
  • rules : Expansion rules for each identifier
  • angle : Explained later
  • max_rounds - Implementation detail. Maximum number of supported iterations before running out of heap memory.

Step 1 - Generate the sequence

Once the LSystem is defined, we can build the drawing sequence. Iterations define the number of rounds the sequence will be expanded using the given rules. The higher the iteration, the better defined the picture is.

#![allow(unused)]
fn main() {
/// Generate the Turtle graphics sequence for given iterations
pub fn expand(&self, iterations: u32) -> String {

    // panic if given iterations are greater than the accepted limit.
    if iterations > self.0.max_rounds {
        panic!("Max limit reached");
    }

    let mut sequence = String::new();
    for i in 0..iterations {
        if i == 0 {
            // First iteration is the axiom
            sequence.insert_str(0, self.0.axiom);
        } else {
            let sequence_copy = sequence.to_string();
            let mut insert_index = 0;
            for identifier in sequence_copy.chars() {
                // Skip if the variable in sequence doesn't have a rule
                if !self.0.variables.contains(identifier) {
                    insert_index += 1;
                    continue;
                }
                // Expand the sequence based on the rule
                let rule = self.0.rules.get(&identifier).unwrap();
                sequence.remove(insert_index);
                sequence.insert_str(insert_index, rule);
                insert_index += &rule.len();
            }
        }
        // The current sequence will be used as the generator for the next round
    }
    sequence
}
}

Note: The function above can also be written recursively but for larger iterations, you would run out of stack depth way before running out of heap memory.

For Barnsley Fern, the sequence expands as follows for subsequent iterations

IterationsSequence
1X
2F-[[X]+X]+F[+FX]-X
3FF-[[F-[[X]+X]+F[+FX]-X]+F-[[X]+X]+F[+FX]-X]+FF[+FFF-[[X]+X]+F[+FX]-X]-F-[[X]+X]+F[+FX]-X
4FFFF-[[FF-[[F-[[X]+X]+F[+FX]-X]+F-[[X]+X]+F[+FX]-X]+FF[+FFF-[[X]+X]+F[+FX]-X]-F-[[X]+X]+F[+FX]-X]+FF-[[F-[[X]+X]+F[+FX]-X]+F-[[X]+X]+F[+FX]-X]+FF[+FFF-[[X]+X]+F[+FX]-X]-F-[[X]+X]+F[+FX]-X]+FFFF[+FFFFFF-[[F-[[X]+X]+F[+FX]-X]+F-[[X]+X]+F[+FX]-X]+FF[+FFF-[[X]+X]+F[+FX]-X]-F-[[X]+X]+F[+FX]-X]-FF-[[F-[[X]+X]+F[+FX]-X]+F-[[X]+X]+F[+FX]-X]+FF[+FFF-[[X]+X]+F[+FX]-X]-F-[[X]+X]+F[+FX]-X

Step 2 - Draw the canvas based on the sequence

Now that the sequence is generated, we can start building the canvas. Each character in the sequence defines a particular set of operations on the canvas. This allows us to convert the sequence into a picture.

#![allow(unused)]
fn main() {
// Define length
// Set the angle to the LSystem angle
let angle_rad = -1.0 * PI * angle / 180.0;
let (mut x, mut y) = (0.0, 0.0);
let mut stack = vec![];

for seq in sequence.chars() {
    // perform operations on the canvas 
    match seq {
        // draw a line to (x,y) at given length and angle
        'F' | 'A' | 'B' => {
            x += length * angle.cos();
            y += length * angle.sin();
            context.line_to(x, y);
            context.stroke();
        }
        'S' => {
        // move along a line to (x,y) at given length and angle
            x += length * angle.cos();
            y += length * angle.sin();
            context.move_to(x, y);
        }
        '+' => {
        // rotate counterclockwise by angle_rad
            angle += angle_rad;
        }
        '-' => {
        // rotate clockwise by angle rad
            angle -= angle_rad;
        }
        '[' => {
        // push a point into stack
            stack.push(Line { x, y, angle });
        }
        ']' => {
        // Pop a point from the stack and move to it.
            let line = stack.pop().unwrap();
            (x, y, angle) = (line.x, line.y, line.angle);
            context.move_to(x, y);
        }
        // For others, skip and continue
        _ => continue,
    }
}
}
IterationsBarnsley Fern
1None
2Barnsley Fern 2
3Barnsley Fern 3
4Barnsley Fern 4
5Barnsley Fern 5
......
8Barnsley Fern 8

Step 3 - Color the canvas using a linear gradient

The final step in generating our fractal is applying a linear gradient between two colors over our canvas. This can be achieved by applying color interpolation over each dark pixel.

#![allow(unused)]
fn main() {
let image_data = context
    .get_image_data(0.0, 0.0, width as f64, height as f64)
    .unwrap();

// data contains 4 u8 values per pixel indicating RGBA (red, green, blue, alpha) values
let mut data = image_data.data();

// Set the linear gradient colors : color1 -> color2 

let c1 = Rgb::from_hex_str(&color1).unwrap();
let c2 = Rgb::from_hex_str(&color2).unwrap();

// linear gradient is defined using interpolation
// Assume the fraction grows from 0 to 1
// pixel.r = c1.r + (c2.r - c1.r) * fraction
// pixel.g = c1.g + (c2.g - c1.g) * fraction
// pixel.b = c1.b + (c2.b - c1.b) * fraction

for index in 0..(width * height * 4) as usize {
    // Since the canvas is drawn with black, only alpha will be set initially
    if data[index] > 0 {
    // pixel's alpha is set
    // Update the color using interpolation
        let fraction = index as f32 / (height * width * 4) as f32;
        data[index - 3] = (c1.get_red() + (c2.get_red() - c1.get_red()) * fraction) as u8;
        data[index - 2] = (c1.get_green() + (c2.get_green() - c1.get_green()) * fraction) as u8;
        data[index - 1] = (c1.get_blue() + (c2.get_blue() - c1.get_blue()) * fraction) as u8;
    }
}
let slice_data = Clamped(&data.0[..]);
let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(
    slice_data,
    width as u32,
    height as u32,
)
.unwrap();

// load the data back into the canvas
context.put_image_data(&image_data, 0.0, 0.0).unwrap();
}

With this, a linear gradient between #C6EA8D and #FE90AF gives us Barnsley Fern

Note: Colors don't exactly behave well with linear interpolation. For more details check out The Secrets of Colour Interpolation

Fractal Tree

Axiom : F
Rules : 
 - F -> FF-[-F+F+F]+[+F-F-F]
Angle : 22.5

Fractal Tree

Fractal Tree 2

Axiom : VZFFF
Rules : 
 - F -> F
 - V -> [+++W][---W]YV
 - W -> +X[-W]Z
 - X -> -W[+X]Z
 - Y -> YZ
 - Z -> [-FFF][+FFF]F
Angle : 18

Fractal Tree 2

Dragon Curve

Axiom : FX
Rules : 
 - X -> X+YF+
 - Y -> -FX-Y
Angle : 90

Dragon Curve

32 Segment Curve

Axiom : F+F+F+F
Rules : 
 - F -> -F+F-F-F+F+FF-F+F+FF+F-F-FF+FF-FF+F+F-FF-F-F+FF-F-F+F+F-F+
Angle : 90

32 Segment Curve

Peano Gosper Curve

Axiom : A
Rules : 
 - A -> A-B--B+A++AA+B-
 - B -> +A-BB--B-A++A+B
Angle : 60

Peano Gosper Curve

Koch Snowflake

Axiom : F++F++F
Rules : 
 - F -> F-F++F-F
Angle : 60

Koch Snowflake

Koch Snowflake 2

Axiom : F+F+F+F
Rules : 
 - F -> F-F+F+F-F
Angle : 60

Koch Snowflake 2

Koch Snowflake 3

Axiom : F
Rules : 
 - F -> F-F+F+F-F
Angle : 85

Koch Snowflake 3

Quadratic Koch Island

Axiom : F+F+F+F
Rules : 
 - F -> F+F-F-FF+F+F-F
Angle : 90

quad. Koch Island

Quadratic Koch Island 2

Axiom : F+F+F+F
Rules : 
 - F -> F+FF-FF-F-F+F+FF-F-F+F+FF+FF-F
Angle : 90

quad. Koch Island 2

Islands

Axiom : F+F+F+F
Rules : 
 - F -> F-SFF+F+FF+F-S-FFF-F+F+F-FFFF
 - S -> SSSSSSSS
Angle : 90

Islands

Islands 2

Axiom : F+F+F+F
Rules : 
 - F -> F-SF+FF+F+FF-S-FF+SF-FF-F-FF+S+FFF
 - S -> SSSSSS
Angle : 90

Islands 2

Sierpinski Triangle

Axiom : FXF--FF--FF
Rules : 
 - F -> FF
 - X -> --FXF++FXF++FXF--
Angle : 60

Sierpinski Triangle

Sierpinski Square

Axiom : F+F+F+F
Rules : 
 - F -> FF+F+F+F+FF
Angle : 90

Sierpinski Square

Hilbert Curve

Axiom : X
Rules : 
 - X -> +YF-XFX-FY+
 - Y -> -XF+YFY+FX-
Angle : 90

Hilbert Curve

Frec Fractal

Axiom : XYXYXYX+XYXYXYX+XYXYXYX+XYXYXYX
Rules : 
 - F -> 
 - X -> FX+FX+FXFY-FY-
 - Y -> +FX+FXFY-FY-FY
Angle : 90

Frec Fractal