Noel's Room

Using a circle-packing algorithm to make a pen-plot with a name in the middle.

Log in to post a comment.

// Forked from "Packed in for a Good Wander" by ProjectGrantwood
// https://turtletoy.net/turtle/04187a129d

// Forked from "Beadpackpathwander" by ProjectGrantwood
// https://turtletoy.net/turtle/5f76422d98

Canvas.setpenopacity(1);

const LETTER_HEIGHT = 45;
const LETTER_WIDTH = 30;
const LETTER_UNIT_SIZE = 4;
const LETTER_DIAGONAL = Math.sqrt(LETTER_HEIGHT * LETTER_HEIGHT + LETTER_WIDTH * LETTER_WIDTH) - Math.sqrt(LETTER_UNIT_SIZE * 2);
const LETTER_DIAGONAL_ANGLE = Math.atan2(LETTER_HEIGHT, LETTER_WIDTH);
const LETTER_CIRCLE_UNIT = 180 / LETTER_UNIT_SIZE - 1;
const MARGIN = LETTER_UNIT_SIZE * 3.5;

let shrinkageAcceleration = 50.0;
let shrinkageAcceleration_FineAdjust = 0.0;
let AngleIncrementA = 0.0 //min=-180, max=180, step=1
let AngleIncrementA_FineAdjust = 0 //min=0, max=1, step=0.01
let AngleIncrementB = -95 // min=-180, max=180, step=1
let AngleIncrementB_FineAdjust = 0.0 //min=0, max=1, step=0.01
let minimumRadius = 0.5 // min=0.05, max=4, step=0.01
let initialRadius = LETTER_UNIT_SIZE;
let maxFailures = 0 //min=0, max=180, step=1

shrinkageAcceleration = (101 - (shrinkageAcceleration + shrinkageAcceleration_FineAdjust)) / 1000;
initialRadius = minimumRadius > initialRadius ? minimumRadius : initialRadius;

const angleIncrement1 = (AngleIncrementA + AngleIncrementA_FineAdjust) * Math.PI / 180;
const angleIncrement2 = (AngleIncrementB + AngleIncrementB_FineAdjust) * Math.PI / 180;
const t = new Turtle();

let circleArray = [
    
    //Letter N
    
    new Array(Math.floor(LETTER_HEIGHT / LETTER_UNIT_SIZE)).fill().map((e, i) => createCircle(-LETTER_WIDTH * 3 + MARGIN, LETTER_HEIGHT / 2 + i * -LETTER_UNIT_SIZE, LETTER_UNIT_SIZE / 2, -Math.PI / 2, '00')),
    new Array(Math.floor(LETTER_DIAGONAL / LETTER_UNIT_SIZE)).fill().map((e, i) => createCircle(-LETTER_WIDTH * 3 + MARGIN + i * LETTER_UNIT_SIZE * Math.cos(LETTER_DIAGONAL_ANGLE), -LETTER_HEIGHT / 2 + LETTER_UNIT_SIZE - i * -LETTER_UNIT_SIZE, LETTER_UNIT_SIZE / 2, -Math.PI / 2, '00')),
    new Array(Math.floor(LETTER_HEIGHT / LETTER_UNIT_SIZE)).fill().map((e, i) => createCircle(-LETTER_WIDTH * 2 + MARGIN - LETTER_UNIT_SIZE, LETTER_HEIGHT / 2 + i * -LETTER_UNIT_SIZE, LETTER_UNIT_SIZE / 2, -Math.PI / 2, '00')),
    
    //Letter O
    
    new Array(Math.round(LETTER_CIRCLE_UNIT + 1)).fill().map((e, i) => createCircle(-LETTER_WIDTH + MARGIN - LETTER_UNIT_SIZE / 2 + (LETTER_WIDTH / 2) * Math.cos(Math.PI  * 2/ LETTER_CIRCLE_UNIT * i),  LETTER_UNIT_SIZE / 2 + (LETTER_HEIGHT / 2) * Math.sin(Math.PI * 2 / LETTER_CIRCLE_UNIT * i), LETTER_UNIT_SIZE / 2, Math.round(i / 90) * 90 * Math.PI / 180 + Math.PI / 4, '00')),
    
    //Letter E
    
    new Array(Math.floor(LETTER_HEIGHT / LETTER_UNIT_SIZE)).fill().map((e, i) => createCircle(MARGIN + LETTER_UNIT_SIZE / 2, LETTER_HEIGHT / 2 + i * -LETTER_UNIT_SIZE, LETTER_UNIT_SIZE / 2, -Math.PI / 2, '00')),
    new Array(Math.floor(LETTER_WIDTH / LETTER_UNIT_SIZE)).fill().map((e, i) => createCircle(MARGIN + LETTER_UNIT_SIZE / 2 + i * LETTER_UNIT_SIZE, LETTER_HEIGHT / 2, LETTER_UNIT_SIZE / 2, 0, '00')),
    new Array(Math.floor(LETTER_WIDTH / LETTER_UNIT_SIZE - 1)).fill().map((e, i) => createCircle(MARGIN + LETTER_UNIT_SIZE + i * LETTER_UNIT_SIZE, LETTER_UNIT_SIZE / 2, LETTER_UNIT_SIZE / 2, 0, '00')),
    new Array(Math.floor(LETTER_WIDTH / LETTER_UNIT_SIZE)).fill().map((e, i) => createCircle(MARGIN + LETTER_UNIT_SIZE / 2 + i * LETTER_UNIT_SIZE, -LETTER_HEIGHT / 2 + LETTER_UNIT_SIZE / 2 , LETTER_UNIT_SIZE / 2, 0, '00')),
    
    //Letter L
    
    new Array(Math.floor(LETTER_HEIGHT / LETTER_UNIT_SIZE)).fill().map((e, i) => createCircle(LETTER_WIDTH + MARGIN * 2, LETTER_HEIGHT / 2 + i * -LETTER_UNIT_SIZE, LETTER_UNIT_SIZE / 2, -Math.PI / 2, '00')),
    new Array(Math.floor(LETTER_WIDTH / LETTER_UNIT_SIZE - 1)).fill().map((e, i) => createCircle(LETTER_WIDTH + MARGIN * 2 + i * LETTER_UNIT_SIZE, LETTER_HEIGHT / 2, LETTER_UNIT_SIZE / 2, 0, '00'))
    
    ].flat(1);

let ratio = 0.8;
let ratio2 = 0.999;
const initialRatio = ratio;

let currentCircle = circleArray[0]; 
let failures = 0;

t.radians();
t.pd();


let time = 0;

function walk(){
    if (time === 0){
        for (let z = 0; z < circleArray.length; z++){
         draw(circleArray[z], t, '10');
        }
         time += 1;
    } else {
        for (let j = 0; j < 200; j++){
       currentCircle = getAndCheck(currentCircle, circleArray, time);
        }
        time += 1;
    }
        
    return time < 20000;
}

function createCircle(x, y, rad, heading, connectRadii = false){
    return [x, y, rad, heading, connectRadii];
}

function checkCircleOverlap(c1, c2){
    const xd = (c2[0] - c1[0]) ** 2;
    const yd = (c2[1] - c1[1]) ** 2;
    const dsq = xd + yd;
    return dsq >= (c1[2] + c2[2]) ** 2;
}

function draw(c, turtle, i, flags = c[4]){
    turtle.jmp(c[0], c[1] - c[2]);
    if (flags[0] === '1'){
    turtle.jmp(c[0], c[1])
    turtle.circle(c[2])
    }
    if (flags[1] === '1') {
        turtle.jmp(c[0], c[1]);
    turtle.seth(c[3] - Math.PI)
    let travelDistance = c[2];
    travelDistance = c[2] === minimumRadius ? c[2] + c[2] + minimumRadius : c[2] + c[2] / ratio + minimumRadius
     turtle.forward(travelDistance)
    }
}

function getNextCircle(c){
    let radiusOld = c[2];
    let radiusNew = radiusOld * ratio
    radiusNew = radiusNew < minimumRadius ? minimumRadius : radiusNew
    let headingNew = c[3] += angleIncrement1;
    c[3] = headingNew;
    let d = ((radiusOld + radiusNew) + minimumRadius);
    let xOld = c[0];
    let yOld = c[1];
    let xNew = xOld + d * Math.cos(headingNew);
    let yNew = yOld + d * Math.sin(headingNew);
    return createCircle(xNew, yNew, radiusNew, headingNew, '01');
}

function checkBounds(c){
    let add = true;
    add &= c[0] < 100 - MARGIN - c[2];
    add &= c[0] > -100 + MARGIN + c[2];
    add &= c[1] < 100 - MARGIN - c[2];
    add &= c[1] > -100 + MARGIN + c[2];
    return add;
}

function getAndCheck(c1, circleArray, i){
    let c2 = getNextCircle(currentCircle);
    let add = 1;
    add *= checkBounds(c2);
    let newC;
    if (add) {
    for (let c of circleArray){
        
        add *= checkCircleOverlap(c, c2);
        if (!add){
            newC = c;
            break;
        }
    }
    }
    if (!add){
        c1[3] += angleIncrement2
        failures++;
        if (failures >= maxFailures){
            failures = 0;
             c1[3] += angleIncrement2
             ratio *= initialRatio + shrinkageAcceleration;
             radius = initialRadius;
             let index = circleArray.indexOf(c1) - 1;
             index = index < 0 || Math.random() < 0.04 ? Math.floor(Math.random() * circleArray.length) : index;
          
             return circleArray[index];
            //  return circleArray[Math.floor(Math.random() * circleArray.length)]
            
        }
        return currentCircle;
    } else {
        //ratio /= initialRatio + shrinkageAcceleration;
        draw(c2, t, i);
        circleArray.push(c2);
        return c2;
    }
        }
        
        function map(val, hi, lo, newhi, newlo){
            return ((val - lo) * (newhi - newlo)) / (hi - lo) + newlo;
        }