Know thy roots

Root system via dynamic L-system

A significantly dumbed-down 2D version of RootBox

Leitner et al (2010) The algorithmic beauty of plant roots – an L-System model for dynamic root growth simulation doi.org/10.1080/13873954.2010.491360

Leitner et al (2010) A dynamic root system growth model based on L-Systems doi.org/10.1007/s11104-010-0284-7

RootBox csc.univie.ac.at/rootbox/

Log in to post a comment.

Canvas.setpenopacity(-1);

const top_h = -60;

const t = new Turtle();

function draw_dashed_line(y, Ndash) {
    const dash = 200 / (2 * Ndash - 1)
    
    t.jump(-100, y)
    
    for (let i = 0; i < Ndash; i++) {
        t.forward(dash)
        t.penup()
        t.forward(dash)
        t.pendown()
    }
}

draw_dashed_line(top_h, 100)

function random_normal(mu, sigma) {
    let u = Math.random(), v = Math.random()
    let z = Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v) // Box–Muller transform
    
    //z = Math.min(Math.max(z, -3.0), 3.0) // clip to 3 sigma
    
    return mu + sigma * z
}

// function check_block(draw_step) { // sphere
//     let cy = -30
    
//     if (t.x()**2 + (t.y() - cy) **2 < 20**2) {
//         t.back(draw_step)
        
//         let new_dir = Math.atan2(t.x(), -(t.y() - cy)) / Math.PI * 180
//         if (new_dir < 0) {
//             new_dir += 180
//         }
//         else if (new_dir > 180) {
//             new_dir -= 180
//         }
        
//         t.setheading(new_dir)
//     }
// }

let cf_y1 = top_h + 25, cf_y2 = cf_y1 + 10, cf_y3 = cf_y2 + 10
let cf_w = 15

// draw coffin
t.jump(-cf_w, cf_y1)
t.goto(cf_w, cf_y1)
t.goto(cf_w + (cf_y2 - cf_y1), cf_y2)
t.goto(cf_w + (cf_y2 - cf_y1) - (cf_y3 - cf_y2), cf_y3)
t.goto(-cf_w - (cf_y2 - cf_y1) + (cf_y3 - cf_y2), cf_y3)
t.goto(-cf_w + -(cf_y2 - cf_y1), cf_y2)
t.goto(-cf_w, cf_y1)

t.jump(-cf_w - (cf_y2 - cf_y1), cf_y2)
t.goto(cf_w + (cf_y2 - cf_y1), cf_y2)


function check_block(draw_step) { // coffin
    if ((t.y() < cf_y1) || (t.y() > cf_y3)) {
        return
    }
    else if ((t.y() < cf_y2) && (Math.abs(t.x()) > cf_w + (t.y() - cf_y1))) {
        return
    }
    else if ((t.y() > cf_y2) && (Math.abs(t.x()) > cf_w + (cf_y2 - cf_y1) - (t.y() - cf_y2))) {
        return
    }
    else {
        t.back(draw_step)
        
        if (t.y() < cf_y2) {
            if (t.x() < -cf_w) {
                t.setheading(136)
            }
            else if (t.x() < 0) {
                t.setheading(180)
            }
            else if (t.x() < cf_w) {
                t.setheading(0)
            }
            else {
                t.setheading(45)
            }
        }
        else {
            t.setheading(90)
        }
    }
}

function draw_root(x0, y0, angle0,
                   max_angle,
                   l_base,
                   l_end,
                   l_mid, l_mid_sd,
                   n_branches,
                   N_try_grav, try_grav_depth,
                   angle_sigma,
                   draw_step,
                   br_theta, br_theta_sd, br_l, br_l_sd) {
    let L_max = l_base + l_end

    let L_mid = []
    for (let i = 0; i < n_branches; i++) {
        Lm = random_normal(l_mid, l_mid_sd)
        
        if (i == 0) {
            L_mid[0] = l_base + Lm
        }
        else {
            L_mid[i] = L_mid[i - 1] + Lm
        }
        
        L_max += Lm
    }
    
    t.jump(x0, y0)
    
    t.setheading(angle0)
    let prev_heading = angle0
    
    let L = 0.0
    
    let n_steps = 0
    
    let n_br = 0
    
    while (L < L_max) {
        t.forward(draw_step)

        L += draw_step
        
        if (t.y() < y0) {
            t.sety(y0)
            t.setheading(0)
        }
        
        check_block(draw_step)
        
        if ((n_br < n_branches) && (L > L_mid[n_br])) {
            let mem_x = t.x(), mem_y = t.y(), mem_h = t.heading()
            
            let dtheta = random_normal(br_theta, br_theta_sd)
            let angle_br = (Math.random() < 0.5) ? (mem_h + dtheta) : (mem_h - dtheta)
            let L_br = random_normal(br_l, br_l_sd)
            
            draw_root(mem_x, mem_y, angle_br, 360, L_br, 0, 0, 0, 0, 1, 0, 18, draw_step, 0, 0, 0, 0)
            
            t.jump(mem_x, mem_y)
            t.setheading(mem_h)
            
            n_br++
        }
        
        
        let N = 1
        if ((t.y() - y0) > try_grav_depth) {
            N = N_try_grav
        }
        else if (Math.random() <= 0.3) {
            N = Math.floor(1 + N_try_grav * Math.random())
        }
        
        let best_angle = -90

        for (let i = 0; i < N; i++) {
            let new_angle = t.heading() + random_normal(0, Math.sqrt(draw_step) * angle_sigma)
           
            if (Math.abs(new_angle - 90) < Math.abs(best_angle - 90)) {
                best_angle = new_angle
            }
        }
        
        best_angle = Math.min(Math.max(best_angle, 90 - max_angle), 90 + max_angle)

        prev_heading = t.heading()
        t.setheading(best_angle)
    }
}


const R = 3

t.jump(-R, top_h)
t.goto(-R, -100)

t.jump(R, top_h)
t.goto(R, -100)

const d = 2.0, Nd = 60

for (let i = 0; i < Nd; i++) {
    let y = -100 + (top_h + 100) * Math.random()
    let dd = d * Math.random()
    let x = (Math.random() <= 0.25) ? -R : ((Math.random() <= 0.33) ? (R - dd) : -R + (2 * R - dd) * Math.random())
    
    t.jump(x, y)
    t.goto(x + dd, y)
}


const s = 1.5
const N0 = 30
const mod = 0.5

for (let i = 0; i < N0; i++) {
    let angle = 180 / (N0 + 2) * (1 + i + Math.random() - 0.5)
    let x = (R + 1) * (-1 + 2 * Math.random())
    
    draw_root(x, top_h + 0.5, angle, 110, 0.1 * s, 18 * s, 0.6 * s / mod, 0.06 * s / mod, 120 * mod, 2, 30 * s, 9, 0.25, 70, 15, 1.5 * s, 0.5 * s)
}