Circle Packing + Drawing lines in certain range
Log in to post a comment.
// Forked from "Drawing Circles ⭕" by markknol
// https://turtletoy.net/turtle/70cec84ef6
const pathInput = `
M-95,-42
C-95,-63 -50,-75 -33,-79
C25,-92 84,-85 99,-24
C105,-2 89,19 71,29
C57,37 43,37 28,40
C-9,46 -38,41 -45,0
C-46,-6 -47,-14 -46,-20
C-42,-41 -7,-47 11,-47
C44,-47 63,-31 78,-4
C85,8 90,20 88,34
C86,44 76,55 69,62
C57,74 42,79 27,85
C7,93 -14,92 -35,90
C-60,88 -83,76 -100,59`; // type=path
const path = Path(pathInput);
const turtle = new Turtle();
const circles = new Circles();
const randomOffset = 12.5;// min=0, max=50, step=0.5
const iterationsPerStep = 500;
const circleRadius = 10.0; // min=4, max=50, step=0.25
const lineRange = 10; // min=1, max= 50, step=1
const steps = path.length() | 0;
function walk(i) {
let j = i / steps;
const pos = path.p(j % 1);
const t = 1 - (j / iterationsPerStep);
const radius = lerp(0.01, circleRadius, t);
const circle = [
pos[0]+range(-randomOffset, randomOffset),
pos[1]+range(-randomOffset, randomOffset),
radius
];
circles.insert(circle);
if (i === steps*iterationsPerStep) {
let p2,dx,dy,r;
for(let p1 of circles.pts) for(let p2 of circles.pts) {
if (p1 !== p2) {
dx = p1[0]-p2[0], dy = p1[1]-p2[1], r = p1[2]+p2[2];
if (r > 1 && dx*dx+dy*dy < lineRange*lineRange) {
turtle.jump(p1);
turtle.goto(p2);
}
}
}
}
return i < steps*iterationsPerStep;
}
// utils
function clamp(v,min,max) { if (min>max) [min,max]=[max,min]; return v<min?min:(v>max?max:v); }
function lerp(a,b,t) { return a + (b-a)*t; }
function range(a,b) { return lerp(a,b,Math.random()); }
// Circle pack by Mark Knol (@mknol) -- https://turtletoy.net/turtle/483faa0615
function Circles() {
class Circles {
constructor() {
this.pts = [];
}
insert(p1) {
let p2,dx,dy,r;
for (let i=0,leni=this.pts.length;i<leni;i++) {
p2 = this.pts[i];
dx = p1[0]-p2[0], dy = p1[1]-p2[1], r = p1[2]+p2[2];
if (dx*dx+dy*dy < r*r) {
return false;
}
}
this.pts.push(p1);
return true;
}
get length() {
return this.pts.length;
}
}
return new Circles();
}
////////////////////////////////////////////////////////////////
// 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);
}