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