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