Drawing Circles ⭕

Circle Packing + Drawing

Log in to post a comment.

const pathInput = `
M-98,11
C-98,15 -92,15 -91,18
C-87,25 -74,28 -68,34
C-57,45 -36,52 -23,59
C-18,61 -11,60 -6,63
C-1,66 18,64 25,66
C37,70 58,68 63,58
C72,40 60,26 44,18
C32,12 19,5 5,0
C4,-0 2,1 1,0
C-2,-2 -4,-5 -8,-6
C-19,-10 -35,-16 -39,-23
C-41,-28 -50,-29 -52,-34
C-54,-39 -55,-43 -57,-47
C-58,-48 -60,-56 -58,-58
C-54,-62 -52,-67 -46,-70
C-34,-76 -2,-72 10,-68
C16,-66 23,-61 28,-59
C37,-56 44,-53 50,-47
C57,-40 68,-37 71,-31
C75,-24 98,-9 98,-6`; // 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 = 250; // min=1, max=1000, step=1
const maxRadius = 10.0; // min=1, max=50, step=0.25
const minRadius = 0.05; // min=0.01, max=20, step=0.01
const drawDensity = 0.5; // min=0, max=1, step=0.01

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(minRadius, maxRadius, t);
    const circle = [
        pos[0]+range(-randomOffset, randomOffset), 
        pos[1]+range(-randomOffset, randomOffset), 
        radius
    ];
    if (circles.insert(circle)) {
        drawCircle(circle);
    }
    return i < steps*iterationsPerStep;
}

function drawCircle(pt) {
    let r = pt[2];
    while(r > 0) {
        turtle.jump(pt[0], pt[1]-r);
        turtle.circle(r);
        r -= drawDensity * maxRadius;
        if (drawDensity === 0) break;
    }
}

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