Kauri

A turtle to celebrate the birth of our daughter Kauri.

Log in to post a comment.

// Kauri. Created by Reinder Nijhoff 2022
// @reindernijhoff
//
// https://turtletoy.net/turtle/eac2d350cd
//


const waveSeed = 70; // min=1, max=100, step=1
const wrinkles = 14; // min=0, max=20, step=.1
const frequency = 0.3; //min=.1, max=1, step=.01
const waveSize = 17.5; // min=0, max=60, step=.1
const xDisplacement = .24; // min=0, max=1, step=.01

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 turtle = new Turtle();
const noise  = new SimplexNoise(waveSeed);
const collisionPolygon = [];

kauriTree();
kauriSea();
kauriWind();

////////////////////////////////////////////////////////////////
// Tree, Sea, Wind
////////////////////////////////////////////////////////////////

function kauriTree() {
    const root = new PythagorasTree(size, aspect, rotAngle, symmetry, cutOffSize, 5, 1);
    const collisionRoot = new PythagorasTree(size, aspect, rotAngle, symmetry, 1, 5, 1);

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

    // create collision polygon
    const collisionStack = [collisionRoot];
    
    while(collisionStack.length > 0) {
        const node = collisionStack.pop();
        const corners = node.corners;
        
        if (!node.rightSide) {
            node.rightSide = true;
            collisionStack.push(node); // push node so we can draw left side when childs are all done
            collisionPolygon.push(t(corners[1]));

            if (node.left || node.right) {
                if (node.right) collisionStack.push(node.right);
                if (node.left) collisionStack.push(node.left);
            } else { // outline, draw top of node
                collisionPolygon.push(t(corners[2]));
            }
        } else  { // outline, draw left side
            collisionPolygon.push(t(corners[3]));
        }
    }
    
    const turtle = new Leonardo();
    
    // 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];

    while(stack.length > 0) {
        const node = stack.pop();
        const corners = node.corners;
        
        if (!node.rightSide) {
            node.rightSide = true;
            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 { // outline, draw top of node
                turtle.goto(t(corners[2]));
            }
        } else  { // outline, draw left side
            turtle.goto(t(corners[3]));
        }
    }
    turtle.goto(t(root.corners[3])); // cool down of leonardo at last walk
}


function kauriWind() {
    const turtle = new Tortoise().addTransform(simplexDistort);
    
    const lines = 90;

    const dither = new Dither(turtle, (p, l) => 
        .5 * noise.noise2D([0.5,l*.3]) - Math.max(-.5, .005 * p[1])
        - 7 / (.1 + pDistance(collisionPolygon, p))
    );
    
    for (let y=-10; y<=lines; y++) {
        const ycor = -2.5 * y + 10;
        turtle.jump([-110, ycor]);
        dither.goto([100, ycor]);
    }        
}

function kauriSea() {
    const turtle = new Turtle();
    let   xp, yp, z;
    const dither = new Dither(turtle, (p, l) => noise.noise2D([yp,l*.3])*2 + 2 - z/20
      - 7 / (.1 + pDistance(collisionPolygon, p))
    );
    
    const dz = 50;
    
    for (let r=0; r<45; r+=2) {
        const delta = 0.01;
        for (let i=-delta; i<=1; i+=delta) {
            const a = i * Math.PI*2;
            const rl = r + Math.sin(a * 20) * 0.25;
            const x = Math.sin(a)*rl;
            z = dz + Math.cos(a)*rl;
            
            xp = 9 + 100 * x / z, yp = 100 * 8 / z + 28;
            if (i < 0) turtle.jump([xp, yp]);
            dither.goto([xp,yp]);
        }
    }
}

////////////////////////////////////////////////////////////////
// Utility code
////////////////////////////////////////////////////////////////

function simplexDistort(p) { 
    const n = noise.noise2D([p[0]*frequency/200, p[1]*frequency/200]);
    const dist = waveSize * .2 * Math.sin(n * 3. * wrinkles) * ((.5 + .5 *n)**2);
    return [p[0] + dist * xDisplacement, p[1] + dist];
}

// find distance from point to polygon p
function pDistance(p, point) {
    let d = 100000;
    for (let i=0; i<p.length; i++) {
        const a = p[i], b = p[(i+1) % p.length];
        const ba = sub(b, a);
        const pa = sub(point, a);
        const h = Math.max(0, Math.min(1, dot(pa,ba)/dot(ba,ba)));
        d = Math.min(d, dist(pa, scale(ba, h)));
    }
    return d;
}

function Dither(turtle, densFunc) {
    class Dither {
        constructor(turtle, densFunc) {
            this.turtle = turtle;
            this.densFunc = densFunc;
            this.dist = 0;
        }
        goto(dest) {
            const p = this.turtle.pos();
            const d = dist(p, dest);
            const startDist = this.dist, steps = Math.max(d*5, 1);
            for (let i=0; i<=steps; i++) {
                const c = lerp(p, dest, i/steps);
                if (this.densFunc(c, startDist + d * i/steps) <= 0) {
                    this.turtle.up();
                } else {
                    this.turtle.down();
                }
                this.turtle.goto(c);
            }
            this.dist += d;
        }
    }
    return new Dither(turtle, densFunc);
}

function scale(a,b)    { return [a[0]*b,a[1]*b]; }
function add(a,b)      { return [a[0]+b[0],a[1]+b[1]]; }
function sub(a,b)      { return [a[0]-b[0],a[1]-b[1]]; }
function dot(a,b)      { return a[0]*b[0]+a[1]*b[1]; }
function dist_sqr(a,b) { return (a[0]-b[0])**2+(a[1]-b[1])**2; }
function dist(a,b)     { return Math.sqrt(dist_sqr(a,b)); }
function length(a)     { return Math.sqrt(dot(a,a)); }
function lerp(a,b,t)   { return [a[0]*(1-t)+b[0]*t,a[1]*(1-t)+b[1]*t]; }
function rot(a)        { return [Math.cos(a), -Math.sin(a), Math.sin(a), Math.cos(a)]; }
function trans(m,a)    { return [m[0]*a[0]+m[2]*a[1], m[1]*a[0]+m[3]*a[1]]; }


function SimplexNoise(seed = 1) {
	const grad = [  [1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0],
	            	[1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1],
            		[0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1] ];
	const perm = new Uint8Array(512);
            		
	const F2 = (Math.sqrt(3) - 1) / 2;
	const G2 = (3 - Math.sqrt(3)) / 6;

	class SimplexNoise {
		constructor(seed = 1) {
			for (let i = 0; i < 512; i++) {
				perm[i] = i & 255;
			}
			for (let i = 0; i < 255; i++) {
				const r = (seed = this.hash(i+seed)) % (256 - i)  + i;
				const swp = perm[i];
				perm[i + 256] = perm[i] = perm[r];
				perm[r + 256] = perm[r] = swp;
			}
		}
		noise2D(p) {
			const s = dot(p, [F2, F2]);
			const c = [Math.floor(p[0] + s), Math.floor(p[1] + s)];
			const i = c[0] & 255, j = c[1] & 255;
			const t = dot(c, [G2, G2]);

			const p0 = sub(p, sub(c, [t, t]));
			const o  = p0[0] > p0[1] ? [1, 0] : [0, 1];
			const p1 = sub(sub(p0, o), [-G2, -G2]);
			const p2 = sub(p0, [1-2*G2, 1-2*G2]);
			
			let n =  Math.max(0, 0.5-dot(p0, p0))**4 * dot(grad[perm[i+perm[j]] % 12], p0);
			    n += Math.max(0, 0.5-dot(p1, p1))**4 * dot(grad[perm[i+o[0]+perm[j+o[1]]] % 12], p1);
		    	n += Math.max(0, 0.5-dot(p2, p2))**4 * dot(grad[perm[i+1+perm[j+1]] % 12], p2);
			
			return 70 * n;
		}
		hash(i) {
            i = 1103515245 * ((i >> 1) ^ i);
            const h32 = 1103515245 * (i ^ (i>>3));
            return h32 ^ (h32 >> 16);
		}
	}
	return new SimplexNoise(seed);
}


function Tortoise(x, y) {
    class Tortoise extends Turtle {
        constructor(x, y) {
            super(x, y);
            this.ps = Array.isArray(x) ? [...x] : [x || 0, y || 0];
            this.transforms = [];
        }
        addTransform(t) {
            this.transforms.push(t);
            this.jump(this.ps);
            return this;
        }
        applyTransforms(p) {
            if (!this.transforms) return p;
            let pt = [...p];
            this.transforms.map(t => { pt = t(pt); });
            return pt;
        }
        goto(x, y) {
            const p = Array.isArray(x) ? [...x] : [x, y];
            const pt = this.applyTransforms(p);

            if (this.isdown() && (this.pt[0]-pt[0])**2 + (this.pt[1]-pt[1])**2 > 4) {
               this.goto((this.ps[0]+p[0])/2, (this.ps[1]+p[1])/2);
               this.goto(p);
            } else {
                super.goto(pt);
                this.ps = p;
                this.pt = pt;
            }
        }
        position() { return this.ps; }
    }
    return new Tortoise(x,y);
}

function Leonardo(x, y) {
    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 = dist(p1, p2)|0 + 8;
            
            const t01 = Math.pow(dist(p0, p1), this.alpha);
            const t12 = Math.pow(dist(p1, p2), this.alpha);
            const t23 = Math.pow(dist(p2, p3), this.alpha);
            
            const m1 = scale( add(sub(p2, p1), scale( sub(scale( sub(p1, p0), 1 / t01), scale( sub(p2, p0), 1 / (t01 + t12))), t12)), 1 - this.tension);
            const m2 = scale( add(sub(p2, p1), scale( sub(scale( sub(p3, p2), 1 / t23), scale( sub(p3, p1), 1 / (t12 + t23))), t12)), 1 - this.tension);
                
            const a = add( add( scale( sub(p1, p2), 2), m1), m2);
            const b = sub( sub( sub( scale(sub(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(add( add( add ( scale(a, t * t * t), scale(b, t * t)), scale(m1, t)), p1));
            }
            super.goto(p2);
        }
    }
    return new Leonardo(x,y);
}


function PythagorasTree(size, aspect, rotAngle, symmetry, cutOffSize, angleMod = 0, angleTest = 0, maxDepth = 19) {
    const lerpScalar = (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 = lerpScalar(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, 
                                    add(this.corners[1], trans(rot(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, 
                                     add(this.corners[2], trans(rot(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 => add(add(this.center, scale(up, c[0])), scale(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);
}