439 lines
14 KiB
JavaScript
439 lines
14 KiB
JavaScript
/*
|
|
L-System Fractal Generator
|
|
|
|
https://paulbourke.net/fractals/lsys/
|
|
|
|
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'
|
|
};
|
|
|
|
//* L-System presets array **AI generated**
|
|
const presets = [
|
|
{ name: 'Crystal', axiom: 'F+F+F+F', rules: { 'F': 'FF+F++F+F' }, angle: 90 },
|
|
{ name: 'Koch Curve', axiom: 'F+F+F+F', rules: { 'F': 'F+F-F-FF+F+F-F' }, angle: 90 },
|
|
{ name: 'Von Koch Snowflake', axiom: 'F++F++F', rules: { 'F': 'F-F++F-F' }, angle: 60 },
|
|
{ name: 'Hilbert', axiom: 'X', rules: { 'X': '-YF+XFX+FY-', 'Y': '+XF-YFY-FX+' }, angle: 90 },
|
|
{ name: 'Dragon Curve', axiom: 'FX', rules: { 'X': 'X+YF+', 'Y': '-FX-Y' }, angle: 90 },
|
|
{ name: 'Levy Curve', axiom: 'F', rules: { 'F': '-F++F-' }, angle: 45 },
|
|
{ name: 'Sierpinski Arrowhead', axiom: 'YF', rules: { 'X': 'YF+XF+Y', 'Y': 'XF-YF-X' }, angle: 60 },
|
|
{ name: 'Peano Curve', axiom: 'X', rules: { 'X': 'XFYFX+F+YFXFY-F-XFYFX', 'Y': 'YFXFY-F-XFYFX+F+YFXFY' }, angle: 90 },
|
|
{ name: 'Plant', axiom: 'X', rules: { 'X': 'F+[[X]-X]-F[-FX]+X', 'F': 'FF' }, angle: 25 },
|
|
{ name: 'Triangle', axiom: 'F+F+F', rules: { 'F': 'F-F+F' }, angle: 120 },
|
|
{ name: 'Quadratic Gosper', axiom: '-YF', rules: { 'X': 'XFX-YF-YF+FX+FX-YF-YFFX+YF+FXFXYF-FX+YF+FXFX+YF-FXYF-YF-FX+FX+YFYF-', 'Y': '+FXFX-YF-YF+FX+FXYF+FX-YFYF-FX-YF+FXYFYF-FX-YFFX+FX+YF-YF-FX+FX+YFY' }, angle: 90 },
|
|
{ name: 'Square Sierpinski', axiom: 'F+XF+F+XF', rules: { 'X': 'XF-F+F-XF+F+XF-F+F-X' }, angle: 90 },
|
|
{ name: 'Quadratic Snowflake', axiom: 'F', rules: { 'F': 'F-F+F+F-F' }, angle: 90 },
|
|
{ name: 'Quadratic Koch Island', axiom: 'F+F+F+F', rules: { 'F': 'F+F-F-FFF+F+F-F' }, angle: 90 },
|
|
{ name: 'Board', axiom: 'F+F+F+F', rules: { 'F': 'FF+F+F+F+FF' }, angle: 90 },
|
|
{ name: 'Sierpinski Arrowhead 2', axiom: 'YF', rules: { 'X': 'YF+XF+Y', 'Y': 'XF-YF-X' }, angle: 60 },
|
|
{ name: 'Cross 1', axiom: 'F+F+F+F', rules: { 'F': 'F+FF++F+F' }, angle: 90 },
|
|
{ name: 'Cross 2', axiom: 'F+F+F+F', rules: { 'F': 'F+F-F+F+F' }, angle: 90 },
|
|
{ name: 'Pentaplexity', axiom: 'F++F++F++F++F', rules: { 'F': 'F++F++F|F-F++F' }, angle: 36 },
|
|
{ name: 'Tiles', axiom: 'F+F+F+F', rules: { 'F': 'FF+F-F+F+FF' }, angle: 90 },
|
|
{ name: 'Rings', axiom: 'F+F+F+F', rules: { 'F': 'FF+F+F+F+F+F-F' }, angle: 90 },
|
|
{ name: 'Hexagonal Gosper', axiom: 'XF', rules: { 'X': 'X+YF++YF-FX--FXFX-YF+', 'Y': '-FX+YFYF++YF+FX--FX-Y' }, angle: 60 },
|
|
{ name: 'Classic Sierpinski Curve', axiom: 'F--XF--F--XF', rules: { 'X': 'XF+F+XF--F--XF+F+X' }, angle: 45 },
|
|
{ name: 'Krishna Anklets', axiom: '-X--X', rules: { 'X': 'XFX--XFX' }, angle: 45 },
|
|
{ name: 'Mango Leaf', axiom: 'Y---Y', rules: { 'X': '{F-F}{F-F}--[--X]{F-F}{F-F}--{F-F}{F-F}--', 'Y': 'f-F+X+F-fY' }, angle: 60 },
|
|
{ name: 'Snake Kolam', axiom: 'F+XF+F+XF', rules: { 'X': 'X{F-F-F}+XF+F+X{F-F-F}+X' }, angle: 90 },
|
|
{ name: 'Leaf', axiom: 'a', rules: { 'F': '>F<', 'a': 'F[+x]Fb', 'b': 'F[-y]Fa', 'x': 'a', 'y': 'b' }, angle: 45 },
|
|
{ name: 'Bush 1', axiom: 'Y', rules: { 'X': 'X[-FFF][+FFF]FX', 'Y': 'YFX[+Y][-Y]' }, angle: 25.7 },
|
|
{ name: 'Bush 2', axiom: 'F', rules: { 'F': 'FF+[+F-F-F]-[-F+F+F]' }, angle: 22.5 },
|
|
{ name: 'Bush 3', axiom: 'F', rules: { 'F': 'F[+FF][-FF]F[-F][+F]F' }, angle: 35 },
|
|
{ name: 'Bush 4 (Saupe)', axiom: 'VZFFF', rules: { 'V': '[+++W][---W]YV', 'W': '+X[-W]Z', 'X': '-W[+X]Z', 'Y': 'YZ', 'Z': '[-FFF][+FFF]F' }, angle: 20 },
|
|
{ name: 'Bush 5', axiom: 'FX', rules: { 'X': '>[-FX]+FX' }, angle: 40 },
|
|
{ name: 'Sticks', axiom: 'X', rules: { 'F': 'FF', 'X': 'F[+X]F[-X]+X' }, angle: 20 },
|
|
{ name: 'Algae 1', axiom: 'aF', rules: { 'a': 'FFFFFv[+++h][---q]fb', 'b': 'FFFFFv[+++h][---q]fc', 'c': 'FFFFFv[+++fa]fd', 'd': 'FFFFFv[+++h][---q]fe', 'e': 'FFFFFv[+++h][---q]fg', 'g': 'FFFFFv[---fa]fa', 'h': 'ifFF', 'i': 'fFFF[--m]j', 'j': 'fFFF[--n]k', 'k': 'fFFF[--o]l', 'l': 'fFFF[--p]', 'm': 'fFn', 'n': 'fFo', 'o': 'fFp', 'p': 'fF', 'q': 'rfF', 'r': 'fFFF[++m]s', 's': 'fFFF[++n]t', 't': 'fFFF[++o]u', 'u': 'fFFF[++p]', 'v': 'Fv' }, angle: 12 },
|
|
{ name: 'Algae 2', axiom: 'aF', rules: { 'a': 'FFFFFy[++++n][----t]fb', 'b': '+FFFFFy[++++n][----t]fc', 'c': 'FFFFFy[++++n][----t]fd', 'd': '-FFFFFy[++++n][----t]fe', 'e': 'FFFFFy[++++n][----t]fg', 'g': 'FFFFFy[+++fa]fh', 'h': 'FFFFFy[++++n][----t]fi', 'i': '+FFFFFy[++++n][----t]fj', 'j': 'FFFFFy[++++n][----t]fk', 'k': '-FFFFFy[++++n][----t]fl', 'l': 'FFFFFy[++++n][----t]fm', 'm': 'FFFFFy[---fa]fa', 'n': 'ofFFF', 'o': 'fFFFp', 'p': 'fFFF[-s]q', 'q': 'fFFF[-s]r', 'r': 'fFFF[-s]', 's': 'fFfF', 't': 'ufFFF', 'u': 'fFFFv', 'v': 'fFFF[+s]w', 'w': 'fFFF[+s]x', 'x': 'fFFF[+s]', 'y': 'Fy' }, angle: 12 },
|
|
{ name: 'Weed', axiom: 'F', rules: { 'F': 'FF-[XY]+[XY]', 'X': '+FY', 'Y': '-FX' }, angle: 22.5 },
|
|
{ name: 'Kolam', axiom: 'D--D', rules: { 'A': 'F++FFFF--F--FFFF++F++FFFF--F', 'B': 'F--FFFF++F++FFFF--F--FFFF++F', 'C': 'BFA--BFA', 'D': 'CFC--CFC' }, angle: 45 }
|
|
];
|
|
|
|
let presetIndex = 16;
|
|
|
|
function setup() {
|
|
createCanvas(windowWidth, windowHeight);
|
|
background(255, 255, 255);
|
|
noFill();
|
|
stroke(0, 0, 0);
|
|
strokeWeight(currentLineWidth);
|
|
|
|
// Load the selected preset
|
|
if (presetIndex >= 0 && presetIndex < presets.length) {
|
|
const preset = presets[presetIndex];
|
|
axiom = preset.axiom;
|
|
rules = {};
|
|
for (let key in preset.rules) {
|
|
rules[key] = preset.rules[key];
|
|
}
|
|
turningAngle = preset.angle * Math.PI / 180;
|
|
console.log('Loaded preset:', preset.name);
|
|
} else {
|
|
console.log('Invalid preset index:', presetIndex, 'using default');
|
|
const preset = presets[0];
|
|
axiom = preset.axiom;
|
|
rules = {};
|
|
for (let key in preset.rules) {
|
|
rules[key] = preset.rules[key];
|
|
}
|
|
turningAngle = preset.angle * Math.PI / 180;
|
|
}
|
|
|
|
// Generate the fractal string
|
|
let tempAxiom = axiom;
|
|
for (let i = 0; i < maxIterations; i++) {
|
|
if (tempAxiom.length > maxCalculations) {
|
|
console.log('Reached max calculations, stopping at iteration', i);
|
|
break;
|
|
}
|
|
let newAxiom = '';
|
|
for (let char of tempAxiom) {
|
|
if (rules[char]) {
|
|
newAxiom += rules[char];
|
|
} else {
|
|
newAxiom += char;
|
|
}
|
|
}
|
|
tempAxiom = newAxiom;
|
|
}
|
|
|
|
axiom = tempAxiom;
|
|
|
|
// Calculate bounding box to fit fractal in frame
|
|
let bounds = calculateBounds(axiom);
|
|
let fractalWidth = bounds.maxX - bounds.minX;
|
|
let fractalHeight = bounds.maxY - bounds.minY;
|
|
|
|
// Calculate scale factor to fit in canvas with padding
|
|
let padding = 50;
|
|
let scaleX = (width - padding * 2) / fractalWidth;
|
|
let scaleY = (height - padding * 2) / fractalHeight;
|
|
let scaleFactor = Math.min(scaleX, scaleY);
|
|
|
|
// Calculate translation to center the fractal
|
|
let centerX = (bounds.minX + bounds.maxX) / 2;
|
|
let centerY = (bounds.minY + bounds.maxY) / 2;
|
|
let translateX = width / 2 - centerX * scaleFactor;
|
|
let translateY = height / 2 - centerY * scaleFactor;
|
|
|
|
// Apply transformation
|
|
translate(translateX, translateY);
|
|
scale(scaleFactor, scaleFactor);
|
|
|
|
// Reset position to origin
|
|
currentPosition = [0, 0];
|
|
|
|
generateFractal();
|
|
}
|
|
|
|
|
|
// * real ugly but working AI-generated function to calculate bounds
|
|
function calculateBounds(axiomString) {
|
|
let minX = 0, maxX = 0, minY = 0, maxY = 0;
|
|
let x = 0, y = 0;
|
|
let angle = Math.PI * 1.5;
|
|
let localTurningAngle = turningAngle;
|
|
let localLineLength = lineLength;
|
|
let localStateStack = [];
|
|
let localSwapPlusMinus = false;
|
|
let localLineLengthScaleFactor = lineLengthScaleFactor;
|
|
|
|
for (let i = 0; i < axiomString.length; i++) {
|
|
let c = axiomString.charAt(i);
|
|
|
|
switch (c) {
|
|
case "F":
|
|
x += Math.cos(angle) * localLineLength;
|
|
y += Math.sin(angle) * localLineLength;
|
|
minX = Math.min(minX, x);
|
|
maxX = Math.max(maxX, x);
|
|
minY = Math.min(minY, y);
|
|
maxY = Math.max(maxY, y);
|
|
break;
|
|
case "f":
|
|
x += Math.cos(angle) * localLineLength;
|
|
y += Math.sin(angle) * localLineLength;
|
|
minX = Math.min(minX, x);
|
|
maxX = Math.max(maxX, x);
|
|
minY = Math.min(minY, y);
|
|
maxY = Math.max(maxY, y);
|
|
break;
|
|
case "+":
|
|
if (localSwapPlusMinus) {
|
|
angle -= localTurningAngle;
|
|
} else {
|
|
angle += localTurningAngle;
|
|
}
|
|
break;
|
|
case "-":
|
|
if (localSwapPlusMinus) {
|
|
angle += localTurningAngle;
|
|
} else {
|
|
angle -= localTurningAngle;
|
|
}
|
|
break;
|
|
case "|":
|
|
angle += Math.PI;
|
|
break;
|
|
case "[":
|
|
localStateStack.push({
|
|
x: x,
|
|
y: y,
|
|
angle: angle,
|
|
lineLength: localLineLength,
|
|
swapPlusMinus: localSwapPlusMinus
|
|
});
|
|
break;
|
|
case "]":
|
|
if (localStateStack.length > 0) {
|
|
let state = localStateStack.pop();
|
|
x = state.x;
|
|
y = state.y;
|
|
angle = state.angle;
|
|
localLineLength = state.lineLength;
|
|
localSwapPlusMinus = state.swapPlusMinus;
|
|
}
|
|
break;
|
|
case ">":
|
|
localLineLength *= localLineLengthScaleFactor;
|
|
break;
|
|
case "<":
|
|
localLineLength /= localLineLengthScaleFactor;
|
|
break;
|
|
case "&":
|
|
localSwapPlusMinus = !localSwapPlusMinus;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return { minX, maxX, minY, maxY };
|
|
}
|
|
|
|
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() {
|
|
|
|
} |