Files
p5js/sketches/L-System.js
2026-04-27 18:11:48 +00:00

263 lines
6.2 KiB
JavaScript

/*
L-System Fractal Generator
The following characters have a geometric interpretation:
Character Meaning
F Move forward by line length drawing a line
f Move forward by line length without drawing a line
+ Turn left by turning angle
- Turn right by turning angle
| Reverse direction (ie: turn by 180 degrees)
[ Push current drawing state onto stack
] Pop current drawing state from the stack
# Increment the line width by line width increment
! Decrement the line width by line width increment
@ Draw a dot with line width radius
{ Open a polygon
} Close a polygon and fill it with fill colour
> Multiply the line length by the line length scale factor
< Divide the line length by the line length scale factor
& Swap the meaning of + and -
( Decrement turning angle by turning angle increment
) Increment turning angle by turning angle increment
*/
let angle = Math.PI * 1.5;
let turningAngle = Math.PI * 0.5;
let lineLength = 5;
let lineWidthIncrement = 1;
let currentLineWidth = 1;
let currentPosition = [0, 0];
let stateStack = [];
let lineLengthScaleFactor = 1.5;
let swapPlusMinusState = false;
let turningAngleIncrement = 0.1;
let polygonVertices = [];
let polygonOpen = false;
let axiom = 'F+F+F+F';
let maxIterations = 4;
const maxCalculations = 50000;
let rules = {
'F': 'FF+F-F+F+FF'
};
// Generate the fractal string from axiom and rules
for (let i = 0; i < maxIterations; i++) {
if (axiom.length > maxCalculations) {//hard coded limit to prevent infinite loops
console.log('Reached max calculations, stopping at iteration', i);
break;
}
let newAxiom = '';
for (let char of axiom) {//loop through each character in the axiom and replace it with the corresponding rule
if (rules[char]) {
newAxiom += rules[char];
} else {
newAxiom += char;
}
}
axiom = newAxiom;
}
function setup() {
createCanvas(windowWidth, windowHeight);
background(255, 255, 255);
noFill();
stroke(0, 0, 0);
strokeWeight(currentLineWidth);
currentPosition = [width / 2, height - 100];
generateFractal();
}
function generateFractal() {
for (let i = 0; i < axiom.length; i++) {
let c = axiom.charAt(i);
switch (c) {
case "F":
moveForwardDraw();
break;
case "f":
moveForwardNoDraw();
break;
case "+":
turnLeft();
break;
case "-":
turnRight();
break;
case "|":
reverseDirection();
break;
case "[":
pushState(i);
break;
case "]":
popState(i);
break;
case "#":
incrementLineWidth();
break;
case "!":
decrementLineWidth();
break;
case "@":
drawDot();
break;
case "{":
openPolygon();
break;
case "}":
closePolygon();
break;
case ">":
multiplyLineLength();
break;
case "<":
divideLineLength();
break;
case "&":
swapPlusMinus();
break;
case ")":
incrementTurningAngle();
break;
case "(":
decrementTurningAngle();
break;
default:
// Ignore characters not in the specification (like X)
break;
}
}
}
// F: Move forward by line length drawing a line
function moveForwardDraw() {
let x = currentPosition[0] + cos(angle) * lineLength;
let y = currentPosition[1] + sin(angle) * lineLength;
if (polygonOpen) {
polygonVertices.push([x, y]);
} else {
line(currentPosition[0], currentPosition[1], x, y);
}
currentPosition = [x, y];
}
// f: Move forward by line length without drawing a line
function moveForwardNoDraw() {
let x = currentPosition[0] + cos(angle) * lineLength;
let y = currentPosition[1] + sin(angle) * lineLength;
currentPosition = [x, y];
}
// +: Turn left by turning angle
function turnLeft() {
if (swapPlusMinusState) {
angle -= turningAngle;
return
}
angle += turningAngle;
}
// -: Turn right by turning angle
function turnRight() {
if (swapPlusMinusState) {
angle += turningAngle;
return
}
angle -= turningAngle;
}
// |: Reverse direction (turn by 180 degrees)
function reverseDirection() {
angle += Math.PI;
}
// [: Push current drawing state onto stack
function pushState(i) {
stateStack.push([i, currentPosition[0], currentPosition[1], angle, currentLineWidth]);
}
// ]: Pop current drawing state from the stack
function popState(i) {
let stackIndex = stateStack.length - 1;
currentPosition = [stateStack[stackIndex][1], stateStack[stackIndex][2]];
angle = stateStack[stackIndex][3];
currentLineWidth = stateStack[stackIndex][4];
strokeWeight(currentLineWidth);
}
// #: Increment the line width by line width increment
function incrementLineWidth() {
currentLineWidth += lineWidthIncrement;
strokeWeight(currentLineWidth);
}
// !: Decrement the line width by line width increment
function decrementLineWidth() {
currentLineWidth -= lineWidthIncrement;
strokeWeight(currentLineWidth);
}
// @: Draw a dot with line width radius
function drawDot() {
let x = currentPosition[0];
let y = currentPosition[1];
strokeWeight(currentLineWidth);
point(x, y);
}
// {: Open a polygon
function openPolygon() {
polygonVertices = [[currentPosition[0], currentPosition[1]]];
polygonOpen = true;
}
// }: Close a polygon and fill it with fill colour
function closePolygon() {
if (polygonOpen && polygonVertices.length > 2) {
fill(100, 150, 255); // Default fill colour
beginShape();
for (let v of polygonVertices) {
vertex(v[0], v[1]);
}
endShape(CLOSE);
noFill(); // Reset to no fill
}
polygonVertices = [];
polygonOpen = false;
}
// >: Multiply the line length by the line length scale factor
function multiplyLineLength() {
lineLength *= lineLengthScaleFactor;
}
// <: Divide the line length by the line length scale factor
function divideLineLength() {
lineLength /= lineLengthScaleFactor;
}
// &: Swap the meaning of + and -
function swapPlusMinus() {
swapPlusMinusState = !swapPlusMinusState;
}
// ): Increment turning angle by turning angle increment
function incrementTurningAngle() {
turningAngle += turningAngleIncrement;
}
// (: Decrement turning angle by turning angle increment
function decrementTurningAngle() {
turningAngle -= turningAngleIncrement;
}
function draw() {
}
function mousePressed() {
}