Ringers 🦢

Algo adapted from https://x.com/ix_shells/status/1707573117042729407/photo/1
The winding isnt there yet, but looks very nice to me already.

The Ringer series is originally created by Dmitri Cherniak, it consists of many combos of strings and pegs to create unique geometric works of art.

Log in to post a comment.

// source: https://x.com/ix_shells/status/1707573117042729407/photo/1


const turtle = new Turtle();

let grid = 7; // min=4, max=15, step=1
let size = 140; // min=50, max=200, step=10
let dotSize = 8; // min=1, max=10, step=0.1
let density = 0.6; // min=0.01, max=1, step=0.01

let simple = 0; // min=0, max=1, step=1 (No, Yes)


function sample(gridPoints) {
    let points = [...gridPoints];
    let out = [];
    for(let i=0, leni = density*gridPoints.length; i < leni; i++) {
        let pt;
        while (!pt || out.includes(pt)){
           pt = pick(points);
        } 
        out.push(pt);
    }
    return out;
}

function getBlackPositions(gridPoints) {
    return gridPoints.filter(p => p[0] == 0 || p[1] == 0 || p[0] == grid-1 || p[1] == grid-1);
}

function sortByAngleTo(gridPoints, center) {
    gridPoints.sort((a,b) => atan2(sub2(center,a)) - atan2(sub2(center,b)));
}

function getCenterPoint(gridPoints) {
    return gridPoints.reduce((avg, value, _, { length }) => add2(avg, scl2(value, 1/length)), [0,0]);
}

function drawDots(gridPoints, blackPositions) {
    let scaledPoints = gridPoints.map(p => scl2(add2(p, [-grid/2+0.5, -grid/2+0.5]), size/grid));
    for(let idx=0; idx <= scaledPoints.length; idx++) {
        let p1 = scaledPoints[idx-1];
        let p2 = scaledPoints[idx%scaledPoints.length];
        
        let gp1 = gridPoints[idx-1];
        let gp2 = gridPoints[idx%gridPoints.length];
        
        if (p1) {
            if (!simple) { // it aint easy
                let dotSize1 = gp1[2];
                let dotSize2 = gp2[2];
                
                let angle = atan2(sub2(p2,p1)) - Math.PI*0.5;
                let a = add2(p1, [Math.cos(angle)*dotSize1, Math.sin(angle)*dotSize1]);
                let b = add2(p2, [Math.cos(angle)*dotSize2, Math.sin(angle)*dotSize2]);
                // TODO: fix winding
                turtle.jump(a);
                turtle.goto(b);
            } else {
                turtle.jump(p1);
                turtle.goto(p2);
            }
        }
        drawDot(p2, blackPositions.includes(gp2), gp2[2])
    }
}

function drawPoints(points) {
    if (points.length > 0) turtle.jump(points[points.length-1]);
    points.forEach(point => turtle.goto(point));
}

function drawDot(point, black, size = 3) {
    if (black) {
        while(size >= 0) {
            turtle.jump(add2(point,[0,-size]))
            turtle.circle(size);
            size -= 0.15;
        }
    } else {
        turtle.jump(add2(point,[0, -size]))
        turtle.circle(size);
    }
}

function walk() {
    let gridPoints = Array.from({length:grid*grid}, (_,idx) => ([idx%grid, idx/grid|0, pick([dotSize, dotSize*2/3])]));
    gridPoints = sample(gridPoints);
    let blackPositions = getBlackPositions(gridPoints);
    let center = getCenterPoint(blackPositions);
    sortByAngleTo(gridPoints, center);
    
    drawDots(gridPoints, blackPositions);
}





// vec2 functions
function scl2(a,b)   { return [a[0]*b, a[1]*b]; }
function add2(a,b)   { return [a[0]+b[0], a[1]+b[1]]; }
function sub2(a,b)   { return [a[0]-b[0], a[1]-b[1]]; }
function dot2(a,b)   { return a[0]*b[0] + a[1]*b[1]; }
function cross2(a,b) { return a[0]*b[1] - a[1]*b[0]; }
function len2(a)     { return Math.sqrt(a[0]**2 + a[1]**2); }
function dist2(a, b) { return len2(sub2(a,b)); }
function atan2(a)    { return Math.atan2(a[1], a[0]); }
function nrm2(a)     { return scl2(a, 1/len2(a)); }
function clone(a)    { return [...a]; }
// other utils
function pick(arr) { return arr[(Math.random() * arr.length) | 0]; }
function range(a, b) { return a + (b - a) * Math.random(); }

Array.prototype.remove = function(item) {
    var idx;
    while ((idx = this.indexOf(item)) !== -1) {
        this.splice(idx, 1);
    }
    return this;
}