Beyond Trees: Exploring Abstract Geometry with L-Systems in p5.js

Drawing with the L-system.

Inspired by an article by Gin (@gin_graphic), I decided to dive into the world of L-systems.

【p5.js】L-systemで木を描く - ギンの備忘録

While L-systems are often associated with realistic trees, I found myself captivated by their potential for creating abstract geometric shapes.

The geometric shape with the L-system.

Drawing with the L-system can vary depending on the combination of parameters. And it is hard for me to predict the result by the parameters.

I've created the L-system trial code in p5.js to find the attractive drawing result out of such an infinite number of combinations.

[Japanese version / 日本語版はこちら]

 

How Minimal Angle Changes Drastically Alter Results

I was fascinated by how a simple adjustment to the angle parameter can completely transform the visual outcome. For instance, shifting from 90 to 120 degrees creates an entirely different structural feel.


 let distance = 20;
 let angle = radians(90);

The geometric shape with the L-system.



 let distance = 20;
 let angle = radians(120);

The geometric shape with the L-system.


These geometric patterns are hypnotic. The interplay between production rules, recursion depth, and rotation angles offers an infinite playground for experimentation.

 

An L-System Parameter Explorer

I developed a small tester in p5.js to efficiently explore the vast combination of parameters. This tool allows me to fine-tune the recursion depth and rotation angles in real-time.

You can see the drawing result and the combination of the parameters on the screen.

Controls:
UP/DOWN Arrow Keys: Increase/Decrease recursion depth.
LEFT/RIGHT Arrow Keys: Adjust rotation angles.

The production rule is important, and I don't want to lose it. So I decided to write it on the code.

The organic shape by L-system trial code.

/** 
 * L-system tester.
 * ref. https://gin-graphic.hatenablog.com/entry/2022/12/20/000000
 * 
 * @author @deconbatch
 * @version 0.1
 * @license GPL3
 * p5.js 1.5.0
 * created 2022.12.31
 */

const initProduction = '+F[+F]F';
const maxRepeat = 10;
const maxAngle = 180;
const w = 800;
const h = w;

let repeatNum = 2;
let rotateAngle = 15;

/** 
 * keyPressed : change parameters then re-draw.
 */
function keyPressed() {
  if (keyCode == UP_ARROW) {
    repeatNum++;
  } else if (keyCode == DOWN_ARROW) {
    repeatNum--;
  } else if (keyCode == RIGHT_ARROW) {
    rotateAngle += 15;
  } else if (keyCode == LEFT_ARROW) {
    rotateAngle -= 15;
  }
  repeatNum = constrain(repeatNum, 1, maxRepeat);
  rotateAngle = constrain(rotateAngle, -maxAngle, maxAngle);

  redraw();
}

/** 
 * setup : world famous setup function.
 */
function setup() {
  createCanvas(w, h)
  angleMode(DEGREES);
  noLoop();
  noFill();
  stroke(0);
}

/** 
 * draw : generate the production then draw it.
 */
function draw() {
  // generate the production
  const production = genProduction(initProduction, repeatNum);
  // fit size and position with canvas
  const fit = fitting(production, rotateAngle);
  // draw
  background(240);
  push();
  translate(fit.x, fit.y);
  strokeWeight(3 / repeatNum);
  drawShape(production, rotateAngle, fit.z);
  pop();
  drawParams(initProduction, repeatNum, rotateAngle, fit.z);
}

/** 
 * genProduction : generate the production
 */
function genProduction(_baseP, _num) {
  let word = 'F';
  for (let i = 0; i < _num; i++) {
    let replaced = word.replace(/F/g, _baseP);
    word = replaced;
  }
  return word;
}

/** 
 * fitting : calculate the size and position to fit to the canvas
 */
function fitting(_prd, _angle) {
  let len = 10; // size
  let cX = 0;   // center x
  let cY = 0;   // center y

  let minX = width;
  let minY = height;
  let maxX = 0;
  let maxY = 0;
  let x = 0;
  let y = 0;
  let a = 0;
  const saveX = [];
  const saveY = [];
  const saveA = [];
  for (let c of _prd) {
    switch (c) {
    case "F":
      x += len * sin(a);
      y += len * cos(a);
      break;
    case "+":
      a += _angle;
      break;
    case "-":
      a -= _angle;
      break;
    case "[":
      saveX.push(x);
      saveY.push(y);
      saveA.push(a);
      break;
    case "]":
      x = saveX.pop();
      y = saveY.pop();
      a = saveA.pop();
      break;
    default:
      break;
    }
    minX = min(minX, x);
    minY = min(minY, y);
    maxX = max(maxX, x);
    maxY = max(maxY, y);
  }

  const rate = max((maxX - minX) / width, (maxY - minY) / height) * 1.5;
  if (rate != 0) {
    len /= rate;
    const rminX = minX / rate;
    const rminY = minY / rate;
    const rmaxX = maxX / rate;
    const rmaxY = maxY / rate;
    cX = width * 0.5 - (rminX + (rmaxX - rminX) * 0.5);
    cY = height * 0.5 - (rminY + (rmaxY - rminY) * 0.5);
  }

  return createVector(cX, cY, len);
}

/** 
 * drawShape : draw the shape
 */
function drawShape(_prd, _angle, _len) {
  for (let c of _prd) {
    switch (c) {
    case "F":
      line(0, 0, 0, _len);
      translate(0, _len);
      break;
    case "+":
      rotate(-_angle);
      break;
    case "-":
      rotate(+_angle);
      break;
    case "[":
      push();
      break;
    case "]":
      pop();
      break;
    default:
      break;
    }
  }
}

/** 
 * drawParams : draw parameters
 */
function drawParams(_f, _n, _a, _l) {
  const siz = 12;
  push();
  noStroke();
  fill(0);
  textSize(siz);
  textAlign(LEFT, CENTER);
  textFont('monospace');
  text('PRODUCTION:' + _f, 10, siz);
  text('    REPEAT:' + _n, 10, siz * 2);
  text('     ANGLE:' + _a, 10, siz * 3);
  text('    LENGTH:' + _l, 10, siz * 4);
  pop();
}

/*
Copyright (C) 2022- deconbatch

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

Please feel free to use this example code under the terms of the GPL.

 

Curated Combinations

Let me show you some combinations and the results.

Ghostie

I applied blendMode(SCREEN) to create an ethereal, translucent effect reminiscent of a spectral shadow.

Shape like a ghost shadow.
  • rule : -F[+F+F]-F
  • reccursion : 8
  • angle : 150 degree

 

Harvest

By combining two results and utilizing ellipse() alongside line(), I achieved an organic texture similar to ears of wheat.

The  ears of wheat by L-system.
  • rule : F[[[+FF]-F[-F]]F]-F
  • reccursion : 4
  • angle : 13 degree, -9 degree

With line() without ellipse()

The  ears of wheat after threshing.

 

Abstract painting

Angle Magic: Small, precise increments (like 60.1°) can lead to complex, non-repeating abstract forms.

Example abstract painting by L-system.
  • rule : F[+F]-F[+F[-F]+F]
  • reccursion : 5
  • angle : 60.1 degree, 120.1 degree

Product development.

Example abstract painting by L-system.

 

Getting Lost in the L-System Maze

The beauty of L-systems lies in their unpredictability. Even a tiny deviation can lead to a breathtaking discovery. Feel free to use my code and get lost in the infinite possibilities—it's a truly rewarding way to spend your time.

 

Next Post Previous Post
No Comment
Add Comment
comment url