Petri Dish 🧫

Populations of bacteria

Log in to post a comment.

// You can find the Turtle API reference here: https://turtletoy.net/syntax
Canvas.setpenopacity(1);

const specs = 160; //min=1 max=300 step=1
const petriRadius = 80; //min=50 max=100 step=1
const evenDistribution = 1; //min=0 max=1 step=1 (No, Yes)
const pi2 = Math.PI * 2;
const minPopSize = 2; //min=1 max=10 step=1
const maxPopSize = 9; //min=5 max=20 step=1

// Global code will be evaluated once.
const turtle = new Turtle();

let blobs = [];
for(let i = 0; i < specs; i++) {
    let r = (evenDistribution == 1? Math.sqrt(Math.random()): Math.random()) * petriRadius;
    let a = Math.random() * pi2;
    blobs.push([
        [Math.cos(a) * r, Math.sin(a) * r],
        minPopSize+(Math.random()**2 * Math.max(minPopSize, maxPopSize - minPopSize)),
        [[null, 0]], [[null, pi2]]
    ]);
}

turtle.jump(0,-petriRadius);
turtle.circle(petriRadius);

function processBlobs(blobs) {
        
    function rot2(a) { return [Math.cos(a), -Math.sin(a), Math.sin(a), Math.cos(a)]; }
    function trans2(m, a) { return [m[0]*a[0]+m[2]*a[1], m[1]*a[0]+m[3]*a[1]]; }
    const deepClone = (o) => JSON.parse(JSON.stringify(o));
    const isPointInBlob = (pt, blob) => ((blob[0][0] - pt[0])**2 + (blob[0][1] - pt[1])**2) <= (blob[1]**2);
    function radClip(a) {
        while(a < 0) { a += pi2; }
        while(a > pi2) { a -= pi2; }
        return a;
    }
    
    for(let i = 0; i < blobs.length; i++) {
        let currentBlob = deepClone(blobs[i]);
        for(let j = 0; j < blobs.length; j++) {
            if(i == j) continue;
            
            let otherBlob = deepClone(blobs[j]);
            otherBlob[0][0] -= currentBlob[0][0];
            otherBlob[0][1] -= currentBlob[0][1];
    
            let angle = 0;
            let dx = otherBlob[0][0];
            let dy = otherBlob[0][1];
            if(dx == 0) {
                angle = Math.PI * (dy > 0? .5: 1.5);
            } else {
                let dydx = dy/dx;
                angle = Math.atan(dy/dx);
                
                if(dx < 0) {
                    angle += Math.PI
                }
            }
            
            otherBlob[0] = trans2(rot2(angle), otherBlob[0]);
            dx = otherBlob[0][0];
            dy = otherBlob[0][1];
            
            if(dx == 0) continue; //blob has same center coordinates
    
            let x = (dx**2 - otherBlob[1]**2 + currentBlob[1]**2) / (2 * dx);
    
            if(Math.abs(x) > currentBlob[1] ) continue; // blob is further away than currentBlob
    
            //let y = Math.sqrt(currentBlob[1]**2 - x**2);
            if(Math.acos(x/currentBlob[1]) == NaN) continue;
            if(blobs[i][2].length == 1 && blobs[i][2][0][0] == null) {
                blobs[i][2] = [];
            }
            if(blobs[i][3].length == 1 && blobs[i][3][0][0] == null) {
                blobs[i][3] = [];
            }
            
            //inangle
            blobs[i][2].push([j, radClip(Math.acos(x/currentBlob[1]) + angle)]);
            //outangle
            blobs[i][3].push([j, radClip((Math.PI * 2 - Math.acos(x/currentBlob[1])) + angle)]);
        }
    }

    //remove all ins and outs from blobs that are in a blob that is not the blob itself or the intersection blob
    //for every blob
    for(let i = 0; i < blobs.length; i++) {
        
        //for every draw-start (in)
        for(let ins = 0; ins < blobs[i][2].length; ins++) {
            let deleted = false;
            //for every other blob        
            for(let j = 0; j < blobs.length; j++) {
                //than the 'current' one (i)
                if(i == j) continue;
                //or the one associated with the draw-start (in)
                if(blobs[i][2][ins][0] == j) continue;
                
                //todo: check if point is in circle j, and if so, remove it from the list
                if(isPointInBlob([
                    blobs[i][0][0] + (Math.cos(blobs[i][2][ins][1]) * blobs[i][1]),
                    blobs[i][0][1] + (Math.sin(blobs[i][2][ins][1]) * blobs[i][1])
                ], blobs[j])) {
                    blobs[i][2] = blobs[i][2].filter((elm, idx) => idx != ins);
                    ins--;
                    deleted = true;
                    j = blobs.length;
                }
            }
            //check if 'in' is outside of petri dish
            if( !deleted && //blobs[i][2][ins][0] == -1 &&
                (blobs[i][0][0] + (Math.cos(blobs[i][2][ins][1]) * blobs[i][1]))**2+
                (blobs[i][0][1] + (Math.sin(blobs[i][2][ins][1]) * blobs[i][1]))**2
                > petriRadius**2
            ) {
                blobs[i][2] = blobs[i][2].filter((elm, idx) => idx != ins);
                ins--;
            }
        }
    }
    
    //Add ins and outs for petri dish itself
    for(let i = 0; i < blobs.length; i++) {
        let currentBlob = deepClone(blobs[i]);
        let otherBlob = [[0,0], petriRadius, [null, 0], [null, pi2]];

        otherBlob[0][0] -= currentBlob[0][0];
        otherBlob[0][1] -= currentBlob[0][1];
    
        let angle = 0;
        let dx = otherBlob[0][0];
        let dy = otherBlob[0][1];
        if(dx == 0) {
            angle = Math.PI * (dy > 0? .5: 1.5);
        } else {
            let dydx = dy/dx;
            angle = Math.atan(dy/dx);
            
            if(dx < 0) {
                angle += Math.PI
            }
        }
        
        otherBlob[0] = trans2(rot2(angle), otherBlob[0]);
        dx = otherBlob[0][0];
        dy = otherBlob[0][1];
        
        if(dx == 0) continue; //blob has same center coordinates

        let x = (dx**2 - otherBlob[1]**2 + currentBlob[1]**2) / (2 * dx);

        if(Math.abs(x) > currentBlob[1] ) continue; // blob is further away than currentBlob

        //let y = Math.sqrt(currentBlob[1]**2 - x**2);
        if(Math.acos(x/currentBlob[1]) == NaN) continue;
        if(blobs[i][2].length == 1 && blobs[i][2][0][0] == null) {
            blobs[i][2] = [];
        }
        if(blobs[i][3].length == 1 && blobs[i][3][0][0] == null) {
            blobs[i][3] = [];
        }
        
        //inangle
        blobs[i][3].push([-1, radClip(Math.acos(x/currentBlob[1]) + angle)]);
        //outangle
        blobs[i][2].push([-1, radClip((pi2 - Math.acos(x/currentBlob[1])) + angle)]);
    }
    //remove all ins and outs from blobs that are in a blob that is not the blob itself or the intersection blob
    //for every blob
    for(let i = 0; i < blobs.length; i++) {
        
        //for every draw-start (in)
        for(let ins = 0; ins < blobs[i][2].length; ins++) {
            let deleted = false;
            //for every other blob        
            for(let j = 0; j < blobs.length; j++) {
                //than the 'current' one (i)
                if(i == j) continue;
                //or the one associated with the draw-start (in)
                if(blobs[i][2][ins][0] == j) continue;
                
                //todo: check if point is in circle j, and if so, remove it from the list
                if(isPointInBlob([
                    blobs[i][0][0] + (Math.cos(blobs[i][2][ins][1]) * blobs[i][1]),
                    blobs[i][0][1] + (Math.sin(blobs[i][2][ins][1]) * blobs[i][1])
                ], blobs[j])) {
                    blobs[i][2] = blobs[i][2].filter((elm, idx) => idx != ins && elm[0] != -1);
                    ins--;
                    deleted = true;
                    j = blobs.length;
                }
            }
        }
    }
    
    for(let i = 0; i < blobs.length; i++) {
        blobs[i][2].sort((a, b) => (a[1] < b[1]) ? -1 : 1)
        blobs[i][3].sort((a, b) => (a[1] < b[1]) ? -1 : 1)
    }
    
    return blobs;
}

let circles = processBlobs(blobs);

// The walk function will be called until it returns false.
function walk(i) {
    //return;
    for(let  j = 0; j < circles[i][2].length; j++) {
        drawCirclePart(turtle, circles[i], circles[i][2][j][1], circles[i][3][getFirstOutAfter(circles[i][3], circles[i][2][j][1])][1] );
    }
    return i < circles.length - 1;
}

function getFirstOutAfter(outs, start) {
    let min = 2000;
    let idx = null;
    for(let i = 0; i < outs.length; i++) {
        let test = outs[i][1] + (outs[i][1] < start? pi2: 0);
        if(test < min) {
            min = test;
            idx = i;
        }
    }
    return idx;
}

function drawCirclePart(turtle, blob, from = 0, to = pi2) {
    if(to < from) { to += pi2; }
    turtle.jump(getBlobCoordinateAt(blob, from));
    let step = pi2 / Math.ceil(Math.PI / Math.asin(.5 / blob[1]));
    for(let a = from + step; a <= to; a+=step) {
        turtle.goto(getBlobCoordinateAt(blob, a));
    }
    turtle.goto(getBlobCoordinateAt(blob, to));
}

const getBlobCoordinateAt = (blob, angle) => [
    blob[0][0] + (Math.cos(angle) * blob[1]),
    blob[0][1] + (Math.sin(angle) * blob[1])
];