Curricular materials and resources to introduce your students to space plant biology and prepare them to participate in the Growing Beyond Earth classroom-based citizen science project
let currentCultivar = "Buena_Mulata_Pepper";
let growthProgress = 1; // continuous value, 1..18
let isPlaying = false;
let timerId = null;
// Seconds the trial has been actively running (playing). Resets to
// 0 on Reset, but otherwise only ever counts up while playing — the
// longer the trial runs, the higher the growth-rate bonus below.
let elapsedSeconds = 0;
// Base URL for the WordPress Media Library uploads folder holding
// the stage photos.
const IMAGE_BASE_URL = "https://fairchildgarden.org/wp-content/uploads/2026/08/";
// Maps each cultivar's internal name to the short filename prefix
// used in the uploaded image filenames (e.g. Robin_11.jpg).
const CULTIVAR_PREFIX = {
"Buena_Mulata_Pepper": "Buena",
"Heartbreaker_Dora_Red_Hybrid_Tomato": "Heart",
"Microtom_Tomato": "Micro",
"Red_Robin_Tomato": "Robin"
};
const TICK_MS = 250;
// How much each second of trial length adds to growthScore. No cap:
// this term keeps climbing for as long as the simulation plays.
const TRIAL_LENGTH_FACTOR = 0.05;
// Temperature bell curve driving growth rate: full strength
// (gaussian = 1) anywhere in the 22-25°C plateau, tapering off the
// further temp drifts outside that window.
const TEMP_PEAK_LOW = 22;
const TEMP_PEAK_HIGH = 25;
const TEMP_SIGMA = 2.5;
// Fruit set is shown visually through the image sequence rather
// than as a number. Buena Mulata, Microtom, and Red Robin are
// hard-capped at stage 12 — they never reveal the final 6 stages,
// regardless of temperature. Heartbreaker Dora Red Hybrid Tomato is
// also capped at 12 by default, but gradually unlocks into stages
// 13-18 the closer temperature sits to the 22-23°C plateau (see
// LAST6_PEAK_* below). The growth stage counter and progress bar
// still advance 1-18 as normal underneath.
const IMAGE_STAGE_CAP = {
"Buena_Mulata_Pepper": 12,
"Heartbreaker_Dora_Red_Hybrid_Tomato": 12,
"Microtom_Tomato": 12,
"Red_Robin_Tomato": 12
};
// Bell curve controlling how many of the last 6 images (13-18) are
// unlocked for Heartbreaker Dora Red: full 6 unlocked (gaussian = 1)
// anywhere in the 22-23°C plateau, fewer the further temp drifts
// outside that window.
const LAST6_PEAK_LOW = 22;
const LAST6_PEAK_HIGH = 23;
const LAST6_SIGMA = 1.5;
function last6UnlockGaussian(temp){
let distance = 0;
if (temp LAST6_PEAK_HIGH) distance = temp - LAST6_PEAK_HIGH;
return Math.exp(-(distance * distance) / (2 * LAST6_SIGMA * LAST6_SIGMA));
}
// Effective image-stage cap for a cultivar at a given temperature.
function getEffectiveCap(cultivar, temp){
const baseCap = IMAGE_STAGE_CAP[cultivar] || 18;
if (baseCap >= 18) return 18;
if (cultivar === "Heartbreaker_Dora_Red_Hybrid_Tomato") {
const unlockedExtra = Math.round(last6UnlockGaussian(temp) * 6);
return Math.min(18, baseCap + unlockedExtra);
}
// Buena Mulata, Microtom, Red Robin: hard cap, no unlock.
return baseCap;
}
function temperatureGaussian(temp){
let distance = 0;
if (temp TEMP_PEAK_HIGH) distance = temp - TEMP_PEAK_HIGH;
return Math.exp(-(distance * distance) / (2 * TEMP_SIGMA * TEMP_SIGMA));
}
function setCultivar(cultivar){
currentCultivar = cultivar;
document.querySelectorAll("#cultivarButtons button").forEach(btn => {
btn.classList.toggle("active", btn.dataset.cultivar === cultivar);
});
document.getElementById("cultivarLabel").textContent =
cultivar.replaceAll("_"," ");
resetGrowth();
}
const tempSlider = document.getElementById("tempSlider");
const humiditySlider = document.getElementById("humiditySlider");
const pressureSlider = document.getElementById("pressureSlider");
const co2Slider = document.getElementById("co2Slider");
function computeGrowthScore(){
const temp = Number(tempSlider.value);
const humidity = Number(humiditySlider.value);
const pressure = Number(pressureSlider.value);
const co2 = Number(co2Slider.value);
document.getElementById("tempLabel").textContent = temp + "°C";
document.getElementById("humidityLabel").textContent = humidity + "%";
document.getElementById("pressureLabel").textContent =
(pressure/1000).toFixed(1) + " kPa";
document.getElementById("co2Label").textContent = co2 + " ppm";
let growthScore = 0;
// Temperature: normal-distribution-shaped bonus peaking across
// the 22-25°C plateau (scaled so full peak contributes +6).
growthScore += temperatureGaussian(temp) * 6;
growthScore += (humidity - 40) * 1;
growthScore += 10 - Math.abs(pressure - 100000) / 500;
growthScore += (co2 - 400) * 0.01;
switch(currentCultivar){
case "Buena_Mulata_Pepper": break;
case "Heartbreaker_Dora_Red_Hybrid_Tomato": growthScore += 1.5; break;
case "Microtom_Tomato": growthScore += 3; break;
case "Red_Robin_Tomato": growthScore += 2.5; break;
}
// Trial length: the longer the trial has been running, the
// higher the growth rate — uncapped, so this keeps climbing.
growthScore += elapsedSeconds * TRIAL_LENGTH_FACTOR;
return growthScore;
}
// Converts the combined slider/cultivar/trial-length score into a
// rate, in growth-stages per second. Harsh conditions still creep
// forward (floor of 0.02) rather than freezing completely; ideal
// conditions move noticeably faster than poor ones.
function scoreToRate(growthScore){
const rate = 0.05 + growthScore * 0.03;
return Math.max(0.02, rate);
}
function refreshRateDisplay(){
const rate = scoreToRate(computeGrowthScore());
document.getElementById("rateLabel").textContent = rate.toFixed(2);
document.getElementById("trialLengthLabel").textContent = elapsedSeconds.toFixed(0) + "s";
}
function renderStage(){
const stage = Math.min(18, Math.max(1, Math.round(growthProgress)));
document.getElementById("stageLabel").textContent = stage;
const pct = ((growthProgress - 1) / 17) * 100;
document.getElementById("progressFill").style.width = pct + "%";
const cap = getEffectiveCap(currentCultivar, Number(tempSlider.value));
const imageStage = Math.min(stage, cap);
const prefix = CULTIVAR_PREFIX[currentCultivar];
// Try the PNG first — if the server has a real transparent PNG
// for this stage, it renders with its true colors and no
// blend-mode trick needed. If it 404s, handleImageError() falls
// back to the JPG with the fake-transparency blend.
const pngPath = IMAGE_BASE_URL + prefix + "_" + imageStage + ".png";
const jpgPath = IMAGE_BASE_URL + prefix + "_" + imageStage + ".jpg";
const img = document.getElementById("growthImage");
img.dataset.jpgFallback = jpgPath;
img.dataset.attemptedFallback = "false";
img.classList.remove("blend-fallback");
// Hide any stale placeholder while the new image attempts to load.
img.style.display = "block";
document.getElementById("imagePlaceholder").style.display = "none";
document.getElementById("missingPathLabel").textContent = pngPath;
img.src = pngPath;
}
function handleImageError(){
const img = document.getElementById("growthImage");
if (img.dataset.attemptedFallback !== "true" && img.dataset.jpgFallback){
// PNG not found at this stage — fall back to the JPG with
// the blend-mode fake-transparency trick.
img.dataset.attemptedFallback = "true";
img.classList.add("blend-fallback");
document.getElementById("missingPathLabel").textContent = img.dataset.jpgFallback;
img.src = img.dataset.jpgFallback;
return;
}
// Both PNG and JPG failed — genuinely missing.
img.style.display = "none";
document.getElementById("imagePlaceholder").style.display = "block";
}
function handleImageLoad(){
document.getElementById("growthImage").style.display = "block";
document.getElementById("imagePlaceholder").style.display = "none";
}
function tick(){
elapsedSeconds += TICK_MS / 1000;
const rate = scoreToRate(computeGrowthScore());
document.getElementById("rateLabel").textContent = rate.toFixed(2);
document.getElementById("trialLengthLabel").textContent = elapsedSeconds.toFixed(0) + "s";
growthProgress += rate * (TICK_MS / 1000);
if (growthProgress >= 18){
growthProgress = 18;
renderStage();
pausePlayback();
return;
}
renderStage();
}
function togglePlay(){
if (isPlaying){
pausePlayback();
} else {
startPlayback();
}
}
function startPlayback(){
if (growthProgress >= 18) return;
isPlaying = true;
document.getElementById("playBtn").textContent = "Pause";
timerId = setInterval(tick, TICK_MS);
}
function pausePlayback(){
isPlaying = false;
document.getElementById("playBtn").textContent = "Play";
if (timerId){
clearInterval(timerId);
timerId = null;
}
}
function resetGrowth(){
pausePlayback();
growthProgress = 1;
elapsedSeconds = 0;
refreshRateDisplay();
renderStage();
}
[tempSlider, humiditySlider, pressureSlider, co2Slider].forEach(s => {
s.addEventListener("input", refreshRateDisplay);
});
tempSlider.addEventListener("input", renderStage);
document.querySelector('[data-cultivar="Buena_Mulata_Pepper"]').classList.add("active");
refreshRateDisplay();
renderStage();
More questions about Growing Beyond Earth? Join us for Open Office Hours
Office Hours will be virtual from October to April. If these times are not available for you or your students, you can schedule a one-on-one by emailing gbe@fairchildgarden.org
Monday, February 9th, 2026 – 10:30AM-1:00PM Miami-Time – Zoom Link
Thursday, February 19th, 2026 – 2-4PM Miami-Time –Zoom Link
Monday, March 9, 2026 – 10:30AM-1:00PM Miami-Time – Zoom Link
Thursday, April 2nd, 2026 – 2-4PM Miami-Time –Zoom Link
This website is based upon work supported by NASA Grant No. 80NCCS22M0125-SciAct. Any opinions, findings and conclusions or recommendations expressed in this material are those of the authors and do not necessarily reflect the views of the National Aeronautics and Space Administration.