const c = document.createElement('canvas');
document.body.appendChild(c);
c.style.width = '100%'; c.style.height = '100%';
const ctx = c.getContext('2d');
const cs = getComputedStyle(document.documentElement);
const ACCENT3 = cs.getPropertyValue('--accent-3').trim() || '#18c29c';
let w, h;
function resize() {
const dpr = Math.min(devicePixelRatio || 1, 2);
w = innerWidth; h = innerHeight;
c.width = w * dpr; c.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
addEventListener('resize', resize);
resize();
function expand(axiom, rules, gens) {
let s = axiom;
for (let g = 0; g < gens; g++) { let next = ''; for (const ch of s) next += rules[ch] ?? ch; s = next; }
return s;
}
const RULES = { X: 'F+[[X]-X]-F[-FX]+X', F: 'FF' };
const STR = expand('X', RULES, 4);
const ANGLE = (25 * Math.PI) / 180, STEP = 6.2;
function buildSegments() {
let x = 0, y = 0, ang = -Math.PI / 2, stack = [];
const segs = [];
let minX = 0, maxX = 0, minY = 0, maxY = 0;
for (const ch of STR) {
if (ch === 'F') {
const nx = x + Math.cos(ang) * STEP, ny = y + Math.sin(ang) * STEP;
segs.push([x, y, nx, ny]);
x = nx; y = ny;
minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y);
} else if (ch === '+') ang += ANGLE;
else if (ch === '-') ang -= ANGLE;
else if (ch === '[') stack.push([x, y, ang]);
else if (ch === ']') [x, y, ang] = stack.pop();
}
return { segs, bounds: [minX, maxX, minY, maxY] };
}
const { segs, bounds } = buildSegments();
const treeW = bounds[1] - bounds[0], treeH = bounds[3] - bounds[2];
function draw(frac) {
const dpr = Math.min(devicePixelRatio || 1, 2);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
const scale = Math.min(w / treeW, h / treeH) * 0.82;
ctx.save();
ctx.translate(w / 2 - ((bounds[0] + bounds[1]) / 2) * scale, h * 0.94 - bounds[3] * scale);
ctx.scale(scale, scale);
ctx.strokeStyle = ACCENT3; ctx.lineWidth = 1.4 / scale; ctx.lineCap = 'round';
const count = Math.floor(segs.length * frac);
for (let i = 0; i < count; i++) { const s = segs[i]; ctx.beginPath(); ctx.moveTo(s[0], s[1]); ctx.lineTo(s[2], s[3]); ctx.stroke(); }
ctx.restore();
}
let start = performance.now();
const GROW_MS = 1100, HOLD_MS = 2200;
(function loop(t) {
const elapsed = (t - start) % (GROW_MS + HOLD_MS);
draw(Math.min(1, elapsed / GROW_MS));
requestAnimationFrame(loop);
})(performance.now());