// math functions
// pythagoras
function calculateDistance(x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
}
// random number function
function randomNumber(start, end) {
// both including;
return Math.floor(Math.random() * end) + start;
}
// parameters
var _scale = 1; // canvas scale, useful for zoom
var _speed = 50; // tick interval speed
// panning vars
var netPanningX = 0;
var netPanningY = 0;
// plant modelling
var PlantsArray = [];
var id = 0;
function Plant(x, y, radius, wi = false, vi = false, li = false) {
this.id = id++;
this.XPos = x;
this.YPos = y;
this.radius = radius; // size of plant
// our binary data
this.waterstress = wi;
this.virusinfection = vi;
this.lightstress = li;
// waterlevel
this.waterLevel = 50;
this.waterTolerance = 200; // +- this value is fine for the plants waterlevel
this.bestWaterLevel = 75; // perfect waterLevelValue for this plant
this.waterStressAmount = 0;
this.waterStressMax = 100000; // lethal stress
this.alive = true;
this.virus = {};
PlantsArray.push(this);
this.initRandomStress = () => {
this.waterstress = Math.random() > 0.5 ? false : true;
this.virusinfection = Math.random() > 0.5 ? false : true;
this.lightstress = Math.random() > 0.5 ? false : true;
};
// neighbors
this.neighbors = { n: null, e: null, s: null, w: null }; // north, east, south, west
this.getNearestNeighbors = () => {};
this.render = () => {
drawCircle(
this.XPos + netPanningX,
this.YPos + netPanningY,
this.radius,
this.alive ? "green" : "black",
this.virusinfection ? "red" : "green"
);
drawRect(
this.XPos + this.radius / 2 + netPanningX,
this.YPos + netPanningY,
this.radius - 10,
this.radius + 10,
"white",
"blue"
);
// waterlevel
let wl = this.waterLevel / 100;
drawRect(
this.XPos + this.radius / 2 + netPanningX,
this.YPos + netPanningY,
this.radius - 10,
this.radius + 10,
"blue",
"blue"
);
writeMessage(
this.id,
this.XPos + netPanningX,
this.YPos + netPanningY
);
writeMessage(
this.waterLevel,
this.XPos + this.radius / 2 + netPanningX,
this.YPos + 25 + netPanningY
);
};
}
// draw all plants
function render(mode = "standard") {
context.clearRect(0, 0, canvas.width, canvas.height);
context.save();
context.scale(_scale, _scale);
if (mode === "standard") {
for (let i = 0; i < PlantsArray.length; i++) {
PlantsArray[i].render();
}
}
context.restore();
}
let time = 1;
function nextTick() {
// random rain
let rain = 0;
if (randomNumber(1, 50) === 7) {
rain = randomNumber(20, 80); // rain amount
}
// events
for (let i = 0; i < PlantsArray.length; i++) {
if (randomNumber(1, 20000) === 7) {
if (PlantsArray[i].virusinfection) {
PlantsArray[i].alive = false;
} else {
PlantsArray[i].virusinfection = true;
}
}
if (PlantsArray[i].virusinfection && PlantsArray[i].alive) {
if (randomNumber(1, 100) === 7) {
try {
PlantsArray[i].neighbors.s.virusinfection = true;
} catch (e) {}
}
}
// waterstress
if (PlantsArray[i].alive) {
PlantsArray[i].waterLevel -= 1;
}
// random rain
if (rain > 0) {
PlantsArray[i].waterLevel += rain;
}
if (
PlantsArray[i].waterLevel >
PlantsArray[i].bestWaterLevel + PlantsArray[i].waterTolerance ||
PlantsArray[i].waterLevel <
PlantsArray[i].bestWaterLevel + PlantsArray[i].waterTolerance
) {
PlantsArray[i].waterstress = true;
} else {
PlantsArray[i].waterstress = false;
}
if (PlantsArray[i].waterstress) {
PlantsArray[i].waterStressAmount++;
}
if (
PlantsArray[i].waterStressAmount > PlantsArray[i].waterStressMax
) {
PlantsArray[i].alive = false;
}
}
time++;
}
var dayCounter = 0;
function renderNextTick() {
nextTick();
render();
dayCounter++;
}
// throttle function to prevent functions spamming too much and killing performance
// (function, ms, null)
function throttle(t, u, a) {
var e,
i,
r,
o = null,
c = 0;
a = a || {};
function p() {
(c = !1 === a.leading ? 0 : Date.now()),
(o = null),
(r = t.apply(e, i)),
o || (e = i = null);
}
return function () {
var n = Date.now();
c || !1 !== a.leading || (c = n);
var l = u - (n - c);
return (
(e = this),
(i = arguments),
l <= 0 || u < l
? (o && (clearTimeout(o), (o = null)),
(c = n),
(r = t.apply(e, i)),
o || (e = i = null))
: o || !1 === a.trailing || (o = setTimeout(p, l)),
r
);
};
}
// scroll functions
let Body = document.body;
let lastScrollTop = 0;
function scrollFunc() {
var e = window.pageYOffset || document.documentElement.scrollTop,
t = Body.offsetHeight;
Body.classList.remove("scrolling-none"),
0 === e
? (Body.classList.remove("scroll-foot"),
Body.classList.add("scroll-head"))
: window.innerHeight + window.scrollY >= t &&
(Body.classList.remove("scroll-head"),
Body.classList.add("scroll-foot")),
0 < e &&
e <= t &&
(Body.classList.remove("scroll-head"),
Body.classList.remove("scroll-foot")),
300 < e
? (Body.classList.remove("scroll-lt-300"),
Body.classList.add("scroll-gt-300"))
: (Body.classList.remove("scroll-gt-300"),
Body.classList.add("scroll-lt-300")),
1200 < e
? (Body.classList.remove("scroll-lt-1200"),
Body.classList.add("scroll-gt-1200"))
: (Body.classList.remove("scroll-gt-1200"),
Body.classList.add("scroll-lt-1200")),
1500 < e
? (Body.classList.remove("scroll-lt-1500"),
Body.classList.add("scroll-gt-1500"))
: (Body.classList.remove("scroll-gt-1500"),
Body.classList.add("scroll-lt-1500")),
e > lastScrollTop
? (Body.classList.remove("scrolling-up"),
Body.classList.add("scrolling-down"),
50 < e && Body.classList.remove("menu-open"))
: e < lastScrollTop
? (Body.classList.remove("scrolling-down"),
Body.classList.add("scrolling-up"),
Body.classList.remove("menu-open"))
: e === lastScrollTop && Body.classList.add("scrolling-none"),
(lastScrollTop = e <= 0 ? 0 : e),
e > screen.height
? (Body.classList.remove("scroll-lt-screenheight"),
Body.classList.add("scroll-gt-screenheight"))
: (Body.classList.remove("scroll-gt-screenheight"),
Body.classList.add("scroll-lt-screenheight"));
}
window.addEventListener("scroll", throttle(scrollFunc, 150, null), {
passive: !0,
}),
window.addEventListener("resize", throttle(scrollFunc, 80, null));
document.addEventListener("DOMContentLoaded", function () {
scrollFunc();
});
// draw functions
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");
function writeMessage(message, x, y) {
context.font = "15pt Verdana";
context.fillStyle = "black";
context.fillText(message, x, y);
}
function drawCircle(
centerX,
centerY,
radius,
fillC = "white",
strokeC = "black"
) {
context.beginPath();
context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
context.fillStyle = fillC;
context.fill();
context.lineWidth = 5;
context.strokeStyle = strokeC || "#003300";
context.stroke();
}
function drawRect(
centerX,
centerY,
width,
height,
fillC = "white",
strokeC = "black"
) {
context.beginPath();
context.lineWidth = 2;
context.fillStyle = fillC || "white";
context.strokeStyle = strokeC;
context.rect(centerX, centerY, width, height);
context.stroke();
context.fill();
}
// html listener and functions
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top,
};
}
// hover overlay
let info = document.getElementById("INFO");
canvas.addEventListener(
"mousemove",
function (evt) {
var mousePos = getMousePos(canvas, evt);
let clientWidth = document.body.clientWidth;
let canvasWidth = canvas.width;
let factor = (clientWidth / canvasWidth) * _scale;
//console.log(message);
for (let i = 0; i < PlantsArray.length; i++) {
if (
mousePos.x <=
(PlantsArray[i].XPos + PlantsArray[i].radius + netPanningX) *
factor
) {
if (
mousePos.y <=
(PlantsArray[i].YPos + PlantsArray[i].radius + netPanningY) *
factor
) {
// console.log(PlantsArray[i]);
var message =
PlantsArray[i].id +
" is alive: " +
PlantsArray[i].alive +
"
" +
" waterlevel: " +
PlantsArray[i].waterLevel +
"
" +
" is infected?: ";
// message += PlantsArray[i].waterStresswaterStressAmount > 900 ? "yes" : "no";
message += PlantsArray[i].virusinfection ? "yes" : "no";
message += "
days since simulation started: " + dayCounter;
info.style.transform =
"translate(" + evt.clientX + "px," + evt.clientY + "px)";
info.innerHTML = message;
// PlantsArray[i].virusinfection = true;
break;
}
}
}
},
false
);
// start and stop functions
let x;
let running = false;
let startstopbtn = document.getElementById("startstopbtn");
function startInterval() {
if (running) return;
x = setInterval(() => {
renderNextTick();
}, _speed);
running = true;
}
function stopInterval() {
clearInterval(x);
running = false;
}
function startStop() {
if (running) {
startstopbtn.innerHTML = "Start";
stopInterval();
} else {
startstopbtn.innerHTML = "Stop";
startInterval();
}
}
// parameter handling
// set scale
let zoominput = document.getElementById("zoom");
let zoomvaluedis = document.getElementById("zoomvalue");
function setScale(e = 1) {
if (e != 1 && e.value) {
_scale = e.value;
} else {
_scale = e;
}
zoominput.value = _scale;
zoomvaluedis.innerHTML = _scale;
render();
}
// set speed
let speedinput = document.getElementById("speed");
let speedvaluedis = document.getElementById("speedvalue");
function setSpeed(e = 1) {
if (e != 1 && e.value) {
_speed = e.value;
} else {
_scale = e;
}
speedinput.value = _speed;
speedvaluedis.innerHTML = _speed;
if (running) {
stopInterval();
startInterval();
}
}
// panning
let isDragging = false;
let startX = 0;
let startY = 0;
// account for scrolling
function reOffset() {
var BB = canvas.getBoundingClientRect();
offsetX = BB.left;
offsetY = BB.top;
}
var offsetX, offsetY;
reOffset();
window.onscroll = function (e) {
reOffset();
};
window.onresize = function (e) {
reOffset();
};
canvas.addEventListener("mousedown", (e) => handleMouseDown(e));
canvas.addEventListener("mouseup", (e) => handleMouseUp(e));
canvas.addEventListener("mousemove", (e) => {
handleMouseMove(e);
});
canvas.addEventListener("mouseout", (e) => handleMouseOut(e));
function handleMouseDown(e) {
e.preventDefault();
e.stopPropagation();
// calc the starting mouse X,Y for the drag
startX = parseInt(e.clientX - offsetX);
startY = parseInt(e.clientY - offsetY);
isDragging = true;
}
function handleMouseUp(e) {
e.preventDefault();
e.stopPropagation();
isDragging = false;
}
function handleMouseOut(e) {
e.preventDefault();
e.stopPropagation();
isDragging = false;
}
function handleMouseMove(e) {
if (!isDragging) {
return;
}
e.preventDefault();
e.stopPropagation();
// get the current mouse position
mouseX = parseInt(e.clientX - offsetX);
mouseY = parseInt(e.clientY - offsetY);
// dx & dy are the distance the mouse has moved since
// the last mousemove event
let dx = mouseX - startX;
let dy = mouseY - startY;
// reset the vars for next mousemove
startX = mouseX;
startY = mouseY;
// accumulate the net panning done
netPanningX += dx;
netPanningY += dy;
render();
}
var ln = [];
function initLines(mode = "rectangle") {
if (mode === "rectangle") {
for (let i = 30; i > 0; i--) {
ln.push({
width: 3800,
marginToNextLine: 45 + randomNumber(-15, 15),
});
}
}
}
function initField(lines, plantsize = 40) {
let x = 90;
let y = 90;
let m = lines[0].marginToNextLine;
let x1 = x + randomNumber(-m / 2, m * 1.5);
let y1 = y + randomNumber(-m / 2, 0);
let previousPlant = null;
let previousLine = [];
let thisLine = [];
// for every line
for (let i = 0; i < lines.length; i++) {
let count = 0;
while (x1 < lines[i].width) {
let plant = new Plant(
x1 + randomNumber(-5, 5),
y1 + randomNumber(-5, 15),
plantsize
);
thisLine.push(plant);
plant.neighbors.w = previousPlant;
if (previousPlant) {
previousPlant.neighbors.e = plant;
}
if (previousLine.length > 0) {
// find north neighbor
let pd = 0;
let d = 0;
let shortest = 0;
for (let i = 0; i < previousLine.length; i++) {
//console.log(i);
pd = calculateDistance(
plant.XPos,
plant.YPos,
previousLine[i].XPos,
previousLine[i].YPos
);
try {
d = calculateDistance(
plant.XPos,
plant.YPos,
previousLine[i + 1].XPos,
previousLine[i + 1].YPos
);
} catch (e) {
//end of line
}
if (d < pd) {
shortest = d;
continue;
}
plant.neighbors.n = previousLine[i];
PlantsArray[previousLine[i].id].neighbors.s = plant;
break;
}
}
count++;
previousPlant = plant;
x1 += x + lines[i].marginToNextLine + randomNumber(-m / 2, m * 1.5);
}
console.log(lines[i] + " finished with " + count + " plants");
previousLine = thisLine;
thisLine = [];
x1 = x + randomNumber(-m / 2, m / 2);
y1 += y + lines[i].marginToNextLine + randomNumber(-m / 2, 0);
}
}
initLines();
initField(ln);
render();