You can create a pattern using the path tool. To make it easier, consider enabling 'helperGrid' to display the snap points your path will align to, and adjust 'snapGrid' to set the grid size for the pattern. For the most visually appealing results, begin your path at a leftmost point and end it at a rightmost point with the same vertical alignment.
The nBlocks setting controls how many times the pattern is repeated around a circle defined by innerRadius and outerRadius. Enabling the 'mirror' option lets you explore alternative versions of your meander design.
Meander disc ⌘ (variation)
Meander disc ⌘ (variation)
Log in to post a comment.
const path = 'M-90,90L-30,90L-30,30L-60,30L-60,60L-90,60L-90,-90L0,-90L0,-60L-60,-60L-60,0L-30,0L-30,-30L30,-30L30,-90L90,-90L90,-60L60,-60L60,-30L90,-30L90,60L30,60L30,30L60,30L60,0L30,0L0,0L0,90L90,90'; //type=path const snapGrid = 7; //min=4 max=14 step=1 const nBlocks = 12; //min=1 max=50 step=1 const innerRadius = 50; //min=0 max=100 step=1 const outerRadius = 80; //min=0 max=100 step=1 const mirror = 2; //min=0 max=2 step=1 (No, Yes, Toggle when nBlock is even) const extraCircle = 1; //min=0 max=1 step=1 (No, Yes) const helperGrid = 0; //min=0 max=1 step=1 (No, Yes) const snapResolution = 100 / snapGrid; // You can find the Turtle API reference here: https://turtletoy.net/syntax Canvas.setpenopacity(1); // Global code will be evaluated once. turtlelib_init(); const turtle = new Turtle(); const gridPoints = Array.from({length: snapGrid}).flatMap((e, c) => Array.from({length: snapGrid}).map((e, r) => [180*c/(snapGrid-1) - 90, 180*r/(snapGrid-1) - 90])); const snappedPath = snapPathToGrid(path, gridPoints); const thetaSpan = 2 * Math.PI / nBlocks; const transformedBlocks = ((pts) => { const transformedBlock = mapBlockToWedge(pts, thetaSpan, innerRadius, outerRadius); const transformedBlockMirrored = mapBlockToWedge(pts.map((e, i, a) => [-a[a.length-1-i][0] + 1, a[a.length-1-i][1]]), thetaSpan, innerRadius, outerRadius); return [transformedBlock, transformedBlockMirrored]; })(snappedPath); // The walk function will be called until it returns false. function walk(i) { if(helperGrid == 1) { gridPoints.forEach(pt => { turtle.jump(pt[0], pt[1]-2); turtle.circle(2); }); return false; } const useBlock = transformedBlocks[((mirror < 2 || nBlocks % 2 == 1)? mirror % 2: i)%2]; if(i == 0) { if(extraCircle == 1) { turtle.jump(0, -outerRadius); turtle.circle(outerRadius); } turtle.jump(useBlock[0]); } const rot = V.rot2d(-i * 2 * Math.PI / nBlocks); useBlock.forEach(pt => turtle.goto(V.trans(rot, pt))); return i < nBlocks - 1; } function snapPathToGrid(path, grid) { const po = new Path(path); const sl = (po.length() / snapResolution) | 0; const snappedPattern = Array.from({length: sl + 1}, (e, i) => po.p(i/sl).filter((e,i) => i < 2)) .map(pt => [pt[0], -pt[1]]) .map(pt => grid.map(ppt => [ppt, V.lenSq(V.sub(pt, ppt))]).sort((a,b) => a[1] < b[1]? 1: -1).pop()[0]) .reduce((a, c) => { if(a.length == 0) { return [c]; } if(a[a.length - 1][0] == c[0] && a[a.length - 1][1] == c[1]) { return a; } return [...a, c]; }, []) .reduce((a, c, i, arr) => { if(i == 0) { return [c]; } if(i == arr.length - 1) { return [...a, c]; } if( (arr[i - 1][0] == c[0] && c[0] == arr[i + 1][0]) || (arr[i - 1][1] == c[1] && c[1] == arr[i + 1][1]) ) { return a; } return [...a, c]; }, []) .map(pt => V.scale(V.add(pt, [90 + (90 / snapGrid), 90]), 1/(180 + (180 / snapGrid)) )); return [ [0, snappedPattern[0][1]], ...snappedPattern, [1, snappedPattern[snappedPattern.length - 1][1]] ]; } function mapBlockToWedge(block, thetaSpan, innerRadius, outerRadius) { return block.flatMap((pt, i, a) => { const radius = innerRadius + pt[1] * (outerRadius - innerRadius); const [steps, to] = (i == a.length - 1 || pt[0] === a[i+1][0])? [1, [...pt]]: [Math.max(20, Math.abs((pt[0] - a[i+1][0]) * 2 * radius) | 0), a[i+1]]; const result = []; for(let j = 0; j <= 1; j+= 1/steps) { const ppt = V.add(pt, V.scale(V.sub(to, pt), j)); const theta = ppt[0] * thetaSpan; const px = radius * Math.cos(theta); const py = radius * Math.sin(theta); result.push([px, py]); } return result; }); } //////////////////////////////////////////////////////////////// // Path utility code. Created by Reinder Nijhoff 2023 // Parses a single SVG path (only M, C and L statements are // supported). The p-method will return // [...position, ...derivative] for a normalized point t. // // https://turtletoy.net/turtle/46adb0ad70 //////////////////////////////////////////////////////////////// function Path(svg) { class MoveTo { constructor(p) { this.p0 = p; } p(t, s) { return [...this.p0, 1, 0]; } length() { return 0; } } class LineTo { constructor(p0, p1) { this.p0 = p0, this.p1 = p1; } p(t, s = 1) { const nt = 1 - t, p0 = this.p0, p1 = this.p1; return [ nt*p0[0] + t*p1[0], nt*p0[1] + t*p1[1], (p1[0] - p0[0]) * s, (p1[1] - p0[1]) * s, ]; } length() { const p0 = this.p0, p1 = this.p1; return Math.hypot(p0[0]-p1[0], p0[1]-p1[1]); } } class BezierTo { constructor(p0, c0, c1, p1) { this.p0 = p0, this.c0 = c0, this.c1 = c1, this.p1 = p1; } p(t, s = 1) { const nt = 1 - t, p0 = this.p0, c0 = this.c0, c1 = this.c1, p1 = this.p1; return [ nt*nt*nt*p0[0] + 3*t*nt*nt*c0[0] + 3*t*t*nt*c1[0] + t*t*t*p1[0], nt*nt*nt*p0[1] + 3*t*nt*nt*c0[1] + 3*t*t*nt*c1[1] + t*t*t*p1[1], (3*nt*nt*(c0[0]-p0[0]) + 6*t*nt*(c1[0]-c0[0]) + 3*t*t*(p1[0]-c1[0])) * s, (3*nt*nt*(c0[1]-p0[1]) + 6*t*nt*(c1[1]-c0[1]) + 3*t*t*(p1[1]-c1[1])) * s, ]; } length() { return this._length || ( this._length = Array.from({length:25}, (x, i) => this.p(i/25)).reduce( (a,c,i,v) => i > 0 ? a + Math.hypot(c[0]-v[i-1][0], c[1]-v[i-1][1]) : a, 0)); } } class Path { constructor(svg) { this.segments = []; this.parsePath(svg); } parsePath(svg) { const t = svg.match(/([0-9.-]+|[MLC])/g); for (let s, i=0; i<t.length;) { switch (t[i++]) { case 'M': this.add(new MoveTo(s=[t[i++],t[i++]])); break; case 'L': this.add(new LineTo(s, s=[t[i++],t[i++]])); break; case 'C': this.add(new BezierTo(s, [t[i++],t[i++]], [t[i++],t[i++]], s=[t[i++],t[i++]])); break; default: i++; } } } add(segment) { this.segments.push(segment); this._length = 0; } length() { return this._length || (this._length = this.segments.reduce((a,c) => a + c.length(), 0)); } p(t) { t = Math.max(Math.min(t, 1), 0) * this.length(); for (let l=0, i=0, sl=0; i<this.segments.length; i++, l+=sl) { sl = this.segments[i].length(); if (t > l && t <= l + sl) { return this.segments[i].p((t-l)/sl, sl/this.length()); } } return this.segments[Math.min(1, this.segments.length-1)].p(0); } } return new Path(svg); } // Below is automatically maintained by Turtlelib 1.0 // Changes below this comment might interfere with its correct functioning. function turtlelib_init() { turtlelib_ns_c6665b0e9b_Jurgen_Vector_Math(); } // Turtlelib Jurgen Vector Math v 4 - start - {"id":"c6665b0e9b","package":"Jurgen","name":"Vector Math","version":"4"} function turtlelib_ns_c6665b0e9b_Jurgen_Vector_Math() { ///////////////////////////////////////////////////////// // Vector functions - Created by Jurgen Westerhof 2024 // ///////////////////////////////////////////////////////// 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 equals(a,b) { return !a.some((e, i) => e != b[i]); } 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)); } static clamp(a,min,max) { return a.map((e,i) => Math.min(Math.max(e, min[i]), max[i])) }; static rotateClamp(a,min,max) { return a.map((e,i) => {const d = max[i]-min[i];if(d == 0) return min[i];while(e < min[i]) { e+=d; }while(e > max[i]) { e-=d; }return e;}); } } this.V = Vector; } // Turtlelib Jurgen Vector Math v 4 - end