Pythagoras Tree

My take on the Pythagoras Tree.

- Symmetry is used to lerp the angle to PI/4 at branches
- AngleMod and AngleTest can be used to swap the left- and right-angle at certain depths.


Pythagoras Tree (variation)
Pythagoras Tree (variation)
Pythagoras Tree (variation)

#fractal #pythagor

Log in to post a comment.

const turtle = new Leonardo();

const rotAngle = 1.07; // min=0, max=1.57, step=0.001
const aspect = 3.5;  // min=.5, max=10, step=0.001
const size = 10;   // tree will be transformed to fit inside the bounding box of the canvas 
const symmetry = 0.6; // min=0, max=1, step=0.001
const cutOffSize = 0.125; // min=0, max=0.5, step=0.001
const drawMode = 0; // min=0, max=1, step=1 (Outline, Boxes)
const angleModulo = 3; // min=0, max=3, step=1 (Regular, Coniferous, Semi-Coniferous, Irregular)
const tension = 1; // min=0, max=1, step=0.001

turtle.tension = tension;

const [angleMod, angleTest] = angleModulo == 0 ? [0,0] : angleModulo == 1 ? [2, 0] : angleModulo == 2 ? [4, 1] : [5, 1];

const root = new PythagorasTree(size, aspect, rotAngle, symmetry, cutOffSize, angleMod, angleTest);

// find scale and offset to transform tree inside bounding box of canvas
const scale = 180 / Math.max(root.max[0]-root.min[0], root.max[1]-root.min[1]);
const offset = [-(root.max[0]+root.min[0])/2, -(root.max[1]+root.min[1])/2];
const t = (p) => [(p[0]+offset[0])*scale, (p[1]+offset[1])*scale];

// We use a 'leonardo', so we have to move to warm up at the start point of the curve
turtle.jump(t(root.corners[0]));
turtle.goto(t(root.corners[0]));

const stack = [root];

function walk(i) {
    const node = stack.pop();
    const corners = node.corners;
    
    if (!node.rightSide) {
        node.rightSide = true;
        
        if (drawMode == 1) { // box
            turtle.penup();
            for (let j=0; j<7; j++) {
                turtle.goto(t(node.corners[j % 4]));
                turtle.pendown();
            }
        } else { // outline
            stack.push(node); // push node so we can draw left side when childs are all done
            turtle.goto(t(corners[1]));
        }
        
        if (node.left || node.right) {
            if (node.right) stack.push(node.right);
            if (node.left) stack.push(node.left);
        } else if (drawMode == 0) { // outline, draw top of node
            turtle.goto(t(corners[2]));
        }
    } else if (drawMode == 0) { // outline, draw left side
        turtle.goto(t(corners[3]));
    }
    
    return stack.length > 0 ? true : turtle.goto(t(root.corners[3])); // cool down of leonardo at last walk
}

////////////////////////////////////////////////////////////////
// Pythagoras Tree code - Created by Reinder Nijhoff 2022
//
// A really simple implementation. If you want a more scalable
// solution, it is probably better to use a stack based approach.
//
// https://turtletoy.net/turtle/da47b4247b
////////////////////////////////////////////////////////////////

function PythagorasTree(size, aspect, rotAngle, symmetry, cutOffSize, angleMod = 0, angleTest = 0, maxDepth = 19) {
    const scl2   = (a,b)   => [a[0]*b, a[1]*b];
    const add2   = (a,b)   => [a[0]+b[0], a[1]+b[1]];
    const rot2   = (a)     => [Math.cos(a), -Math.sin(a), Math.sin(a), Math.cos(a)];
    const trans2 = (m,a)   => [m[0]*a[0]+m[2]*a[1], m[1]*a[0]+m[3]*a[1]];
    const lerp   = (a,b,t) => a * (1 - t) + b * t;
    
    class Node {
        constructor ( angle, size, depth = 0, center = [0,0] ) {
            this.angle = angle;
            this.size = size;
            this.center = center;
            this.depth = depth;
            this.max = [-1e5, -1e5];
            this.min = [ 1e5,  1e5];
            
            this.left = undefined;
            this.right = undefined;
            
            this.bbox(...this.corners);
            
            if (depth < maxDepth) {
                let   leftAngle = depth % angleMod > angleTest ? rotAngle : Math.PI / 2 - rotAngle;
                      leftAngle = lerp(leftAngle, Math.PI / 4, symmetry * depth / maxDepth);
                const rightAngle = Math.PI / 2 - leftAngle;
    
                const leftWidth = Math.cos(leftAngle) * size;
                if (leftWidth > cutOffSize) {
                    this.left = new Node( angle - leftAngle, leftWidth, depth + 1, 
                                    add2(this.corners[1], trans2(rot2(angle - leftAngle), [leftWidth, aspect * leftWidth])));
                    this.bbox(this.left.min, this.left.max);
                }
                const rightWidth = Math.cos(rightAngle) * size;
                if (rightWidth > cutOffSize) {
                    this.right = new Node( angle + rightAngle, rightWidth, depth + 1, 
                                     add2(this.corners[2], trans2(rot2(angle + rightAngle), [-rightWidth, aspect * rightWidth])));
                    this.bbox(this.right.min, this.right.max);
                }
            }
        }
        
        get corners() {
            const up    = [ Math.sin(this.angle)*this.size*aspect, Math.cos(this.angle)*this.size*aspect];
            const right = [ Math.cos(this.angle)*this.size, -Math.sin(this.angle)*this.size];
            return [[-1,-1],[1,-1], [1,1], [-1,1]].map(c => add2(add2(this.center, scl2(up, c[0])), scl2(right, c[1])));
        }

        bbox(...points) {
            points.forEach( p => {
                this.max[0] = Math.max(this.max[0], p[0]), this.max[1] = Math.max(this.max[1], p[1]);
                this.min[0] = Math.min(this.min[0], p[0]), this.min[1] = Math.min(this.min[1], p[1]);
            });
        }
    }
    
    return new Node(Math.PI, size);
}

////////////////////////////////////////////////////////////////
// Catmull-Rom Splines utility code. Created by Reinder Nijhoff 2022
// https://turtletoy.net/turtle/01e218e32f
// https://qroph.github.io/2018/07/30/smooth-paths-using-catmull-rom-splines.html
////////////////////////////////////////////////////////////////

function Leonardo(x, y) {
    function scl2(a,b)   { return [a[0]*b, a[1]*b]; }
    function add2(a,b)   { return [a[0]+b[0], a[1]+b[1]]; }
    function sub2(a,b)   { return [a[0]-b[0], a[1]-b[1]]; }
    function len2(a)     { return Math.sqrt(a[0]**2 + a[1]**2); }
    function dist2(a, b) { return len2(sub2(a,b)); }

    class Leonardo extends Turtle {
        constructor(x, y) {
            super(x, y);
            this.alpha = 0;
            this.tension = 0;
        }
        
        goto(x, y) {
            const p = Array.isArray(x) ? [...x] : [x, y];
            this.path = this.path ? this.path : [];
            this.path.push(p);
            if (this.isdown() && this.path.length >= 4) {
                this.path = this.path.slice(-4);
                this.catmullRomSpline(...this.path);
            } else if (!this.isdown()) {
                this.path = [p];
            }
        }
        
        catmullRomSpline(p0, p1, p2, p3) {
            const subdiv = dist2(p1, p2)|0 + 8;
            
            const t01 = Math.pow(dist2(p0, p1), this.alpha);
            const t12 = Math.pow(dist2(p1, p2), this.alpha);
            const t23 = Math.pow(dist2(p2, p3), this.alpha);
            
            const m1 = scl2( add2(sub2(p2, p1), scl2( sub2(scl2( sub2(p1, p0), 1 / t01), scl2( sub2(p2, p0), 1 / (t01 + t12))), t12)), 1 - this.tension);
            const m2 = scl2( add2(sub2(p2, p1), scl2( sub2(scl2( sub2(p3, p2), 1 / t23), scl2( sub2(p3, p1), 1 / (t12 + t23))), t12)), 1 - this.tension);
                
            const a = add2( add2( scl2(sub2(p1, p2), 2), m1), m2);
            const b = sub2( sub2( sub2( scl2(sub2(p1, p2), -3), m1), m1), m2);
            
            if (this.isdown() && (this.x() != p1[0] || this.y() != p1[1])) {
                this.penup();
                super.goto(p1);
                this.pendown();
            }
            
            for (let i=0; i<subdiv; i++) {
                const t = i/subdiv;
                super.goto(add2( add2( add2 ( scl2(a, t * t * t), scl2(b, t * t)), scl2(m1, t)), p1));
            }
            super.goto(p2);
        }
    }
    return new Leonardo(x,y);
}