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 pen_opacity = 0.2; //min=-0.9 max=0.9 step=0.05 Opacity of the drawing.
const pen_thickness = 1.0; //min=1.0 max=10.0 step=0.1 The width of the paths, which stays fairly uniform.
const pen_density_gain = 1.0; //min=0.1 max=5.0 step=0.1 The increase in density towards the end of the trail.
/// BEETLE PARAMETERS
const beetle_brood_size = 4; //min=0 max=15 step=1 The chance that a beetle is spawned each step.
const beetle_path_variation = 0.5; //min=0 max=1 step=0.01 The amount of rotation in each step of the path.
const beetle_pupation_time = 20; //min=12 max=100 step=1 The number of weeks before larvae reach adulthood.
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];
}
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) {
// create a gradient in a circle around the point
for (let i = 0; i < pen_thickness * pencil_tip.size; i++) {
// apply some scattering to get a better gradient effect
random_numbers = random_normal_dist(pen_thickness, 0);
turtle.jump(pencil_tip.x + random_numbers[0],
pencil_tip.y + random_numbers[1]);
turtle.setheading(pencil_tip.o + random(-pen_thickness, pen_thickness));
turtle.forward(1);
}
}
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
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 > beetle_pupation_time;
}
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 * pen_density_gain};
}
reorient(other_beetles) {
if (this.emerged) {
return;
}
let vec_sum = {x: 0, y: 0};
for (const other_beetle of other_beetles) {
if (other_beetle != this) {
const vec_diff = {x: this.x - other_beetle.x, y: this.y - other_beetle.y};
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
};
}
}
this.o = Math.atan2(vec_sum.y, vec_sum.x);
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, this.y, start_ang)];
}
}
const broodmother = new Larvae(0, 0, -Math.PI / 2);
let beetles = [broodmother];
function walk(i) {
for (const beetle of beetles) {
if (!beetle.emerged) {
beetle_state = beetle.step();
shade(beetle_state);
}
}
// Re-orient in second pass when all beetles are in same week.
for (const beetle of beetles) {
if (!beetle.emerged) {
beetle.reorient(beetles);
}
}
if (random(0, 10) < beetle_brood_size) {
beetles.push(...broodmother.give_birth());
}
return !beetles.every((beetle) => beetle.emerged);
}