BarkBeetleGalleryGenerator

Simulating beetle galleries. These bugs tunnel into tree bark to lay eggs, and in that bark their young will complete their first 3 life stages. Once they finish pupation, they emerge from their tunnel as adults to attack new trees. The tunnels they carve expand out from the initial entrypoint from the tree, and do not typically intersect; they use the nutrients of the tree as their food source. Although they often do significant damage to the trees, allowing dangerous fungus to proliferate under the protective bark, they do leave pretty patterns.

Log in to post a comment.

// Inspired by bark beetle galleries
// e.g. https://tidcf.nrcan.gc.ca/en/insects/factsheet/2850

let seed = 1.0; //min=1.0 max=100.0 step=1.0 Seed to give reproducible patterns.
const scale_factor = 1.0; //min=0.05 max=2.0 step=0.05 Scale factor for whole canvas

const pen_opacity = 0.7; //min=-0.9 max=0.9 step=0.05 Opacity of the drawing.
const pen_use_circles = 1; //min=0 max=1 step=1 (false, true)
const pen_distribution = 0; //min=0 max=2 step=1 (uniform, normal, single)
/// BEETLE PARAMETERS
const beetle_repellant_factor = 0.83; //min=0.0 max=1.0 step=0.01 How much beetles hate other beetles.
const beetle_brood_size = 2.3; //min=0 max=10 step=0.1 The chance that a beetle is spawned each step.
const beetle_pupation_time = 20; //min=12 max=100 step=1 The number of weeks before larvae reach adulthood.
const beetle_density_gain = 1.0; //min=0.1 max=5.0 step=0.1 The increase in density towards the end of the trail.
const beetle_thickness = 1.3; //min=1.0 max=10.0 step=0.1 The width of the paths, which stays fairly uniform.
const beetle_path_variation = 0.52; //min=0 max=2.5 step=0.01 The amount of rotation in each step of the path.
const beetle_broodmother_density = 20; //min=1 max=100 step=1 The fixed density of the adult broodmother
const beetle_broodmother_thickness = 1.4; //min=1 max=10 step=0.1 The fixed width of the adult broodmother path
const beetle_broodmother_variation = 0.05; //min=0 max=1.0 step=0.05 The broodmother's path variation.
const beetle_broodmother_death_age = 10; //min=10 max=100 step=1

Canvas.setpenopacity(pen_opacity);

////////////////////////////////////////////////////////////////
// Pseudorandom number generator. Created by Reinder Nijhoff 2024
// https://turtletoy.net/turtle/a2274fd1fe
////////////////////////////////////////////////////////////////
function random(min = 0.0, max = 1.0) { // returns a number [0, 1)
    const mod = 1 << 20;
    let r = 1103515245 * (((seed+=12345) >> 1) ^ seed);
    r = 1103515245 * (r ^ (r >> 3));
    r = r ^ (r >> 16);
    const rand_normalized = (r % mod) / mod;
    return (max - min) * rand_normalized + min;
}

////////////////////////////////////////////////////////////////
// Generates two numbers generated from a normal distribution.
// Based on the Box-Muller transform:
// https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform
////////////////////////////////////////////////////////////////
function random_normal_dist(std = 1.0, avg = 0.0) {
    //create two random numbers, make sure u1 is greater than zero
    let u1;
    let u2;
    do {
        u1 = random();
    } while (u1 == 0);
    u2 = random();
    const mag = std * Math.sqrt(-2.0 * Math.log(u1));
    const z0  = mag * Math.cos(Math.PI * 2 * u2) + avg;
    const z1  = mag * Math.sin(Math.PI * 2 * u2) + avg;

    return [z0, z1];
}

////////////////////////////////////////////////////////////////
// Normalize an angle to be between -2pi and 2pi
////////////////////////////////////////////////////////////////
function normalize(angle) {
    angle = angle % (2 * Math.PI);
    angle = (angle + 2 * Math.PI) % (2 * Math.PI);
    if (angle > Math.PI) {
        angle -= 2 * Math.PI;
    }
    return angle;
}

const turtle = new Turtle();
turtle.radians();

////////////////////////////////////////////////////////////////
// Shade function to create a rough gradient around the point,
// intended to simulate graphite pencil marks.
////////////////////////////////////////////////////////////////
function shade(pencil_tip, thickness) {
    // create a gradient in a circle around the point
    turtle.pendown()
    turtle.setheading(pencil_tip.o + Math.PI/4);
    for (let i = 0; i < thickness * pencil_tip.size; i++) {
        // apply some scattering to get a better gradient effect
        let random_numbers = [0, 0]
        if (pen_distribution == 1) {
            random_numbers = random_normal_dist(thickness * scale_factor, 0);
        } else if (pen_distribution == 0) {
            random_numbers = [
                random(-thickness * scale_factor, thickness * scale_factor),
                random(-thickness * scale_factor, thickness * scale_factor)];
        }
        turtle.jump(scale_factor * (pencil_tip.x + random_numbers[0]),
                    scale_factor * (pencil_tip.y + random_numbers[1]));
        if (pen_use_circles) {
            turtle.circle(0.1);
        } else {
            turtle.forward(1);
            if (i % 2 == 0) {
                turtle.left(Math.PI / 2);
            } else {
                turtle.right(Math.PI / 2);
            }
        }
    }
}

class Larvae {
    x; // X position of the larvae
    y; // Y position of the larvae
    o; // Orientation of the larvae
    age; // The age of the larvae; as it gets older, the path grows wider
    death_age = beetle_pupation_time;
    constructor(start_x, start_y, start_orientation) {
        this.x = start_x;
        this.y = start_y;
        this.o = start_orientation;
        this.age = 0.0;
    }
    
    get emerged() {
        return this.age > this.death_age;
    }
    
    step() {
        this.age += 0.5;
        this.x += Math.cos(this.o);
        this.y += Math.sin(this.o);
        // Arbitrarily scaling the size field to get the right amount of density increase
        return {"x": this.x, "y": this.y, "o": this.o, "size": this.age * beetle_density_gain};
    }

    reorient(other_beetles) {
        if (this.emerged) {
            return;
        }
        let vec_sum = {x: 0, y: 0};
        for (const other_beetle of other_beetles) {
            const vec_diff = {x: this.x - other_beetle.x, y: this.y - other_beetle.y};
            if (Math.abs(vec_diff.x + vec_diff.y) > 1) {
                const mag_diff = Math.sqrt(vec_diff.x * vec_diff.x + vec_diff.y * vec_diff.y); 
                vec_sum = {
                    x: vec_sum.x + vec_diff.x / mag_diff,
                    y: vec_sum.y + vec_diff.y / mag_diff
                };
            }
        }
        // normalize
        let o_diff = normalize(Math.atan2(vec_sum.y, vec_sum.x) - this.o);
        this.o += beetle_repellant_factor * o_diff;
        this.o += random(-beetle_path_variation, beetle_path_variation);
    }

    give_birth() {
        if (this.emerged) {
            return [];
        }
        let start_ang = this.o;
        if (random() < 0.5) {
            start_ang += Math.PI / 2;
        } else {
            start_ang -= Math.PI / 2;    
        }
        return [new Larvae(this.x + 2 * Math.cos(start_ang), this.y + 2 * Math.sin(start_ang), start_ang)];
    }
}

const broodmother = new Larvae(0, 50, -Math.PI / 2);
broodmother.death_age = beetle_broodmother_death_age;
let beetles = [];
function walk(i) {
    // Re-orient in separate pass so all beetles are in same week when evaluating separation.
    for (const beetle of beetles) {
        if (!beetle.emerged) {
            beetle.reorient([broodmother, ...beetles]);
        }
    }
    for (const beetle of beetles) {
        if (!beetle.emerged) {
            let beetle_state = beetle.step();
            shade(beetle_state, beetle_thickness);
        }
    }
    if (!broodmother.emerged) {
        let broodmother_state = broodmother.step();
        broodmother_state.size = beetle_broodmother_density;
        shade(broodmother_state, beetle_broodmother_thickness);
        broodmother.o += random(-beetle_broodmother_variation, beetle_broodmother_variation);
        if (random(0, 10) < beetle_brood_size) {
            beetles.push(...broodmother.give_birth());
        }
    }
    return !broodmother.emerged || !beetles.every((beetle) => beetle.emerged);
}