One of the 'painted' eggs is upside down
Combining Hatchery Hatchakidée 🐣 and Eggs 🥚
Log in to post a comment.
const gridSize = 7; // You can find the Turtle API reference here: https://turtletoy.net/syntax Canvas.setpenopacity(1); // Global code will be evaluated once. init(); loadHatcheryNamespace(); const turtle = new Turtle(); const polygons = new Polygons(); const grid = new SquareGrid(gridSize, 5); class Hatchery { static getRandom(location) { return [Math.random()].map(rnd => [ (location) => new HerringboneHatching(1, [[7,0], [0,3.5]]), (location) => new ChevronHatching(0, [[9,0],[2,3]]), (location) => new CircularHatching(2, location), (location) => new HerringboneHatching(1, [[7,0], [2,3.5]], 3), (location) => new SpiralHatching(3, location, 1, 1, -1.4), (location) => new BrickHatching(.2, 5, 3), (location) => new TreadPlateHatching(7, .5, .7, 3), (location) => new SineHatching(10, 2, 1, 4), (location) => new PolkaDotHatching(Math.PI / 5, 6, 1.5), (location) => new HoneyCombHatching(1.2, 10), (location) => new TriangleHatching(1.5, 4), (location) => new QBertHatching(1.2, 8), (location) => new ZigZagHatching(7, 3, 2.55, 4), (location) => new WeaveHatching(8, .5, 1, .3, 3), (location) => new RadialHatching(20, location), (location) => new SineRadialHatching(15, location), (location) => new WeaveHatching(7, .7, 1, .5), (location) => new WeaveHatching(6, .4, .4, .4, 2, 0, false), (location) => new WeaveHatching(6, .8, .3, 1.4), (location) => new LineHatching(1 + Math.PI / 2, 2), (location) => new ZigZagRadialHatching(15, location), (location) => new BlockRadialHatching(15, location), ].filter((e,i,a)=>i==(rnd*a.length|0))[0](location)).pop(); } } // The walk function will be called until it returns false. const iterator = grid.iterator(); const chosen = Math.random() * grid.length | 0; const innerEgg = eggPoints(100).map(pt => V.add(pt, [0, 50])); const outerEgg = eggPoints(160).map(pt => V.add(pt, [0, 80])); let miniCount = 0; function walk(i) { if(miniCount > 0) { const rpt = [Math.random(), Math.random()].map(l => (l - .5) * 200); if(!Intersection.inside(outerEgg, rpt) && miniCount < 1000) return true; const p = polygons.create(); const s = Intersection.inside(innerEgg, rpt)? 40: !Intersection.inside(outerEgg, rpt)? 7: 25; p.addPoints(...eggPoints(s).map(pt => V.add([0, s/2], V.add(pt, rpt)))) p.addOutline(); polygons.draw(turtle, p); return miniCount++ < 5000; } const iteration = iterator.next(); const cell = iteration.value; const p = polygons.create(); p.addPoints(...eggPoints(cell.size).map(pt => V.add([0, cell.size/2 * (i == chosen?-1:1)], V.add(cell.center, V.trans(V.rot2d(i == chosen? Math.PI: 0), pt))))); p.addOutline(); p.addHatching(Hatchery.getRandom(V.add(cell.center, [0, -cell.size/1.8]))); polygons.draw(turtle, p); if(iteration.done) miniCount++; return true; } function eggPoints(height) { const width = 2 * height / (4 - Math.SQRT2); const innerRadius = width / 2; const upperRadius = height - width; const O = [0, -innerRadius]; //innerCenter const Q = [0, -width]; //upperCenter const A = [-innerRadius, -innerRadius]; //most left pt const B = [innerRadius, -innerRadius]; //most right pt return [ ...PT.arc(innerRadius, Math.PI/2, Math.PI/2).map(pt => V.add(pt, O)), ...PT.arc(2*innerRadius, Math.PI/4, Math.PI).map(pt => V.add(pt, B)), ...PT.arc(upperRadius, Math.PI/2, 5*Math.PI/4).map(pt => V.add(pt, Q)), ...PT.arc(2*innerRadius, Math.PI/4, -Math.PI/4).map(pt => V.add(pt, A)), ...PT.arc(innerRadius, Math.PI/2, 0).map(pt => V.add(pt, O)), ]; } //////////////////////////////////////////////////////////////// // Square Grid utility code - Created by Jurgen Westerhof 2024 // Modified: https://turtletoy.net/turtle/6c24a2a461 //////////////////////////////////////////////////////////////// function SquareGrid(grid, padding=2, margin = 5, populateFn = (column, row) => false, canvasSize = 200) { class SquareGrid { //cells[column][row] is a 2d array holding the values of the grid constructor(grid, padding = 2, margin = 5, populateFn = (column, row) => false, canvasSize = 200) {this.gridSize = grid;this.padding = padding;this.margin = margin;this.cells = Array.from({length: this.gridSize}).map((e, column) => Array.from({length: this.gridSize}).map((e, row) => populateFn(column, row)));this.canvasSize = canvasSize;this.resetCellSize();} resetCellSize() { this.cellSize = ((this.canvasSize - this.margin - this.margin - ((this.gridSize - 1) * this.padding)) / this.gridSize);} getColumnRow(i) { return [i % this.gridSize, i / this.gridSize | 0]; } getCellCenter(col, row) { return [col, row].map(v => this.margin - 100 + (v * this.padding) + ((v + .5) * this.cellSize)); } getCell(col, row) { return { center: this.getCellCenter(col, row), colrow: [col, row], iteration: col + row * this.gridSize, size: this.cellSize, value: this.cells[col][row] }} get length() { return this.gridSize**2; } *iterator() { let ptr = 0; while(ptr < this.length - 1) { yield this.getCell(...this.getColumnRow(ptr++)); } return this.getCell(...this.getColumnRow(ptr++)); } getValue(col, row) { return this.cells[col][row]; } setValue(col, row, value) { this.cells[col][row] = value; } isValid(col, row) { return 0 <= col && col < this.gridSize && 0 <= row && row < this.gridSize; } } return new SquareGrid(grid, padding, margin, populateFn, canvasSize); } function init() { /////////////////////////////////////////////////////// // Vector functions - Created by Jurgen Westerhof 2024 // https://turtletoy.net/turtle/d068ad6040 /////////////////////////////////////////////////////// class Vector { static add (a,b) { return a.map((v,i)=>v+b[i]); } static sub (a,b) { return a.map((v,i)=>v-b[i]); } static mul (a,b) { return a.map((v,i)=>v*b[i]); } static div (a,b) { return a.map((v,i)=>v/b[i]); } static scale(a,s) { return a.map(v=>v*s); } static det(m) { return m.length == 1? m[0][0]: m.length == 2 ? m[0][0]*m[1][1]-m[0][1]*m[1][0]: m[0].reduce((r,e,i) => r+(-1)**(i+2)*e*this.det(m.slice(1).map(c => c.filter((_,j) => i != j))),0); } static angle(a) { return Math.PI - Math.atan2(a[1], -a[0]); } //compatible with turtletoy heading static rot2d(angle) { return [[Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]]; } static rot3d(yaw,pitch,roll) { return [[Math.cos(yaw)*Math.cos(pitch), Math.cos(yaw)*Math.sin(pitch)*Math.sin(roll)-Math.sin(yaw)*Math.cos(roll), Math.cos(yaw)*Math.sin(pitch)*Math.cos(roll)+Math.sin(yaw)*Math.sin(roll)],[Math.sin(yaw)*Math.cos(pitch), Math.sin(yaw)*Math.sin(pitch)*Math.sin(roll)+Math.cos(yaw)*Math.cos(roll), Math.sin(yaw)*Math.sin(pitch)*Math.cos(roll)-Math.cos(yaw)*Math.sin(roll)],[-Math.sin(pitch), Math.cos(pitch)*Math.sin(roll), Math.cos(pitch)*Math.cos(roll)]]; } static trans(matrix,a) { return a.map((v,i) => a.reduce((acc, cur, ci) => acc + cur * matrix[ci][i], 0)); } //Mirror vector a in a ray through [0,0] with direction mirror static mirror2d(a,mirror) { return [Math.atan2(...mirror)].map(angle => this.trans(this.rot2d(angle), this.mul([-1,1], this.trans(this.rot2d(-angle), a)))).pop(); } static approx(a,b,p) { return this.len(this.sub(a,b)) < (p === undefined? .001: p); } static norm (a) { return this.scale(a,1/this.len(a)); } static len (a) { return Math.hypot(...a); } static lenSq (a) { return a.reduce((a,c)=>a+c**2,0); } static lerp (a,b,t) { return a.map((v, i) => v*(1-t) + b[i]*t); } static dist (a,b) { return Math.hypot(...this.sub(a,b)); } static dot (a,b) { return a.reduce((a,c,i) => a+c*b[i], 0); } static cross(...ab) { return ab[0].map((e, i) => ab.map(v => v.filter((ee, ii) => ii != i))).map((m,i) => (i%2==0?-1:1)*this.det(m)); } } this.V = Vector; class Intersection2D { //a-start, a-direction, b-start, b-direction //returns false on no intersection or [[intersection:x,y], scalar a-direction, scalar b-direction static info(as, ad, bs, bd) { const d = V.sub(bs, as), det = -V.det([bd, ad]); if(det === 0) return false; const res = [V.det([d, bd]) / det, V.det([d, ad]) / det]; return [V.add(as, V.scale(ad, res[0])), ...res]; } static ray(a, b, c, d) { return this.info(a, b, c, d); } static segment(a,b,c,d, inclusiveStart = true, inclusiveEnd = true) { const i = this.info(a, V.sub(b, a), c, V.sub(d, c)); return i === false? false: ( (inclusiveStart? 0<=i[1] && 0<=i[2]: 0<i[1] && 0<i[2]) && (inclusiveEnd? i[1]<=1 && i[2]<=1: i[1]<1 && i[2]<1) )?i[0]:false;} static tour(tour, segmentStart, segmentDirection) { return tour.map((e, i, a) => [i, this.info(e, V.sub(a[(i+1)%a.length], e), segmentStart, segmentDirection)]).filter(e => e[1] !== false && 0 <= e[1][1] && e[1][1] <= 1).filter(e => 0 <= e[1][2]).map(e => ({position: e[1][0],tourIndex: e[0],tourSegmentPortion: e[1][1],segmentPortion: e[1][2],}));} static inside(tour, pt) { return tour.map((e,i,a) => this.segment(e, a[(i+1)%a.length], pt, [Number.MAX_SAFE_INTEGER, 0], true, false)).filter(e => e !== false).length % 2 == 1; } static circles(centerA, radiusA, centerB, radiusB) {const result = {intersect_count: 0,intersect_occurs: true,one_is_in_other: false,are_equal: false,point_1: [null, null],point_2: [null, null],};const dx = centerB[0] - centerA[0];const dy = centerB[1] - centerA[1];const dist = Math.hypot(dy, dx);if (dist > radiusA + radiusB) {result.intersect_occurs = false;}if (dist < Math.abs(radiusA - radiusB) && !N.approx(dist, Math.abs(radiusA - radiusB))) {result.intersect_occurs = false;result.one_is_in_other = true;}if (V.approx(centerA, centerB) && radiusA === radiusB) {result.are_equal = true;}if (result.intersect_occurs) {const centroid = (radiusA**2 - radiusB**2 + dist * dist) / (2.0 * dist);const x2 = centerA[0] + (dx * centroid) / dist;const y2 = centerA[1] + (dy * centroid) / dist;const prec = 10000;const h = (Math.round(radiusA**2 * prec)/prec - Math.round(centroid**2 * prec)/prec)**.5;const rx = -dy * (h / dist);const ry = dx * (h / dist);result.point_1 = [x2 + rx, y2 + ry];result.point_2 = [x2 - rx, y2 - ry];if (result.are_equal) {result.intersect_count = null;} else if (result.point_1.x === result.point_2.x && result.point_1.y === result.point_2.y) {result.intersect_count = 1;} else {result.intersect_count = 2;}}return result;} } this.Intersection = Intersection2D; class PathTools { static bezier(p1, cp1, cp2, p2, steps = null) {steps = (steps === null? (V.len(V.sub(cp1, p1)) + V.len(V.sub(cp2, cp1)) + V.len(V.sub(p2, cp2))) | 0: steps) - 1;return Array.from({length: steps + 1}).map((v, i, a, f = i/steps) => [[V.lerp(p1, cp1, f),V.lerp(cp1, cp2, f),V.lerp(cp2, p2, f)]].map(v => V.lerp(V.lerp(v[0], v[1], f), V.lerp(v[1], v[2], f), f))[0]);} // https://stackoverflow.com/questions/18655135/divide-bezier-curve-into-two-equal-halves#18681336 static splitBezier(p1, cp1, cp2, p2, t=.5) {const e = V.lerp(p1, cp1, t);const f = V.lerp(cp1, cp2, t);const g = V.lerp(cp2, p2, t);const h = V.lerp(e, f, t);const j = V.lerp(f, g, t);const k = V.lerp(h, j, t);return [[p1, e, h, k], [k, j, g, p2]];} static circular(radius,verticeCount,rotation=0) {return Array.from({length: verticeCount}).map((e,i,a,f=i*2*Math.PI/verticeCount+rotation) => [radius*Math.cos(f),radius*Math.sin(f)])} static circle(r){return this.circular(r,Math.max(12, r*2*Math.PI|0));} static arc(radius, extend = 2 * Math.PI, clockWiseStart = 0, steps = null, includeLast = false) { return [steps == null? (radius*extend+1)|0: steps].map(steps => Array.from({length: steps}).map((v, i, a) => [radius * Math.cos(clockWiseStart + extend*i/(a.length-(includeLast?1:0))), radius * Math.sin(clockWiseStart + extend*i/(a.length-(includeLast?1:0)))])).pop(); } static draw(turtle, path) {path.forEach((pt, i) => turtle[i==0?'jump':'goto'](pt));} static drawTour(turtle, path) {this.draw(turtle, path.concat([path[0]]));} static drawPoint(turtle, pt, r = .1) {this.drawTour(turtle, this.circle(r).map(e => V.add(e, pt)));} static drawArrow(turtle, s, d, width = 6, length = 3) {turtle.jump(s);const arrowHeadBase = V.add(s,d);turtle.goto(arrowHeadBase);turtle.goto(V.add(arrowHeadBase, V.trans(V.rot2d(-V.angle(d)), [-length, width/2])));turtle.jump(V.add(arrowHeadBase, V.trans(V.rot2d(-V.angle(d)), [-length, -width/2])));turtle.goto(arrowHeadBase);} } this.PT = PathTools; class Complex { static add(a,b) { return V.add(a,b); } static sub(a,b) { return V.sub(a,b); } static scale(a,s) { return V.scale(a,s); } static mult(a,b) { return [a[0]*b[0]-a[1]*b[1],a[0]*b[1]+a[1]*b[0]]; } static sqrt(a) { return [[Math.hypot(...a)**.5, Math.atan2(...a.reverse()) / 2]].map(ra => [ra[0]*Math.cos(ra[1]), ra[0]*Math.sin(ra[1])]).pop(); } } this.C = Complex; class Numbers { static approx(a,b,p) { return Math.abs(a-b) < (p === undefined? .001: p); } static clamp(a, min, max) { return Math.min(Math.max(a, min), max); } } this.N = Numbers; } //////////////////////////////////////////////////////////////// // Polygon Clipping utility code - Created by Reinder Nijhoff 2019 // (Polygon binning by Lionel Lemarie 2021) https://turtletoy.net/turtle/95f33bd383 // (Delegated Hatching by Jurgen Westerhof 2024) https://turtletoy.net/turtle/d068ad6040 // (Deferred Polygon Drawing by Jurgen Westerhof 2024) https://turtletoy.net/turtle/6f3d2bc0b5 // https://turtletoy.net/turtle/a5befa1f8d // // const polygons = new Polygons(); // const p = polygons.create(); // polygons.draw(turtle, p); // polygons.list(); // polygons.startDeferSession(); // polygons.stopDeferring(); // polygons.finalizeDeferSession(turtle); // // p.addPoints(...[[x,y],]); // p.addSegments(...[[x,y],]); // p.addOutline(); // p.addHatching(angle, distance); OR p.addHatching(HatchObject); where HatchObject has a method 'hatch(PolygonClass, thisPolygonInstance)' // p.inside([x,y]); // p.boolean(polygon, diff = true); // p.segment_intersect([x,y], [x,y], [x,y], [x,y]); //////////////////////////////////////////////////////////////// function Polygons(){const t=[],s=25,e=Array.from({length:s**2},t=>[]),n=class{constructor(){this.cp=[],this.dp=[],this.aabb=[]}addPoints(...t){let s=1e5,e=-1e5,n=1e5,h=-1e5;(this.cp=[...this.cp,...t]).forEach(t=>{s=Math.min(s,t[0]),e=Math.max(e,t[0]),n=Math.min(n,t[1]),h=Math.max(h,t[1])}),this.aabb=[s,n,e,h]}addSegments(...t){t.forEach(t=>this.dp.push(t))}addOutline(){for(let t=0,s=this.cp.length;t<s;t++)this.dp.push(this.cp[t],this.cp[(t+1)%s])}draw(t){for(let s=0,e=this.dp.length;s<e;s+=2)t.jump(this.dp[s]),t.goto(this.dp[s+1])}addHatching(t, s) {if(typeof t == 'object') return t.hatch(n, this);const e=new n;e.cp.push([-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);const h=Math.sin(t)*s,o=Math.cos(t)*s,a=200*Math.sin(t),i=200*Math.cos(t);for(let t=.5;t<150/s;t++) {e.dp.push([h*t+i,o*t-a],[h*t-i,o*t+a]);e.dp.push([-h*t+i,-o*t-a],[-h*t-i,-o*t+a]);}e.boolean(this,!1);this.dp=[...this.dp,...e.dp]}inside(t){let s=0;for(let e=0,n=this.cp.length;e<n;e++)this.segment_intersect(t,[.1,-1e3],this.cp[e],this.cp[(e+1)%n])&&s++;return 1&s}boolean(t,s=!0){const e=[];for(let n=0,h=this.dp.length;n<h;n+=2){const h=this.dp[n],o=this.dp[n+1],a=[];for(let s=0,e=t.cp.length;s<e;s++){const n=this.segment_intersect(h,o,t.cp[s],t.cp[(s+1)%e]);!1!==n&&a.push(n)}if(0===a.length)s===!t.inside(h)&&e.push(h,o);else{a.push(h,o);const n=o[0]-h[0],i=o[1]-h[1];a.sort((t,s)=>(t[0]-h[0])*n+(t[1]-h[1])*i-(s[0]-h[0])*n-(s[1]-h[1])*i);for(let n=0;n<a.length-1;n++)(a[n][0]-a[n+1][0])**2+(a[n][1]-a[n+1][1])**2>=.001&&s===!t.inside([(a[n][0]+a[n+1][0])/2,(a[n][1]+a[n+1][1])/2])&&e.push(a[n],a[n+1])}}return(this.dp=e).length>0}segment_intersect(t,s,e,n){const h=(n[1]-e[1])*(s[0]-t[0])-(n[0]-e[0])*(s[1]-t[1]);if(0===h)return!1;const o=((n[0]-e[0])*(t[1]-e[1])-(n[1]-e[1])*(t[0]-e[0]))/h,a=((s[0]-t[0])*(t[1]-e[1])-(s[1]-t[1])*(t[0]-e[0]))/h;return o>=0&&o<=1&&a>=0&&a<=1&&[t[0]+o*(s[0]-t[0]),t[1]+o*(s[1]-t[1])]}};const y=function(n,j=[]){const h={},o=200/s;for(var a=0;a<s;a++){const c=a*o-100,r=[0,c,200,c+o];if(!(n[3]<r[1]||n[1]>r[3]))for(var i=0;i<s;i++){const c=i*o-100;r[0]=c,r[2]=c+o,n[0]>r[2]||n[2]<r[0]||e[i+a*s].forEach(s=>{const e=t[s];n[3]<e.aabb[1]||n[1]>e.aabb[3]||n[0]>e.aabb[2]||n[2]<e.aabb[0]||j.includes(s)||(h[s]=1)})}}return Array.from(Object.keys(h),s=>t[s])};return{list:()=>t,create:()=>new n,draw:(n,h,o=!0)=>{rpl=y(h.aabb, this.dei === undefined? []: Array.from({length: t.length - this.dei}).map((e, i) => this.dsi + i));for(let t=0;t<rpl.length&&h.boolean(rpl[t]);t++);const td=n.isdown();if(this.dsi!==undefined&&this.dei===undefined)n.pu();h.draw(n),o&&function(n){t.push(n);const h=t.length-1,o=200/s;e.forEach((t,e)=>{const a=e%s*o-100,i=(e/s|0)*o-100,c=[a,i,a+o,i+o];c[3]<n.aabb[1]||c[1]>n.aabb[3]||c[0]>n.aabb[2]||c[2]<n.aabb[0]||t.push(h)})}(h);if(td)n.pd();},startDeferSession:()=>{if(this.dei!==undefined)throw new Error('Finalize deferring before starting new session');this.dsi=t.length;},stopDeferring:()=>{if(this.dsi === undefined)throw new Error('Start deferring before stopping');this.dei=t.length;},finalizeDeferSession:(n)=>{if(this.dei===undefined)throw new Error('Stop deferring before finalizing');for(let i=this.dsi;i<this.dei;i++) {rpl = y(t[i].aabb,Array.from({length:this.dei-this.dsi+1}).map((e,j)=>i+j));for(let j=0;j<rpl.length&&t[i].boolean(rpl[j]);j++);t[i].draw(n);}this.dsi=undefined;this.dei=undefined;}}} /////////////////////////////////////////////////////////////////// // Polygon Hatching utility code - Created by Jurgen Westerhof 2024 // https://turtletoy.net/turtle/d068ad6040 // //////////////////////////////////////////////////////////////// // // To be used with modified Polygon Clipping utility code // // Orginal: https://turtletoy.net/turtle/a5befa1f8d // // Polygon binning: https://turtletoy.net/turtle/95f33bd383 // // Delegated Hatching: https://turtletoy.net/turtle/d068ad6040 /////////////////////////////////////////////////////////////////// function loadHatcheryNamespace() { //for convenience on Turtletoy you can click the arrow to the right of the line number to collapse a class //////////////////////////////////////////////////// root class PolygonHatching { constructor() { if (this.constructor === PolygonHatching) { throw new Error("PolygonHatching is an abstract class and can't be instantiated."); } this.minX = -100; this.minY = -100; this.maxX = 100; this.maxY = 100; this.width = 200; this.height = 200; this.segments = []; this.init(); } hatch(polygonsClass, thePolygonToHatch) { const e = new polygonsClass; e.cp.push(...thePolygonToHatch.aabb);//[-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]); this.addHatchSegments(e.dp); e.boolean(thePolygonToHatch,!1); thePolygonToHatch.dp=[...thePolygonToHatch.dp,...e.dp] } addHatchSegments(segments) { this.getSegments().forEach(e => segments.push(e)); } getSegments() { return this.segments; } init() { /////////////////////////////////////////////////////// // Vector functions - Created by Jurgen Westerhof 2024 // https://turtletoy.net/turtle/d068ad6040 /////////////////////////////////////////////////////// class Vector { static add (a,b) { return a.map((v,i)=>v+b[i]); } static sub (a,b) { return a.map((v,i)=>v-b[i]); } static mul (a,b) { return a.map((v,i)=>v*b[i]); } static div (a,b) { return a.map((v,i)=>v/b[i]); } static scale(a,s) { return a.map(v=>v*s); } static det(m) { return m.length == 1? m[0][0]: m.length == 2 ? m[0][0]*m[1][1]-m[0][1]*m[1][0]: m[0].reduce((r,e,i) => r+(-1)**(i+2)*e*this.det(m.slice(1).map(c => c.filter((_,j) => i != j))),0); } static angle(a) { return Math.PI - Math.atan2(a[1], -a[0]); } //compatible with turtletoy heading static rot2d(angle) { return [[Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]]; } static rot3d(yaw,pitch,roll) { return [[Math.cos(yaw)*Math.cos(pitch), Math.cos(yaw)*Math.sin(pitch)*Math.sin(roll)-Math.sin(yaw)*Math.cos(roll), Math.cos(yaw)*Math.sin(pitch)*Math.cos(roll)+Math.sin(yaw)*Math.sin(roll)],[Math.sin(yaw)*Math.cos(pitch), Math.sin(yaw)*Math.sin(pitch)*Math.sin(roll)+Math.cos(yaw)*Math.cos(roll), Math.sin(yaw)*Math.sin(pitch)*Math.cos(roll)-Math.cos(yaw)*Math.sin(roll)],[-Math.sin(pitch), Math.cos(pitch)*Math.sin(roll), Math.cos(pitch)*Math.cos(roll)]]; } static trans(matrix,a) { return a.map((v,i) => a.reduce((acc, cur, ci) => acc + cur * matrix[ci][i], 0)); } //Mirror vector a in a ray through [0,0] with direction mirror static mirror2d(a,mirror) { return [Math.atan2(...mirror)].map(angle => this.trans(this.rot2d(angle), this.mul([-1,1], this.trans(this.rot2d(-angle), a)))).pop(); } static approx(a,b,p) { return this.len(this.sub(a,b)) < (p === undefined? .001: p); } static norm (a) { return this.scale(a,1/this.len(a)); } static len (a) { return Math.hypot(...a); } static lenSq (a) { return a.reduce((a,c)=>a+c**2,0); } static lerp (a,b,t) { return a.map((v, i) => v*(1-t) + b[i]*t); } static dist (a,b) { return Math.hypot(...this.sub(a,b)); } static dot (a,b) { return a.reduce((a,c,i) => a+c*b[i], 0); } static cross(...ab) { return ab[0].map((e, i) => ab.map(v => v.filter((ee, ii) => ii != i))).map((m,i) => (i%2==0?-1:1)*this.det(m)); } } this.V = Vector; class Intersection2D { //a-start, a-direction, b-start, b-direction //returns false on no intersection or [[intersection:x,y], scalar a-direction, scalar b-direction static info(as, ad, bs, bd) { const d = V.sub(bs, as), det = -V.det([bd, ad]); if(det === 0) return false; const res = [V.det([d, bd]) / det, V.det([d, ad]) / det]; return [V.add(as, V.scale(ad, res[0])), ...res]; } static ray(a, b, c, d) { return this.info(a, b, c, d); } static segment(a,b,c,d, inclusiveStart = true, inclusiveEnd = true) { const i = this.info(a, V.sub(b, a), c, V.sub(d, c)); return i === false? false: ( (inclusiveStart? 0<=i[1] && 0<=i[2]: 0<i[1] && 0<i[2]) && (inclusiveEnd? i[1]<=1 && i[2]<=1: i[1]<1 && i[2]<1) )?i[0]:false; } } this.Intersection = Intersection2D; } } this.PolygonHatching = PolygonHatching; //////////////////////////////////////////////////// first gen // extending PolygonHatching class CircularHatching extends PolygonHatching { constructor(distance = 360, center = [0,0], precision = 1) { super(); const dist = typeof distance == 'function'? distance: (c) => distance; this.segments = []; for(let j = 0, r = dist(j)/2; r < 201; r+=dist(j++)) { for(let i = 0, max = Math.max(12, 2*Math.PI*r/precision | 0); i < max; i++) { this.segments.push( [center[0]+r*Math.sin(i*2*Math.PI/max),center[1]+r*-Math.cos(i*2*Math.PI/max)], [center[0]+r*Math.sin((i+1)*2*Math.PI/max),center[1]+r*-Math.cos((i+1)*2*Math.PI/max)] ); } } } } this.CircularHatching = CircularHatching; class GridHatching extends PolygonHatching { constructor(cellSize, angle = 0) { super(); if (this.constructor === GridHatching) { throw new Error("GridHatching is an abstract class and can't be instantiated."); } this.cellSize = cellSize; this.rotation = this.V.rot2d(angle); this.grid = ((this.width * 1.5 / cellSize) | 0) + 1; this.cells = this.grid**2; this.segments = []; } getCell(n) { if(n > this.cells) return false; const col = n % this.grid; const row = n / this.grid | 0; return [ (col + .5) - (this.grid / 2), (row + .5) - (this.grid / 2), ]; } oddness(cellN, n = 2) { return ((cellN % this.grid) // column + (cellN / this.grid | 0)) // row % n; } getSegments() { if(this.segments.length == 0) { const segmentsSet = this.getSegmentsSet(); for(let i = 0; i < this.cells; i++) { segmentsSet[this.oddness(i, segmentsSet.length)].forEach(segment => { const cellLocation = this.getCell(i); this.segments.push( this.V.trans(this.rotation, this.V.scale(this.V.add(cellLocation, segment[0]), this.cellSize)), this.V.trans(this.rotation, this.V.scale(this.V.add(cellLocation, segment[1]), this.cellSize)) ); }); } } return this.segments; } getSegmentsSet() { return [this.getTileSegments()].flatMap(ts => [ts, ts.map(segment => segment.map(pt => [...pt].reverse()))]); } } this.GridHatching = GridHatching; class LaticeHatching extends PolygonHatching { constructor(angle = 0, xUnit = [10, 0], yUnit = [0, 5]) { super(); this.tile = [[...xUnit], [...yUnit]]; this.rotation = this.V.rot2d(angle); } setLaticeSegments(info) { for(let i = 0, max = info.columns * info.rows - 1; i < max; i++) { const col = i % info.columns; const row = i / info.columns | 0; const position = this.V.add(this.V.scale(info.rowIncrement, row - info.rows / 2 + .5), this.V.scale(info.columnIncrement, col - info.columns / 2)); info.lines.forEach(e => { this.segments.push( this.V.trans(this.rotation, this.V.add(e[0], position)), this.V.trans(this.rotation, this.V.add(e[1], position)) )}); } } } this.LaticeHatching = LaticeHatching; class LineHatching extends PolygonHatching { constructor(angle, distance, inp = 0) { super(); const h=Math.sin(angle)*distance,o=Math.cos(angle)*distance,a=200*Math.sin(angle),i=200*Math.cos(angle); this.segments = Array.from({length: 150/distance}).flatMap((x,y,z,t=.5+y) => [ [h*t+i+inp*(Math.random()-.5),o*t-a+inp*(Math.random()-.5)],[h*t-i+inp*(Math.random()-.5),o*t+a+inp*(Math.random()-.5)], [-h*t+i+inp*(Math.random()-.5),-o*t-a+inp*(Math.random()-.5)],[-h*t-i+inp*(Math.random()-.5),-o*t+a+inp*(Math.random()-.5)] ]) } } this.LineHatching = LineHatching; class PoissonDiscHatching extends PolygonHatching { constructor(radius = 1.75, connect = false, randomGrowOrder = .1, loosePacking = .5, startPoint = [0,0]) { super(); //////////////////////////////////////////////////////////////// // Poisson-Disc utility code. Created by Reinder Nijhoff 2019 // https://turtletoy.net/turtle/b5510898dc //////////////////////////////////////////////////////////////// function PoissonDisc(startPoints, radius) { class PoissonDiscGrid { constructor(sp, radius) { this.cellSize = 1/Math.sqrt(2)/radius; this.radius2 = radius*radius; this.cells = []; sp.forEach( p => this.insert(p) ); } insert(p) { const x = p[0]*this.cellSize|0, y=p[1]*this.cellSize|0; for (let xi = x-1; xi<=x+1; xi++) { for (let yi = y-1; yi<=y+1; yi++) { const ps = this.cell(xi,yi); for (let i=0; i<ps.length; i++) { if ((ps[i][0]-p[0])**2 + (ps[i][1]-p[1])**2 < this.radius2) { return false; } } } } this.cell(x, y).push(p); return true; } cell(x,y) { const c = this.cells; return (c[x]?c[x]:c[x]=[])[y]?c[x][y]:c[x][y]=[]; } } class PoissonDisc { constructor(sp, radius) { this.result = [...sp]; this.active = [...sp]; this.grid = new PoissonDiscGrid(sp, radius); } addPoints(count, maxTries=16, loosePacking=0, randomGrowOrder=0) { mainLoop: while (this.active.length > 0 && count > 0) { const index = (Math.random() * this.active.length * randomGrowOrder) | 0; const point = this.active[index]; for (let i=0; i < maxTries; i++) { const a = Math.random() * 2 * Math.PI; const d = (Math.random()*loosePacking + 1) * radius; const p = [point[0] + Math.cos(a)*d, point[1] + Math.sin(a)*d, point]; if (this.grid.insert(p)) { this.result.push(p); this.active.push(p); count--; continue mainLoop; } } this.active.splice(index, 1); } return this.result; } } return new PoissonDisc(startPoints, radius); } const disc = new PoissonDisc([startPoint], radius); let index = 0; const maxR = Math.max( this.V.lenSq(this.V.sub([this.minX, this.minY], startPoint)), this.V.lenSq(this.V.sub([this.maxX, this.minY], startPoint)), this.V.lenSq(this.V.sub([this.minX, this.maxY], startPoint)), this.V.lenSq(this.V.sub([this.maxX, this.maxY], startPoint)) ); while(disc.active.some(pt => this.V.lenSq([pt[0], pt[1]]) < maxR)) { const points = disc.addPoints(1, 32, loosePacking, randomGrowOrder); while (index<points.length) { if (points[index][2]) { this.segments.push( points[index], connect? points[index][2]: this.V.add(points[index], [0,.25]) ) } index++; } } } } this.PoissonDiscHatching = PoissonDiscHatching; class RadialHatching extends PolygonHatching { constructor(n = 30, origin = [0,0], phaseExp = 1, phaseMod = 1, ampMod = 2, angle = 0) { super(); function redistributeLinearPath(path, length = .5) {path = path.map(pt => [...pt]); const sub = (a,b) => a.map((v,i)=>v-b[i]);const len = (a) => Math.hypot(...a);const lerp = (a,b,t) => a.map((v, i) => v*(1-t) + b[i]*t);const result = [path[0]];const vectors = Array.from({length: path.length - 1}).map((e, i) => [path[i], len(sub(path[i+1], path[i]))]);done: {for(let i = 0; i < vectors.length; i++) {let l = length;while(l > 0 && i < vectors.length) {l-=vectors[i][1];i++;if(i == vectors.length) break done;}i--;[lerp(path[i], path[i+1], -l/vectors[i][1])].forEach(v => {result.push(v);path[i] = v;vectors[i] = [v, len(sub(path[i+1], v))];});}}result.push(path[path.length - 1]);return result;} const rotation = this.V.rot2d(angle); const diameter = (1.5 + 2*this.V.len(origin)/this.width) * this.width; const path = redistributeLinearPath(Array.from({length: ((diameter / 2) ** 2| 0) + 1}).map((e, i) => this.V.trans(this.V.rot2d(this.getX(i**.5, phaseExp, phaseMod) * Math.PI * ampMod / n), [i**.5, 0])), this.getRedistributionPrecision()); for(let i = 0; i < n; i++) { const currentPath = path.map(e => this.V.trans(rotation, this.V.add(origin, this.V.trans(this.V.rot2d(i * 2 * Math.PI / n), e)))); for(let i = 0; i < currentPath.length - 1; i++) { this.segments.push(currentPath[i], currentPath[i+1]); } } } getX(i, phaseExp, phaseMod) { return 0; } getRedistributionPrecision() { return .25; } } this.RadialHatching = RadialHatching; class SineHatching extends PolygonHatching { constructor(waveLength = 5, amplitude = 1, angle = 0, distance = 1, samplesPerWavelength = waveLength * amplitude) { super(); const rotation = this.V.rot2d(angle); const oneWaveLengthPath = Array.from({length: samplesPerWavelength}).map((v, i, a, f = i/a.length) => [f*waveLength, Math.sin(f*2*Math.PI) * amplitude]); const halfNWaveLengths = (this.maxX * 1.5 / waveLength | 0) + 1; const fullWave = Array.from({length: halfNWaveLengths * 2}) .map((v, i) => [(i - halfNWaveLengths) * waveLength, 0]) .flatMap(v => oneWaveLengthPath.map(pt => this.V.add(v, pt))) .map((v, i, a) => [v, a[(i+1)%a.length]]); fullWave.pop(); for(let y = ((this.minY * 1.5) / distance | 0) - 1; y < ((this.maxY * 1.5) / distance | 0) + 1; y++) { fullWave.forEach(segment => { this.segments.push( this.V.trans(rotation, this.V.add([0, y * distance], segment[0])), this.V.trans(rotation, this.V.add([0, y * distance], segment[1])) ); }); } } } this.SineHatching = SineHatching; //////////////////////////////////////////////////// second gen // extending GridHatching class TreadPlateHatching extends GridHatching { constructor(size = 7, width = .4, length = .8, angle = 0, treads = 2, skew = 0) { super(size, angle); this.width = width; this.length = length; this.treads = treads; this.skew = skew; } getTileSegments() { const l = (2-this.width) * this.length; const w = this.width/(2*this.treads -1); const r = (w**2 + l**2) / (4*w); const skewAngle = (this.skew/10)*2*Math.PI; const rotationSkewMatrix = this.V.rot2d(skewAngle); const alpha = Math.asin(l/(2*r)); const steps = (this.cellSize * 2) | 0; const arc = []; for(let i = 0; i < steps; i++) { arc.push([ r * Math.cos(Math.PI/2 - alpha + (i * 2 * alpha / steps)), r * (Math.sin(Math.PI/2 - alpha + (i * 2 * alpha / steps)) - Math.sin(Math.PI/2 - alpha)) ]) } arc.forEach(pt => arc.push([-pt[0], -pt[1]])); const path = arc.map(pt => [this.V.trans(rotationSkewMatrix, pt)].map(ppt => [pt[0], ppt[1]]).pop()) const paths = []; for(let i = 0; i < this.treads; i++) { paths.push(path.map(pt => [pt[0], pt[1] - ((this.treads-1) * w) + (2*i*w)])); } return paths.flatMap(path => path.map((pt, i, a) => [pt, a[(i+1)%a.length]])); } } this.TreadPlateHatching = TreadPlateHatching; class WeaveHatching extends GridHatching { constructor(size = 7, width = .7, length = 1, angle = 0, threads = 1, skew = 0, closeSegments = true) { super(size, angle); this.width = width; this.length = length; this.threads = threads; this.skew = skew; this.closeSegments = closeSegments; } getTileSegments() { const skewAngle = (this.skew/10)*2*Math.PI; const rotationSkewMatrix = this.V.rot2d(skewAngle); const path = [ [(-.5 - (1-this.width)/2) * this.length, -this.width/2], [(.5 + (1-this.width)/2) * this.length, -this.width/2], [(.5 + (1-this.width)/2) * this.length, this.width/2], [(-.5 - (1-this.width)/2) * this.length, this.width/2] ].map((pt, i) => this.V.add(pt, [(i < 2? .5:-.5)*Math.sin(skewAngle), 0])) .map(pt => this.V.trans(rotationSkewMatrix, pt)); const segments = path.map((pt, i, a) => [pt, a[(i+1)%a.length]]) .map((segment, i, a) => i < a.length/2? segment: [...segment.reverse()]); const hSegments = segments.filter((v, i) => i%2 == 0); const vSegments = segments.filter((v, i) => i%2 == 1 && (this.length < 1 || this.skew != 0)); return Array.from({length: this.threads+1}).map((e, i) => [this.V.lerp(hSegments[0][0], hSegments[1][0], i/this.threads), this.V.lerp(hSegments[0][1], hSegments[1][1], i/this.threads)]).concat(this.closeSegments? vSegments: []); } } this.WeaveHatching = WeaveHatching; // extending LaticeHatching class ChevronHatching extends LaticeHatching { constructor(angle, tile, offset = 0, margin = 0) { super(angle, ...tile); this.setLaticeSegments(this.getInfo(offset, margin)); } getInfo(offset, margin) { const mirror = this.V.mirror2d(...this.tile); const vOffset = this.V.scale(this.tile[1], offset); const vMargin = this.tile.map(v => this.V.scale(v, margin/this.V.len(v)/2)); const lines = [...this.tile.map((v, i) => [[0,0], this.V.sub(v, this.V.scale(vMargin[i], 2))])]; if(margin == 0) { lines.push([[0,0], mirror].map(e => this.V.add(vOffset, e))); lines.push(lines[1].map(pt => this.V.add(pt, lines[2][1]))); } else { lines.push(lines[0].map(v => this.V.add(lines[1][1], v))); lines.push(lines[1].map(v => this.V.add(lines[0][1], v))); [this.V.add(...vMargin)].forEach(os => lines.forEach((e, i) => lines[i] = lines[i].map(pt => this.V.add(pt, os))) ); lines.forEach(l => lines.push(l.map(pt => this.V.add(vOffset, this.V.mirror2d(pt, this.tile[1]))))); } return { lines: lines, columns: ((this.width * 1.5 + this.V.len(this.V.add(this.V.add(vOffset, mirror), this.tile[0]))) / this.V.len(this.tile[1]) | 0) + 2, rows: (this.width * 1.5 / this.V.len(this.V.sub(this.tile[0], mirror)) | 0) + 2, columnIncrement: [...this.tile[1]], rowIncrement: this.V.add(this.V.scale(this.tile[0], -1), mirror), } } } this.ChevronHatching = ChevronHatching; class HerringboneHatching extends LaticeHatching { constructor(angle, tile, poly = 1) { super(angle, ...tile); this.setLaticeSegments(this.getInfo(poly)); } getInfo(poly) { const otherTile = this.tile.map((e, i, a) => this.V.scale(e, this.V.len(a[(i+1)%a.length])/this.V.len(e))) const colInc = this.V.add(this.tile[1], otherTile[0]); const rowInc = this.V.sub(otherTile[1], this.tile[0]); const lines = [[this.V.scale(otherTile[0], -1), this.tile[0]], [[0,0], this.V.add(this.tile[1], otherTile[1])]]; const colWidth = this.V.len(colInc); const colStartOffset = colWidth / 2 + this.V.len(this.V.sub(...lines[0])); const rowHeight = this.V.len(rowInc); for(let i = 1; i < poly; i++) { lines.push([this.V.scale(this.tile[1] , i / poly)].map(v => [v, this.V.add(v, this.tile[0] )]).pop()); lines.push([this.V.scale(otherTile[0], i / -poly)].map(v => [v, this.V.add(v, otherTile[1])]).pop()); } return { lines: lines, columns: ((this.width * 1.5 + colStartOffset) / colWidth | 0) + 2, rows: (this.width * 1.5 / rowHeight | 0) + 2, columnIncrement: colInc, rowIncrement: rowInc, } } } this.HerringboneHatching = HerringboneHatching; class HexGridHatching extends LaticeHatching { constructor(angle = 0, size = 10, cellSize = 1) { const vSize = size/2; const hSize = vSize*3**.5/2; super( angle, ...[[hSize, 1.5*vSize], [2*hSize, 0]] ); this.vSize = vSize; this.hSize = hSize; this.setLaticeSegments(this.getInfo(cellSize)); } getInfo(cellSize) { return { lines: this.getPath() .map(pt => this.V.scale(pt, cellSize)) .map((e,i,a) => [e, a[(i+1)%a.length]]) .filter((e,i) => cellSize < 1 || i < 3), columns: this.width * 1.5 / this.V.len(this.tile[0]) | 0, rows: this.height * 1.5 / this.V.len(this.tile[1]) | 0, columnIncrement: [...this.tile[1]], rowIncrement: [...this.tile[0]], } } getPath() { return [ [-this.hSize, this.vSize/2], [-this.hSize, -this.vSize/2], [ 0, -this.vSize ], [ this.hSize, -this.vSize/2], [ this.hSize, this.vSize/2], [ 0, this.vSize ] ]; } } this.HexGridHatching = HexGridHatching; // extending RadialHatching class BlockRadialHatching extends RadialHatching { constructor(n = 30, origin = [0,0], phaseExp = 1, phaseMod = 1, ampMod = 1, angle = 0) { super(n, origin, phaseExp, phaseMod, ampMod, angle); } getX(i, phaseExp, phaseMod) { return Math.sign(Math.sin(i**phaseExp*phaseMod)); } getRedistributionPrecision() { return .1; } } this.BlockRadialHatching = BlockRadialHatching; class SineRadialHatching extends RadialHatching { getX(i, phaseExp, phaseMod) { return Math.sin(i**phaseExp*phaseMod); } } this.SineRadialHatching = SineRadialHatching; class SpiralHatching extends RadialHatching { getX(i, phaseExp, phaseMod) { return i**phaseExp*phaseMod; } } this.SpiralHatching = SpiralHatching; class ZigZagRadialHatching extends RadialHatching { getX(i, phaseExp, phaseMod) { return 2 * Math.asin(Math.sin(i**phaseExp*phaseMod)) / Math.PI; } } this.ZigZagRadialHatching = ZigZagRadialHatching; // extending SineHatching class ZigZagHatching extends SineHatching { constructor(waveLength = 5, amplitude = 2, angle = 0, distance = 1) { super(waveLength, amplitude, angle, distance, 4); } } this.ZigZagHatching = ZigZagHatching; //////////////////////////////////////////////////// third gen // extending ChevronHatchting class BrickHatching extends ChevronHatching { constructor(angle = 0, width = 5, height = 2.5, margin = 0) { super(angle, [[0,height], [width,0]], .5, margin); } } this.BrickHatching = BrickHatching; // extending HexGridHatching class HoneyCombHatching extends HexGridHatching { constructor(angle, hexSize, drawSize = .8) { super(angle, hexSize, drawSize); } } this.HoneyCombHatching = HoneyCombHatching; class QBertHatching extends HexGridHatching { getInfo() { return { lines: this.getPath().map(e => [[0, 0], e]), columns: this.width * 1.5 / Math.max(this.tile[0][0], this.tile[1][0]) | 0, rows: this.height * 1.5 / Math.max(this.tile[0][1], this.tile[1][1]) | 0, columnIncrement: [...this.tile[1]], rowIncrement: [...this.tile[0]], } } } this.QBertHatching = QBertHatching; class TriangleHatching extends HexGridHatching { getPath() { return super.getPath().filter((e,i) => i%2 == 1); } getInfo(cellSize) { const info = super.getInfo(cellSize); info.columns *= 2; info.rows *= 2; return info; } } this.TriangleHatching = TriangleHatching; // extending TreadPlateHatching class PolkaDotHatching extends TreadPlateHatching { constructor(angle = 0, distance = 5, radius = 1) { super(distance, 2 * (radius+(radius == distance? .00001: 0))/distance, radius/(distance-radius+(radius == distance? .00001: 0)), angle, 1); } } this.PolkaDotHatching = PolkaDotHatching; }