Esja

A turtle to celebrate the birth of our daughter Esja.

Log in to post a comment.

// Esja. Created by Reinder Nijhoff 2020
// @reindernijhoff
//
// https://turtletoy.net/turtle/d4e8c7f64d
//


let   mountainSeed = 54; // min=1, max=100, step=1
const waveSeed = 30; // 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 = 24.; // min=0, max=60, step=.1
const xDisplacement = .24; // min=0, max=1, step=.01

const turtle = new Turtle();
const goldenRatio = 1.61803398875;
const noise  = new SimplexNoise(waveSeed);
const mountainTops = [];

esjaSea();
esjaMountains();
esjaWind();

////////////////////////////////////////////////////////////////
// Sea, Mountains, Wind
////////////////////////////////////////////////////////////////

function esjaWind() {
    const turtle = new Tortoise().addTransform(simplexDistort);
    
    const curls = [];
    const lines = 70;
    
    for (let y=0; y<lines; y++) {
        const c = y * goldenRatio;
        const up = mountainRandom() > .5;
        curls.push( { x: (c - Math.floor(c)) * .75, y, up });
        curls.push( { x: (c - Math.floor(c)) * .75 + .75, y, up });
    }
    curls.sort( (a,b) => a.x-b.x );
    
    const dither = new Dither(turtle, (p, l) => 
        .5 * noise.noise2D([0.5,l*.3]) - .125 + Math.pow(Math.abs(.005 * p[1]), 1)
        - 7 / (.1+dist(p, [-45, -5]))
    );
    
    for (let y=0; y<=lines; y++) {
        const ycor = -2 * y + 10;
        turtle.jump([-110, ycor]);
        
        const collisions = curls.filter(a => Math.abs(a.y-y) <= 3.5);
        
        collisions.forEach(curl => {
           const xcen = curl.x * 200 - 100;
           const ycen = -2 * curl.y + 10;
           
           if (curl.y === y + (curl.up ? 3 : -3)) {
               dither.goto([xcen - .5, ycor]);
               for (let a=0; a<20; a+=.1) {
                   const cos = Math.cos(a) * (curl.up ? 1 : -1);
                   const sin = Math.sin(a);
                   dither.goto(add([xcen - .5, ycen], scale([sin, cos], 6 - a/4)));
               }
           } else {
               const xl = xcen - Math.sqrt(40 - (ycor - ycen)**2);
               dither.goto([xl, ycor]);
           }
           const xr = xcen + Math.sqrt(40 - (ycor - ycen)**2);
           turtle.jump([xr, ycor]);
        });
        dither.goto([100, ycor]);
    }        
}

function esjaMountains() {
    const numberMountains = 7;
    const allLines = [];
    const collisionLines = [];
    
    class MountainLine {
        constructor(p, dx) {
            this.start = this.end = p;
            this.dx = dx;
        }
        walkAndSplit() {
            const dy = 10 + 20 * mountainRandom() - this.start[1];
            this.end = add(this.start, [this.dx*dy, dy]);
            collisionLines.forEach(line => {
                const int = segment_intersect(this.start, this.end, line.start, line.end);
                if (int !== false && !equal(this.start, int)) {
                    this.end = int;
                }
            });
            collisionLines.push(this);
            const len = dist(this.start, this.end);
            if (len > 4) {
                const f = 2-goldenRatio;
                allLines.push(new MountainLine(lerp(this.start, this.end, f), -this.dx));
            }
        }
    }
    for (let i=0; i<numberMountains; i++) {
        const top =[(i+.5*mountainRandom()-.25)*200/(numberMountains-1)-100, -mountainRandom()*35];
        allLines.push(new MountainLine(top, 1), new MountainLine(top,-1));
        mountainTops.push(top);
    }
    
    const dither = new Dither(turtle, (p, l) => noise.noise2D([0.5,l*.3]) + 1 - .05 * p[1]);
 
    do {
        allLines.sort( (a,b) => 2 * mountainRandom() - 1);
        const line = allLines.pop();
        line.walkAndSplit();
        
        turtle.jump(line.start);
        dither.goto(line.end);
    } while (allLines.length > 0);
}

function esjaSea() {
    const turtle = new Tortoise().addTransform(simplexDistort);
    
    for (let z=0; z<65; z+=1.5) {
        const y = 20 / (1 + z) * 100;
        const dither = new Dither(turtle, (p, l) => noise.noise2D([y,l*.3]) + 1.2 - (z/37));
        
        turtle.jump([-110,y]);
        dither.goto([110 ,y ]);
    }
}

////////////////////////////////////////////////////////////////
// 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];
}

function mountainRandom() {
    mountainSeed = 1103515245 * ((mountainSeed >> 1) ^ mountainSeed);
    mountainSeed = 1103515245 * (mountainSeed ^ (mountainSeed>>3));
    mountainSeed = mountainSeed ^ (mountainSeed >> 16);
    return mountainSeed / 1103515245 % 1;	
}

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 equal(a,b) { return .001>dist_sqr(a,b); }
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 segment_intersect(a,b,d,c) {
    const e=(c[1]-d[1])*(b[0]-a[0])-(c[0]-d[0])*(b[1]-a[1]);
    if(0==e)return false;
    c=((c[0]-d[0])*(a[1]-d[1])-(c[1]-d[1])*(a[0]-d[0]))/e;
    d=((b[0]-a[0])*(a[1]-d[1])-(b[1]-a[1])*(a[0]-d[0]))/e;
    return 0<=c&&1>=c&&0<=d&&1>=d?[a[0]+c*(b[0]-a[0]),a[1]+c*(b[1]-a[1])]:false;
}

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);
            
            for (let i=0; i<mountainTops.length; i++) {
                const top = mountainTops[i];
                if (Math.abs(top[0]-pt[0]) < pt[1]-top[1]) {
                    this.up();
                    break;
                }
            }
            
            
            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 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);
}