Packed Custom Text

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

Log in to post a comment.

// Forked from "Noel's Room" by ProjectGrantwood
// https://turtletoy.net/turtle/12fa82d902

// Forked from "Hello, world" by reinder
// https://turtletoy.net/turtle/1713ddbe99


let text = "ABCD\nEFGH"
let scale = 2 //min=0.2, max=20, step=0.1
let margin = [10,10,10,10] // NORTH EAST SOUTH WEST
let italic = 0
let kerning = 1
let radius = scale*2



Canvas.setpenopacity(1);

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 = scale;
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();


const startPos = getStartPos(text, scale, italic, kerning, radius)
let circleArray = getCircleArray(getTextPathList(text, startPos, scale, italic, kerning), radius)


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 < 5000;
}

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[1] - c[2];
    add &= c[0] > -100 + margin[3] + c[2];
    add &= c[1] < 100 - margin[2] - c[2];
    add &= c[1] > -100 + margin[0] + 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;
        }
        

function getTextPathList(str, startPos, scale = 1, italic = 0, kerning = 1) {
    const dat = ('br>eoj^jl<jqirjskrjq>brf^fe<n^ne>`ukZdz<qZjz<dgrg<cmqm>`thZhw<lZlw<qao_l^h^e_caccdeefg'+
'gmiojpkqmqporlshsercp>^vs^as<f^h`hbgdeeceacaab_d^f^h_k`n`q_s^<olmmlolqnspsrrspsnqlol>]wtgtfsereqfph'+
'nmlpjrhsdsbraq`o`makbjifjekckaj_h^f_eaecffhimporqssstrtq>eoj`i_j^k_kajcid>cqnZl\\j_hcghglhqjulxnz>c'+
'qfZh\\j_lcmhmllqjuhxfz>brjdjp<egom<ogem>]wjajs<ajsj>fnkojpiojnkokqis>]wajsj>fnjniojpkojn>_usZaz>`ti'+
'^f_dbcgcjdofrisksnrpoqjqgpbn_k^i^>`tfbhak^ks>`tdcdbe`f_h^l^n_o`pbpdofmicsqs>`te^p^jfmfogphqkqmppnrk'+
'shserdqco>`tm^clrl<m^ms>`to^e^dgefhekenfphqkqmppnrkshserdqco>`tpao_l^j^g_ebdgdlepgrjsksnrppqmqlping'+
'kfjfggeidl>`tq^gs<c^q^>`th^e_dadceegfkgnhpjqlqopqorlshserdqcocldjfhigmfoepcpao_l^h^>`tpeohmjjkikfjd'+
'hcecddaf_i^j^m_oapepjoomrjshserdp>fnjgihjikhjg<jniojpkojn>fnjgihjikhjg<kojpiojnkokqis>^vrabjrs>]wag'+
'sg<amsm>^vbarjbs>asdcdbe`f_h^l^n_o`pbpdofngjijl<jqirjskrjq>]xofndlcicgdfeehekfmhnknmmnk<icgefhfkgmh'+
'n<ocnknmpnrntluiugtdsbq`o_l^i^f_d`bbad`g`jambodqfrislsorqqrp<pcokompn>asj^bs<j^rs<elol>_tc^cs<c^l^o'+
'_p`qbqdpfoglh<chlhoipjqlqopqorlscs>`urcqao_m^i^g_eadccfckdnepgrismsorqprn>_tc^cs<c^j^m_oapcqfqkpnop'+
'mrjscs>`sd^ds<d^q^<dhlh<dsqs>`rd^ds<d^q^<dhlh>`urcqao_m^i^g_eadccfckdnepgrismsorqprnrk<mkrk>_uc^cs<'+
'q^qs<chqh>fnj^js>brn^nnmqlrjshsfreqdndl>_tc^cs<q^cl<hgqs>`qd^ds<dsps>^vb^bs<b^js<r^js<r^rs>_uc^cs<c'+
'^qs<q^qs>_uh^f_daccbfbkcndpfrhslsnrppqnrkrfqcpan_l^h^>_tc^cs<c^l^o_p`qbqepgohlici>_uh^f_daccbfbkcnd'+
'pfrhslsnrppqnrkrfqcpan_l^h^<koqu>_tc^cs<c^l^o_p`qbqdpfoglhch<jhqs>`tqao_l^h^e_caccdeefggmiojpkqmqpo'+
'rlshsercp>brj^js<c^q^>_uc^cmdpfrisksnrppqmq^>asb^js<r^js>^v`^es<j^es<j^os<t^os>`tc^qs<q^cs>asb^jhjs'+
'<r^jh>`tq^cs<c^q^<csqs>cqgZgz<hZhz<gZnZ<gznz>cqc^qv>cqlZlz<mZmz<fZmZ<fzmz>brj\\bj<j\\rj>asazsz>fnkc'+
'ieigjhkgjfig>atpeps<phnfleiegfehdkdmepgrislsnrpp>`sd^ds<dhffhekemfohpkpmopmrkshsfrdp>asphnfleiegfeh'+
'dkdmepgrislsnrpp>atp^ps<phnfleiegfehdkdmepgrislsnrpp>asdkpkpiognfleiegfehdkdmepgrislsnrpp>eqo^m^k_j'+
'bjs<gene>atpepuoxnylzizgy<phnfleiegfehdkdmepgrislsnrpp>ate^es<eihfjemeofpips>fni^j_k^j]i^<jejs>eoj^'+
'k_l^k]j^<kekvjyhzfz>are^es<oeeo<ikps>fnj^js>[y_e_s<_ibfdegeifjijs<jimfoeretfuius>ateees<eihfjemeofp'+
'ips>atiegfehdkdmepgrislsnrppqmqkphnfleie>`sdedz<dhffhekemfohpkpmopmrkshsfrdp>atpepz<phnfleiegfehdkd'+
'mepgrislsnrpp>cpgegs<gkhhjfleoe>bsphofleieffehfjhkmlompopporlsisfrep>eqj^jokrmsos<gene>ateeeofrhsks'+
'mrpo<peps>brdejs<pejs>_ubefs<jefs<jens<rens>bseeps<pees>brdejs<pejshwfydzcz>bspees<eepe<esps>cqlZj['+
'i\\h^h`ibjckekgii<j[i]i_jakbldlfkhgjkllnlpkrjsiuiwjy<ikkmkojqirhthvixjylz>fnjZjz>cqhZj[k\\l^l`kbjci'+
'eigki<j[k]k_jaibhdhfihmjilhnhpirjskukwjy<kkimiojqkrltlvkxjyhz>^vamakbhdgfghhlknlplrksi<akbidhfhhill'+
'nmpmrlsisg>brb^bscsc^d^dsese^f^fsgsg^h^hsisi^j^jsksk^l^lsmsm^n^nsoso^p^psqsq^r^rs').split('>').map(
    r=> { return [r.charCodeAt(0)-106,r.charCodeAt(1)-106, r.substr(2).split('<').map(a => {const ret 
    = []; for (let i=0; i<a.length; i+=2) {ret.push(a.substr(i, 2).split('').map(b => b.charCodeAt(0)
    -106));} return ret; })]; });
    
    function rotAdd (a, b, h) {
        return [Math.cos(h)*a[0] - Math.sin(h)*a[1] + b[0], 
                Math.cos(h)*a[1] + Math.sin(h)*a[0] + b[1]];
    }
    
    let pathList = []
    let pos = startPos, h = 0, o = pos;
    str.split('').map(c => {
        const i = c.charCodeAt(0) - 32;
        if (i < 0 ) {
            pos = o = rotAdd([0, 32*scale], o, h);
        } else if (i > 96 ) {
            pos = rotAdd([16*scale, 0], o, h);
        } else {
            const d = dat[i], lt = d[0]*scale, rt = d[1]*scale, paths = d[2];
            paths.map( p => {
                let path = []
            	p.map( s=> {
                	path.push(rotAdd([(s[0]-s[1]*italic)*scale - lt, s[1]*scale], pos, h));
                });
                pathList.push(path)
            });
            pos = rotAdd([(rt - lt)*kerning, 0], pos, h);
        }
        
    });
    return pathList
}


function getCircleArray(pathList, radius){
    let circleArray = []
    for (path of pathList){
        for (i in path){
            circleArray.push(createCircle(path[i][0], path[i][1], radius/2, -Math.PI / 2, '00'))
            if (i>0){
                const dx = path[i][0]-path[i-1][0], dy = path[i][1]-path[i-1][1], dist = Math.sqrt(dx * dx + dy * dy)
                const circleCount = Math.ceil(dist/(radius*0.8)), ddx = dx/circleCount, ddy = dy/circleCount
                for (let j=1; j<circleCount; j++){
                    circleArray.push(createCircle(path[i-1][0]+j*ddx, path[i-1][1]+j*ddy, radius/2, -Math.PI / 2, '00'))
                }
            }
        }
    }
    return circleArray
}

function getStartPos(text, scale, italic, kerning, radius){
    const circleArrayMid = getCircleArray(getTextPathList(text, [0,0], scale, italic, kerning), radius=radius)
    let maxX = minX = maxY = minY = 0
    for (p of circleArrayMid){
        if (p[0]>maxX){
            maxX = p[0]
        } else if (p[0]<minY){
            minX = p[0]
        }
        if (p[1]>maxY){
            maxY = p[1]
        } else if (p[1]<minY){
            minY = p[1]
        }
    }
    return [-(minX+maxX)/2, -(minY+maxY)/2]
}