L-system tree · minimax-m3
Using the pre-provided p5.min.js (global p5), grow a procedural L-system fractal tree that branches recursively and sways gently in the wind.
transcript · 14 messages
Using the pre-provided p5.min.js (global p5), grow a procedural L-system fractal tree that branches recursively and sways gently in the wind.
thinking
The user wants a procedural L-system fractal tree using p5.js that branches recursively and sways in the wind. Let me design this carefully.
Key requirements:
1. Single self-contained index.html
2. Use p5.min.js (loaded via script tag from current directory)
3. L-system based fractal tree
4. Recursive branching
5. Gentle wind sway animation
6. No external resources, no network calls
7. CSS in <style>, JS in <script>
Let me think about the L-system approach:
- An L-system has an axiom, rules, and iterations
- For a tree, classic rules like F → F[+F]F[-F]F work, but I'll use a turtle-graphics style approach where each segment becomes two sub-segments
Actually, the cleanest approach for an L-system tree is:
- Axiom: F
- Rule: F → FF+[+F-F-F]-[-F+F+F] (classic)
- Then interpret the resulting string as turtle commands
But for a natural tree, I think it's nicer to use a recursive approach where each branch has:
- Some length, drawn
- At the tip, split into 2-3 sub-branches with smaller length and random angle
- Recurse with depth limit
Let me combine: Use the L-system axiom/rules to generate the command string, then interpret it with a turtle that has state (position, angle, depth, length).
Let me design a simple L-system:
- Axiom: X
- Rules:
- X → F[+X][-X][^X][&X]FX
- F → FF
- Iterations: 3-4
Turtle commands:
- F: move forward
- +/-: rotate around z (left/right)
- ^/&: rotate around x/y (up/down pitch)
- [/]: push/pop state
For wind sway: apply a small global rotation/offset based on sin/cos of time, with depth-proportional amplitude (leaves sway more than trunk).
Let me build this with p5:
- Use setup() to build the L-system string
- Use draw() to render the tree with current wind state
- Use a depth-based color gradient (dark brown trunk → green tips)
- Add a ground line and maybe some background gradient
Let me sketch the code:
```javascript
function generateLSystem(axiom, rules, iterations) {
let current = axiom;
for (let i = 0; i < iterations; i++) {
let next = '';
for (let c of current) {
next += rules[c] || c;
}
current = next;
}
return current;
}
const axiom = 'X';
const rules = {
'X': 'F[+X][-X]F[&X][^X]FX',
'F': 'FF'
};
const lsysStr = generateLSystem(axiom, rules, 3);
```
For the turtle:
```javascript
function drawTree(commands) {
const stack = [];
let state = {
x: width/2,
y: height,
angle: -PI/2, // pointing up
length: height * 0.1,
depth: 0,
thickness: 12
};
for (let c of commands) {
if (c === 'F') {
// apply wind sway based on depth
const sway = sin(time + state.x * 0.01) * state.depth * 0.5;
const newX = state.x + cos(state.angle + sway * 0.05) * state.length;
const newY = state.y + sin(state.angle + sway * 0.05) * state.length;
stroke(...colorForDepth(state.depth));
strokeWeight(state.thickness);
line(state.x, state.y, newX, newY);
state.x = newX;
state.y = newY;
} else if (c === '+') {
state.angle += randomAngle();
} else if (c === '-') {
state.angle -= randomAngle();
} else if (c === '[') {
stack.push({...state});
state.length *= 0.7;
state.thickness *= 0.7;
state.depth++;
} else if (c === ']') {
state = stack.pop();
}
// etc.
}
}
```
Hmm, but iterating 3 times with that rule will explode in size. Let me think... 'F[+X][-X]F[&X][^X]FX' - 3 children at each level. With 3 iterations that's a lot.
Let me use a simpler rule set with controlled branching:
- X → F[+X]F[-X]+X
- F → FF
- 3 iterations
Or actually let me just go with a recursive tree function which is more straightforward and produces a more natural-looking tree. The user said "L-system fractal tree that branches recursively" - I can use the L-system to define structure and then use recursion to render it.
Actually let me do both - generate the L-system string, then use a recursive approach to render it but tied to the L-system structure.
Wait, the simplest and most beautiful approach: use a stochastic L-system where rules are picked randomly from a set, and use recursion. Let me do a recursive tree that is "L-system inspired":
```javascript
function branch(x, y, len, angle, depth, maxDepth, t) {
if (depth >= maxDepth || len < 2) return;
const sway = sin(t + depth * 0.5) * (depth * 1.5);
const tipX = x + cos(angle + sway * 0.02) * len;
const tipY = y + sin(angle + sway * 0.02) * len;
strokeWeight(map(depth, 0, maxDepth, 8, 0.5));
stroke(lerpColor(trunkColor, leafColor, depth / maxDepth));
line(x, y, tipX, tipY);
if (depth < maxDepth - 1) {
// Draw leaves at tip
noStroke();
fill(leafColor);
circle(tipX, tipY, 6 + random(4));
}
// L-system branching: 2-3 children with angle perturbations
const branchCount = random() < 0.3 ? 3 : 2;
for (let i = 0; i < branchCount; i++) {
const newAngle = angle + (random() * 0.8 - 0.4) + (i - branchCount/2) * 0.5;
const newLen = len * 0.7;
branch(tipX, tipY, newLen, newAngle, depth + 1, maxDepth, t);
}
}
```
But the user specifically said "L-system" so I should implement the L-system properly. Let me do this:
1. Define L-system rules
2. Generate the command string by iterating
3. Interpret with a turtle
```javascript
const lSystem = {
axiom: 'X',
rules: {
'X': 'F[+X]F[-X]+X', // simplified
'F': 'FF'
},
iterations: 4
};
```
Actually, with 'F[+X]F[-X]+X' as X rule and 4 iterations, we get a lot of X's. Let me count:
- Iteration 0: X (1)
- Iteration 1: F[+X]F[-X]+X (3 X)
- Iteration 2: FF[+F[+X]F[-X]+X]FF[-F[+X]F[-X]+X]+F[+X]F[-X]+X (9 X)
- Iteration 3: 27 X
- Iteration 4: 81 X
With each X being 7 F's, that's 567 F's at iteration 4. That's a lot of segments. With F → FF doubling each iteration, F count grows 2^n. After 4 iterations, we have a lot.
Maybe I should use a single F rule to keep things tractable, or fewer iterations. Let me use 3 iterations.
Actually, let me think about this differently. The L-system should produce a tree, and the rendering should respect depth. Let me use a depth-tracking turtle where depth is incremented by '[' and decremented by ']'.
Let me also add randomness/stochasticity by using multiple rules per symbol and picking one.
Here's my plan:
1. L-system with stochastic rules
2. Generate string with 4 iterations
3. Render with turtle that:
- Tracks position, angle, depth, length, thickness
- F: draws a line, applying wind offset based on depth
- +: rotate by random angle in range
- -: rotate by negative random angle
- [: push state, decrease thickness/length
- ]: pop state
For wind sway, I'll use a global sin wave based on time, modulated by depth. The deeper branches (more terminal) sway more.
For colors, gradient from dark brown trunk to lighter brown branches to green leaves at tips.
Let me also add:
- A ground/grass area
- Falling leaves (occasional)
- Maybe a sun/moon in background
- Stars in background (since trees can look great at dusk)
Let me code this up now.
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>L-System Fractal Tree</title>
<style>
html, body { margin: 0; padding: 0; overflow: hidden; background: #0a0e1a; }
canvas { display: block; }
.info { position: fixed; top: 10px; left: 10px; color: rgba(255,255,255,0.6); font-family: sans-serif; font-size: 12px; pointer-events: none; }
</style>
</head>
<body>
<div class="info">L-System Fractal Tree</div>
<script src="p5.min.js"></script>
<script>
let lsysStr = '';
let time = 0;
let stars = [];
const W = 800, H = 800;
// L-system definition
const axiom = 'X';
const rules = {
'X': 'F[+X][-X]FX',
'F': 'FF'
};
const iterations = 4;
function generateLSystem(axiom, rules, iterations) {
let current = axiom;
for (let i = 0; i < iterations; i++) {
let next = '';
for (let c of current) {
next += rules[c] || c;
}
current = next;
}
return current;
}
function setup() {
createCanvas(W, H);
colorMode(HSB, 360, 100, 100, 1);
lsysStr = generateLSystem(axiom, rules, iterations);
// Create stars
for (let i = 0; i < 200; i++) {
stars.push({
x: random(W),
y: random(H * 0.6),
b: random(0.3, 1),
twinkle: random(TWO_PI)
});
}
}
function draw() {
// Night sky gradient
background(220, 80, 8);
noStroke();
for (let y = 0; y < H; y++) {
const t = y / H;
fill(220, 80 * (1-t), 5 + 25 * t);
rect(0, y, W, 1);
}
// Stars
for (let s of stars) {
const b = s.b * (0.7 + 0.3 * sin(time * 2 + s.twinkle));
fill(60, 10, 100, b);
noStroke();
circle(s.x, s.y, 1.5);
}
// Moon
fill(50, 20, 95);
noStroke();
circle(120, 120, 50);
fill(220, 80, 5);
circle(140, 110, 50);
// Ground
noStroke();
for (let y = H * 0.85; y < H; y++) {
const t = (y - H * 0.85) / (H * 0.15);
fill(150, 60, 8 + 8 * t);
rect(0, y, W, 1);
}
// Draw tree
interpretLSystem(lsysStr);
time += 0.02;
}
function interpretLSystem(str) {
const stack = [];
let state = {
x: W / 2,
y: H * 0.88,
angle: -PI / 2,
length: H * 0.12,
thickness: 10,
depth: 0
};
// Pre-compute depth by simulating the string first? Or do two passes?
// Actually, we can compute depth on the fly with stack
const angleStep = PI / 8;
const lengthFactor = 0.95; // length doesn't shrink much per F
for (let c of str) {
if (c === 'F') {
// Wind sway: deeper branches sway more
const swayFactor = state.depth * state.depth * 0.0008;
const sway = sin(time + state.x * 0.005 + state.depth * 0.3) * swayFactor;
const effectiveAngle = state.angle + sway;
const nx = state.x + cos(effectiveAngle) * state.length;
const ny = state.y + sin(effectiveAngle) * state.length;
// Color: trunk is brown, tips are green
const d = state.depth;
const maxD = 8;
const tCol = constrain(d / maxD, 0, 1);
const hue = lerp(25, 110, tCol);
const sat = lerp(70, 60, tCol);
const bri = lerp(35, 70, tCol);
stroke(hue, sat, bri);
strokeWeight(max(state.thickness, 0.5));
line(state.x, state.y, nx, ny);
state.x = nx;
state.y = ny;
} else if (c === '+') {
state.angle += angleStep * (0.8 + random(0.4));
} else if (c === '-') {
state.angle -= angleStep * (0.8 + random(0.4));
} else if (c === '[') {
stack.push({...state});
state.length *= lengthFactor;
state.thickness *= 0.72;
state.depth++;
} else if (c === ']') {
state = stack.pop();
// Draw a small leaf at the tip
if (random() < 0.4) {
noStroke();
fill(110, 70, 70, 0.8);
for (let i = 0; i < 3; i++) {
const r = 3 + random(3);
const ox = random(-2, 2);
const oy = random(-2, 2);
circle(state.x + ox, state.y + oy, r);
}
}
}
}
}
</script>
</body>
</html>
```
Wait, the angle isn't right. The L-system '+' and '-' should rotate the turtle. Initial angle should be -PI/2 (pointing up). The turtle draws relative to the screen.
Let me think about the F → FF doubling - this means each branch becomes 2 segments in the next iteration. That makes branches thicker. Good.
X → F[+X][-X]FX - at each X, draw F, then branch left and right, then draw F, then continue. After 4 iterations, the 'X' rule fires many times. Let me check the depth at the end:
- After 1 iter: F[+X][-X]FX, max depth 2 (inside [+X])
- After 2 iter: each X becomes F[+X][-X]FX. The first +X's X becomes a tree, then [-X]'s X becomes another, then the trailing X. So depth goes 2 → 3.
- After 3 iter: depth 4
- After 4 iter: depth 5
So with 4 iterations, max depth is 5. That's manageable.
Hmm but with F → FF, after 4 iterations we have 2^4 = 16 F's per original F. The "F" segments inside brackets are also doubled. The total F count grows.
Let me just make the initial length smaller so the tree fits, and use 4 iterations.
Actually, let me reconsider. The X rule 'F[+X][-X]FX' - the second F in 'F[..]FX' is after the brackets, so it continues the main line. After 4 iterations, the main spine gets really long because of F → FF doubling. Let me compute approximate length.
Initial length L. After 4 iterations of F → FF, the main spine has 2^4 = 16 F segments before any branching. Total main spine length = 16 * L. Way too long.
So I need to either:
1. Use a smaller initial length
2. Use fewer iterations
3. Use a different F rule
Let me use iterations=3. Then main spine = 8 * L. Still long. With L = H * 0.05 = 40px, main spine = 320px. OK, fits.
Actually let me think about this more carefully. The 'F' rule 'F → FF' makes the trunk grow. With iterations=3, the trunk doubles 3 times. That's an 8x trunk. With initial length = H * 0.03 = 24, trunk = 192px. Fits in 800px height.
The branches then get length 0.95^n * base length. With max depth 5, branches are 0.95^5 ≈ 0.77 of base. So branches are about the same as base length. That might be too long.
Let me use lengthFactor = 0.85 to shrink branches faster.
Hmm, actually I realize the issue. The L-system 'F' rule applies to all F's, including those inside brackets. So every branch doubles. This means the leaves are very far from the main line, which is bad.
Wait, no. The brackets are matched. The F's inside brackets become FF after one iteration. The F's outside also become FF. So the doubling applies uniformly. But the visual effect is just longer branches at all levels.
Actually maybe a better rule for a tree is:
- X → F[+X][-X][^X]X (3 branches at each X)
- F → F (don't double)
This way the tree structure grows but individual branches don't get longer.
Let me try this. Or use F → F but with one F that adds length per iteration.
You know what, let me just experiment. Let me use:
- X → F[+X][-X][^X][&X]F[+X]X (variable branching)
- F → F (no doubling)
- iterations = 4
Or simpler:
- X → F-[[X]+X]+F[+FX]-X
- F → FF
- iterations = 3
That's the classic "bush" L-system. Let me try that.
Actually for a tree, the most natural is:
- F → F[+F]F[-F]F (the original Lindenmayer "weed")
- But this doesn't have the recursive X
For a true fractal tree, the classic is:
- X → F[+X]F[-X]+X
- F → FF
- iterations = 4 or 5
Let me check this. With iterations=4:
- iter 0: X
- iter 1: F[+X]F[-X]+X
- iter 2: FF[+F[+X]F[-X]+X]FF[-F[+X]F[-X]+X]+F[+X]F[-X]+X
- iter 3: 4 F's in main spine from the FF's at start, then [+X] becomes 4-char F[+X]F[-X]+X, etc.
Main spine of iter 3: 4 F's + branch + 4 F's + branch + 4 F's + branch + (4-char F[+X]F[-X]+X)
Hmm this is getting complex. Let me just try and see.
Actually for the simplest approach, let me use a stochastic L-system with a recursive interpreter:
```javascript
function branch(x, y, len, ang, dep, t) {
if (dep > maxDep || len < 1) {
// Leaf
fill(110, 60, 70, 0.7);
noStroke();
circle(x, y, 4);
return;
}
const sway = sin(t * 0.5 + dep * 0.3) * dep * 1.2;
const tipX = x + cos(ang + sway * 0.01) * len;
const tipY = y + sin(ang + sway * 0.01) * len;
// Draw branch
...
// L-system style branching
const r = random();
if (r < 0.3) {
// 2 branches
branch(tipX, tipY, len * 0.7, ang + PI/6 + random(0.1), dep + 1, t);
branch(tipX, tipY, len * 0.7, ang - PI/6 - random(0.1), dep + 1, t);
} else {
// 3 branches
branch(tipX, tipY, len * 0.7, ang, dep + 1, t);
branch(tipX, tipY, len * 0.65, ang + PI/5, dep + 1, t);
branch(tipX, tipY, len * 0.65, ang - PI/5, dep + 1, t);
}
}
```
But this isn't really an L-system, it's just a recursive tree.
OK let me commit to the L-system approach. I'll use a well-known one:
Axiom: F
Rules:
- F → F[+F]F[-F][F]
Hmm, no X. Let me think.
The cleanest L-system for a tree uses F only, with bracketing for branches:
- Axiom: F
- F → F[+F]F[-F]F (or similar)
- iterations: 3-4
Let me trace:
- iter 0: F (1 segment)
- iter 1: F[+F]F[-F]F (5 F's, depth 1)
- iter 2: Each F → F[+F]F[-F]F. So F[+F]F[-F]F → F[+F]F[-F]F[+F[+F]F[-F]F]F[+F]F[-F]F[-F[+F]F[-F]F]F[+F]F[-F]F. That's a lot of F's.
After 3 iterations of F → F[+F]F[-F]F, we have 5^3 = 125 F's. With each F = length L, total tree is 125L of branches. With L = 8px, tree is 1000px of branch length. Fits in 800x800 canvas if structured well.
OK let me use this approach. Actually, the rules need to allow for variation. Let me use a stochastic set:
```javascript
const rules = {
'F': [
{ prob: 0.33, str: 'F[+F]F[-F]F' },
{ prob: 0.33, str: 'F[-F]F[+F]F' },
{ prob: 0.34, str: 'F[+F][-F]F' }
]
};
```
Hmm, this is getting complex. Let me just use a deterministic one and add randomness in the interpretation (angle perturbation, length variation).
Final plan:
1. L-system: F → F[+F][-F]F[+F] (no X needed, branches from F)
2. Iterations: 3
3. Interpretation: turtle with state stack, depth tracking
4. Angle: ±PI/7 per +/- (with random ±10%)
5. Length factor: 0.9 per F (so each iteration's F = 2 F's, but each F is 0.9 of previous)
6. After interpretation, add leaves at terminal points
Wait, but with F → F[+F][-F]F[+F], the recursive structure isn't fractal in the usual sense. Let me check depth.
iter 1: F[+F][-F]F[+F], max depth 1
iter 2: F[+F][-F]F[+F][+F[+F][-F]F[+F]][-F[+F][-F]F[+F]]F[+F][-F]F[+F][+F[+F][-F]F[+F]]
- The bracketed F's become their own trees at depth 1
- Max depth 2
After 3 iterations, max depth 3. Each F is 0.9 of parent, so leaf F's are 0.9^3 = 0.73 of base. That's a lot of leaves at moderate distance. Total F count: 4^3 = 64 F's. Good.
OK let me try this. Actually, I want a more "tree-like" structure. Let me use:
X → F[+X]F[-X]+X (the classic)
Or even better for a tree: F → F[+F]F[-F]F with iteration 4 (gives 5^4 = 625 F's, too many).
Let me just go with:
- F → F[+F][-F]F (3 F's per branch, 2 sub-branches at depth)
- iterations = 4
- total F's = 3^4 = 81
- max depth 4
With length factor 0.85 per depth: leaf F's are 0.85^4 = 0.52 of base. Nice.
With base length = 12, total tree height ≈ 12 + 0.85*12 + 0.85²*12 + ... ≈ 12*1/(1-0.85) = 80px main spine. Then branches add more height. Should fit in 800.
Let me just code it and tune.
Actually I realized for a 2D L-system, after interpretation we don't have leaves at "tips" - we just have terminal F's. The brackets push/pop state, and a terminal F means the F was drawn but no further F's followed it in the current branch. So tips are at end of bracketed sub-trees and at the very end of the string.
To find leaves/tips, I can check if the F is followed by a ']' or end of string. But that's hard during single-pass. Easier: render normally, then in a second pass identify tips.
Or: use a "color the tip different" approach where I track how many F's remain in the current branch.
Actually simplest: just make all branches look like branches (gradient color from trunk to tip), and add a few decorative leaves at random positions at the end.
OK let me just write the code.
```javascript
function setup() {
createCanvas(800, 800);
colorMode(HSB, 360, 100, 100, 1);
lsysStr = generateLSystem('F', { 'F': 'F[+F][-F]F' }, 4);
// F → F[+F][-F]F, 4 iterations
// Length: 1 → 3 → 9 → 27 → 81 F's
// Compute total length to scale
// ...
}
function generateLSystem(axiom, rules, iter) {
let s = axiom;
for (let i = 0; i < iter; i++) {
let n = '';
for (let c of s) {
n += rules[c] || c;
}
s = n;
}
return s;
}
function draw() {
// Background sky
...
// Ground
...
// Tree
interpretLSystem(lsysStr, time);
time += 0.016;
}
function interpretLSystem(str, t) {
const stack = [];
let st = {
x: 400, y: 750,
a: -PI/2, // pointing up
l: 80, // segment length
w: 12, // thickness
d: 0 // depth
};
// Count remaining F's to detect tips? Or count ']' and end of str.
// Actually, a tip is a segment that is the last F in a contiguous F-sequence (no F after it until we pop).
// For simplicity, let's track: is this F the last before ']' or end?
// We can pre-process: for each F, count F's until ']' or end. If 0, it's a tip.
// Easier: render normally, collect tip positions, then draw leaves on top
const tips = [];
for (let i = 0; i < str.length; i++) {
const c = str[i];
if (c === 'F') {
// Check if this is a tip: the next character (in current branch) is ']' or end
// We can look ahead in str, but need to respect stack depth
// For simplicity, just check: does this F lead to any further F before ']'?
let isTip = true;
let depth = 0;
for (let j = i + 1; j < str.length; j++) {
const cc = str[j];
if (cc === '[') depth++;
else if (cc === ']') { if (depth === 0) break; else depth--; }
else if (cc === 'F' && depth === 0) { isTip = false; break; }
}
if (isTip) tips.push({ x: st.x, y: st.y });
// Apply wind sway
const swayFactor = 0.002 * st.d * st.d;
const sway = sin(t * 1.5 + st.d * 0.4 + st.x * 0.003) * swayFactor;
const effA = st.a + sway;
const nx = st.x + cos(effA) * st.l;
const ny = st.y + sin(effA) * st.l;
// Color
const tCol = constrain(st.d / 4, 0, 1);
const hue = lerp(25, 120, tCol);
const sat = lerp(60, 70, tCol);
const bri = lerp(30, 65, tCol);
stroke(hue, sat, bri);
strokeWeight(st.w);
line(st.x, st.y, nx, ny);
st.x = nx;
st.y = ny;
} else if (c === '+') {
st.a += PI / 7 + random(-0.1, 0.1);
} else if (c === '-') {
st.a -= PI / 7 + random(-0.1, 0.1);
} else if (c === '[') {
stack.push({...st});
st.l *= 0.85;
st.w *= 0.7;
st.d++;
} else if (c === ']') {
st = stack.pop();
}
}
// Draw leaves at tips
noStroke();
for (let tip of tips) {
fill(110, 65, 70, 0.7);
const leafSize = 4 + random(3);
circle(tip.x, tip.y, leafSize);
}
}
```
Wait, there's a bug in my tip detection. The tip is where the branch ends. The current state.x, state.y is the START of this F segment, not the end. The end of the tip F should be the leaf position. Let me fix:
After drawing F, update st.x, st.y to the end. Then if this F is a tip, store the end position (st.x, st.y after update).
Actually wait, but I'm doing the tip check BEFORE drawing. So the check is "is this the last F in this branch". If yes, the END of this F is a tip. Let me restructure:
```javascript
if (c === 'F') {
// Check if this is a tip
let isTip = checkIfTip(str, i); // helper
// Draw segment
const nx = st.x + cos(st.a + sway) * st.l;
const ny = st.y + sin(st.a + sway) * st.l;
// ... draw line ...
st.x = nx;
st.y = ny;
if (isTip) tips.push({ x: st.x, y: st.y });
}
```
This is fine. Let me also think about the wind sway magnitude. With swayFactor = 0.002 * d^2, at d=4 we get 0.032 rad ≈ 1.8 degrees. That's subtle. Let me increase it.
Actually let me reconsider the L-system. With F → F[+F][-F]F, after 4 iterations, the F count is 3^4 = 81. The max depth is 4. So we have 81 segments. Each segment has length 80 * 0.85^depth. Total visible structure: lots of small branches. Good.
But wait, the way depth changes: at each '[', depth++. So depth = number of open brackets. Max depth 4 means 4 nested branches. That's not much depth.
Let me make the F rule produce deeper trees:
- F → FF[+F][-F] (each F becomes 2 F's plus 2 branches)
- After 4 iterations, F count = 4^4 = 256. Max depth 4.
That's a lot of F's. Performance might suffer but should be OK.
Or:
- F → F[+F]F[-F][+F]F (5 F's per branch, 3 sub-branches)
- F count = 5^4 = 625. Too many.
Let me stick with F → F[+F][-F]F, 4 iterations. Or use 3 iterations for a more sparse tree: 3^3 = 27 F's, max depth 3.
For a 800x800 canvas with a tall tree, max depth 3 might be sparse enough. Let me use 4 iterations for fullness.
Actually let me use 4 iterations with F → F[+F][-F]F. That's 81 F's, max depth 4. Length 80 * 0.85^4 = 41 at leaves. Trunk total length 80 + 0.85*80 + 0.85²*80 + 0.85³*80 ≈ 80*(1-0.85^4)/(1-0.85) ≈ 80*3.6 = 290. Hmm, that's 290 for the main spine. But it's split across 27 F's at depth 4 (or however the doubling works).
Wait, the main spine in F → F[+F][-F]F is FF (2 segments) at each iteration. So after 4 iterations, the main spine has 2^4 = 16 F's. That's a long trunk.
Length per F at the trunk: st.l = base_l. So 16 * 80 = 1280. Too long!
I need to reduce base_l or the F rule. Let me use base_l = 30. Then main spine = 480. Better.
Or, use a different F rule: F → F[+F]F[-F]F. Wait this gives 5 F's per branch (depth +1) but the F's in the bracketed sub-branches don't add to the main spine. Hmm but they still produce segments. Total F count = 5^4 = 625. Too many.
OK let me think clearly. With F → F[+F][-F]F:
- The main F at start has no F before a '['. So it continues the parent's direction.
- The [+F] is a sub-branch.
- The [-F] is another sub-branch.
- The F at the end continues the parent's direction.
So the "main spine" of the recursion continues with the first F and the last F. After one iteration, the F becomes 2 F's in the spine. After n iterations, 2^n F's in the spine.
To control spine length, I need to either:
- Reduce base_l
- Use a different rule where the spine doesn't double
Or: scale the spine length with the iteration count.
Let me set base_l = 8. Then 16 * 8 = 128 trunk. Plus branches go sideways. Total tree ~250px tall. Good for 800x800.
Wait but then branches are 8 * 0.85 = 6.8 at depth 1, 5.8 at depth 2, 4.9 at depth 3, 4.2 at depth 4. That's quite small. Leaves would be tiny.
Let me use base_l = 15. Spine = 240. Branches: 13, 11, 9.4, 8. Better.
Actually, you know what, let me just use a simpler L-system that doesn't have the F-doubling problem:
F → F[+F]F[-F]F (this is the classic "weed" L-system)
- The main F at the start continues the direction.
- The brackets add sub-branches.
- The F at the end (just before the next F that becomes the rule) continues the direction.
Wait, this rule has 5 F's and 2 brackets. Let me count spine:
- iter 0: F (1 F)
- iter 1: F[+F]F[-F]F (5 F's). The spine is the first F and the F after the first ']'. Hmm.
Let me think of it as a turtle. Starting at F. After 1 iter, we have: F [+F] F [-F] F. Drawing:
- Draw F (straight)
- Push state, rotate +, draw F, pop state
- Draw F (straight, same as before because state didn't change)
- Push state, rotate -, draw F, pop state
- Draw F (straight)
So the main spine is F (drawn first), then F (drawn third), then F (drawn fifth). So 3 F's in a row at depth 0.
After 2 iter: F → F[+F]F[-F]F. Each F in the iter 1 string becomes this 5-F pattern.
The main spine (no brackets crossed) in iter 2: starts with F, then the third F in 'F[+F]F[-F]F' (the one after the first ']'), then the fifth F.
Wait, in iter 2 the string is: F[+F]F[-F]F[+F[+F]F[-F]F]F[+F]F[-F]F[-F[+F]F[-F]F]F[+F]F[-F]F
Let me just count: the depth-0 F's are those that are not inside any brackets. From the structure, after each F[+F]F[-F]F expansion, the depth-0 F's are at the start and end (and between brackets in the original pattern).
Hmm, getting complex. Let me just count F's in spine:
- Each F → 2 spine F's (first and last in F[+F]F[-F]F).
- 2^n spine F's after n iterations.
So with 4 iterations: 16 spine F's. Same as before. Hmm.
OK the issue is fundamental: any L-system rule where F generates F's produces an exponentially growing main spine.
To avoid this, I can:
1. Use a small base length
2. Use a different rule structure
Let me try rule: F → F[+F]F[-F] with 3 iterations:
- iter 0: F (1 F)
- iter 1: F[+F]F[-F] (3 F's in spine + 2 in branches)
- iter 2: F[+F]F[-F][+F[+F]F[-F]]F[+F]F[-F][-F[+F]F[-F]] (5 F's in spine)
- iter 3: 9 F's in spine
So with 3 iterations, 9 spine F's. With base_l = 25, spine = 225. OK.
But the structure: F → F[+F]F[-F] means the F is the trunk and we branch off twice. The last F continues the trunk. After 3 iterations, we have a trunk that branches at multiple levels. With angle PI/5 per +/- the branches spread out.
Total F's in 3 iterations:
- iter 1: 4 (2 spine + 2 branch)
- iter 2: each F → 4 F's, so 16 F's (10 spine + 6 branches)
- iter 3: 64 F's total
With length 0.9 per depth (length doesn't change much), the total visible tree:
- Spine: 9 F's, each 0.9^(spine_position) of base. Roughly 9 * 25 * 0.7 = 160 (averaging).
- Plus branches at various levels.
OK let me just code it up with 3 iterations and tune visually.
Actually, I think the simpler approach is to just go with iterations=3 and adjust the rule to produce a nice tree:
```
F → F[+F][-F]F
```
This gives:
- iter 0: F
- iter 1: F[+F][-F]F (3 F's, 2 branches, 1 sub-branch at end)
- iter 2: F[+F][-F]F[+F[+F][-F]F][-F[+F][-F]F]F[+F][-F]F (5 F's in spine, more branches)
- iter 3: 7 F's in spine
Spine length: 7 F's. With base = 35, total = 245. Good.
Total F's at iter 3: 3^3 = 27.
Hmm, that's not many. Let me increase iterations to 4: 81 F's, 11 spine F's. Spine = 11 * 35 = 385. Hmm getting long.
OK let me use iterations=3 and base_l=40. Spine = 7 * 40 = 280. Total F's = 27. Branches spread out.
Or use a rule that doesn't double the spine: have a different "spine continuation" rule. Actually, the easiest is to make the F rule produce a constant number of spine F's:
F → F[+F]F[-F] (3 F's, 2 branches, but no F at the end)
Wait this is asymmetric. Let me think. F → F[+F][-F]F means the F rule has 3 F's in the spine. After 1 iter, the original F becomes 3 spine F's. After 2 iter, 3 of those F's each become 3 spine F's, so 9 spine F's. So 3^n spine F's.
That's worse! F → F[+F]F[-F]F has 3 F's, 5 total. F → F[+F][-F]F has 3 F's, 3 total.
Actually F → F[+F][-F]F has 3 F's. The first and third are at depth 0 (spine). The middle is also at depth 0 (between brackets).
Wait no, the middle F is also at depth 0 because it's not inside any brackets. Let me re-examine: 'F[+F][-F]F' - the F's at positions 0, 6 are spine F's. The [+F] and [-F] are bracketed. So 2 spine F's in the rule.
OK so F → F[+F][-F]F gives 2 spine F's per F. After n iter: 2^n spine F's. Same exponential.
Hmm there's no way around it if F produces multiple F's. Unless I use a different symbol for "spine continuation" vs "drawing segment".
Lindenmayer's actual approach for trees: use F for "move forward and draw" and use multiple rules. The 'F' rule might not double the spine if the F's in the rule are all in brackets.
E.g., X → F[+X]F[-X]+X (X is the recursive symbol, F is just drawing)
- iter 0: X (no F's)
- iter 1: F[+X]F[-X]+X (2 F's)
- iter 2: FF[+F[+X]F[-X]+X]FF[-F[+X]F[-X]+X]+F[+X]F[-X]+X (6 F's, with X's at depth 0 and 1)
- iter 3: Each F → FF (6 → 12 F's), each X → F[+X]F[-X]+X (more F's and X's)
- iter 4: 24 F's from F→FF doubling, plus more F's from X expansion
This is more interesting. The X is the "growth point" that branches, and F is just a drawing segment that doubles.
So:
- Axiom: X
- Rules: X → F[+X]F[-X]+X, F → FF
- Iterations: 4
After 4 iterations, max depth of X nesting: 4 (in the brackets). So depth 4 for branches.
F count: starts at 0. After 1 iter, 2 F's (from the F's in X rule). After 2 iter, each F becomes 2 F's (4 F's) and each X becomes 2 F's (so 2 X's in main X → 4 F's). Total after 2: 4 + 4 = 8 F's. Hmm let me just count F's in iter 2 string:
F[+X]F[-X]+X → F[+F[+X]F[-X]+X]F[-F[+X]F[-X]+X]+F[+X]F[-X]+X
F's: 1, 1, 1, 1, 1, 1, 1, 1 = 8 F's. Yes 8.
After 3 iter: 16 F's.
After 4 iter: 32 F's.
Plus the X's add more F's. After 4 iter:
- X count: 1 → 3 → 9 → 27 → 81 X's
- Each X becomes 2 F's. So additional F's from X expansion at iter 4: 2*81 = 162.
- Plus the F's already present: doubled from iter 3.
Total F's at iter 4: 32 (from F doubling) + 162 (from new X expansion) = 194 F's.
OK 194 segments, max depth 4. Manageable.
Let me use this. With base_l = 20, the main spine (which has 32 F's in a line, depth 0): 32 * 20 = 640. Hmm too long.
Wait, the F doubling only applies to existing F's. So the original F's get longer, but new F's from X expansion have their own length.
Actually no, all F's are treated the same. So every F gets the F → FF rule applied. So a single F becomes 2 F's, which become 4, 8, 16, 32 F's in 5 iterations.
So the original F at the base of the X → F[+X]F[-X]+X expansion becomes 2^4 = 16 F's after 4 iterations of F → FF.
Total length of trunk: 16 * base_l.
With base_l = 20, trunk = 320. With base_l = 30, trunk = 480.
Let me use base_l = 25. Trunk = 400. Tree width should be ~200 on each side. Total tree fits in 400 wide x 600 tall. Good for 800x800 canvas.
But wait, the F → FF rule applies to F's inside brackets too. So the bracketed F's also double. The branch F's in [+X]F[-X] (which come from X expansion) - the F's there are also doubled.
Hmm. Let me think again about what the "spine" is. The L-system after 4 iterations will have F's at various depths. F's at depth 0 are the main spine. F's at depth 1 are in [+X] etc.
Actually, the original F → FF rule applies regardless of position. So all F's double. But the result is the same visually - the F segments just become 2 segments each, which is just longer branches everywhere.
For visual purposes, we want the branch lengths to decrease at each branching level. But the F rule doesn't help with that - it just makes all F's 2x.
So we need to also reduce the length per depth in our interpretation. We already do that: st.l *= 0.85 when entering a '['. So branches are 0.85 of parent.
OK let me just code it up.
```javascript
const axiom = 'X';
const rules = {
'X': 'F[+X]F[-X]+X',
'F': 'FF'
};
const iterations = 4;
```
With this, max depth = 4 (from X nesting), but the F → FF also adds depth visually (more F's per line).
Let me compute the actual depth of each F. In iter 4, F's are at various depths based on their nesting in the string. The X's are at depths 0, 1, 2, 3, 4 (max). The F's are at the same depths as the X's they're generated from.
Hmm wait, the F's in the rule X → F[+X]F[-X]+X are at the same depth as the X they're generated from. The +X and -X are one level deeper. So F's at depth 0 generate F's at depth 0 (the first F and the second F), F's at depth 1 (the +X and -X) generate F's at depth 1.
Actually the F's are not "generated from" anything, they're just F's. The depth of an F is determined by its position in the string after the X's have been expanded.
Let me think of it this way. In the X rule, the F's are at the same depth as the X. The +X is one depth deeper. So:
- iter 0: X (depth 0)
- iter 1: F[+X]F[-X]+X. F's at depth 0. X's at depth 0 and depth 1.
- iter 2: F[+F[+X]F[-X]+X]F[-F[+X]F[-X]+X]+F[+X]F[-X]+X. The first two F's (at depth 0) become F's inside. The +X at depth 1 becomes [+F[+X]F[-X]+X] which contains F's at depth 1. The -X similarly. The trailing +X is at depth 0, its expansion has F's at depth 0.
So in iter 2, we have F's at depth 0 (from main X), depth 1 (from +X and -X of main X), and depth 2 (from +X and -X of inner X's). Wait no, the +X inside the [+X] is at depth 2 if we count from outside. Hmm.
Actually depth in my turtle is the number of '[' currently open. The X's at iter 0 are all at depth 0. After X → F[+X]F[-X]+X, the first F is at depth 0, the +X is at depth 1 (after the [), etc.
But wait, in my L-system I'm not tracking depth in the string. I'm just generating the string. Then I parse it with a turtle, and the turtle tracks depth via the stack.
OK so the depth in my turtle interpretation is purely based on the '[' in the string. The depth is the number of nested brackets. For an F at depth d, that means there are d open brackets at the point of the F.
For the F → F[+F][-F]F rule (let me use this simpler one), let me check max depth.
iter 0: F. Depth 0.
iter 1: F[+F][-F]F. The bracketed F's are at depth 1.
iter 2: F[+F][-F]F → F[+F[+F][-F]F][-F[+F][-F]F]F[+F[+F][-F]F][-F[+F][-F]F]. Wait, when we expand the rule, the brackets are preserved. So:
F → F[+F][-F]F becomes F[+F[+F][-F]F][-F[+F][-F]F]F[+F[+F][-F]F][-F[+F][-F]F]? No, that's not right either.
Let me be careful. Each F in the string is replaced by 'F[+F][-F]F'. The brackets of the rule are added. So:
- F → F[+F][-F]F
- The F's bracketed in the rule are at depth 1 (one bracket open).
- After iter 1: F[+F][-F]F
- The F's at depth 1 (in the brackets) get expanded in iter 2: [+F[+F][-F]F] (the +F at depth 1 becomes F[+F][-F]F at depth 1, with sub-F's at depth 2).
- After iter 2: F[+F[+F][-F]F][-F[+F][-F]F]F[+F[+F][-F]F][-F[+F][-F]F]
- Wait, this isn't right. The expansion is character-by-character. So F → F[+F][-F]F means each F becomes that 9-char string.
- Iter 1: F[+F][-F]F. 4 F's.
- Iter 2: each of the 4 F's becomes F[+F][-F]F. So:
F → F[+F][-F]F
[ → [ (kept)
+ → + (kept)
F → F[+F][-F]F
] → ] (kept)
[ → [
- → -
F → F[+F][-F]F
] → ]
F → F[+F][-F]F
- So iter 2: F[+F[+F][-F]F][-F[+F][-F]F]F[+F[+F][-F]F][-F[+F][-F]F]
- Wait, that doesn't look right. Let me re-expand.
Actually let me just expand carefully. Iter 1 = "F[+F][-F]F". Length 9.
For iter 2, replace each F in "F[+F][-F]F" with "F[+F][-F]F":
- Char 0: F → F[+F][-F]F
- Char 1: [ → [
- Char 2: + → +
- Char 3: F → F[+F][-F]F
- Char 4: ] → ]
- Char 5: [ → [
- Char 6: - → -
- Char 7: F → F[+F][-F]F
- Char 8: ] → ]
- Char 9: F → F[+F][-F]F
So iter 2 = "F[+F][-F]F" + "[" + "+" + "F[+F][-F]F" + "]" + "[" + "-" + "F[+F][-F]F" + "]" + "F[+F][-F]F"
= "F[+F][-F]F[+F[+F][-F]F][-F[+F][-F]F]F[+F][-F]F"
Let me re-examine. The iter 1 string is "F[+F][-F]F". I replace each F with "F[+F][-F]F" character by character.
F → F[+F][-F]F
[ → [
+ → +
F → F[+F][-F]F
] → ]
[ → [
- → -
F → F[+F][-F]F
] → ]
F → F[+F][-F]F
Concatenated: "F[+F][-F]F[+F[+F][-F]F][-F[+F][-F]F]F[+F][-F]F"
Wait that has the F's not in the right places. Let me try once more.
iter 1: "F[+F][-F]F" (positions 0-8, 9 chars)
For iter 2, expand each char:
0: F → "F[+F][-F]F" (9 chars)
1: [ → "[" (1 char)
2: + → "+" (1 char)
3: F → "F[+F][-F]F" (9 chars)
4: ] → "]" (1 char)
5: [ → "[" (1 char)
6: - → "-" (1 char)
7: F → "F[+F][-F]F" (9 chars)
8: ] → "]" (1 char)
9: F → "F[+F][-F]F" (9 chars) [wait, position 8 is the last ']' before the final F?]
Hmm let me recount. "F[+F][-F]F" - let me index:
- 0: F
- 1: [
- 2: +
- 3: F
- 4: ]
- 5: [
- 6: -
- 7: F
- 8: ]
- 9: F
So 10 chars total. Length 10. The 4 F's are at positions 0, 3, 7, 9.
For iter 2:
- 0: F → F[+F][-F]F
- 1: [ → [
- 2: + → +
- 3: F → F[+F][-F]F
- 4: ] → ]
- 5: [ → [
- 6: - → -
- 7: F → F[+F][-F]F
- 8: ] → ]
- 9: F → F[+F][-F]F
Concatenated:
F[+F][-F]F [ + F[+F][-F]F ] [ - F[+F][-F]F ] F[+F][-F]F
= "F[+F][-F]F[+F[+F][-F]F][-F[+F][-F]F]F[+F][-F]F"
Yes that's right. Now let's count depth in iter 2:
- F (depth 0)
- [ (depth 1)
- +
- F (depth 1)
- [ (depth 2)
- +
- F (depth 2)
- ] (back to depth 1)
- [ (depth 2)
- -
- F (depth 2)
- ] (back to depth 1)
- F (depth 1)
- ] (back to depth 0)
- [ (depth 1)
- -
- F (depth 1)
- [ (depth 2)
- +
- F (depth 2)
- ] (back to depth 1)
- [ (depth 2)
- -
- F (depth 2)
- ] (back to depth 1)
- F (depth 1)
- ] (back to depth 0)
- F (depth 0)
- [ (depth 1)
- +
- F (depth 1)
- [ (depth 2)
- +
- F (depth 2)
- ] (back to depth 1)
- [ (depth 2)
- -
- F (depth 2)
- ] (back to depth 1)
- F (depth 1)
- ] (back to depth 0)
- F (depth 0)
So max depth is 2.
After iter 3: max depth 3.
After iter 4: max depth 4.
After iter 5: max depth 5.
F count grows as 4^n. After iter 4: 256 F's. After iter 5: 1024 F's. Hmm.
OK let me use iter 4. 256 F's should be fine performance-wise. Max depth 4.
But the F count of 4^n is the issue. The tree is going to be dense. Let me think about what rule to use.
Maybe I should use a sparser rule: F → F[+F]F[-F] (3 F's, 2 branches, 1 trailing F that doubles)
Wait, that has 3 F's. 3^4 = 81 F's. Max depth grows by 1 per iter (since one level of brackets is added at each level of recursion). After iter 4: 81 F's, max depth 4.
But this rule: the first F is the spine start. The [+F] is a branch. The F after is the spine continuation. The [-F] is another branch. So 1 spine F and 2 branch F's per F.
After 1 iter, 1 F → 1 spine F + 2 branch F's (1 in [+F], 1 in [-F]). Hmm wait, 1 spine F (the first), 1 spine F (the middle, between brackets), 2 branch F's (in brackets). Total 4 F's. So 3^n. After 4 iter: 81 F's. Spine F's per main F: 2 (first and middle). 2^4 = 16 spine F's.
Hmm, still exponential in spine. But the tree density (F's per area) is lower with this rule because branches are short.
Let me go with F → F[+F]F[-F] (3 F's, 2 brackets). With iter 4: 81 F's, max depth 4, 16 spine F's.
If base_l = 18, spine = 16 * 18 = 288. Good.
OK let me commit to this. Or maybe even simpler: F → F[+F][-F]F (3 F's, 2 brackets with single F, 1 trailing F). Same as my original.
Wait, F → F[+F][-F]F has 3 F's, 2 brackets each with 1 F. F → F[+F]F[-F]F has 5 F's, 2 brackets each with 1 F.
F → F[+F]F[-F] (3 F's, 2 brackets each with 1 F, no trailing F outside brackets) - the trailing F is between the two brackets. Same as F → F[+F][-F]F.
OK they're the same rule. 3 F's per F, 81 F's after 4 iter.
Let me just code this up.
Actually I realize the rule F → F[+F]F[-F] is identical to F → F[+F][-F]F because the F between [+F] and [-F] is at depth 0 (not inside brackets). Yes same rule.
OK final L-system:
- Axiom: F
- Rules: F → F[+F]F[-F]
- Iterations: 4
- 81 F's, max depth 4
Or with iterations=5: 243 F's, max depth 5. More detail but performance.
Let me go with 4 iterations.
Now for the interpretation:
- Initial position: (W/2, H - 50) at the bottom
- Initial angle: -PI/2 (pointing up)
- Initial length: depends on tree size
- Initial thickness: 12
- Per F: draw line, apply wind sway
- Per +: rotate by PI/6 + small random
- Per -: rotate by -PI/6 + small random
- Per [: push state, decrease length and thickness
- Per ]: pop state, optionally draw leaf
For wind sway, let me compute a global wind vector that affects all branches. Deeper branches sway more.
Wind: w(t) = baseSway * sin(t * 0.5)
Apply to each F as: effective_angle = turtle.angle + w(t) * (1 + depth * 0.3)
Or apply a per-F sway that accumulates.
Let me think about it more carefully. The "sway" should be the entire tree bending in the wind. So I can apply a horizontal offset based on depth, not on the F itself.
For each F:
- Original endpoint: (x + cos(a) * l, y + sin(a) * l)
- Wind offset: dx = windAt(x, depth, t) where windAt returns a horizontal offset
- The endpoint moves by dx, but we should also consider that the next F starts from the offset position
This is tricky because the state is propagated. Let me use a simpler model:
- Add a horizontal offset to the entire F segment based on depth
- The next F starts from the offset endpoint
- This makes deeper branches visibly swayed
```javascript
const windX = sin(t * 0.7) * 8 + sin(t * 1.3) * 4; // global wind
const dx = windX * depth * 0.4;
const nx = x + cos(angle) * length + dx;
const ny = y + sin(angle) * length;
```
But then the next F uses the offset position, so the sway accumulates. Hmm, but if a branch is short, the accumulated sway is small. If it's long, the sway is large. Actually that's not quite right either.
Let me think physically. The branch is a rigid line. The whole tree sways. The tip of a branch at depth d sways by an amount proportional to d (or d^2 for cantilever). All points on the branch are displaced by the same horizontal amount (approximately).
So the displacement is per-branch, not per-F. But the L-system doesn't have explicit branches - it has a path.
Approximation: the entire F segment is displaced by dx (depth-dependent). The next F starts from the displaced position. So accumulation happens naturally.
Actually, the cleanest: just apply a horizontal offset to the F's drawing position. The next F's start position is the offset end. This naturally accumulates.
```javascript
const dx = sin(t * 0.5 + depth * 0.3) * depth * 0.8;
const nx = x + cos(angle) * length + dx;
const ny = y + sin(angle) * length;
// Draw line from (x, y) to (nx, ny)
x = nx;
y = ny;
```
This way:
- Trunk (depth 0) doesn't sway
- Branches (depth 1) sway a bit
- Leaves (depth 4) sway a lot
But the sway oscillates with time. We want all branches to sway in the same direction at the same time (global wind). So all F's at the same time have similar dx.
Let me set: dx = sin(t * 0.5) * depth * 0.8
All F's at time t have the same wind direction, but different magnitudes based on depth.
OK let me also add: a slight rotation of the trunk too. The whole tree rotates by a small angle around the base.
```javascript
const windRotation = sin(t * 0.5) * 0.05; // global rotation in radians
const dx = sin(t * 0.5) * depth * 0.6;
const baseAngle = -PI/2 + windRotation;
```
Then F's are drawn relative to (baseAngle + state.angle - initial_angle), with the dx offset.
Actually let me just do this cleanly. The state has angle. The drawing uses:
```javascript
const windTilt = sin(t * 0.5) * 0.04;
const effAngle = state.angle + windTilt * state.depth;
const swayOffset = sin(t * 0.7 + state.x * 0.001) * state.depth * 0.5;
const nx = state.x + cos(effAngle) * state.length + swayOffset;
const ny = state.y + sin(effAngle) * state.length;
```
This combines rotation and offset. Both are depth-dependent.
Hmm but I want the whole tree to sway in unison, not each branch with its own phase. Let me use:
```javascript
const windTime = t * 0.5;
const windTilt = sin(windTime) * 0.05;
const windOffset = sin(windTime) * state.depth * 0.6;
```
All F's at time t have the same windTilt and the same windOffset (modulated by depth).
OK let me also think about leaves. After the tree is drawn, I want to draw leaves at the tips. Tips are F's that are the last in their branch (followed by ']' or end of string without crossing ']').
To detect tips, I can preprocess the string. For each F at position i, check if there's any F before the matching ']' (or end of string). If not, it's a tip.
Alternatively, I can do a first pass that just counts F's in each branch, and a second pass that draws.
Or, simpler: just draw a leaf at the end of every F that is followed by ']' (immediately, in the current branch context). But that would draw leaves at every F that's the last in its parent branch, not just the tips.
Hmm. The cleanest is: tip = the position where the turtle state is popped or the string ends, AT a specific F.
Let me think. In the string, a ']' pops the state. The F just before the ']' is the last F in that branch. So that F is a tip.
Wait, but the tip's endpoint is the leaf position, not the start. The leaf is at the end of the F segment.
So: when we draw an F, if the next non-bracket character is ']' (meaning this F is the last in its branch), then mark the end position as a tip.
Actually, "next non-bracket character" - we want the next character in the same bracket level. If the next char is ']', this F is at the end of the branch. If 'F' or another '[' starts, the F is not at the end.
Implementation:
```javascript
function isTip(str, pos) {
// Returns true if the F at pos is the last F before a matching ']' or end of string
for (let i = pos + 1; i < str.length; i++) {
const c = str[i];
if (c === '[') return false; // entering a new branch, not a tip
if (c === 'F') return false; // another F in this branch
if (c === ']') return true; // this F was the last in the branch
}
return true; // end of string
}
```
Wait, this isn't quite right. If we have 'F[+F]F' and pos points to the first F, the next char is '[', not 'F' or ']'. So isTip returns false (because '[' was seen). But actually, the first F is followed by a sub-branch, so it's not a tip. Correct.
If we have 'F[+F]' and pos points to the F inside brackets, the next char is ']'. So isTip returns true. Correct - this F is the tip of its branch.
If we have 'FF' and pos points to the first F, the next char is 'F'. So isTip returns false. Correct - not a tip, more F's follow.
If we have 'F' at the end, isTip returns true (end of string).
But what about 'F[+F]+F' and pos points to the F inside the brackets? Next chars: ']'. So isTip returns true. But there's another F after the ']' in the same branch (the +F). The bracketed F is at depth 1, the +F is at depth 0. So the bracketed F IS a tip (end of its branch). The +F is also not a tip in this case (it's followed by end of string or more). Wait, the +F is at the end, so it is a tip too.
OK the logic is: the F is a tip if, going forward in the string without exiting any brackets, the next F is not found (i.e., the next ']' or end of string is reached first).
In 'F[+F]+F' at pos of '+F' (depth 1): the next char is ']'. Going forward, we exit the bracket. At the same depth 0, we have '+F'. But we exited the bracket, so the +F at depth 0 is in a different branch. The original F at depth 1 is the last in its branch. So isTip = true.
My implementation: when we see '[', we return false. But that means the F at 'F[' would be considered "not a tip" because of the '['. But that's wrong - the F at 'F[' is followed by a sub-branch, so it's not a tip. Hmm wait, that's actually correct. The F at 'F[' is followed by '[' which starts a new branch. The F is not the last in its branch because more F's come after. Wait, is the 'F' at 'F[' a tip?
'F[+F]' - the F is at depth 0, followed by a sub-branch. The F at depth 0 is not the last in its branch (the sub-branch returns and there's nothing more at depth 0). Hmm, actually in 'F[+F]', the F at depth 0 is the only F at depth 0. After the ']', the string ends. So this F is the last (and only) F at depth 0.
Wait, is it a tip? It's the last in its branch. The "branch" here is depth 0. So yes, this F is a tip in the depth-0 sense.
Hmm, my logic said: if next char is '[', return false. But the F is actually a tip at depth 0.
OK my isTip logic is buggy. Let me fix:
```javascript
function isTip(str, pos) {
// Check if F at pos is the last F in its branch (at the same bracket depth)
for (let i = pos + 1; i < str.length; i++) {
const c = str[i];
if (c === 'F') return false; // another F in this branch
if (c === ']') return true; // this F was the last in the branch
// '[' or '+' or '-' or other don't affect tip status
}
return true; // end of string
}
```
Now 'F[' + 1 char is '[', not 'F' or ']'. So we continue. We don't return false. We continue until we find 'F' or ']'. If we find ']' before any 'F', it's a tip. Otherwise no.
But in 'F[+F]', the chars after the first F are: [, +, F, ]. We see '[', '+', then 'F' at position 3. Return false. So the first F is NOT a tip? But it should be (it's the only F at depth 0).
Hmm, the F inside the brackets is at depth 1, not depth 0. So when we say "another F in this branch", do we mean same bracket depth?
Yes. The F at depth 0 and the F at depth 1 are in different branches. The F at depth 0 is the only one in its branch, so it's a tip.
But my logic returns false because it sees an F (at depth 1).
Let me fix by tracking bracket depth:
```javascript
function isTip(str, pos) {
let depth = 0;
for (let i = pos + 1; i < str.length; i++) {
const c = str[i];
if (c === '[') depth++;
else if (c === ']') {
if (depth === 0) return true; // closing our branch
depth--;
} else if (c === 'F' && depth === 0) {
return false; // another F in our branch
}
}
return true; // end of string
}
```
Now for 'F[+F]': starting at the F at pos 0. depth = 0.
- i=1: '['. depth++ → 1.
- i=2: '+'. Nothing.
- i=3: 'F'. depth=1, not 0. Nothing.
- i=4: ']'. depth=1, not 0. depth-- → 0.
- End. Return true.
For 'F[+F]F': starting at F at pos 0.
- i=1: '['. depth=1.
- i=2: '+'.
- i=3: 'F'. depth=1.
- i=4: ']'. depth=1, depth-- → 0.
- i=5: 'F'. depth=0. Return false.
For 'F[+F]F' starting at F at pos 3 (the +F at depth 1):
- i=4: ']'. depth=0. Return true.
For 'FF' starting at F at pos 0:
- i=1: 'F'. depth=0. Return false.
For 'F' at pos 0 (end of string):
- Loop ends. Return true.
OK the logic is correct now. Let me also think about performance. isTip is O(n) for each F, so total is O(n^2). With n = 81, that's 6561 operations. Trivial.
OK let me code the whole thing up.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>L-System Fractal Tree</title>
<style>
html, body { margin: 0; padding: 0; overflow: hidden; background: #000; }
canvas { display: block; }
#info {
position: fixed; top: 12px; left: 12px;
color: rgba(255,255,255,0.7);
font-family: 'Georgia', serif; font-size: 13px;
pointer-events: none; z-index: 10;
text-shadow: 0 1px 2px rgba(0,0,0,0.6);
}
#info b { color: rgba(255,255,255,0.95); }
</style>
</head>
<body>
<div id="info">L-System Fractal Tree — <b>recursive branching, gentle wind</b></div>
<script src="p5.min.js"></script>
<script>
let lsysStr = '';
let time = 0;
let stars = [];
let leafParticles = [];
const W = 800, H = 800;
// L-system definition
const axiom = 'F';
const rules = {
'F': 'F[+F]F[-F]'
};
const iterations = 4;
const baseAngle = Math.PI / 7; // branch angle
function generateLSystem(axiom, rules, iters) {
let s = axiom;
for (let i = 0; i < iters; i++) {
let n = '';
for (const c of s) n += rules[c] || c;
s = n;
}
return s;
}
function isTip(str, pos) {
let depth = 0;
for (let i = pos + 1; i < str.length; i++) {
const c = str[i];
if (c === '[') depth++;
else if (c === ']') {
if (depth === 0) return true;
depth--;
} else if (c === 'F' && depth === 0) return false;
}
return true;
}
function setup() {
createCanvas(W, H);
colorMode(HSB, 360, 100, 100, 1);
pixelDensity(1); // for performance
lsysStr = generateLSystem(axiom, rules, iterations);
console.log('L-system string length:', lsysStr.length);
// Stars
for (let i = 0; i < 150; i++) {
stars.push({
x: random(W),
y: random(H * 0.65),
b: random(0.3, 1),
tw: random(Math.PI * 2),
sp: random(0.5, 2)
});
}
}
function draw() {
// Sky gradient
drawSky();
// Stars
drawStars();
// Moon
drawMoon();
// Ground
drawGround();
// Falling leaves
updateAndDrawLeaves();
// Tree
interpretTree(lsysStr, time);
time += 0.016;
}
function drawSky() {
noStroke();
for (let y = 0; y < H; y++) {
const t = y / H;
const hue = lerp(230, 260, t);
const sat = lerp(70, 50, t);
const bri = lerp(8, 25, t);
fill(hue, sat, bri);
rect(0, y, W, 1);
}
}
function drawStars() {
noStroke();
for (const s of stars) {
const b = s.b * (0.6 + 0.4 * Math.sin(time * s.sp + s.tw));
fill(60, 10, 100, b);
circle(s.x, s.y, 1.4);
}
}
function drawMoon() {
noStroke();
// Moon glow
for (let r = 80; r > 30; r -= 4) {
fill(50, 20, 95, 0.05);
circle(640, 140, r);
}
// Moon body
fill(50, 25, 96);
circle(640, 140, 38);
// Moon craters
fill(50, 30, 80, 0.6);
circle(630, 130, 6);
circle(650, 150, 4);
circle(645, 135, 3);
}
function drawGround() {
noStroke();
// Hill silhouette
fill(150, 50, 8);
beginShape();
vertex(0, H);
for (let x = 0; x <= W; x += 20) {
const y = H * 0.88 + Math.sin(x * 0.01) * 8 + Math.sin(x * 0.03) * 4;
vertex(x, y);
}
vertex(W, H);
endShape();
// Grass tufts
fill(120, 50, 25);
for (let i = 0; i < 60; i++) {
const x = random(W);
const y = H * 0.88 + Math.sin(x * 0.01) * 8 + Math.sin(x * 0.03) * 4;
const h = random(4, 10);
triangle(x, y, x - 2, y - h, x + 2, y - h);
}
}
function updateAndDrawLeaves() {
// Add new leaves occasionally
if (random() < 0.05 && leafParticles.length < 40) {
leafParticles.push({
x: W/2 + random(-100, 100),
y: random(H * 0.3, H * 0.7),
vx: random(-0.3, 0.3),
vy: random(0.2, 0.5),
rot: random(Math.PI * 2),
vrot: random(-0.05, 0.05),
sz: random(3, 5),
col: random([110, 120, 100, 90])
});
}
noStroke();
for (let i = leafParticles.length - 1; i >= 0; i--) {
const p = leafParticles[i];
p.x += p.vx + Math.sin(time * 2 + p.rot) * 0.3;
p.y += p.vy;
p.rot += p.vrot;
if (p.y > H) {
leafParticles.splice(i, 1);
continue;
}
push();
translate(p.x, p.y);
rotate(p.rot);
fill(p.col, 65, 65, 0.85);
ellipse(0, 0, p.sz, p.sz * 0.6);
pop();
}
}
function interpretTree(str, t) {
const stack = [];
let state = {
x: W / 2,
y: H * 0.92,
a: -Math.PI / 2,
l: 22,
w: 12,
d: 0
};
const windT = t * 0.6;
const windTilt = Math.sin(windT) * 0.05;
const windOffset = Math.sin(windT) * 1.0;
const tipPositions = [];
for (let i = 0; i < str.length; i++) {
const c = str[i];
if (c === 'F') {
// Wind effect
const tilt = windTilt * state.d;
const off = windOffset * state.d * 0.6;
const effA = state.a + tilt;
const nx = state.x + Math.cos(effA) * state.l + off;
const ny = state.y + Math.sin(effA) * state.l;
// Color: brown trunk to lighter brown branches
const tCol = constrain(state.d / 4, 0, 1);
const hue = lerp(25, 38, tCol);
const sat = lerp(60, 50, tCol);
const bri = lerp(25, 60, tCol);
stroke(hue, sat, bri, 0.95);
strokeWeight(Math.max(state.w, 0.8));
line(state.x, state.y, nx, ny);
// Check if this is a tip
if (isTip(str, i)) {
tipPositions.push({ x: nx, y: ny, d: state.d });
}
state.x = nx;
state.y = ny;
} else if (c === '+') {
state.a += baseAngle * (0.85 + random(0.3));
} else if (c === '-') {
state.a -= baseAngle * (0.85 + random(0.3));
} else if (c === '[') {
stack.push({...state});
state.l *= 0.82;
state.w *= 0.68;
state.d++;
} else if (c === ']') {
state = stack.pop();
}
}
// Draw leaves at tips
noStroke();
for (const tip of tipPositions) {
const tCol = constrain(tip.d / 4, 0, 1);
// Leaves: green clusters
const lHue = lerp(110, 80, tCol) + random(-10, 10);
const lSat = lerp(70, 60, tCol);
const lBri = lerp(60, 75, tCol);
const sz = lerp(3, 7, tCol);
// Sway the leaves too
const leafSway = Math.sin(time * 1.2 + tip.x * 0.01 + tip.y * 0.01) * 2 * (tip.d + 1) * 0.3;
fill(lHue, lSat, lBri, 0.75);
circle(tip.x + leafSway, tip.y, sz);
fill(lHue + 10, lSat, lBri + 5, 0.5);
circle(tip.x + leafSway + random(-1, 1), tip.y + random(-1, 1), sz * 0.7);
}
}
function windowResized() {
resizeCanvas(W, H);
}
</script>
</body>
</html>
```
Wait, I have a bug. The isTip function takes the string and position. But I'm calling it inside the loop after processing 'F', but I need to check based on the position in the string. The position is the index of the F in the L-system string. So:
```javascript
if (c === 'F') {
if (isTip(str, i)) {
tipPositions.push({ x: state.x, y: state.y, d: state.d });
}
// Then draw the segment
// ...
}
```
Wait, I need to push the tip AFTER the segment is drawn, with the new position. Or push the new position. Let me think.
The tip is the END of the last F in the branch. So after drawing the F, the state is at the new position. That's the tip.
```javascript
if (c === 'F') {
// Draw segment
const nx = state.x + cos(effA) * state.l + off;
const ny = state.y + sin(effA) * state.l;
line(state.x, state.y, nx, ny);
if (isTip(str, i)) {
tipPositions.push({ x: nx, y: ny, d: state.d });
}
state.x = nx;
state.y = ny;
}
```
OK that's better. Let me also think about the depth tracking. The depth increases by 1 for each '['. So max depth = max number of nested brackets.
For F → F[+F]F[-F] with 4 iterations, max depth = 4.
Now let me think about the visualization. The tree has:
- Trunk: depth 0, longest segments
- Sub-branches: depth 1, medium
- Sub-sub-branches: depth 2, smaller
- Sub-sub-sub: depth 3, small
- Tips: depth 4 (or 3 if 4 iters give depth 4 at most)
Wait, max depth in my L-system is 4. The leaves are at depth 4. So leaves are at the smallest branches.
For color: depth 0 (trunk) is dark brown. Depth 4 (tips) is light brown / greenish.
Let me set:
- depth 0: dark brown (hue 25, sat 60, bri 25)
- depth 4: green (hue 110, sat 65, bri 70)
For leaves at tips, draw small green circles.
Hmm, I have color at F segment AND leaves. Let me make the F segments all brown, gradient from dark to light, and leaves green at tips. That's a clear distinction.
```javascript
// Branch color
const tCol = constrain(state.d / 4, 0, 1);
const hue = lerp(25, 35, tCol);
const sat = lerp(55, 50, tCol);
const bri = lerp(25, 55, tCol);
```
For leaves:
```javascript
// Leaf color at tip
const lHue = lerp(120, 90, tCol);
const lSat = 70;
const lBri = lerp(60, 75, tCol);
```
OK let me also think about the wind more carefully. The wind should:
- Affect deeper branches more
- Have a slow oscillation
- Look natural, not jittery
Let me use:
- windT = t * 0.5 (slow)
- windTilt = sin(windT) * 0.04 (rotation, applied with depth multiplier)
- windOffset = sin(windT * 1.1 + 0.5) * depth * 0.5 (horizontal offset, depth-proportional)
Actually let me also have a small high-frequency component for "gust" feel:
- windOffset = sin(windT) * depth * 0.5 + sin(t * 3) * depth * 0.1
Hmm, simpler is better. Let me just use a few sines:
```javascript
const windT = t * 0.5;
const windRot = Math.sin(windT) * 0.04 + Math.sin(windT * 2.3) * 0.01;
const windOff = Math.sin(windT * 1.1) * 0.4 + Math.sin(windT * 0.7 + 1) * 0.2;
```
Per F:
```javascript
const tilt = windRot * state.d;
const off = windOff * state.d * state.l;
const effA = state.a + tilt;
const nx = state.x + cos(effA) * state.l + off;
const ny = state.y + sin(effA) * state.l;
```
Where state.l is the segment length. So offset scales with segment length and depth. Deeper + longer = more offset.
But this means the offset can be quite large for a long deep branch. Let me cap it or scale differently.
Hmm let me think. If a deep branch has length 5px and depth 4, offset = 0.4 * 4 * 5 = 8px. That's significant compared to the 5px length. Might look weird.
Let me use a fixed offset not depending on l:
```javascript
const off = windOff * state.d * 2; // 2px per depth level
```
At depth 4, offset = 8px. Still a lot.
Let me reduce:
```javascript
const off = windOff * Math.sqrt(state.d) * 1.5;
```
At depth 4: sqrt(4)*1.5 = 3px. Subtle.
Or use d^1.5 / some constant. Let me just use:
```javascript
const tilt = windRot * state.d; // tilt proportional to depth
const off = windOff * Math.min(state.d, 3) * 0.8; // offset capped
```
Tilt is angle addition. At depth 4: 0.05 * 4 = 0.2 rad = 11.5 degrees. That's noticeable.
Let me reduce windRot:
```javascript
const windRot = Math.sin(windT) * 0.02 + Math.sin(windT * 2.3) * 0.005;
```
At depth 4: 0.025 * 4 = 0.1 rad = 5.7 degrees. Subtle.
And offset:
```javascript
const off = Math.sin(windT * 1.1) * state.d * 0.5;
```
At depth 4: 0.5 * 4 = 2px. Subtle.
Combined, deeper branches visibly sway while trunk is mostly stable. Good.
Let me also have the wind effect the leaf positions:
```javascript
const leafSway = Math.sin(time * 1.5 + tip.x * 0.005) * tip.d * 0.5;
circle(tip.x + leafSway, tip.y + Math.cos(time + tip.x * 0.005) * 0.3, sz);
```
Hmm, OK let me put it all together and test.
One more consideration: the L-system string has 3^4 = 81 F's. But wait, the F rule is F → F[+F]F[-F], which has 3 F's. So 3^4 = 81. Let me double check by counting in the rule:
F → F[+F]F[-F]: 3 F's (positions 0, 3, 7 if 0-indexed, or just count F's: 1 before [+, 1 after + and before ], 1 after [- and before ]).
Yes, 3 F's. So 3^iter F's. After 4 iter: 81 F's.
Actually wait, I need to double-check the F rule. F → F[+F]F[-F]. Let me count: F, [, +, F, ], F, [, -, F, ]. So 3 F's: at positions 0, 3, 7 (0-indexed). Yes 3 F's.
3^4 = 81. Good.
OK let me also make sure the tip detection works. With 81 F's, each with O(81) isTip check, that's 81^2 = 6561 operations. Fast.
But wait, in my string "F[+F]F[-F]" the F at pos 7 (the second F at depth 0, the third F in the rule) is followed by ']'. So isTip returns true at depth 0 for this F. But there's no more depth-0 F after, so this F is the last in the depth-0 branch.
Actually the depth-0 F's in the string are at the very top level (not inside any brackets). In "F[+F]F[-F]", there are 2 F's at depth 0: the first F and the F between the two brackets (the third F in the rule). The first F is at position 0, the third F is at position 5.
For the first F (pos 0): isTip checks pos 1+ chars. Pos 1: '[' (depth++), pos 2: '+', pos 3: 'F' (depth=1, not 0, skip), pos 4: ']' (depth=1, not 0, depth--), pos 5: 'F' (depth=0, return false). So first F is NOT a tip.
For the third F (pos 5): isTip checks pos 6+. Pos 6: '[' (depth++), pos 7: '-', pos 8: 'F' (depth=1, not 0, skip), pos 9: ']' (depth=1, depth--), pos 10: end of string, return true. So third F IS a tip.
For the F inside brackets (pos 3): isTip checks pos 4+. Pos 4: ']' (depth=0, return true). So this F IS a tip.
So in "F[+F]F[-F]", 2 of the 3 F's are tips (the ones inside brackets). That makes sense - each branch in brackets is a tip.
Good. The logic works.
Now let me also think about iteration count. Let me bump to 5 for a denser tree: 3^5 = 243 F's. Max depth 5. 243 F's is fine performance-wise.
With 5 iterations, the tree is denser and more impressive. Let me use 5.
But max depth 5 might be too deep. Let me check: 5 levels of branching. The base angle is PI/7 ≈ 25.7 degrees. After 5 levels, branches could be at angle 5*25.7 = 128 degrees from vertical, which is past horizontal. Some branches would go down.
Let me reduce base angle: PI/8 ≈ 22.5 degrees. After 5 levels: 5*22.5 = 112 degrees. Still past horizontal.
Hmm. Maybe use base angle PI/10 = 18 degrees. 5*18 = 90 degrees. Branches at most horizontal. Better.
Or use a different angle per depth: less angle as depth increases. But that complicates the L-system.
Or just use 4 iterations with PI/7 angle. Let me go with that.
Actually let me use 5 iterations with a smaller angle. The tree will be denser.
You know what, let me just go with 4 iterations and PI/7 angle. It's a good balance.
Let me finalize the code. Let me also add some nice details:
- A subtle vignette
- A few fireflies
- Better star twinkle
- Some background atmosphere
Actually let me keep it focused on the tree. Add:
- Beautiful gradient sky
- Stars
- Moon
- Ground
- Tree
- Falling leaves
- Wind
Let me write the final code:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>L-System Fractal Tree</title>
<style>
html, body { margin: 0; padding: 0; overflow: hidden; background: #050810; }
canvas { display: block; }
#info {
position: fixed; top: 14px; left: 14px;
color: rgba(255,255,255,0.65);
font-family: Georgia, serif;
font-size: 13px;
letter-spacing: 0.5px;
pointer-events: none;
z-index: 10;
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
}
#info b { color: rgba(255,255,255,0.9); font-weight: normal; font-style: italic; }
</style>
</head>
<body>
<div id="info">L-System Fractal Tree · <b>recursive branches sway in the wind</b></div>
<script src="p5.min.js"></script>
<script>
'use strict';
let lsysStr = '';
let time = 0;
let stars = [];
let fireflies = [];
let fallingLeaves = [];
const W = 800, H = 800;
// L-system
const AXIOM = 'F';
const RULES = { 'F': 'F[+F]F[-F]' };
const ITERATIONS = 4;
const BASE_ANGLE = Math.PI / 7;
function generate(axiom, rules, iters) {
let s = axiom;
for (let i = 0; i < iters; i++) {
let n = '';
for (const c of s) n += rules[c] || c;
s = n;
}
return s;
}
function isTip(str, pos) {
let d = 0;
for (let i = pos + 1; i < str.length; i++) {
const c = str[i];
if (c === '[') d++;
else if (c === ']') { if (d === 0) return true; d--; }
else if (c === 'F' && d === 0) return false;
}
return true;
}
function setup() {
createCanvas(W, H);
colorMode(HSB, 360, 100, 100, 1);
lsysStr = generate(AXIOM, RULES, ITERATIONS);
console.log('L-system length:', lsysStr.length, 'chars,', (lsysStr.match(/F/g) || []).length, 'F segments');
for (let i = 0; i < 180; i++) {
stars.push({
x: random(W),
y: random(H * 0.6),
b: random(0.3, 1),
tw: random(Math.PI * 2),
sp: random(0.5, 2.5)
});
}
for (let i = 0; i < 12; i++) {
fireflies.push({
x: random(W),
y: random(H * 0.5, H * 0.9),
phase: random(Math.PI * 2),
speed: random(0.3, 0.8)
});
}
}
function draw() {
drawSky();
drawStars();
drawMoon();
drawGround();
updateFallingLeaves();
drawFireflies();
interpretTree(lsysStr, time);
time += 0.018;
}
function drawSky() {
noStroke();
for (let y = 0; y < H; y++) {
const t = y / H;
fill(lerp(240, 220, t), lerp(80, 40, t), lerp(6, 18, t));
rect(0, y, W, 1);
}
}
function drawStars() {
noStroke();
for (const s of stars) {
const b = s.b * (0.5 + 0.5 * Math.sin(time * s.sp + s.tw));
fill(55, 15, 100, b);
circle(s.x, s.y, 1.4);
}
}
function drawMoon() {
noStroke();
// Halo
for (let i = 8; i > 0; i--) {
fill(50, 30, 92, 0.04);
circle(640, 140, 38 + i * 6);
}
fill(50, 30, 95);
circle(640, 140, 38);
// Craters
fill(45, 35, 75, 0.5);
circle(628, 132, 6);
circle(652, 148, 4);
circle(644, 132, 3);
circle(635, 152, 3);
}
function drawGround() {
noStroke();
// Hill
fill(220, 40, 12);
beginShape();
vertex(0, H);
for (let x = 0; x <= W; x += 12) {
const y = H * 0.88 + Math.sin(x * 0.011) * 9 + Math.sin(x * 0.027) * 4;
vertex(x, y);
}
vertex(W, H);
endShape();
// Grass
fill(120, 50, 30);
for (let i = 0; i < 80; i++) {
const x = random(W);
const y = H * 0.88 + Math.sin(x * 0.011) * 9 + Math.sin(x * 0.027) * 4;
const h = random(3, 8);
triangle(x, y, x - 1.5, y - h, x + 1.5, y - h);
}
// Tree shadow on ground
fill(220, 50, 8, 0.5);
beginShape();
const baseX = W / 2;
const baseY = H * 0.92;
for (let i = 0; i <= 20; i++) {
const a = -PI/2 + (i - 10) * 0.15;
const r = 30 + i * 2;
vertex(baseX + cos(a) * r, baseY + sin(a) * r * 0.3 + 5);
}
endShape(CLOSE);
}
function updateFallingLeaves() {
if (random() < 0.04 && fallingLeaves.length < 30) {
fallingLeaves.push({
x: W/2 + random(-150, 150),
y: random(H * 0.25, H * 0.5),
vx: random(-0.3, 0.3),
vy: random(0.3, 0.7),
rot: random(Math.PI * 2),
vrot: random(-0.04, 0.04),
sz: random(2.5, 4.5),
col: random([100, 110, 120, 90, 80]),
life: 0
});
}
for (let i = fallingLeaves.length - 1; i >= 0; i--) {
const p = fallingLeaves[i];
p.x += p.vx + Math.sin(time * 1.5 + p.rot) * 0.4;
p.y += p.vy;
p.rot += p.vrot;
p.life++;
if (p.y > H * 0.88 || p.life > 600) {
fallingLeaves.splice(i, 1);
}
}
noStroke();
for (const p of fallingLeaves) {
push();
translate(p.x, p.y);
rotate(p.rot);
fill(p.col, 65, 60, 0.9);
ellipse(0, 0, p.sz, p.sz * 0.5);
pop();
}
}
function drawFireflies() {
noStroke();
for (const f of fireflies) {
const x = f.x + Math.sin(time * 0.4 + f.phase) * 25;
const y = f.y + Math.cos(time * 0.3 + f.phase * 1.3) * 15;
const glow = 0.5 + 0.5 * Math.sin(time * 2 + f.phase * 2);
// Glow
fill(55, 80, 100, 0.1 * glow);
circle(x, y, 14);
fill(55, 80, 100, 0.3 * glow);
circle(x, y, 6);
fill(55, 50, 100, 0.9 * glow);
circle(x, y, 2);
}
}
function interpretTree(str, t) {
const stack = [];
let st = {
x: W / 2,
y: H * 0.92,
a: -Math.PI / 2,
l: 26,
w: 14,
d: 0
};
// Wind
const wt = t * 0.5;
const windRot = Math.sin(wt) * 0.025 + Math.sin(wt * 1.7 + 0.3) * 0.01;
const windOff = Math.sin(wt * 1.1 + 0.5) * 0.6 + Math.sin(wt * 0.7) * 0.3;
const tips = [];
for (let i = 0; i < str.length; i++) {
const c = str[i];
if (c === 'F') {
const tilt = windRot * st.d;
const off = windOff * st.d * 0.5;
const effA = st.a + tilt;
const nx = st.x + Math.cos(effA) * st.l + off;
const ny = st.y + Math.sin(effA) * st.l;
// Branch color: dark brown to lighter brown
const tCol = constrain(st.d / 4, 0, 1);
const hue = lerp(22, 35, tCol);
const sat = lerp(60, 50, tCol);
const bri = lerp(22, 55, tCol);
stroke(hue, sat, bri, 0.95);
strokeWeight(Math.max(st.w, 0.7));
line(st.x, st.y, nx, ny);
if (isTip(str, i)) {
tips.push({ x: nx, y: ny, d: st.d });
}
st.x = nx;
st.y = ny;
} else if (c === '+') {
st.a += BASE_ANGLE * (0.85 + random(0.3));
} else if (c === '-') {
st.a -= BASE_ANGLE * (0.85 + random(0.3));
} else if (c === '[') {
stack.push({...st});
st.l *= 0.82;
st.w *= 0.66;
st.d++;
} else if (c === ']') {
st = stack.pop();
}
}
// Leaves at tips
noStroke();
for (const tip of tips) {
const tCol = constrain(tip.d / 4, 0, 1);
const lHue = lerp(120, 85, tCol) + random(-8, 8);
const lSat = lerp(70, 60, tCol);
const lBri = lerp(60, 75, tCol);
const sz = lerp(3, 7, tCol);
// Leaf sway
const lsway = Math.sin(t * 1.5 + tip.x * 0.008 + tip.y * 0.008) * tip.d * 0.4;
// Cluster of small leaves
for (let j = 0; j < 3; j++) {
const ox = random(-sz/2, sz/2);
const oy = random(-sz/2, sz/2);
fill(lHue, lSat, lBri, 0.6);
circle(tip.x + lsway + ox, tip.y + oy, sz * (0.6 + random(0.4)));
}
}
}
</script>
</body>
</html>
```
Let me think about whether this will work and look good.
Issues to consider:
1. The tree size: With 4 iterations and base_l = 26, the main spine is 2^4 * 26 = 416. Tree height ~416. Fits in 800.
2. The base position: H * 0.92 = 736. Tree top reaches 736 - 416 = 320. Visible.
3. Width: branches go at angle ±25.7 degrees. After 4 levels, total spread = 4*25.7 = 102.8 degrees. Branches can be quite wide. Total width might be 200-300. Fits in 800.
Wait, the angle accumulates. If a branch goes right by 25.7 degrees, and its child goes right by another 25.7, that's 51.4 degrees from vertical. The branch length is 26 * 0.82 = 21.3 at depth 1, 17.5 at depth 2, 14.4 at depth 3, 11.8 at depth 4.
Horizontal spread of the tree: at each level, branches add some horizontal distance. Approximating as 25.7 degrees from vertical at each level, the horizontal extent is sum(sin(angle * depth) * length(depth)). Hmm hard to calculate exactly.
Let me just trust it'll look OK and tune visually.
4. Performance: 81 F's, each with isTip O(81) = 6561 ops. Plus drawing. Should be 60 FPS easy.
5. The wind effect: tilt and offset are applied to each F. The offset accumulates because state.x is updated with the offset. So deeper branches have more visible offset.
6. Color and contrast: the brown branches and green leaves against dark blue sky should look nice.
Let me also think about adding a "season" feel. Spring/summer trees have bright green leaves. The L-system is going to produce a tree with green leaves at the tips, which is classic.
Let me also add some birds or clouds? No, keep it focused.
Actually let me reconsider the leaves. Drawing 3 random circles per tip with random offsets might be a lot of circles. With ~50 tips, that's 150 circles. Fast.
Let me also think about whether the wind looks natural. Currently:
- windRot = sin(t * 0.5) * 0.025 + sin(t * 0.85 + 0.3) * 0.01
- windOff = sin(t * 0.55 + 0.5) * 0.6 + sin(t * 0.35) * 0.3
Both are sin sums. The result is a slow undulation with smaller faster waves. Should look natural.
7. The angle adjustment: BASE_ANGLE * (0.85 + random(0.3)) gives variation from 0.85 to 1.15 of base. With BASE_ANGLE = PI/7 = 0.449, range is 0.382 to 0.517. So angles vary between about 22 and 30 degrees. Slight randomness.
8. Hmm, the length factor (0.82 per depth) and the angle accumulation might cause some branches to go downward or cross weirdly. Let me also consider: what if I make the angle slightly different at each depth? Smaller angles for deeper branches. That would make the tree more "conical" rather than spreading.
Let me add: angle *= 0.9 per depth? So deeper branches have smaller angles. That makes the tree more natural.
```javascript
// In the [ handler:
st.angleFactor *= 0.9; // or similar
```
And in the +/- handler:
```javascript
st.a += BASE_ANGLE * st.angleFactor * (0.85 + random(0.3));
```
But this is a per-sub-tree scaling, not a global L-system rule. It works in the interpretation.
Let me add this.
Actually, let me not. The natural randomness might be enough. The visual difference might be subtle.
Let me also think: should the trunk taper smoothly? The thickness is set per depth: 14 at depth 0, 14*0.66=9.2 at depth 1, 6.1 at depth 2, 4 at depth 3, 2.6 at depth 4. That's a smooth taper.
Each F draws a line with strokeWeight = thickness. So the line has constant width along its length, but jumps at each new segment. That might look like discrete cylinders, not smooth. To smooth it, I could draw tapered segments (wider at base, narrower at tip), but that's more complex.
Let me just accept the discrete look. With 81 segments, the tree is dense enough that it looks smooth.
Hmm, actually for a more polished look, let me draw branches as tapered shapes. But that requires a lot more work. Let me skip for now.
9. Let me also think about the leaf color. I have lHue = lerp(120, 85, tCol) which goes from green (120) to yellow-green (85). At higher depth, leaves are more yellow. That's a nice gradient.
10. One concern: the L-system gives 81 F's, and each F at depth 0-4. The number of F's at each depth:
- Depth 0: 2 (from the first F and the third F in each rule expansion, but in the main spine only the first F of each expansion). Let me just count: at depth 0, we have 2^iter = 16 F's. At depth 1, 2*2^iter = 32. At depth 2, 4*2^iter = 64. Wait that's more than 81.
Let me recount. Each F in iter 1 → 3 F's in iter 2. Of these 3 F's, 2 are at the same depth as the original (the 1st and 3rd F's in F[+F]F[-F]), and 1 is one depth deeper (the 2nd F, in [+F] or [-F]).
So each F at depth d becomes 2 F's at depth d and 1 F at depth d+1.
Starting with 1 F at depth 0 (iter 0):
- After iter 1: 2 F's at depth 0, 1 F at depth 1. Total 3.
- After iter 2:
- 2 F's at depth 0 → 2*2=4 at depth 0, 2*1=2 at depth 1.
- 1 F at depth 1 → 1*2=2 at depth 1, 1*1=1 at depth 2.
- Total: 4 at depth 0, 4 at depth 1, 1 at depth 2. Total 9.
- After iter 3:
- 4 at d=0 → 8 at d=0, 4 at d=1.
- 4 at d=1 → 8 at d=1, 4 at d=2.
- 1 at d=2 → 2 at d=2, 1 at d=3.
- Total: 8 at d=0, 12 at d=1, 6 at d=2, 1 at d=3. Total 27.
- After iter 4:
- 8 at d=0 → 16 at d=0, 8 at d=1.
- 12 at d=1 → 24 at d=1, 12 at d=2.
- 6 at d=2 → 12 at d=2, 6 at d=3.
- 1 at d=3 → 2 at d=3, 1 at d=4.
- Total: 16 at d=0, 32 at d=1, 24 at d=2, 8 at d=3, 1 at d=4. Total 81.
So after 4 iterations: 16 trunk F's, 32 at d=1, 24 at d=2, 8 at d=3, 1 at d=4. Wait that doesn't seem right. The tips are at d=3 and d=4? Let me check.
Hmm, the F at depth 0 (trunk) becomes 2 F's at depth 0 + 1 F at depth 1. But that 1 F at depth 1 is inside [+F] or [-F]. After expansion, that 1 F at depth 1 becomes 2 at d=1 + 1 at d=2. So the depth increases by 1 per iteration.
So after 4 iter, max depth = 4. The 1 F at d=4 is the deepest.
The tips are: F's that don't have any more F's in their branch. Let me think about which F's are tips.
Actually in my isTip logic, I check if there are any F's in the same branch (same bracket depth) after the current F. If not, it's a tip.
For depth 0 F's: they have more F's in the same branch (the next depth-0 F). So they're not tips, except the last one.
For depth 1+ F's: most are at the end of their branch (followed by ']'). So they're tips.
With 16 depth-0 F's, only the last one (or the ones at the very end of the string) is a tip at depth 0. The other 15 are not tips.
So at depth 0: 1 tip.
At depth 1: 32 F's, all are tips (each in their own bracket).
At depth 2: 24 F's, all are tips.
At depth 3: 8 F's, all are tips.
At depth 4: 1 F, a tip.
Total tips: 1 + 32 + 24 + 8 + 1 = 66 tips. Hmm, that's a lot of leaf clusters.
Wait, but the F's in brackets at depth 1 - they're followed by ']' (end of bracket). But the bracket might have a longer expansion. Let me think.
In the rule F → F[+F]F[-F], the F inside [+F] is at depth 1. In iter 2, the [+F] becomes [+F[+F]F[-F]], and the F at depth 1 (the +F[+F]F[-F]) - actually wait, the brackets nest. Let me re-examine.
In iter 2 string: "F[+F[+F][-F]F][-F[+F][-F]F]F[+F][-F]F" (approximate)
Wait, my earlier analysis showed iter 2 = "F[+F[+F][-F]F][-F[+F][-F]F]F[+F][-F]F". Let me re-verify.
From before: iter 2 = "F[+F[+F][-F]F][-F[+F][-F]F]F[+F][-F]F"
So the F's in the string are at positions:
- Pos 0: F (depth 0)
- Pos 3: F (depth 1, inside [+...])
- Pos 6: F (depth 2, inside [+F[...])
- Pos 10: F (depth 2, inside [+F][...])
- Pos 13: F (depth 1, after [-F][-F]F])
- Pos 16: F (depth 2, inside [-F[...])
- Pos 20: F (depth 2, inside [-F][...])
- Pos 23: F (depth 0, after [...]F)
- Pos 26: F (depth 1, inside [+F])
- Pos 30: F (depth 1, inside [-F])
So in iter 2: F's at depths 0, 1, 2. Counts: depth 0 = 2 (pos 0, 23), depth 1 = 3 (pos 3, 13, 26 or 30), depth 2 = 5 (pos 6, 10, 16, 20, ...). Hmm let me recount.
Actually my earlier calculation said iter 2 has 9 F's total. Let me recount the string: "F[+F[+F][-F]F][-F[+F][-F]F]F[+F][-F]F"
F's: 0, 3, 6, 10, 13, 16, 20, 23, 26, 30. That's 10 F's, not 9. Hmm.
Wait the F → F[+F]F[-F] rule has 3 F's. iter 0: 1 F. iter 1: 3 F's. iter 2: 9 F's. iter 3: 27 F's. iter 4: 81 F's.
But my count of iter 2 gives 10. Let me re-expand.
iter 1 = "F[+F]F[-F]". F's at positions 0, 3, 7. That's 3 F's.
Wait, "F[+F]F[-F]" - let me count chars:
0: F
1: [
2: +
3: F
4: ]
5: F
6: [
7: -
8: F
9: ]
So F's at positions 0, 3, 5, 8. That's 4 F's. Hmm.
Wait, "F[+F]F[-F]" - I count 4 F's? Let me look character by character: F, [, +, F, ], F, [, -, F, ]. Yes 4 F's. So my rule has 4 F's, not 3.
Let me re-examine. F → F[+F]F[-F]. The rule has:
- F (1)
- [ (bracket)
- + (rotate)
- F (1)
- ] (close)
- F (1)
- [ (bracket)
- - (rotate)
- F (1)
- ] (close)
So 4 F's in the rule. Not 3.
Hmm. So 4^iter F's. iter 4: 256 F's. That's more.
Let me recount: 1, 4, 16, 64, 256. Yes 256.
OK so with 4 iter, 256 F's. That's still manageable. 256 * 256 = 65536 ops for isTip. Still fast.
But the tree is denser. 256 segments.
Let me re-examine the spine. In F[+F]F[-F], depth-0 F's are at pos 0, 5 (the 2nd F in the rule, between the two brackets). So 2 depth-0 F's per F. After 4 iter: 16 depth-0 F's.
With base_l = 26, spine = 16 * 26 = 416. That's the same as before. So spine length is determined by 2^iter * base_l.
With 4 iter and base_l = 26, spine = 416. Fits.
The branches: at each F (other than the 2 depth-0 ones), there's 1 sub-branch. Wait, 4 F's per rule: 2 are depth-0 (spine), 2 are in brackets (sub-branches).
So each F in iter 1 → 2 spine F's + 2 sub-branch F's in iter 2.
After 4 iter: 16 spine F's + many branch F's.
Total F's at each depth:
- iter 0: 1 F at d=0.
- iter 1: 2 at d=0, 2 at d=1. Total 4.
- iter 2: 4 at d=0, 2*2 + 2*1 = 6 at d=1, 2*1 = 2 at d=2. Total 12. Hmm.
Wait, the rule has 2 F's at depth 0 and 2 F's at depth 1 (in brackets). So each F at depth d → 2 at depth d + 2 at depth d+1.
iter 0: 1 at d=0.
iter 1: 1 → 2 at d=0, 2 at d=1. Total 4.
iter 2: each F → 2 at same d, 2 at d+1.
- 2 at d=0 → 4 at d=0, 4 at d=1.
- 2 at d=1 → 4 at d=1, 4 at d=2.
- Total: 4 at d=0, 8 at d=1, 4 at d=2. Total 16.
iter 3:
- 4 at d=0 → 8 at d=0, 8 at d=1.
- 8 at d=1 → 16 at d=1, 16 at d=2.
- 4 at d=2 → 8 at d=2, 8 at d=3.
- Total: 8 at d=0, 24 at d=1, 24 at d=2, 8 at d=3. Total 64.
iter 4:
- 8 at d=0 → 16 at d=0, 16 at d=1.
- 24 at d=1 → 48 at d=1, 48 at d=2.
- 24 at d=2 → 48 at d=2, 48 at d=3.
- 8 at d=3 → 16 at d=3, 16 at d=4.
- Total: 16 at d=0, 64 at d=1, 96 at d=2, 64 at d=3, 16 at d=4. Total 256.
So 256 F's. 16 at d=0, 64 at d=1, 96 at d=2, 64 at d=3, 16 at d=4.
Tips: each F inside brackets is followed by ']' (in the current iter). But the F itself might be expanded further. Hmm.
Actually let me think about isTip for each F. A tip is an F where no more F's follow in the same bracket level.
For depth 0 F's: they're followed by more depth-0 F's (until the end of the string). So only the last d=0 F is a tip.
For depth 1+ F's: each is followed by ']' (the closing of its bracket), unless the bracket is nested and contains more F's. Hmm.
Wait, in iter 1: F[+F]F[-F]. The F at pos 3 is at depth 1, followed by ']'. So it's a tip.
In iter 2: F[+F[+F][-F]F][-F[+F][-F]F]F[+F][-F]F. The F at pos 3 (depth 1) is followed by '[+F]', so it's NOT a tip. The F inside [+F] at pos 6 (depth 2) is followed by ']', so it IS a tip.
Hmm, the tip structure is more complex in iter 2+.
Let me just trust isTip. The number of tips is at most the total F's (256) and at least the deepest F's. In practice, for the L-system F → F[+F]F[-F], the tip count after iter 4 would be the number of leaves/terminal F's.
In iter 1, the F's at pos 3 and 8 are at depth 1, both followed by ']'. Both are tips. Pos 0 and 5 are at depth 0, pos 0 is not a tip (followed by [+F]), pos 5 is not a tip (followed by [-F]). So 2 tips in iter 1.
Wait, pos 5 in iter 1 is the F between the two brackets. It's followed by '[-F]', so the next non-bracket char is '['. Hmm, the F at pos 5 is at depth 0, and it's followed by '['. Going forward, we have '[-F]'. The 'F' is at depth 1, not 0. So the depth-0 F at pos 5 has no depth-0 F after it (within its branch). But the depth-0 branch continues after the ']'... wait no.
Hmm, the depth-0 branch in iter 1 is the top-level. The 'F' at pos 0, the 'F' at pos 5, and the 'F' after the last ']'. Wait there's no F after the last ']'. So depth-0 F's are pos 0 and pos 5.
Pos 0: followed by '[+F]F[-F]'. The next depth-0 F is pos 5 (after the ']' at pos 4). So pos 0 is not a tip.
Pos 5: followed by '[-F]'. Then end of string. No more depth-0 F's. So pos 5 IS a tip.
So in iter 1, 3 tips: pos 3, pos 8, pos 5. (pos 3 and 8 are at depth 1, pos 5 is at depth 0.)
Hmm, so 3 tips per 4 F's. Ratio 0.75. With 256 F's at iter 4, about 192 tips.
That's a LOT of leaf clusters. The tree will be very leafy.
Hmm, maybe too leafy. Let me reduce: only draw leaves at depth 3+ tips, not all tips.
Or: only draw leaves at every other tip (random).
Or: use a sparser L-system.
Let me only draw leaves at depth >= 2:
```javascript
if (tip.d >= 2) {
// draw leaves
}
```
That would reduce the count significantly. At depth 2+ tips, we have all the F's at d=2, 3, 4. From earlier: 96 + 64 + 16 = 176 tips. Still a lot.
Hmm. Let me think differently. Maybe only the deepest tips (depth 3, 4) get leaves, since those are the "real" tips of the tree.
From earlier: 64 at d=3, 16 at d=4. Plus some at d=2 that are true tips (not followed by another F in the same branch). Actually all d=2 F's are followed by ']' (the closing of their bracket in the rule). So they're all tips. Hmm.
Let me just sample: only draw leaves at a fraction of tips (random):
```javascript
for (const tip of tips) {
if (tip.d >= 2 && random() < 0.5) {
// draw leaves
}
}
```
Or: don't draw a leaf cluster per tip, draw a small dot:
```javascript
for (const tip of tips) {
fill(...);
circle(tip.x, tip.y, 3);
}
```
Simple green dots at all tips. That's about 200 dots. Fast.
But then the tree looks like a constellation. Hmm.
Let me try a different approach: only draw leaves at depth 3 and 4 tips. Those are the "outermost" tips. Or, draw leaves only at every 3rd tip.
You know what, let me just draw 1 small leaf at each tip. 256 small leaves is fine visually if they're small.
Actually with my current code, each tip draws 3 circles. So 256 * 3 = 768 circles. Should be fast but might look cluttered.
Let me reduce to 1 circle per tip and adjust size.
Or better: only draw leaves at tips that are at the "end" of a major branch. I can detect this by looking at the depth in the L-system: tips at depth >= 3 are real tips, tips at depth < 3 are intermediate.
Wait, but all tips in my L-system are "ends" of branches. The depth just tells us how nested they are. A tip at depth 0 is the end of the main spine (the last F in the main trunk). A tip at depth 4 is the end of a deep sub-sub-branch.
For leaf distribution, I want leaves at the deep tips (which form the "canopy") and not at the trunk tip.
Let me check: tips at d=0 (1 tip), d=1 (64 tips), d=2 (96 tips), d=3 (64 tips), d=4 (16 tips).
I want to skip d=0 (trunk tip - usually no leaves) and reduce d=1, d=2 (intermediate branches - some leaves).
Or just draw leaves at d >= 2.
d >= 2: 96 + 64 + 16 = 176 leaves. With 3 circles each = 528 circles. OK.
Or draw leaves at d >= 3: 64 + 16 = 80 leaves. With 3 circles each = 240 circles. Better.
Let me go with d >= 3 and 2 circles per leaf.
Actually let me just use 1 circle per tip at d >= 3, with size 4-6. That gives 80 visible green circles. Good.
Wait, even simpler: don't worry about it, just use 1 circle per tip with size 3. With 256 tips, that's 256 small dots. They cluster to form leaf masses.
Let me try 1 circle per tip, small size, color varies:
```javascript
for (const tip of tips) {
const tCol = constrain(tip.d / 4, 0, 1);
const lHue = lerp(110, 80, tCol) + random(-15, 15);
const lSat = 70;
const lBri = lerp(50, 75, tCol);
const sz = lerp(2, 5, tCol) + random(1);
fill(lHue, lSat, lBri, 0.8);
circle(tip.x, tip.y, sz);
}
```
This gives 256 small green/yellow dots. With some overlap, they form a leafy mass.
Hmm but with 256 dots, each at the END of a branch, and branches at depth 0-4, the dots will be distributed throughout the tree including near the trunk. That looks weird.
Let me only draw at d >= 2 (or 3):
```javascript
if (tip.d >= 2) {
// draw leaf
}
```
Actually, let me think about it differently. Each F in the L-system is a branch segment. The "leaves" of the tree are at the tips of the outermost branches. In my L-system, with max depth 4, the outermost branches are at d=4. But the F's inside those branches (at d=2, 3, 4) are all "near the leaves".
For a natural look, I want green leaves on the outermost branches. So leaves at d=3, 4 tips mostly, with some at d=2.
Let me do: 80% chance of leaf at d=2 tips, 100% at d=3,4 tips. And 0% at d=0,1.
Or: leaf at d >= 2 with random chance 0.4-0.7 based on depth.
Let me simplify: draw leaves at d >= 2 tips, 1 circle each, size 4. That's 176 dots.
Actually 176 dots of size 4 will cover a lot of area. Let me reduce to 1 circle per tip with size 3 at d >= 2. Still visible.
Hmm, I'm overthinking this. Let me just code it and see. The current code (3 circles per tip, all tips) might look fine or might be too much. Let me start with 1 circle per tip at d >= 2 and tune.
Let me go with: all tips get 1 circle, but with size scaling by depth (smaller at low depth, bigger at high depth). And a slight color variation.
```javascript
for (const tip of tips) {
const tCol = constrain(tip.d / 4, 0, 1);
const lHue = lerp(100, 80, tCol) + random(-12, 12);
const lSat = lerp(70, 55, tCol);
const lBri = lerp(55, 75, tCol);
const sz = lerp(1.5, 5, tCol) + random(1);
const lsway = Math.sin(t * 1.5 + tip.x * 0.008) * (tip.d + 1) * 0.3;
fill(lHue, lSat, lBri, 0.85);
circle(tip.x + lsway, tip.y, sz);
}
```
At d=0, sz = 1.5. Tiny dot, barely visible. Good.
At d=4, sz = 5. Visible green dot. Good.
Total 256 dots. Should be fine.
OK let me finalize the code. Let me also think about the order of operations:
1. Sky
2. Stars
3. Moon
4. Background atmosphere (fog?)
5. Ground
6. Fireflies
7. Falling leaves
8. Tree
9. Foreground details
Let me also add a subtle "ground glow" or "haze" near the horizon.
OK let me also think about the wind effect again. With windOff = sin(...) * 0.6, and applied as offset = windOff * d * 0.5, at d=4 we get offset = 0.6 * 4 * 0.5 = 1.2px. That's subtle.
Let me increase: offset = windOff * d * 1.5. At d=4: 0.6 * 4 * 1.5 = 3.6px. Better.
And the tilt: tilt = windRot * d. With windRot = 0.025, at d=4: 0.1 rad = 5.7 degrees. Visible.
Let me increase windRot: 0.04. At d=4: 0.16 rad = 9.2 degrees. Definitely visible.
And windOff coefficient: 2.0. At d=4: 0.6 * 4 * 2.0 = 4.8px. Combined with tilt of 9.2 degrees, the branch tip moves about 5 + (l * sin(tilt)) ≈ 5 + 11*sin(0.16) = 5 + 1.7 = 6.7px. Visible sway.
OK let me finalize. Let me also add a slow phase variation to the wind so different parts of the tree sway slightly differently. Use the position as a phase offset:
```javascript
const phase = st.x * 0.005 + st.y * 0.003;
const windOff = sin(wt + phase) * 0.5;
```
Hmm, this would make each branch sway at a slightly different phase. But for a global wind, all branches should sway together. Let me not add position-based phase.
Actually, real wind moves through the tree with some delay (closer branches move first). So a slight position-based phase is natural. Let me add a small one:
```javascript
const phase = st.x * 0.002;
const windOff = sin(wt + phase) * (1 + st.d * 0.5);
```
Hmm, this complicates. Let me just use global wind. Simpler and looks fine.
OK let me write the final code. Let me also consider: should I use HSB or RGB color mode?
HSB is nice for color gradients. Let me use it. fill(hue, sat, bri, alpha).
Wait, in p5 the default colorMode is RGB. With colorMode(HSB, 360, 100, 100, 1), the values are 0-360 for hue, 0-100 for sat and bri, 0-1 for alpha.
Let me write the final code now.
One more thought: the L-system string is regenerated every frame? No, only once in setup. Then interpretTree is called every frame with the string.
interpretTree does O(n) work for drawing and O(n^2) for isTip checks. n=256, so O(65536) per frame. At 60 FPS, that's 4M ops/sec. Should be fine.
Actually I can optimize isTip by precomputing an array of tip positions once, instead of checking for each F. Let me do that.
```javascript
// In setup, after generating lsysStr:
const tipPositions = new Array(lsysStr.length).fill(false);
for (let i = 0; i < lsysStr.length; i++) {
if (lsysStr[i] === 'F') {
tipPositions[i] = isTip(lsysStr, i);
}
}
```
Then in interpretTree:
```javascript
if (tipPositions[i]) {
tips.push({ x: nx, y: ny, d: st.d });
}
```
This precomputes tips in O(n^2) once, instead of every frame. Faster.
Actually, let me think. In the L-system string, the structure is fixed. The tip-ness of each F is determined by its position in the string. So precomputing is correct and efficient.
Let me also precompute other things if needed.
OK let me write the final code.
Actually let me also think about adding small details:
- A few perched birds on branches? No, too complex.
- A bench? No, not needed.
- A path leading to the tree? No.
Keep it simple: tree, leaves, wind, night sky, moon, stars, ground, falling leaves, fireflies.
Let me also add a small info text explaining what the L-system is doing.
OK let me write the code now.
Let me check the file is set up properly first.Looking at this task, I need to: 1. Use the pre-provided p5.min.js with a `<script src="p5.min.js">` tag 2. Build a procedural L-system fractal tree that branches recursively and sways in the wind 3. Keep everything in one self-contained `index.html` Let me first check the current directory and the p5 library.
{"command":"ls -la /tmp/ws/"}total 1012 drwxr-sr-x. 2 node node 23 Aug 2 03:19 . drwxrwsrwx. 3 root node 16 Aug 2 03:19 .. -rw-r--r--. 1 node node 1034532 Aug 2 03:19 p5.min.js
thinking
Good, p5.min.js is in /tmp/ws. Let me create the index.html file with a beautiful L-system fractal tree.
Good, p5.min.js is present. Now let me design and build the L-system tree. **Design plan:** - **L-system:** Axiom `F`, rule `F → F[+F]F[-F]`, iterated 4 times → 256 segments with max depth 4 - **Turtle interpreter:** tracks position, angle, length, thickness, depth via push/pop stack - **Wind sway:** depth-proportional horizontal offset + rotation, slow sine waves (global breeze + small gust) - **Leaves:** pre-detected tip positions, green/yellow gradient circles, also sway with depth - **Atmosphere:** night sky gradient, stars (twinkling), moon, hills, grass, fireflies, falling leaves Let me write the complete file:
/tmp/ws/index.html (12661 bytes)Successfully wrote 12661 bytes to /tmp/ws/index.html
thinking
Let me verify the file was written correctly and check the structure.
Let me verify the file structure and check the result:
{"command":"ls -la /tmp/ws/ && echo \"---\" && head -20 /tmp/ws/index.html && echo \"...\" && tail -5 /tmp/ws/index.html"}total 1028 drwxr-sr-x. 2 node node 41 Aug 2 03:24 . drwxrwsrwx. 3 root node 16 Aug 2 03:19 .. -rw-r--r--. 1 node node 14691 Aug 2 03:24 index.html -rw-r--r--. 1 node node 1034532 Aug 2 03:19 p5.min.js --- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>L-System Fractal Tree</title> <style> html, body { margin: 0; padding: 0; overflow: hidden; background: #050810; height: 100%; } canvas { display: block; } #info { position: fixed; top: 14px; left: 16px; color: rgba(255, 255, 255, 0.7); font-family: 'Georgia', 'Times New Roman', serif; ... } } </script> </body> </html>
thinking
Let me also do a quick sanity check on the L-system by running the generation logic in node to make sure it works.
Let me sanity-check the L-system generation by running the logic in Node:
{"command":"node -e \"\nconst AXIOM = 'F';\nconst RULES = { 'F': 'F[+F]F[-F]' };\nconst ITERATIONS = 4;\n\nfunction gen(axiom, rules, iters)Length: 766 F count: 256 Bracket count: 170 First 80 chars: F[+F]F[-F][+F[+F]F[-F]]F[+F]F[-F][-F[+F]F[-F]][+F[+F]F[-F][+F[+F]F[-F]]F[+F]F[-F Last 80 chars: ]F[-F]]F[+F]F[-F][-F[+F]F[-F]][-F[+F]F[-F][+F[+F]F[-F]]F[+F]F[-F][-F[+F]F[-F]]]] F per depth: 16 64 96 64 16 0 0 0 0 0 Tip count: 171
thinking
The L-system generation works correctly: - 256 F-segments (4^4) - 16 at depth 0 (trunk), 64 at d=1, 96 at d=2, 64 at d=3, 16 at d=4 - 171 leaf tips - Brackets are balanced (170 opens, 170 closes) The tree will have good fractal structure with a clear trunk and dense canopy. Let me also do a quick check that the JavaScript is syntactically valid.
L-system math is correct: 256 F-segments, balanced brackets, 171 tips distributed nicely (16 trunk + 64 + 96 + 64 + 16 at deeper levels). Let me do a final syntax check on the JavaScript:
{"command":"node -e \"\nconst fs = require('fs');\nconst html = fs.readFileSync('index.html', 'utf8');\nconst m = html.match(/<script>([\\s\✓ JavaScript parses OK (11625 chars)
thinking
JavaScript parses cleanly. Let me also verify that there are no external resource references (no CDNs, no external URLs).
JavaScript parses cleanly. Let me verify zero external network resources:
{"command":"grep -E \"https?://|//cdn|cdn\\.\" /tmp/ws/index.html | grep -v \"^[[:space:]]*//\" || echo \"✓ No external network references f✓ No external network references found
thinking
All good. The file is complete: 1. ✅ Single self-contained `index.html` (14.6 KB) 2. ✅ p5.min.js loaded via local `<script src="p5.min.js">` (no CDN) 3. ✅ All CSS inline in `<style>` tag 4. ✅ All JS inline in `<script>` tag 5. ✅ No external resources / network calls 6. ✅ L-system properly defined: axiom F, rule F→F[+F]F[-F], 4 iterations 7. ✅ Recursive branching via turtle stack (push/pop state on '[' / ']') 8. ✅ Wind sway: depth-proportional rotation + horizontal offset, combined sine waves 9. ✅ Rich atmospheric scene (sky, stars, moon, hills, fireflies, falling leaves) 10. ✅ Beautiful gradients (HSB color space) for trunk and leaves 11. ✅ Tip detection precomputed for performance The deliverable is complete. Let me give a concise summary to the user.
Done. Created `/tmp/ws/index.html` — a single self-contained file with everything inlined. **What it does** - **L-system:** Axiom `F`, rule `F → F[+F]F[-F]`, iterated 4× → **256 F-segments** at depths 0–4, with 171 leaf tips - **Turtle interpreter** with a push/pop stack tracks position, angle, length, thickness, and depth - **Branching recursion:** `[` saves state (shorter `l`, thinner `w`, depth+1); `]` restores it — the same L-system string is interpreted every frame, so the structure is true to the rule - **Wind sway** is a global field `windRot + windOff` built from two sine layers (slow primary + smaller gust); the *magnitude* scales with branch depth, so the trunk barely moves while the canopy sways noticeably - Each F-segment also gets a depth-proportional horizontal offset that accumulates through the chain, giving the canopy a natural "lag" effect - Tips are precomputed into a `Uint8Array` once (O(n²) at startup) so the per-frame interpreter stays O(n) **Scene dressing** (all drawn underneath the tree each frame): night-sky gradient, ~200 twinkling stars, soft-haloed moon with craters, layered silhouette hills, tufted ground, 14 drifting fireflies, occasional falling leaves, subtle vignette, and an info overlay showing the L-system rule. Open `index.html` in a browser (with `p5.min.js` in the same directory) and the tree will draw immediately and gently sway in the wind.
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
EVALUATION
L-system tree with glowing buds sways gently in wind against a night sky with moon and fireflies; no distinct input reactions.
Vision grade · anthropic/claude-sonnet-5
MEASUREMENTS
Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.