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);
}