FractalGarden
29 views ยท Dec 30, 2025
Watch tiny seeds drift through the air, sink into a softly glowing, shifting soil, and slowly sprout into branching plants that respond to hidden waves of underground nutrients. Move and resize your browser window to see the ecosystem re-form across the changing landscape.
๐ Embed Code
Copy this code to embed this sketch on your website:
<iframe src="https://p5js.ai/embed/387cdb48-fa5b-4fd3-8a55-aa639cf34629" width="400" height="400" frameborder="0" allow="autoplay" title="FractalGarden"></iframe>
Adjust width and height as needed. Preview embed โ
โ About This Sketch 3 questions
What visual effects can I expect from the FractalGarden sketch?
The FractalGarden sketch creates a mesmerizing visual display of drifting seeds, glowing soil, and branching plants that dynamically respond to hidden nutrient waves.
How can I interact with the FractalGarden sketch while using it?
Users can interact with the sketch by moving and resizing their browser window, which causes the ecosystem to re-form and adapt to the changing landscape.
What creative coding concepts are showcased in the FractalGarden sketch?
This sketch demonstrates techniques such as Perlin noise for simulating natural phenomena, diffusion for nutrient spreading, and L-system-inspired growth for plant development.
๐ฌ AI Chat History 10 messages
The bottom 150 pixels of the canvas will represent the soil. Each cell in this soil grid has a nutrient value (0-100), mapped to colors from dark brown/gray (depleted) to deep amber/red (rich). The simulation includes nutrient wells that regenerate nutrients in their radius, a slow diffusion process where cells share nutrients with neighbors, and a subtle perlin noise effect to make the soil "breathe" dynamically.
---
### `sketch.js`
```js
// ============================================================
// This sketch simulates soil nutrients with wells, diffusion, and noise.
// ============================================================
// --- Global Variables ---
// These variables control the size, appearance, and behavior of the soil simulation.
let cellSize = 10; // The size of each square cell in the soil grid (in pixels).
let soilHeight = 150; // The height of the soil area from the bottom of the canvas (in pixels).
let cols, rows; // Variables to store the number of columns and rows in the soil grid.
let soilGrid = []; // A 2D array to store the nutrient value (0-100) for each cell.
let wells = []; // An array to store the properties of nutrient wells.
let perlinNoiseOffset = 0; // A variable to drift the Perlin noise, creating subtle animation.
let numWells; // The number of nutrient wells (will be randomized between 5 and 8).
let diffusionRate = 0.05; // How quickly nutrients spread to neighboring cells (0 to 1).
let regenerationRate = 0.5; // How quickly wells regenerate nutrients in their vicinity.
let wellRadiusCells = 5; // The radius of influence for each well, measured in cells.
let noiseScale = 0.02; // Determines the "zoom" level or granularity of the Perlin noise.
let noiseAmplitude = 10; // The maximum amount (in nutrient points) that Perlin noise can affect a cell.
// --- setup() function ---
// This function is called once when the sketch starts.
function setup() {
createCanvas(windowWidth, windowHeight); // Create a canvas that fills the entire browser window.
noStroke(); // Disable drawing outlines around shapes for a smoother look.
colorMode(HSB, 360, 100, 100); // Set color mode to HSB (Hue, Saturation, Brightness)
// with ranges 0-360 for Hue and 0-100 for Saturation/Brightness.
// Calculate the number of columns and rows for the soil grid based on canvas size and cell size.
cols = floor(width / cellSize);
rows = floor(soilHeight / cellSize);
// Initialize the soil grid with starting nutrient values and create the nutrient wells.
initSoilGridAndWells();
}
// --- draw() function ---
// This function is called repeatedly, typically 60 times per second.
// All animations and updates happen here.
function draw() {
background(0, 0, 5); // Draw a very dark gray background, clearing the canvas each frame.
// --- Nutrient Well Regeneration ---
// Wells slowly regenerate nutrients within their radius.
for (let well of wells) {
let wellCol = floor(well.x / cellSize); // Convert well's pixel x-position to grid column index.
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
// Calculate the distance between the current cell and the well's center in cell units.
let distance = dist(c, r, wellCol, well.y);
if (distance <= wellRadiusCells) {
// Regenerate nutrients, with a stronger effect closer to the well.
let regenerationAmount = map(distance, 0, wellRadiusCells, regenerationRate, 0);
soilGrid[r][c] = constrain(soilGrid[r][c] + regenerationAmount, 0, 100);
}
}
}
}
// --- Perlin Noise Drift and Nutrient Diffusion ---
// Create a temporary grid to store new nutrient values. This is crucial to ensure
// all calculations for a given frame are based on the state at the beginning of that frame,
// preventing artifacts from using updated values within the same iteration.
let newSoilGrid = [];
for (let r = 0; r < rows; r++) {
newSoilGrid[r] = []; // Initialize a new row for the temporary grid.
for (let c = 0; c < cols; c++) {
// 1. Perlin Noise Drift: Add subtle, organic variation to nutrient values.
let noiseValue = noise(c * noiseScale, r * noiseScale, perlinNoiseOffset);
let noiseEffect = (noiseValue - 0.5) * noiseAmplitude; // Scale noise to a range like -10 to +10.
let currentNutrient = soilGrid[r][c]; // Get the current cell's nutrient value.
let nutrientAfterNoise = constrain(currentNutrient + noiseEffect, 0, 100); // Apply noise effect and clamp.
// 2. Nutrient Diffusion: Cells share nutrients with their immediate neighbors.
let sumNeighbors = 0;
let numNeighbors = 0;
// Define offsets for the four direct neighbors (up, down, left, right).
let neighbors = [[-1, 0], [1, 0], [0, -1], [0, 1]];
for (let neighbor of neighbors) {
let nr = r + neighbor[0]; // Neighbor row index.
let nc = c + neighbor[1]; // Neighbor column index.
// Check if the neighbor is within the grid boundaries.
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
sumNeighbors += soilGrid[nr][nc]; // Add neighbor's nutrient value (from the original grid).
numNeighbors++;
}
}
let neighborAverage = sumNeighbors / numNeighbors; // Calculate the average nutrient of valid neighbors.
// Apply diffusion: Move the cell's nutrient value slightly towards the average of its neighbors.
let nutrientAfterDiffusion = nutrientAfterNoise + diffusionRate * (neighborAverage - nutrientAfterNoise);
// Store the final calculated nutrient value (after noise and diffusion) in the temporary grid.
newSoilGrid[r][c] = constrain(nutrientAfterDiffusion, 0, 100);
}
}
// Update the main soil grid with the new values calculated in the temporary grid.
soilGrid = newSoilGrid;
// Increment the Perlin noise offset to create a slow, continuous drift effect.
perlinNoiseOffset += 0.005;
// --- Visualize Soil ---
// Draw each cell of the soil grid as a colored rectangle.
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
let nutrient = soilGrid[r][c]; // Get the nutrient value for the current cell.
let x = c * cellSize; // Calculate the x-coordinate of the cell.
// Calculate the y-coordinate, positioning cells from the bottom up within the soil area.
let y = height - soilHeight + r * cellSize;
// Map the nutrient value (0-100) to an HSB color:
// - Hue: Transitions from a brownish/amber (around 40) to a more reddish (around 15).
// - Saturation: Transitions from a dull gray (around 10) to a vivid color (around 90).
// - Brightness: Transitions from dark (around 20) to bright (around 90).
let h = map(nutrient, 0, 100, 40, 15);
let s = map(nutrient, 0, 100, 10, 90);
let b = map(nutrient, 0, 100, 20, 90);
fill(h, s, b); // Set the fill color.
rect(x, y, cellSize, cellSize); // Draw the cell as a rectangle.
}
}
}
// --- initSoilGridAndWells() function ---
// This helper function initializes or reinitializes the soil grid and nutrient wells.
// It's called in setup() and windowResized().
function initSoilGridAndWells() {
soilGrid = []; // Clear the existing soil grid.
wells = []; // Clear the existing wells.
// Initialize the soil grid with random initial nutrient values,
// enhanced by Perlin noise for a more natural starting distribution.
for (let r = 0; r < rows; r++) {
soilGrid[r] = []; // Create a new row.
for (let c = 0; c < cols; c++) {
let initialNutrient = random(20, 80); // Start with values between 20 and 80.
let noiseVal = noise(c * noiseScale, r * noiseScale, perlinNoiseOffset);
// Add a scaled Perlin noise effect (more pronounced than the drift in draw()).
initialNutrient += (noiseVal - 0.5) * noiseAmplitude * 2;
soilGrid[r][c] = constrain(initialNutrient, 0, 100); // Clamp values to 0-100.
}
}
// Initialize nutrient wells with random x-positions.
numWells = floor(random(5, 9)); // Randomly choose between 5 and 8 wells.
for (let i = 0; i < numWells; i++) {
let x = random(width); // Random x-position across the canvas width.
let y = rows / 2; // Wells are conceptually at the "surface" of the soil grid (middle row).
wells.push({ x: x, y: y, radius: wellRadiusCells }); // Add well to the array.
}
}
// --- windowResized() function ---
// This function is called automatically whenever the browser window is resized.
function windowResized() {
resizeCanvas(windowWidth, windowHeight); // Resize the canvas to match the new window dimensions.
// Recalculate cols and rows for the soil area based on the new canvas size.
cols = floor(width / cellSize);
rows = floor(soilHeight / cellSize);
// Reinitialize the soil grid and wells to adapt to the new canvas dimensions.
initSoilGridAndWells();
}
```
---
### `index.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Soil Nutrient Simulation</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
<script src="sketch.js"></script>
</body>
</html>
```
---
### `style.css`
```css
html, body {
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars if canvas slightly exceeds window size */
}
canvas {
display: block; /* Remove extra space below the canvas */
}
```
1. **A JavaScript error:** This is the most common reason a p5.js sketch won't run. An error in your code (or even in the library loading) can stop `setup()` from completing or `draw()` from being called.
2. **Incorrect file paths or CDN links:** If `p5.min.js` or `sketch.js` aren't loaded correctly, the p5.js environment won't be set up.
3. **Canvas visibility issues:** Although your `style.css` is minimal and generally good, sometimes other browser extensions or device-specific settings can interfere.
**The very first thing we need to do is check the browser's developer console for errors.**
On a mobile phone:
* **Android (Chrome):** Connect your phone to a computer via USB. On your computer, open Chrome, go to `chrome://inspect/#devices`, find your phone and the open tab, and click "inspect". This will open the developer tools on your computer.
* **iOS (Safari):** On your computer, open Safari, go to `Safari > Preferences > Advanced`, and check "Show Develop menu in menu bar". Then, connect your iPhone/iPad to the computer, open Safari on both devices, and on your computer, select `Develop > [Your Device Name] > [Your Sketch Tab]`.
Please check the console and let me know if you see any errors. The error message will give us a strong clue as to what's going wrong.
In the meantime, I've reviewed the code again. It looks logically sound and should, in principle, create and draw the canvas. I will provide the complete code again, ensuring all links and syntax are correct for p5.js v1.11.3.
---
### `sketch.js`
```js
// ============================================================
// This sketch simulates soil nutrients with wells, diffusion, and noise.
// ============================================================
// --- Global Variables ---
// These variables control the size, appearance, and behavior of the soil simulation.
let cellSize = 10; // The size of each square cell in the soil grid (in pixels).
let soilHeight = 150; // The height of the soil area from the bottom of the canvas (in pixels).
let cols, rows; // Variables to store the number of columns and rows in the soil grid.
let soilGrid = []; // A 2D array to store the nutrient value (0-100) for each cell.
let wells = []; // An array to store the properties of nutrient wells.
let perlinNoiseOffset = 0; // A variable to drift the Perlin noise, creating subtle animation.
let numWells; // The number of nutrient wells (will be randomized between 5 and 8).
let diffusionRate = 0.05; // How quickly nutrients spread to neighboring cells (0 to 1).
let regenerationRate = 0.5; // How quickly wells regenerate nutrients in their vicinity.
let wellRadiusCells = 5; // The radius of influence for each well, measured in cells.
let noiseScale = 0.02; // Determines the "zoom" level or granularity of the Perlin noise.
let noiseAmplitude = 10; // The maximum amount (in nutrient points) that Perlin noise can affect a cell.
// --- setup() function ---
// This function is called once when the sketch starts.
function setup() {
createCanvas(windowWidth, windowHeight); // Create a canvas that fills the entire browser window.
noStroke(); // Disable drawing outlines around shapes for a smoother look.
colorMode(HSB, 360, 100, 100); // Set color mode to HSB (Hue, Saturation, Brightness)
// with ranges 0-360 for Hue and 0-100 for Saturation/Brightness.
// Calculate the number of columns and rows for the soil grid based on canvas size and cell size.
cols = floor(width / cellSize);
rows = floor(soilHeight / cellSize);
// Initialize the soil grid with starting nutrient values and create the nutrient wells.
initSoilGridAndWells();
}
// --- draw() function ---
// This function is called repeatedly, typically 60 times per second.
// All animations and updates happen here.
function draw() {
background(0, 0, 5); // Draw a very dark gray background, clearing the canvas each frame.
// --- Nutrient Well Regeneration ---
// Wells slowly regenerate nutrients within their radius.
for (let well of wells) {
let wellCol = floor(well.x / cellSize); // Convert well's pixel x-position to grid column index.
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
// Calculate the distance between the current cell and the well's center in cell units.
let distance = dist(c, r, wellCol, well.y);
if (distance <= wellRadiusCells) {
// Regenerate nutrients, with a stronger effect closer to the well.
let regenerationAmount = map(distance, 0, wellRadiusCells, regenerationRate, 0);
soilGrid[r][c] = constrain(soilGrid[r][c] + regenerationAmount, 0, 100);
}
}
}
}
// --- Perlin Noise Drift and Nutrient Diffusion ---
// Create a temporary grid to store new nutrient values. This is crucial to ensure
// all calculations for a given frame are based on the state at the beginning of that frame,
// preventing artifacts from using updated values within the same iteration.
let newSoilGrid = [];
for (let r = 0; r < rows; r++) {
newSoilGrid[r] = []; // Initialize a new row for the temporary grid.
for (let c = 0; c < cols; c++) {
// 1. Perlin Noise Drift: Add subtle, organic variation to nutrient values.
let noiseValue = noise(c * noiseScale, r * noiseScale, perlinNoiseOffset);
let noiseEffect = (noiseValue - 0.5) * noiseAmplitude; // Scale noise to a range like -10 to +10.
let currentNutrient = soilGrid[r][c]; // Get the current cell's nutrient value.
let nutrientAfterNoise = constrain(currentNutrient + noiseEffect, 0, 100); // Apply noise effect and clamp.
// 2. Nutrient Diffusion: Cells share nutrients with their immediate neighbors.
let sumNeighbors = 0;
let numNeighbors = 0;
// Define offsets for the four direct neighbors (up, down, left, right).
let neighbors = [[-1, 0], [1, 0], [0, -1], [0, 1]];
for (let neighbor of neighbors) {
let nr = r + neighbor[0]; // Neighbor row index.
let nc = c + neighbor[1]; // Neighbor column index.
// Check if the neighbor is within the grid boundaries.
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
sumNeighbors += soilGrid[nr][nc]; // Add neighbor's nutrient value (from the original grid).
numNeighbors++;
}
}
let neighborAverage = sumNeighbors / numNeighbors; // Calculate the average nutrient of valid neighbors.
// Apply diffusion: Move the cell's nutrient value slightly towards the average of its neighbors.
let nutrientAfterDiffusion = nutrientAfterNoise + diffusionRate * (neighborAverage - nutrientAfterNoise);
// Store the final calculated nutrient value (after noise and diffusion) in the temporary grid.
newSoilGrid[r][c] = constrain(nutrientAfterDiffusion, 0, 100);
}
}
// Update the main soil grid with the new values calculated in the temporary grid.
soilGrid = newSoilGrid;
// Increment the Perlin noise offset to create a slow, continuous drift effect.
perlinNoiseOffset += 0.005;
// --- Visualize Soil ---
// Draw each cell of the soil grid as a colored rectangle.
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
let nutrient = soilGrid[r][c]; // Get the nutrient value for the current cell.
let x = c * cellSize; // Calculate the x-coordinate of the cell.
// Calculate the y-coordinate, positioning cells from the bottom up within the soil area.
let y = height - soilHeight + r * cellSize;
// Map the nutrient value (0-100) to an HSB color:
// - Hue: Transitions from a brownish/amber (around 40) to a more reddish (around 15).
// - Saturation: Transitions from a dull gray (around 10) to a vivid color (around 90).
// - Brightness: Transitions from dark (around 20) to bright (around 90).
let h = map(nutrient, 0, 100, 40, 15);
let s = map(nutrient, 0, 100, 10, 90);
let b = map(nutrient, 0, 100, 20, 90);
fill(h, s, b); // Set the fill color.
rect(x, y, cellSize, cellSize); // Draw the cell as a rectangle.
}
}
}
// --- initSoilGridAndWells() function ---
// This helper function initializes or reinitializes the soil grid and nutrient wells.
// It's called in setup() and windowResized().
function initSoilGridAndWells() {
soilGrid = []; // Clear the existing soil grid.
wells = []; // Clear the existing wells.
// Initialize the soil grid with random initial nutrient values,
// enhanced by Perlin noise for a more natural starting distribution.
for (let r = 0; r < rows; r++) {
soilGrid[r] = []; // Create a new row.
for (let c = 0; c < cols; c++) {
let initialNutrient = random(20, 80); // Start with values between 20 and 80.
let noiseVal = noise(c * noiseScale, r * noiseScale, perlinNoiseOffset);
// Add a scaled Perlin noise effect (more pronounced than the drift in draw()).
initialNutrient += (noiseVal - 0.5) * noiseAmplitude * 2;
soilGrid[r][c] = constrain(initialNutrient, 0, 100); // Clamp values to 0-100.
}
}
// Initialize nutrient wells with random x-positions.
numWells = floor(random(5, 9)); // Randomly choose between 5 and 8 wells.
for (let i = 0; i < numWells; i++) {
let x = random(width); // Random x-position across the canvas width.
let y = rows / 2; // Wells are conceptually at the "surface" of the soil grid (middle row).
wells.push({ x: x, y: y, radius: wellRadiusCells }); // Add well to the array.
}
}
// --- windowResized() function ---
// This function is called automatically whenever the browser window is resized.
function windowResized() {
resizeCanvas(windowWidth, windowHeight); // Resize the canvas to match the new window dimensions.
// Recalculate cols and rows for the soil area based on the new canvas size.
cols = floor(width / cellSize);
rows = floor(soilHeight / cellSize);
// Reinitialize the soil grid and wells to adapt to the new canvas dimensions.
initSoilGridAndWells();
}
```
---
### `index.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Soil Nutrient Simulation</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
<script src="sketch.js"></script>
</body>
</html>
```
---
### `style.css`
```css
html, body {
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars if canvas slightly exceeds window size */
}
canvas {
display: block; /* Remove extra space below the canvas */
}
```
Here's how it will work:
1. **`Seed` Class:**
* Seeds fall from the top of the canvas at random intervals.
* They are small geometric shapes (triangle, square, pentagon, hexagon).
* They have basic physics: gravity and a tiny random wind.
* When a seed hits the soil (the bottom 150 pixels), it checks the nutrient level of that specific cell.
* If nutrients > 40, it germinates and becomes a `Plant`. It also consumes 30 nutrients from that cell.
* If nutrients <= 40, it becomes **dormant**, visualized as a gray/dim shape.
* Dormant seeds will continuously check nutrient levels and can germinate later if conditions improve.
2. **`Plant` Class:**
* Created when a seed germinates.
* Currently, plants are simple green circles that grow slightly over time. You could expand this for more complex plant life later!
3. **Integration into `sketch.js`:**
* New arrays `seeds` and `plants` to manage these objects.
* Logic to spawn new seeds at random intervals.
* Physics and germination logic for seeds.
* Update and draw logic for seeds and plants.
---
### `sketch.js` (Updated)
```js
// ============================================================
// This sketch simulates soil nutrients with wells, diffusion, and noise,
// now including falling seeds and growing plants.
// ============================================================
// --- Global Variables ---
// These variables control the size, appearance, and behavior of the soil simulation.
let cellSize = 10; // The size of each square cell in the soil grid (in pixels).
let soilHeight = 150; // The height of the soil area from the bottom of the canvas (in pixels).
let cols, rows; // Variables to store the number of columns and rows in the soil grid.
let soilGrid = []; // A 2D array to store the nutrient value (0-100) for each cell.
let wells = []; // An array to store the properties of nutrient wells.
let perlinNoiseOffset = 0; // A variable to drift the Perlin noise, creating subtle animation.
let numWells; // The number of nutrient wells (will be randomized between 5 and 8).
let diffusionRate = 0.05; // How quickly nutrients spread to neighboring cells (0 to 1).
let regenerationRate = 0.5; // How quickly wells regenerate nutrients in their vicinity.
let wellRadiusCells = 5; // The radius of influence for each well, measured in cells.
let noiseScale = 0.02; // Determines the "zoom" level or granularity of the Perlin noise.
let noiseAmplitude = 10; // The maximum amount (in nutrient points) that Perlin noise can affect a cell.
// --- New Variables for Seeds and Plants ---
let seeds = []; // Array to hold Seed objects.
let plants = []; // Array to hold Plant objects.
let seedSpawnTimer = 0; // Counter for when to spawn the next seed.
let nextSeedSpawnTime = 0; // Random interval for the next seed spawn.
let gravity = 0.1; // Downward force on seeds.
let windMagnitude = 0.05; // Maximum random horizontal force on seeds.
// --- setup() function ---
// This function is called once when the sketch starts.
function setup() {
createCanvas(windowWidth, windowHeight); // Create a canvas that fills the entire browser window.
noStroke(); // Disable drawing outlines around shapes for a smoother look.
colorMode(HSB, 360, 100, 100); // Set color mode to HSB (Hue, Saturation, Brightness)
// with ranges 0-360 for Hue and 0-100 for Saturation/Brightness.
// Calculate the number of columns and rows for the soil grid based on canvas size and cell size.
cols = floor(width / cellSize);
rows = floor(soilHeight / cellSize);
// Initialize the soil grid with starting nutrient values and create the nutrient wells.
initSoilGridAndWells();
// Initialize the first seed spawn time.
nextSeedSpawnTime = floor(random(60, 120));
}
// --- draw() function ---
// This function is called repeatedly, typically 60 times per second.
// All animations and updates happen here.
function draw() {
background(0, 0, 5); // Draw a very dark gray background, clearing the canvas each frame.
// --- Nutrient Well Regeneration ---
// Wells slowly regenerate nutrients within their radius.
for (let well of wells) {
let wellCol = floor(well.x / cellSize); // Convert well's pixel x-position to grid column index.
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
// Calculate the distance between the current cell and the well's center in cell units.
let distance = dist(c, r, wellCol, well.y);
if (distance <= wellRadiusCells) {
// Regenerate nutrients, with a stronger effect closer to the well.
let regenerationAmount = map(distance, 0, wellRadiusCells, regenerationRate, 0);
soilGrid[r][c] = constrain(soilGrid[r][c] + regenerationAmount, 0, 100);
}
}
}
}
// --- Perlin Noise Drift and Nutrient Diffusion ---
// Create a temporary grid to store new nutrient values. This is crucial to ensure
// all calculations for a given frame are based on the state at the beginning of that frame,
// preventing artifacts from using updated values within the same iteration.
let newSoilGrid = [];
for (let r = 0; r < rows; r++) {
newSoilGrid[r] = []; // Initialize a new row for the temporary grid.
for (let c = 0; c < cols; c++) {
// 1. Perlin Noise Drift: Add subtle, organic variation to nutrient values.
let noiseValue = noise(c * noiseScale, r * noiseScale, perlinNoiseOffset);
let noiseEffect = (noiseValue - 0.5) * noiseAmplitude; // Scale noise to a range like -10 to +10.
let currentNutrient = soilGrid[r][c]; // Get the current cell's nutrient value.
let nutrientAfterNoise = constrain(currentNutrient + noiseEffect, 0, 100); // Apply noise effect and clamp.
// 2. Nutrient Diffusion: Cells share nutrients with their immediate neighbors.
let sumNeighbors = 0;
let numNeighbors = 0;
// Define offsets for the four direct neighbors (up, down, left, right).
let neighbors = [[-1, 0], [1, 0], [0, -1], [0, 1]];
for (let neighbor of neighbors) {
let nr = r + neighbor[0]; // Neighbor row index.
let nc = c + neighbor[1]; // Neighbor column index.
// Check if the neighbor is within the grid boundaries.
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
sumNeighbors += soilGrid[nr][nc]; // Add neighbor's nutrient value (from the original grid).
numNeighbors++;
}
}
let neighborAverage = sumNeighbors / numNeighbors; // Calculate the average nutrient of valid neighbors.
// Apply diffusion: Move the cell's nutrient value slightly towards the average of its neighbors.
let nutrientAfterDiffusion = nutrientAfterNoise + diffusionRate * (neighborAverage - nutrientAfterNoise);
// Store the final calculated nutrient value (after noise and diffusion) in the temporary grid.
newSoilGrid[r][c] = constrain(nutrientAfterDiffusion, 0, 100);
}
}
// Update the main soil grid with the new values calculated in the temporary grid.
soilGrid = newSoilGrid;
// Increment the Perlin noise offset to create a slow, continuous drift effect.
perlinNoiseOffset += 0.005;
// --- Visualize Soil ---
// Draw each cell of the soil grid as a colored rectangle.
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
let nutrient = soilGrid[r][c]; // Get the nutrient value for the current cell.
let x = c * cellSize; // Calculate the x-coordinate of the cell.
// Calculate the y-coordinate, positioning cells from the bottom up within the soil area.
let y = height - soilHeight + r * cellSize;
// Map the nutrient value (0-100) to an HSB color:
// - Hue: Transitions from a brownish/amber (around 40) to a more reddish (around 15).
// - Saturation: Transitions from a dull gray (around 10) to a vivid color (around 90).
// - Brightness: Transitions from dark (around 20) to bright (around 90).
let h = map(nutrient, 0, 100, 40, 15);
let s = map(nutrient, 0, 100, 10, 90);
let b = map(nutrient, 0, 100, 20, 90);
fill(h, s, b); // Set the fill color.
rect(x, y, cellSize, cellSize); // Draw the cell as a rectangle.
}
}
// --- Seed Spawning ---
seedSpawnTimer++;
if (seedSpawnTimer >= nextSeedSpawnTime) {
seeds.push(new Seed());
seedSpawnTimer = 0;
nextSeedSpawnTime = floor(random(60, 120)); // Spawn every 1-2 seconds at 60 FPS.
}
// --- Update and Draw Seeds ---
// Iterate backwards to safely remove seeds from the array.
for (let i = seeds.length - 1; i >= 0; i--) {
let seed = seeds[i];
seed.update();
seed.draw();
// Check for germination of dormant seeds
if (seed.dormant) {
let nutrientValue = soilGrid[seed.soilCellRow][seed.soilCellCol];
if (nutrientValue > 40) {
seed.dormant = false; // Seed is no longer dormant
// Create a new Plant object and add it to the plants array
plants.push(new Plant(seed.x, seed.y, seed.shapeType, frameCount));
// Consume 30 nutrients from the soil cell
soilGrid[seed.soilCellRow][seed.soilCellCol] = constrain(nutrientValue - 30, 0, 100);
// Remove the germinated seed from the seeds array
seeds.splice(i, 1);
}
}
// Remove seeds that have fallen below the soil and are not dormant (i.e., they didn't germinate)
else if (seed.y > height - soilHeight + rows * cellSize && !seed.dormant) {
seeds.splice(i, 1);
}
}
// --- Update and Draw Plants ---
for (let plant of plants) {
plant.update();
plant.draw();
}
}
// --- initSoilGridAndWells() function ---
// This helper function initializes or reinitializes the soil grid and nutrient wells.
// It's called in setup() and windowResized().
function initSoilGridAndWells() {
soilGrid = []; // Clear the existing soil grid.
wells = []; // Clear the existing wells.
// Initialize the soil grid with random initial nutrient values,
// enhanced by Perlin noise for a more natural starting distribution.
for (let r = 0; r < rows; r++) {
soilGrid[r] = []; // Create a new row.
for (let c = 0; c < cols; c++) {
let initialNutrient = random(20, 80); // Start with values between 20 and 80.
let noiseVal = noise(c * noiseScale, r * noiseScale, perlinNoiseOffset);
// Add a scaled Perlin noise effect (more pronounced than the drift in draw()).
initialNutrient += (noiseVal - 0.5) * noiseAmplitude * 2;
soilGrid[r][c] = constrain(initialNutrient, 0, 100); // Clamp values to 0-100.
}
}
// Initialize nutrient wells with random x-positions.
numWells = floor(random(5, 9)); // Randomly choose between 5 and 8 wells.
for (let i = 0; i < numWells; i++) {
let x = random(width); // Random x-position across the canvas width.
let y = rows / 2; // Wells are conceptually at the "surface" of the soil grid (middle row).
wells.push({ x: x, y: y, radius: wellRadiusCells }); // Add well to the array.
}
}
// --- windowResized() function ---
// This function is called automatically whenever the browser window is resized.
function windowResized() {
resizeCanvas(windowWidth, windowHeight); // Resize the canvas to match the new window dimensions.
// Recalculate cols and rows for the soil area based on the new canvas size.
cols = floor(width / cellSize);
rows = floor(soilHeight / cellSize);
// Reinitialize the soil grid and wells to adapt to the new canvas dimensions.
initSoilGridAndWells();
// Reset seed spawning timer
seedSpawnTimer = 0;
nextSeedSpawnTime = floor(random(60, 120));
seeds = []; // Clear seeds on resize
plants = []; // Clear plants on resize
}
// --- Helper Functions ---
// Function to draw a regular polygon
function drawPolygon(x, y, radius, npoints) {
let angle = TWO_PI / npoints;
beginShape();
for (let a = 0; a < TWO_PI; a += angle) {
let sx = x + cos(a) * radius;
let sy = y + sin(a) * radius;
vertex(sx, sy);
}
endShape(CLOSE);
}
// --- Seed Class ---
class Seed {
constructor() {
this.x = random(width);
this.y = -10; // Start above the canvas
this.vx = random(-0.2, 0.2); // Initial horizontal velocity (tiny wind)
this.vy = 0; // Vertical velocity (gravity will affect this)
this.size = random(3, 7); // Small seed size
this.shapeType = random(['triangle', 'square', 'pentagon', 'hexagon']); // Random shape
this.dormant = false;
this.soilCellCol = -1;
this.soilCellRow = -1;
}
update() {
if (this.dormant) {
// Dormant seeds don't move, but might germinate later
return;
}
// Apply gravity
this.vy += gravity;
// Apply tiny random wind
this.vx += random(-windMagnitude, windMagnitude);
this.vx = constrain(this.vx, -1, 1); // Limit wind effect
this.x += this.vx;
this.y += this.vy;
// Keep seed within canvas boundaries horizontally
this.x = constrain(this.x, 0, width);
// Check for collision with soil
// Soil starts at height - soilHeight
let soilSurfaceY = height - soilHeight;
if (this.y >= soilSurfaceY) {
this.y = soilSurfaceY; // Snap to soil surface
this.vy = 0; // Stop falling
this.vx = 0; // Stop horizontal movement
// Determine the soil cell it landed on
this.soilCellCol = floor(this.x / cellSize);
this.soilCellRow = floor((this.y - soilSurfaceY) / cellSize);
// Make sure it's within bounds
this.soilCellCol = constrain(this.soilCellCol, 0, cols - 1);
this.soilCellRow = constrain(this.soilCellRow, 0, rows - 1);
let nutrientValue = soilGrid[this.soilCellRow][this.soilCellCol];
if (nutrientValue > 40) {
// Germinate! (Handled in draw() to create Plant and consume nutrients)
} else {
// Not enough nutrients, become dormant
this.dormant = true;
}
}
}
draw() {
push();
translate(this.x, this.y);
rotate(frameCount * 0.01); // Subtle rotation for falling seeds
if (this.dormant) {
fill(0, 0, 40, 70); // Dim gray for dormant seeds
} else {
fill(30, 70, 80); // Amber/brown for active seeds
}
switch (this.shapeType) {
case 'triangle':
drawPolygon(0, 0, this.size, 3);
break;
case 'square':
rectMode(CENTER);
rect(0, 0, this.size * 1.2, this.size * 1.2);
break;
case 'pentagon':
drawPolygon(0, 0, this.size, 5);
break;
case 'hexagon':
drawPolygon(0, 0, this.size, 6);
break;
}
pop();
}
}
// --- Plant Class ---
class Plant {
constructor(x, y, seedShape, germinationFrame) {
this.x = x;
this.y = y;
this.seedShape = seedShape; // Keep track of the shape it germinated from
this.size = 5; // Initial plant size
this.germinationFrame = germinationFrame; // Frame when it germinated
this.growthRate = 0.05; // How fast the plant grows
this.maxSize = random(15, 30); // Max size a plant can grow
}
update() {
let framesAlive = frameCount - this.germinationFrame;
this.size = min(this.maxSize, 5 + framesAlive * this.growthRate); // Grow over time
}
draw() {
push();
translate(this.x, this.y);
rotate(sin(frameCount * 0.02) * 0.1); // Gentle sway for plants
fill(120, 80, 70); // Green color for plants
noStroke();
// For now, drawing a simple circle. Could use seedShape later for more variety.
ellipse(0, 0, this.size, this.size * 1.5); // Slightly elongated ellipse
// Optional: Draw a tiny stem
stroke(120, 50, 40);
line(0, 0, 0, -this.size / 2);
pop();
}
}
```
---
### `index.html` (No changes needed)
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Soil Nutrient Simulation</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
<script src="sketch.js"></script>
</body>
</html>
```
---
### `style.css` (No changes needed)
```css
html, body {
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars if canvas slightly exceeds window size */
}
canvas {
display: block; /* Remove extra space below the canvas */
}
```
---
**Explanation of Changes:**
* **New Global Variables:** `seeds`, `plants`, `seedSpawnTimer`, `nextSeedSpawnTime`, `gravity`, and `windMagnitude` are added to manage the new objects and their behavior.
* **`setup()`:** `nextSeedSpawnTime` is initialized.
* **`draw()`:**
* **Seed Spawning:** `seedSpawnTimer` increments. When it reaches `nextSeedSpawnTime`, a new `Seed` object is created and added to the `seeds` array. The timer is reset, and a new random interval is set for the next seed.
* **Seed Logic:** The code iterates through the `seeds` array. Each seed's `update()` method is called to apply physics and check for soil collision. Its `draw()` method renders its shape.
* **Dormant Seed Germination:** A new check is added for dormant seeds. If a dormant seed's soil cell nutrient level rises above 40, it germinates:
* It becomes a `Plant`.
* It consumes 30 nutrients from its soil cell.
* The seed object is removed from the `seeds` array.
* **Seed Removal:** Seeds that fall below the soil and **do not** become dormant (meaning they failed to germinate) are removed from the `seeds` array to keep it clean.
* **Plant Logic:** The code iterates through the `plants` array, calling `update()` (for growth) and `draw()` for each plant.
* **`windowResized()`:** Clears the `seeds` and `plants` arrays and resets the seed spawn timer when the window is resized to avoid visual artifacts.
* **`drawPolygon()` Helper Function:** A general function to draw regular polygons, used by the `Seed` class for its shapes.
* **`Seed` Class:**
* **`constructor()`:** Sets initial position, random shape, size, and velocity.
* **`update()`:** Handles gravity, wind, and collision with the soil surface. It determines if a seed should be dormant or ready for germination based on nutrient levels.
* **`draw()`:** Renders the seed's geometric shape, changing its color if it's dormant.
* **`Plant` Class:**
* **`constructor()`:** Initializes the plant at the seed's germination location.
* **`update()`:** Simulates growth over time by increasing the plant's `size`.
* **`draw()`:** Renders a simple green ellipse (representing a plant) that grows.
Now, you should see seeds falling, interacting with the soil, and either becoming dormant or germinating into plants!
The **absolute most important step** is to check the browser's developer console for errors. Without that information, I'm essentially guessing in the dark.
**How to Check the Console on Mobile:**
* **Android (Chrome):**
1. Connect your Android phone to your computer via a USB cable.
2. On your computer, open Google Chrome.
3. In Chrome, go to `chrome://inspect/#devices`.
4. You should see your connected phone listed. Find the tab where you're running the p5.js sketch and click the "inspect" button next to it.
5. This will open a developer tools window on your computer, showing the console for your phone's browser tab.
* **iOS (Safari):**
1. On your computer, open Safari.
2. Go to `Safari > Preferences > Advanced` and make sure "Show Develop menu in menu bar" is checked.
3. Connect your iPhone/iPad to your computer via a USB cable.
4. On your iPhone/iPad, open Safari and navigate to your p5.js sketch.
5. On your computer, go to `Develop > [Your Device Name] > [Your Sketch Tab]`.
6. This will open a developer tools window on your computer, showing the console for your phone's browser tab.
**Please share any errors you see in the console.** This is the fastest way to pinpoint the problem.
---
**In the meantime, let's double-check everything again.**
Here is the complete, updated code for all three files. Please ensure you copy and paste them *exactly* as provided.
### `sketch.js` (Updated with Seed and Plant classes)
```js
// ============================================================
// This sketch simulates soil nutrients with wells, diffusion, and noise,
// now including falling seeds and growing plants.
// ============================================================
// --- Global Variables ---
// These variables control the size, appearance, and behavior of the soil simulation.
let cellSize = 10; // The size of each square cell in the soil grid (in pixels).
let soilHeight = 150; // The height of the soil area from the bottom of the canvas (in pixels).
let cols, rows; // Variables to store the number of columns and rows in the soil grid.
let soilGrid = []; // A 2D array to store the nutrient value (0-100) for each cell.
let wells = []; // An array to store the properties of nutrient wells.
let perlinNoiseOffset = 0; // A variable to drift the Perlin noise, creating subtle animation.
let numWells; // The number of nutrient wells (will be randomized between 5 and 8).
let diffusionRate = 0.05; // How quickly nutrients spread to neighboring cells (0 to 1).
let regenerationRate = 0.5; // How quickly wells regenerate nutrients in their vicinity.
let wellRadiusCells = 5; // The radius of influence for each well, measured in cells.
let noiseScale = 0.02; // Determines the "zoom" level or granularity of the Perlin noise.
let noiseAmplitude = 10; // The maximum amount (in nutrient points) that Perlin noise can affect a cell.
// --- New Variables for Seeds and Plants ---
let seeds = []; // Array to hold Seed objects.
let plants = []; // Array to hold Plant objects.
let seedSpawnTimer = 0; // Counter for when to spawn the next seed.
let nextSeedSpawnTime = 0; // Random interval for the next seed spawn.
let gravity = 0.1; // Downward force on seeds.
let windMagnitude = 0.05; // Maximum random horizontal force on seeds.
// --- setup() function ---
// This function is called once when the sketch starts.
function setup() {
createCanvas(windowWidth, windowHeight); // Create a canvas that fills the entire browser window.
noStroke(); // Disable drawing outlines around shapes for a smoother look.
colorMode(HSB, 360, 100, 100); // Set color mode to HSB (Hue, Saturation, Brightness)
// with ranges 0-360 for Hue and 0-100 for Saturation/Brightness.
// Calculate the number of columns and rows for the soil grid based on canvas size and cell size.
cols = floor(width / cellSize);
rows = floor(soilHeight / cellSize);
// Initialize the soil grid with starting nutrient values and create the nutrient wells.
initSoilGridAndWells();
// Initialize the first seed spawn time.
nextSeedSpawnTime = floor(random(60, 120));
}
// --- draw() function ---
// This function is called repeatedly, typically 60 times per second.
// All animations and updates happen here.
function draw() {
background(0, 0, 5); // Draw a very dark gray background, clearing the canvas each frame.
// --- Nutrient Well Regeneration ---
// Wells slowly regenerate nutrients within their radius.
for (let well of wells) {
let wellCol = floor(well.x / cellSize); // Convert well's pixel x-position to grid column index.
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
// Calculate the distance between the current cell and the well's center in cell units.
let distance = dist(c, r, wellCol, well.y);
if (distance <= wellRadiusCells) {
// Regenerate nutrients, with a stronger effect closer to the well.
let regenerationAmount = map(distance, 0, wellRadiusCells, regenerationRate, 0);
soilGrid[r][c] = constrain(soilGrid[r][c] + regenerationAmount, 0, 100);
}
}
}
}
// --- Perlin Noise Drift and Nutrient Diffusion ---
// Create a temporary grid to store new nutrient values. This is crucial to ensure
// all calculations for a given frame are based on the state at the beginning of that frame,
// preventing artifacts from using updated values within the same iteration.
let newSoilGrid = [];
for (let r = 0; r < rows; r++) {
newSoilGrid[r] = []; // Initialize a new row for the temporary grid.
for (let c = 0; c < cols; c++) {
// 1. Perlin Noise Drift: Add subtle, organic variation to nutrient values.
let noiseValue = noise(c * noiseScale, r * noiseScale, perlinNoiseOffset);
let noiseEffect = (noiseValue - 0.5) * noiseAmplitude; // Scale noise to a range like -10 to +10.
let currentNutrient = soilGrid[r][c]; // Get the current cell's nutrient value.
let nutrientAfterNoise = constrain(currentNutrient + noiseEffect, 0, 100); // Apply noise effect and clamp.
// 2. Nutrient Diffusion: Cells share nutrients with their immediate neighbors.
let sumNeighbors = 0;
let numNeighbors = 0;
// Define offsets for the four direct neighbors (up, down, left, right).
let neighbors = [[-1, 0], [1, 0], [0, -1], [0, 1]];
for (let neighbor of neighbors) {
let nr = r + neighbor[0]; // Neighbor row index.
let nc = c + neighbor[1]; // Neighbor column index.
// Check if the neighbor is within the grid boundaries.
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
sumNeighbors += soilGrid[nr][nc]; // Add neighbor's nutrient value (from the original grid).
numNeighbors++;
}
}
let neighborAverage = sumNeighbors / numNeighbors; // Calculate the average nutrient of valid neighbors.
// Apply diffusion: Move the cell's nutrient value slightly towards the average of its neighbors.
let nutrientAfterDiffusion = nutrientAfterNoise + diffusionRate * (neighborAverage - nutrientAfterNoise);
// Store the final calculated nutrient value (after noise and diffusion) in the temporary grid.
newSoilGrid[r][c] = constrain(nutrientAfterDiffusion, 0, 100);
}
}
// Update the main soil grid with the new values calculated in the temporary grid.
soilGrid = newSoilGrid;
// Increment the Perlin noise offset to create a slow, continuous drift effect.
perlinNoiseOffset += 0.005;
// --- Visualize Soil ---
// Draw each cell of the soil grid as a colored rectangle.
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
let nutrient = soilGrid[r][c]; // Get the nutrient value for the current cell.
let x = c * cellSize; // Calculate the x-coordinate of the cell.
// Calculate the y-coordinate, positioning cells from the bottom up within the soil area.
let y = height - soilHeight + r * cellSize;
// Map the nutrient value (0-100) to an HSB color:
// - Hue: Transitions from a brownish/amber (around 40) to a more reddish (around 15).
// - Saturation: Transitions from a dull gray (around 10) to a vivid color (around 90).
// - Brightness: Transitions from dark (around 20) to bright (around 90).
let h = map(nutrient, 0, 100, 40, 15);
let s = map(nutrient, 0, 100, 10, 90);
let b = map(nutrient, 0, 100, 20, 90);
fill(h, s, b); // Set the fill color.
rect(x, y, cellSize, cellSize); // Draw the cell as a rectangle.
}
}
// --- Seed Spawning ---
seedSpawnTimer++;
if (seedSpawnTimer >= nextSeedSpawnTime) {
seeds.push(new Seed());
seedSpawnTimer = 0;
nextSeedSpawnTime = floor(random(60, 120)); // Spawn every 1-2 seconds at 60 FPS.
}
// --- Update and Draw Seeds ---
// Iterate backwards to safely remove seeds from the array.
for (let i = seeds.length - 1; i >= 0; i--) {
let seed = seeds[i];
seed.update();
seed.draw();
// Check for germination of dormant seeds
if (seed.dormant) {
let nutrientValue = soilGrid[seed.soilCellRow][seed.soilCellCol];
if (nutrientValue > 40) {
seed.dormant = false; // Seed is no longer dormant
// Create a new Plant object and add it to the plants array
plants.push(new Plant(seed.x, seed.y, seed.shapeType, frameCount));
// Consume 30 nutrients from the soil cell
soilGrid[seed.soilCellRow][seed.soilCellCol] = constrain(nutrientValue - 30, 0, 100);
// Remove the germinated seed from the seeds array
seeds.splice(i, 1);
}
}
// Remove seeds that have fallen below the soil and are not dormant (i.e., they didn't germinate)
else if (seed.y > height - soilHeight + rows * cellSize && !seed.dormant) {
seeds.splice(i, 1);
}
}
// --- Update and Draw Plants ---
for (let plant of plants) {
plant.update();
plant.draw();
}
}
// --- initSoilGridAndWells() function ---
// This helper function initializes or reinitializes the soil grid and nutrient wells.
// It's called in setup() and windowResized().
function initSoilGridAndWells() {
soilGrid = []; // Clear the existing soil grid.
wells = []; // Clear the existing wells.
// Initialize the soil grid with random initial nutrient values,
// enhanced by Perlin noise for a more natural starting distribution.
for (let r = 0; r < rows; r++) {
soilGrid[r] = []; // Create a new row.
for (let c = 0; c < cols; c++) {
let initialNutrient = random(20, 80); // Start with values between 20 and 80.
let noiseVal = noise(c * noiseScale, r * noiseScale, perlinNoiseOffset);
// Add a scaled Perlin noise effect (more pronounced than the drift in draw()).
initialNutrient += (noiseVal - 0.5) * noiseAmplitude * 2;
soilGrid[r][c] = constrain(initialNutrient, 0, 100); // Clamp values to 0-100.
}
}
// Initialize nutrient wells with random x-positions.
numWells = floor(random(5, 9)); // Randomly choose between 5 and 8 wells.
for (let i = 0; i < numWells; i++) {
let x = random(width); // Random x-position across the canvas width.
let y = rows / 2; // Wells are conceptually at the "surface" of the soil grid (middle row).
wells.push({ x: x, y: y, radius: wellRadiusCells }); // Add well to the array.
}
}
// --- windowResized() function ---
// This function is called automatically whenever the browser window is resized.
function windowResized() {
resizeCanvas(windowWidth, windowHeight); // Resize the canvas to match the new window dimensions.
// Recalculate cols and rows for the soil area based on the new canvas size.
cols = floor(width / cellSize);
rows = floor(soilHeight / cellSize);
// Reinitialize the soil grid and wells to adapt to the new canvas dimensions.
initSoilGridAndWells();
// Reset seed spawning timer
seedSpawnTimer = 0;
nextSeedSpawnTime = floor(random(60, 120));
seeds = []; // Clear seeds on resize
plants = []; // Clear plants on resize
}
// --- Helper Functions ---
// Function to draw a regular polygon
function drawPolygon(x, y, radius, npoints) {
let angle = TWO_PI / npoints;
beginShape();
for (let a = 0; a < TWO_PI; a += angle) {
let sx = x + cos(a) * radius;
let sy = y + sin(a) * radius;
vertex(sx, sy);
}
endShape(CLOSE);
}
// --- Seed Class ---
class Seed {
constructor() {
this.x = random(width);
this.y = -10; // Start above the canvas
this.vx = random(-0.2, 0.2); // Initial horizontal velocity (tiny wind)
this.vy = 0; // Vertical velocity (gravity will affect this)
this.size = random(3, 7); // Small seed size
this.shapeType = random(['triangle', 'square', 'pentagon', 'hexagon']); // Random shape
this.dormant = false;
this.soilCellCol = -1;
this.soilCellRow = -1;
}
update() {
I'll create a more sophisticated `Plant` class that manages an array of `Branch` objects. Each branch will have properties like start, end, direction, length, and depth. The `Plant`'s `update()` method will then apply growth rules based on its `seedShape`, biasing growth towards light, away from other plants, and consuming nutrients from the soil.
Here's the updated `sketch.js` with the new `Plant` class and a minor helper function for vector rotation.
---
### `sketch.js` (Updated with Advanced Plant Growth)
```js
// ============================================================
// This sketch simulates soil nutrients with wells, diffusion, and noise,
// now including falling seeds and L-system-inspired growing plants.
// ============================================================
// --- Global Variables ---
// These variables control the size, appearance, and behavior of the soil simulation.
let cellSize = 10; // The size of each square cell in the soil grid (in pixels).
let soilHeight = 150; // The height of the soil area from the bottom of the canvas (in pixels).
let cols, rows; // Variables to store the number of columns and rows in the soil grid.
let soilGrid = []; // A 2D array to store the nutrient value (0-100) for each cell.
let wells = []; // An array to store the properties of nutrient wells.
let perlinNoiseOffset = 0; // A variable to drift the Perlin noise, creating subtle animation.
let numWells; // The number of nutrient wells (will be randomized between 5 and 8).
let diffusionRate = 0.05; // How quickly nutrients spread to neighboring cells (0 to 1).
let regenerationRate = 0.5; // How quickly wells regenerate nutrients in their vicinity.
let wellRadiusCells = 5; // The radius of influence for each well, measured in cells.
let noiseScale = 0.02; // Determines the "zoom" level or granularity of the Perlin noise.
let noiseAmplitude = 10; // The maximum amount (in nutrient points) that Perlin noise can affect a cell.
// --- New Variables for Seeds and Plants ---
let seeds = []; // Array to hold Seed objects.
let plants = []; // Array to hold Plant objects.
let seedSpawnTimer = 0; // Counter for when to spawn the next seed.
let nextSeedSpawnTime = 0; // Random interval for the next seed spawn.
let gravity = 0.1; // Downward force on seeds.
let windMagnitude = 0.05; // Maximum random horizontal force on seeds.
// --- setup() function ---
// This function is called once when the sketch starts.
function setup() {
createCanvas(windowWidth, windowHeight); // Create a canvas that fills the entire browser window.
noStroke(); // Disable drawing outlines around shapes for a smoother look.
colorMode(HSB, 360, 100, 100); // Set color mode to HSB (Hue, Saturation, Brightness)
// with ranges 0-360 for Hue and 0-100 for Saturation/Brightness.
// Calculate the number of columns and rows for the soil grid based on canvas size and cell size.
cols = floor(width / cellSize);
rows = floor(soilHeight / cellSize);
// Initialize the soil grid with starting nutrient values and create the nutrient wells.
initSoilGridAndWells();
// Initialize the first seed spawn time.
nextSeedSpawnTime = floor(random(60, 120));
}
// --- draw() function ---
// This function is called repeatedly, typically 60 times per second.
// All animations and updates happen here.
function draw() {
background(0, 0, 5); // Draw a very dark gray background, clearing the canvas each frame.
// --- Nutrient Well Regeneration ---
// Wells slowly regenerate nutrients within their radius.
for (let well of wells) {
let wellCol = floor(well.x / cellSize); // Convert well's pixel x-position to grid column index.
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
// Calculate the distance between the current cell and the well's center in cell units.
let distance = dist(c, r, wellCol, well.y);
if (distance <= wellRadiusCells) {
// Regenerate nutrients, with a stronger effect closer to the well.
let regenerationAmount = map(distance, 0, wellRadiusCells, regenerationRate, 0);
soilGrid[r][c] = constrain(soilGrid[r][c] + regenerationAmount, 0, 100);
}
}
}
}
// --- Perlin Noise Drift and Nutrient Diffusion ---
// Create a temporary grid to store new nutrient values. This is crucial to ensure
// all calculations for a given frame are based on the state at the beginning of that frame,
// preventing artifacts from using updated values within the same iteration.
let newSoilGrid = [];
for (let r = 0; r < rows; r++) {
newSoilGrid[r] = []; // Initialize a new row for the temporary grid.
for (let c = 0; c < cols; c++) {
// 1. Perlin Noise Drift: Add subtle, organic variation to nutrient values.
let noiseValue = noise(c * noiseScale, r * noiseScale, perlinNoiseOffset);
let noiseEffect = (noiseValue - 0.5) * noiseAmplitude; // Scale noise to a range like -10 to +10.
let currentNutrient = soilGrid[r][c]; // Get the current cell's nutrient value.
let nutrientAfterNoise = constrain(currentNutrient + noiseEffect, 0, 100); // Apply noise effect and clamp.
// 2. Nutrient Diffusion: Cells share nutrients with their immediate neighbors.
let sumNeighbors = 0;
let numNeighbors = 0;
// Define offsets for the four direct neighbors (up, down, left, right).
let neighbors = [[-1, 0], [1, 0], [0, -1], [0, 1]];
for (let neighbor of neighbors) {
let nr = r + neighbor[0]; // Neighbor row index.
let nc = c + neighbor[1]; // Neighbor column index.
// Check if the neighbor is within the grid boundaries.
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
sumNeighbors += soilGrid[nr][nc]; // Add neighbor's nutrient value (from the original grid).
numNeighbors++;
}
}
let neighborAverage = sumNeighbors / numNeighbors; // Calculate the average nutrient of valid neighbors.
// Apply diffusion: Move the cell's nutrient value slightly towards the average of its neighbors.
let nutrientAfterDiffusion = nutrientAfterNoise + diffusionRate * (neighborAverage - nutrientAfterNoise);
// Store the final calculated nutrient value (after noise and diffusion) in the temporary grid.
newSoilGrid[r][c] = constrain(nutrientAfterDiffusion, 0, 100);
}
}
// Update the main soil grid with the new values calculated in the temporary grid.
soilGrid = newSoilGrid;
// Increment the Perlin noise offset to create a slow, continuous drift effect.
perlinNoiseOffset += 0.005;
// --- Visualize Soil ---
// Draw each cell of the soil grid as a colored rectangle.
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
let nutrient = soilGrid[r][c]; // Get the nutrient value for the current cell.
let x = c * cellSize; // Calculate the x-coordinate of the cell.
// Calculate the y-coordinate, positioning cells from the bottom up within the soil area.
let y = height - soilHeight + r * cellSize;
// Map the nutrient value (0-100) to an HSB color:
// - Hue: Transitions from a brownish/amber (around 40) to a more reddish (around 15).
// - Saturation: Transitions from a dull gray (around 10) to a vivid color (around 90).
// - Brightness: Transitions from dark (around 20) to bright (around 90).
let h = map(nutrient, 0, 100, 40, 15);
let s = map(nutrient, 0, 100, 10, 90);
let b = map(nutrient, 0, 100, 20, 90);
fill(h, s, b); // Set the fill color.
rect(x, y, cellSize, cellSize); // Draw the cell as a rectangle.
}
}
// --- Seed Spawning ---
seedSpawnTimer++;
if (seedSpawnTimer >= nextSeedSpawnTime) {
seeds.push(new Seed());
seedSpawnTimer = 0;
nextSeedSpawnTime = floor(random(60, 120)); // Spawn every 1-2 seconds at 60 FPS.
}
// --- Update and Draw Seeds ---
// Iterate backwards to safely remove seeds from the array.
for (let i = seeds.length - 1; i >= 0; i--) {
let seed = seeds[i];
seed.update();
seed.draw();
// Check for germination of dormant seeds
if (seed.dormant) {
// Ensure soilCellRow and soilCellCol are valid before accessing soilGrid
if (seed.soilCellRow >= 0 && seed.soilCellRow < rows &&
seed.soilCellCol >= 0 && seed.soilCellCol < cols) {
let nutrientValue = soilGrid[seed.soilCellRow][seed.soilCellCol];
if (nutrientValue > 40) {
seed.dormant = false; // Seed is no longer dormant
// Create a new Plant object and add it to the plants array
plants.push(new Plant(seed.x, seed.y, seed.shapeType, frameCount));
// Consume 30 nutrients from the soil cell
soilGrid[seed.soilCellRow][seed.soilCellCol] = constrain(nutrientValue - 30, 0, 100);
// Remove the germinated seed from the seeds array
seeds.splice(i, 1);
}
} else {
// If a seed is dormant but somehow has invalid soil cell indices, remove it.
seeds.splice(i, 1);
}
}
// Remove seeds that have fallen below the soil and are not dormant (i.e., they didn't germinate)
else if (seed.y > height - soilHeight + rows * cellSize && !seed.dormant) {
seeds.splice(i, 1);
}
}
// --- Update and Draw Plants ---
// Iterate backwards to safely remove plants if they die (e.g., run out of nutrients)
for (let i = plants.length - 1; i >= 0; i--) {
let plant = plants[i];
plant.update();
plant.draw();
// Example: If a plant runs out of branches or nutrients, remove it
if (plant.branches.length === 0) { // Or add a nutrient-starvation check
plants.splice(i, 1);
}
}
}
// --- initSoilGridAndWells() function ---
// This helper function initializes or reinitializes the soil grid and nutrient wells.
// It's called in setup() and windowResized().
function initSoilGridAndWells() {
soilGrid = []; // Clear the existing soil grid.
wells = []; // Clear the existing wells.
// Initialize the soil grid with random initial nutrient values,
// enhanced by Perlin noise for a more natural starting distribution.
for (let r = 0; r < rows; r++) {
soilGrid[r] = []; // Create a new row.
for (let c = 0; c < cols; c++) {
let initialNutrient = random(20, 80); // Start with values between 20 and 80.
let noiseVal = noise(c * noiseScale, r * noiseScale, perlinNoiseOffset);
// Add a scaled Perlin noise effect (more pronounced than the drift in draw()).
initialNutrient += (noiseVal - 0.5) * noiseAmplitude * 2;
soilGrid[r][c] = constrain(initialNutrient, 0, 100); // Clamp values to 0-100.
}
}
// Initialize nutrient wells with random x-positions.
numWells = floor(random(5, 9)); // Randomly choose between 5 and 8 wells.
for (let i = 0; i < numWells; i++) {
let x = random(width); // Random x-position across the canvas width.
let y = rows / 2; // Wells are conceptually at the "surface" of the soil grid (middle row).
wells.push({ x: x, y: y, radius: wellRadiusCells }); // Add well to the array.
}
}
// --- windowResized() function ---
// This function is called automatically whenever the browser window is resized.
function windowResized() {
resizeCanvas(windowWidth, windowHeight); // Resize the canvas to match the new window dimensions.
// Recalculate cols and rows for the soil area based on the new canvas size.
cols = floor(width / cellSize);
rows = floor(soilHeight / cellSize);
// Reinitialize the soil grid and wells to adapt to the new canvas dimensions.
initSoilGridAndWells();
// Reset seed spawning timer
seedSpawnTimer = 0;
nextSeedSpawnTime = floor(random(60, 120));
seeds = []; // Clear seeds on resize
plants = []; // Clear plants on resize
}
// --- Helper Functions ---
// Function to draw a regular polygon
function drawPolygon(x, y, radius, npoints) {
let angle = TWO_PI / npoints;
beginShape();
for (let a = 0; a < TWO_PI; a += angle) {
let sx = x + cos(a) * radius;
let sy = y + sin(a) * radius;
vertex(sx, sy);
}
endShape(CLOSE);
}
// Function to rotate a p5.Vector by an angle (in radians)
function rotateVector(v, angle) {
let x = v.x * cos(angle) - v.y * sin(angle);
let y = v.x * sin(angle) + v.y * cos(angle);
return createVector(x, y);
}
// --- Seed Class ---
class Seed {
constructor() {
this.x = random(width);
this.y = -10; // Start above the canvas
this.vx = random(-0.2, 0.2); // Initial horizontal velocity (tiny wind)
this.vy = 0; // Vertical velocity (gravity will affect this)
this.size = random(3, 7); // Small seed size
this.shapeType = random(['triangle', 'square', 'pentagon', 'hexagon']); // Random shape
this.dormant = false;
this.soilCellCol = -1;
this.soilCellRow = -1;
}
update() {
if (this.dormant) {
// Dormant seeds don't move, but might germinate later
return;
}
// Apply gravity
this.vy += gravity;
// Apply tiny random wind
this.vx += random(-windMagnitude, windMagnitude);
this.vx = constrain(this.vx, -1, 1); // Limit wind effect
this.x += this.vx;
this.y += this.vy;
// Keep seed within canvas boundaries horizontally
this.x = constrain(this.x, 0, width);
// Check for collision with soil
// Soil starts at height - soilHeight
let soilSurfaceY = height - soilHeight;
if (this.y >= soilSurfaceY) {
this.y = soilSurfaceY; // Snap to soil surface
this.vy = 0; // Stop falling
this.vx = 0; // Stop horizontal movement
// Determine the soil cell it landed on
this.soilCellCol = floor(this.x / cellSize);
this.soilCellRow = floor((this.y - soilSurfaceY) / cellSize);
// Make sure it's within bounds
this.soilCellCol = constrain(this.soilCellCol, 0, cols - 1);
this.soilCellRow = constrain(this.soilCellRow, 0, rows - 1);
let nutrientValue = soilGrid[this.soilCellRow][this.soilCellCol];
if (nutrientValue > 40) {
// Germinate! (Handled in draw() to create Plant and consume nutrients)
} else {
// Not enough nutrients, become dormant
this.dormant = true;
}
}
}
draw() {
push();
translate(this.x, this.y);
rotate(frameCount * 0.01); // Subtle rotation for falling seeds
if (this.dormant) {
fill(0, 0, 40, 70); // Dim gray for dormant seeds
} else {
fill(30, 70, 80); // Amber/brown for active seeds
}
switch (this.shapeType) {
case 'triangle':
drawPolygon(0, 0, this.size, 3);
break;
case 'square':
rectMode(CENTER);
rect(0, 0, this.size * 1.2, this.size * 1.2);
break;
case 'pentagon':
drawPolygon(0, 0, this.size, 5);
break;
case 'hexagon':
drawPolygon(0, 0, this.size, 6);
break;
}
pop();
}
}
// --- Branch Class (Helper for Plant) ---
class Branch {
constructor(start, direction, length, thickness, depth) {
this.start = start.copy();
this.direction = direction.copy().normalize();
this.length = length;
this.thickness = thickness;
this.depth = depth;
this.end = p5.Vector.add(this.start, p5.Vector.mult(this.direction, this.length));
this.active = true; // Can this branch grow further?
}
draw(hue, saturation, brightness) {
stroke(hue, saturation, brightness);
strokeWeight(this.thickness);
line(this.start.x, this.start.y, this.end.x, this.end.y);
}
}
// --- Plant Class ---
class Plant {
constructor(x, y, seedShape, germinationFrame) {
this.x = x;
this.y = y;
this.seedShape = seedShape;
this.germinationFrame = germinationFrame;
this.age = 0;
this.maxSize = random(40, 80); // Overall scale of the plant
this.maxBranches = floor(random(25, 60)); // Limit to prevent too many branches
this.growthChance = random(0.1, 0.3); // Probability of adding a new branch each frame
this.nutrientConsumptionRate = random(0.1, 0.5); // How much nutrient per frame, distributed
this.hue; // Base color for the plant
this.saturation = 80;
this.brightness = 70;
this.branches = []; // Array of Branch objects
// Determine hue based on seedShape
switch (seedShape) {
case 'triangle':
this.hue = map(random(), 0, 1, 30, 50); // Yellow/orange (warm)
break;
case 'square':
this.hue = map(random(), 0, 1, 45, 65); // Orange/amber (warm)
break;
case 'pentagon':
case 'hexagon':
this.hue = map(random(), 0, 1, 180, 220); // Blue/purple (cool)
break;
}
// Create initial trunk
let trunkLength = random(10, 20);
let trunkThickness = random(2, 4);
// Initial trunk grows upwards relative to plant's base (0,0 in its local space)
this.branches.push(new Branch(createVector(0, 0), createVector(0, -1), trunkLength, trunkThickness, 0));
}
update() {
this.age++;
// --- Nutrient Consumption ---
// Approximate the plant's footprint for nutrient consumption
let plantBaseX = floor(this.x / cellSize);
let plantBaseY = floor((this.y - (height - soilHeight)) / cellSize);
// Consume from the cell it germinated on and a few directly above it
for (let rOffset = 0; rOffset < 3; rOffset++) {
let cellR = plantBaseY - rOffset;
let cellC = plantBaseX;
if (cellR >= 0 && cellR < rows && cellC >= 0 && cellC < cols) {
soilGrid[cellR][cellC] = constrain(soilGrid[cellR][cellC] - (this.nutrientConsumptionRate / 3), 0, 100);
}
}
// --- Growth Logic ---
if (random() < this.growthChance && this.branches.length < this.maxBranches) {
// Find an active branch to extend or split from
let activeBranches = this.branches.filter(b => b.active);
if (activeBranches.length === 0) return; // No active branches left to grow
let parentBranch = random(activeBranches);
// Growth biases
let lightDirection = createVector(0, -1); // Upwards
let avoidanceDirection = createVector(0, 0);
// Avoid nearby plants
for (let otherPlant of plants) {
if (otherPlant !== this) {
let distToOther = dist(this.x, this.y, otherPlant.x, otherPlant.y);
if (distToOther < this.maxSize * 0.75 + otherPlant.maxSize * 0.75) { // Check if plants are close
let dirToOther = createVector(otherPlant.x - this.x, otherPlant.y - this.y).normalize();
avoidanceDirection.sub(dirToOther); // Push away from other plants
}
}
}
avoidanceDirection.normalize();
// Combine biases
let baseDirection = parentBranch.direction.copy();
baseDirection.add(p5.Vector.mult(lightDirection, 0.5)); // Bias towards light
baseDirection.add(p5.Vector.mult(avoidanceDirection, 0.3)); // Bias away from neighbors
baseDirection.normalize();
// Implement growth patterns based on seedShape
switch (this.seedShape) {
case 'triangle':
// Sharp angular fractal branches
if (parentBranch.depth < 3) { // Limit fractal depth
let newLength = parentBranch.length * random(0.6, 0.8);
let newThickness = parentBranch.thickness * random(0.6, 0.8);
let angle1 = random(-PI / 4, PI / 4); // Random angle
let newDir1 = rotateVector(baseDirection, angle1);
this.branches.push(new Branch(parentBranch.end, newDir1, newLength, newThickness, parentBranch.depth + 1));
if (random() < 0.5) { // Sometimes add a second branch
let angle2 = random(-PI / 4, PI / 4);
let newDir2 = rotateVector(baseDirection, angle2);
this.branches.push(new Branch(parentBranch.end, newDir2, newLength, newThickness, parentBranch.depth + 1));
}
}
break;
case 'square':
// Blocky, terrace-like structures
let newLengthSquare = parentBranch.length * random(0.8, 1.1);
let newThicknessSquare = parentBranch.thickness * random(0.9, 1.1);
let angleSquare = random([-PI / 2, PI / 2]); // Only 90-degree turns
angleSquare += random(-PI / 12, PI / 12); // Slight wobble
let newDirSquare = rotateVector(baseDirection, angleSquare);
this.branches.push(new Branch(parentBranch.end, newDirSquare, newLengthSquare, newThicknessSquare, parentBranch.depth + 1));
break;
case 'pentagon':
case 'hexagon':
// Spiraling organic curves
let newLengthCurve = parentBranch.length * random(0.7, 0.9);
let newThicknessCurve = parentBranch.thickness * random(0.7, 0.9);
// Introduce a slight bias to create spiral
let spiralBias = (this.seedShape === 'pentagon' ? PI / 12 : -PI / 12);
spiralBias += random(-PI / 24, PI / 24); // Random wobble
let newDirCurve = rotateVector(baseDirection, spiralBias);
this.branches.push(new Branch(parentBranch.end, newDirCurve, newLengthCurve, newThicknessCurve, parentBranch.depth + 1));
if (random() < 0.3) { // Occasionally fork
let newDirCurve2 = rotateVector(baseDirection, -spiralBias);
this.branches.push(new Branch(parentBranch.end, newDirCurve2, newLengthCurve, newThicknessCurve, parentBranch.depth + 1));
}
break;
}
if (parentBranch.length < 5) parentBranch.active = false; // Mark small branches as inactive for further growth
}
}
draw() {
push();
translate(this.x, this.y);
rotate(sin(frameCount * 0.02) * 0.1); // Gentle sway for plants
// Draw branches
for (let branch of this.branches) {
branch.draw(this.hue, this.saturation, this.brightness);
}
pop();
}
}
```
---
### `index.html` (No changes needed)
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Soil Nutrient Simulation</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
<script src="sketch.js"></script>
</body>
</html>
```
---
### `style.css` (No changes needed)
```css
html, body {
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars if canvas slightly exceeds window size */
}
canvas {
display: block; /* Remove extra space below the canvas */
}
```
---
**Summary of Changes:**
1. **`Branch` Class:** A new helper class to represent individual segments of a plant. It stores its start point, end point, direction, length, thickness, and depth (for fractal growth limits).
2. **`Plant` Class Overhaul:**
* **Properties:** Added `age`, `maxSize`, `maxBranches`, `growthChance`, `nutrientConsumptionRate`, `hue`, `saturation`, `brightness`, and a `branches` array.
* **`constructor()`:**
* Determines the plant's base `hue` based on `seedShape`: warm tones (yellow/orange) for triangle/square, cool tones (blue/purple) for pentagon/hexagon.
* Creates the initial `Branch` (the "trunk") that grows upwards from the plant's base.
* **`update()`:**
* **Nutrient Consumption:** Approximates the plant's footprint by consuming nutrients from the soil cell it germinated on and two cells directly above it. This is a simplified approach to keep performance high.
* **Growth Logic:**
* Checks `growthChance` and `maxBranches` before attempting to grow.
* Selects an `active` parent branch to grow from.
* Calculates a `baseDirection` for new branches, biasing it towards the top of the canvas (light) and away from other nearby plants (avoidance).
* Applies distinct growth rules based on `seedShape`:
* **Triangle:** Creates 1-2 shorter, thinner branches at random angles from the parent branch. Limited by `depth` to create a fractal appearance.
* **Square:** Creates a new branch that mostly extends the parent or turns at a 90-degree angle with a slight wobble, creating blocky structures.
* **Pentagon/Hexagon:** Creates 1-2 new branches with a slight rotational bias (`spiralBias`) to encourage a spiraling, curved growth.
* Marks very short parent branches as `inactive` to prevent infinite tiny growth.
* **`draw()`:** Iterates through all `Branch` objects in the `branches` array and draws them using `line()`, applying the plant's unique color and the branch's thickness.
3. **`rotateVector()` Helper:** A new function to simplify rotating `p5.Vector` objects, used by the `Plant` class for calculating new branch directions.
4. **`draw()` in `sketch.js`:**
* The `Plant` update and draw loops now manage the more complex `Plant` objects.
* Added a basic check to remove plants if they have no branches left (you could expand this to include nutrient starvation, age, etc.).
5. **`windowResized()`:** Clears the `plants` array as well, similar to `seeds`, to handle canvas resizing.
Now, when you run the sketch, you should see seeds falling and, upon germination, growing into unique, L-system-inspired plant forms that interact with the soil's nutrient levels and each other!