As I worked on these, I began to think of them as pinwheels. Getting the right “flutter” out was a challenge, and I also had a bug for a little where content ended up mostly off the page. I ended up trying to “weave” in the iteration by flipping whether I add or subtract it in the simplex noise for the x or y.
const Simplex = require("https://unpkg.com/simplex-noise@2.4.0/simplex-noise.js"); const simplex = new Simplex(); const width = 761; const height = width; // source: https://rosettacode.org/wiki/Map_range#ES6 const rangeMap = (a, b) => (s) => { const [a1, a2] = a; const [b1, b2] = b; // Scaling up an order, and then down, to bypass a potential, // precision issue with negative numbers. return ((((b2 - b1) * (s - a1)) / (a2 - a1)) * 10 + 10 * b1) / 10; } function generatePaths() { const start = [Math.random() * width, Math.random() * height]; const step = 200.0; const path = [start]; for (let i = 0; i < 100; i++) { const [px, py] = path.at(-1); const theta = simplex.noise2D(px * px * 10 * +i * i, py * py * 10 - i * i); const t = rangeMap([-1, 1], [-Math.PI, Math.PI])(theta); const x = Math.cos(t) * step + simplex.noise2D(px + i, py - i) * 100 + width / 2; const y = Math.sin(t) * step + simplex.noise2D(px - i, py + i) * 100 + height / 2; path.push([x, y]); } return path; } function draw() { const svg = d3.create("svg").attr("width", width).attr("height", height); svg .append("path") .attr("d", d3.line()(generatePaths())) .attr("fill", "none") .attr("stroke", "black"); return svg.node(); }