What this page does
Imports the core React library that provides component architecture and rendering capabilities.
Part: Foundation
Section: Module Imports
Depends on: None
This step is part of the larger process of establishing which tools the application is allowed to use.
Code (this page)
import React from 'react';
Explanation
React is the core library for building user interfaces
- Required for JSX syntax transformation
- Provides the component model we'll use throughout
- No application logic runs yetβthis only makes React available
Why this matters
Without this import, the browser cannot interpret JSX syntax or create React components. This single line unlocks the entire React ecosystem for our simulation.
β If something breaks here
Common issues at this step:Module not found: 'react' β React not installed in project
- Using
require() instead of import (use ES modules) How to recover:
- Run
npm install react if not installed
- Ensure file has
.jsx extension
What this page does
Imports the useRef hook for creating mutable references that persist across renders.
Part: Foundation
Section: Module Imports
Depends on: Page 1
This step is part of the larger process of importing React hooks for state management.
Code (this page)
import React, { useRef } from 'react';
Explanation
useRef creates a mutable object that persists between renders
- The
.current property can hold any value
- Changes to refs don't trigger re-renders
- Essential for animation data that updates every frame
Why this matters
Particle positions change 60 times per second. Using `useState` would cause 60 re-renders per second, killing performance. `useRef` lets us update data without re-rendering.
β If something breaks here
Common issues at this step:- Forgetting curly braces:
{ useRef }
- Misspelling as
useref (case-sensitive) How to recover:
- Named imports require curly braces
- Use exact capitalization:
useRef
What this page does
Imports the useEffect hook for running side effects after render.
Part: Foundation
Section: Module Imports
Depends on: Page 2
This step is part of the larger process of importing React hooks for lifecycle management.
Code (this page)
import React, { useRef, useEffect } from 'react';
Explanation
useEffect runs code after the component renders
- Used to start animation loops, set up event listeners
- The return function handles cleanup (stopping animations)
- Dependency array controls when effect re-runs
Why this matters
Our animation loop needs to start after the canvas exists in the DOM. `useEffect` guarantees the canvas is ready before we try to draw to it.
β If something breaks here
Common issues at this step:- Missing comma between imports
- Duplicate import statements
How to recover:
- Use single import with comma-separated hooks
- Format:
{ useRef, useEffect }
What this page does
Imports the useCallback hook for memoizing function references.
Part: Foundation
Section: Module Imports
Depends on: Page 3
This step is part of the larger process of importing React hooks for performance optimization.
Code (this page)
import React, { useRef, useEffect, useCallback } from 'react';
Explanation
useCallback returns a memoized version of a callback function
- The function reference stays stable between renders
- Prevents unnecessary re-creation of functions
- Critical for animation loops that reference each other
Why this matters
Our `gameLoop`, `updatePhysics`, and `render` functions call each other. Without stable references, React would create new function instances every render, breaking the animation cycle.
β If something breaks here
Common issues at this step:useCallback is not defined β missing from import
- Too many separate import statements
How to recover:
- Add
useCallback to the destructured import
- Combine all hooks in one import statement
What this page does
Declares the fixed width of the simulation canvas in pixels.
Part: Foundation
Section: Constants
Depends on: Page 4
This step is part of the larger process of defining hardcoded simulation parameters.
Code (this page)
Explanation
WIDTH sets the horizontal dimension of our simulation space
- 700 pixels provides good visibility without being too large
- Used for canvas size, particle boundaries, and spatial grid
- Constant defined at module level for easy access
Why this matters
Every part of the simulation needs to know the boundaries. Defining once and reusing prevents inconsistencies between rendering and physics calculations.
β If something breaks here
Common issues at this step:- Placing inside component function (wrong scope)
- Using
let instead of const How to recover:
- Define constants before the component function
- Use
const for values that never change
What this page does
Declares the fixed height of the simulation canvas in pixels.
Part: Foundation
Section: Constants
Depends on: Page 5
This step is part of the larger process of defining hardcoded simulation parameters.
Code (this page)
Explanation
HEIGHT sets the vertical dimension of our simulation space
- Square canvas (700Γ700) provides uniform behavior in all directions
- Particles wrap or bounce at this boundary
- Matches width for symmetrical simulation
Why this matters
A square simulation space ensures particles behave the same horizontally and vertically. This simplifies physics calculations and creates more natural-looking patterns.
β If something breaks here
Common issues at this step:- Typo in constant name
- Different value than WIDTH (unintentional)
How to recover:
- Use exact name:
HEIGHT
- Verify both dimensions match for square canvas
What this page does
Declares the total number of particles in the simulation.
Part: Foundation
Section: Constants
Depends on: Page 6
This step is part of the larger process of defining hardcoded simulation parameters.
Code (this page)
const PARTICLE_COUNT = 1500;
Explanation
PARTICLE_COUNT determines how many particles exist in the simulation
- 1500 provides rich emergent behavior without performance issues
- Particles are distributed evenly across species
- More particles = more complex patterns, but slower performance
Why this matters
The particle count directly affects both visual complexity and computational cost. 1500 is the sweet spot for interesting behavior at 60fps on most devices.
β If something breaks here
Common issues at this step:- Value too high (performance issues)
- Using string instead of number
How to recover:
- Start with 1500, adjust based on device performance
- Use numeric literal without quotes
What this page does
Declares how many distinct particle types (colors) exist in the simulation.
Part: Foundation
Section: Constants
Depends on: Page 7
This step is part of the larger process of defining hardcoded simulation parameters.
Code (this page)
Explanation
NUM_SPECIES defines how many different particle types exist
- Each species has unique attraction/repulsion relationships
- 6 species creates 36 possible interaction pairs (6Γ6 matrix)
- More species = more complex rules, more varied behavior
Why this matters
The number of species determines the complexity of the rules matrix. Six species provides enough variety for interesting emergent patterns without overwhelming complexity.
β If something breaks here
Common issues at this step:- Value exceeds COLORS array length (later)
- Zero or negative value
How to recover:
- Keep between 2 and 8 for best results
- Must be positive integer
What this page does
Declares the velocity damping factor applied each frame.
Part: Foundation
Section: Constants
Depends on: Page 8
This step is part of the larger process of defining physics parameters.
Code (this page)
Explanation
FRICTION multiplies velocity each frame (0.5 = 50% retained)
- Lower values = more damping, particles slow quickly
- Higher values = less damping, particles maintain momentum
- Value between 0 and 1 (exclusive)
Why this matters
Without friction, particles would accelerate infinitely. The friction value controls how "loose" or "tight" the simulation feels. 0.5 provides responsive but controlled movement.
β If something breaks here
Common issues at this step:- Value >= 1 (particles accelerate forever)
- Value <= 0 (particles stop instantly)
How to recover:
- Keep between 0.1 and 0.99
- 0.5 is a safe default
What this page does
Declares the maximum distance at which particles can affect each other.
Part: Foundation
Section: Constants
Depends on: Page 9
This step is part of the larger process of defining physics parameters.
Code (this page)
const MAX_DISTANCE = 100;
Explanation
MAX_DISTANCE sets the range of particle interactions (in pixels)
- Particles beyond this distance don't affect each other
- Also used as the cell size for spatial hashing
- Larger values = longer-range forces, more computation
Why this matters
This value directly impacts both behavior and performance. The spatial hash grid uses this as cell size, so it determines how many neighbors each particle checks.
β If something breaks here
Common issues at this step:- Value too small (no interactions)
- Value too large (poor performance)
How to recover:
- Keep between 50 and 200 for good results
- Must be less than canvas dimensions
What this page does
Declares the strength multiplier for all particle interactions.
Part: Foundation
Section: Constants
Depends on: Page 10
This step is part of the larger process of defining physics parameters.
Code (this page)
Explanation
FORCE_FACTOR scales all attraction and repulsion forces
- Higher values = stronger forces, faster movement
- Lower values = weaker forces, gentler movement
- Applied to both attraction and repulsion equally
Why this matters
This single multiplier lets us tune the overall "intensity" of the simulation without changing individual rules. Foundation uses 1 (neutral); Configuration will make this adjustable.
β If something breaks here
Common issues at this step:- Value of 0 (no movement)
- Negative value (inverts all forces)
How to recover:
- Use positive values only
- 1 is the neutral default
What this page does
Declares the distance below which particles always repel (prevents overlap).
Part: Foundation
Section: Constants
Depends on: Page 11
This step is part of the larger process of defining physics parameters.
Code (this page)
Explanation
MIN_DISTANCE defines the "personal space" of particles
- Particles closer than this always repel, regardless of rules
- Prevents particles from overlapping or clumping into singularities
- Creates a soft collision boundary
Why this matters
Without minimum distance repulsion, attractive particles would collapse into a single point. This creates natural spacing and prevents the simulation from degenerating.
β If something breaks here
Common issues at this step:- Value of 0 (particles can overlap completely)
- Value >= MAX_DISTANCE (no attraction possible)
How to recover:
- Keep much smaller than MAX_DISTANCE
- 20 works well with MAX_DISTANCE of 100
What this page does
Declares the color palette used to render different particle species.
Part: Foundation
Section: Constants
Depends on: Page 12
This step is part of the larger process of defining visual parameters.
Code (this page)
const COLORS = [
'#ff6b6b',
'#4ecdc4',
'#ffe66d',
'#95e1d3',
'#f38181',
'#aa96da',
'#fcbad3',
'#a8d8ea',
];
Explanation
COLORS array provides visually distinct colors for each species
- 8 colors available (more than NUM_SPECIES for flexibility)
- Hex color codes for canvas rendering
- Species index maps to array index:
COLORS[species]
Why this matters
Distinct colors let users visually track how different species interact. The palette is chosen for good contrast and visibility on dark backgrounds.
β If something breaks here
Common issues at this step:- Missing
# prefix on colors
- Fewer colors than NUM_SPECIES
How to recover:
- All hex colors need
# prefix
- Array length must be >= NUM_SPECIES
What this page does
Defines the function that creates individual particle objects.
Part: Foundation
Section: Particle System
Depends on: Page 13
This step is part of the larger process of building the particle data structure.
Code (this page)
function createParticle(species) {
Explanation
createParticle is a factory function that produces particle objects
- Takes
species parameter (integer 0 to NUM_SPECIES-1)
- Returns a complete particle object with position and velocity
- Called once per particle during initialization
Why this matters
Factory functions centralize object creation logic. If we need to change the particle structure, we only modify this one function.
β If something breaks here
Common issues at this step:- Missing parameter name
- Using arrow function inconsistently
How to recover:
- Include
species parameter
- Either style works; be consistent
What this page does
Returns the complete particle object with all required properties.
Part: Foundation
Section: Particle System
Depends on: Page 14
This step is part of the larger process of building the particle data structure.
Code (this page)
return {
x: Math.random() * WIDTH,
y: Math.random() * HEIGHT,
vx: 0,
vy: 0,
species: species,
};
}
Explanation
x, y: Random position within canvas bounds
vx, vy: Initial velocity (zero β particles start stationary)
species: The species index passed to the factory
Math.random() * WIDTH produces value from 0 to WIDTH
Why this matters
Each particle needs position, velocity, and species to participate in the simulation. Starting with zero velocity lets the rules dictate initial movement.
β If something breaks here
Common issues at this step:- Missing comma after properties
- Using
width instead of WIDTH How to recover:
- Each property needs trailing comma (except last)
- Use the constant names exactly
What this page does
Defines the function that creates the attraction/repulsion rules matrix.
Part: Foundation
Section: Rules System
Depends on: Page 15
This step is part of the larger process of building the rules matrix.
Code (this page)
function generateRandomRules(numSpecies) {
Explanation
generateRandomRules creates the interaction matrix
- Takes
numSpecies to determine matrix dimensions
- Returns an object where
rules[i][j] is how species i reacts to species j
- Called once at initialization and on reset
Why this matters
The rules matrix is the heart of particle life. This function generates random rules that create emergent behavior.
β If something breaks here
Common issues at this step:- Hardcoding species count instead of parameter
- Missing function keyword
How to recover:
- Accept
numSpecies as parameter for flexibility
- Use
function keyword for declaration
What this page does
Creates the empty container object that will hold all species rules.
Part: Foundation
Section: Rules System
Depends on: Page 16
This step is part of the larger process of building the rules matrix.
Code (this page)
Explanation
rules is an object that will hold nested objects
- Structure:
rules[speciesA][speciesB] = attractionValue
- Using object instead of 2D array for cleaner access syntax
- Will be populated by nested loops
Why this matters
The rules object is the data structure that stores all 36 interaction values (for 6 species). Object syntax makes lookups readable: `rules[0][1]`.
β If something breaks here
Common issues at this step:- Using array
[] instead of object {}
- Defining outside the function
How to recover:
- Use
{} for object initialization
- Keep inside the function body
What this page does
Iterates over each species as the "source" of the interaction.
Part: Foundation
Section: Rules System
Depends on: Page 17
This step is part of the larger process of building the rules matrix.
Code (this page)
for (let i = 0; i < numSpecies; i++) {
rules[i] = {};
Explanation
- Outer loop iterates
i from 0 to numSpecies-1
i represents the species that is *reacting* to others
rules[i] = {} creates a nested object for this species
- Each species gets its own set of reactions
Why this matters
The rules matrix has two dimensions: who is reacting (i) and who they're reacting to (j). The outer loop handles the first dimension.
β If something breaks here
Common issues at this step:- Using
<= instead of < (off-by-one)
- Forgetting to initialize
rules[i] How to recover:
- Use
< numSpecies not <= numSpecies
- Must create nested object before assigning to it
What this page does
Iterates over each species as the "target" and assigns random attraction values.
Part: Foundation
Section: Rules System
Depends on: Page 18
This step is part of the larger process of building the rules matrix.
Code (this page)
for (let j = 0; j < numSpecies; j++) {
rules[i][j] = Math.random() * 2 - 1;
}
}
Explanation
- Inner loop iterates
j from 0 to numSpecies-1
j represents the species being *reacted to*
Math.random() * 2 - 1 produces value from -1 to +1
- Negative = repulsion, Positive = attraction
Why this matters
This creates all 36 interaction rules (6Γ6). Each value is random, so every simulation produces unique emergent behavior.
β If something breaks here
Common issues at this step:- Using
Math.random() alone (only 0 to 1)
- Wrong formula for range
How to recover:
- Formula
Math.random() * 2 - 1 gives -1 to +1
- Verify with console.log if needed
What this page does
Returns the fully populated rules matrix from the function.
Part: Foundation
Section: Rules System
Depends on: Page 19
This step is part of the larger process of building the rules matrix.
Code (this page)
Explanation
- Returns the complete rules object
- Contains all species-to-species interaction values
- Caller stores this for use in physics calculations
- Can be regenerated anytime for new random rules
Why this matters
Returning the rules object makes it available to the simulation. The Reset button will call this function again to generate fresh random rules.
β If something breaks here
Common issues at this step:- Returning before loops complete
- Returning wrong variable
How to recover:
return rules must be after both loops close
- Verify both closing braces are before return
What this page does
Declares the class that optimizes neighbor lookups using spatial partitioning.
Part: Foundation
Section: Spatial Optimization
Depends on: Page 20
This step is part of the larger process of building the performance optimization system.
Code (this page)
Explanation
SpatialHashGrid divides space into cells for fast neighbor queries
- Without it, checking every particle against every other is O(nΒ²)
- With it, we only check particles in nearby cells: O(n)
- Critical for performance with 1500+ particles
Why this matters
At 1500 particles, O(nΒ²) means 2.25 million distance checks per frame. The spatial hash reduces this to roughly 15,000 checksβa 150x improvement.
β If something breaks here
Common issues at this step:- Missing
class keyword
- Lowercase class name (convention is PascalCase)
How to recover:
- Use
class SpatialHashGrid
- Class names should be PascalCase
What this page does
Creates the constructor that initializes the grid with dimensions.
Part: Foundation
Section: Spatial Optimization
Depends on: Page 21
This step is part of the larger process of building the spatial hash grid.
Code (this page)
constructor(cellSize, width, height) {
this.cellSize = cellSize;
this.width = width;
this.height = height;
this.cells = new Map();
}
Explanation
cellSize: Size of each grid cell (typically MAX_DISTANCE)
width, height: Canvas dimensions for bounds checking
this.cells: Map storing arrays of particles by cell key
- Map provides fast key-based lookup
Why this matters
The cell size should match MAX_DISTANCE so each cell contains all particles that could possibly interact with each other.
β If something breaks here
Common issues at this step:- Forgetting
this. prefix
- Using object
{} instead of Map() How to recover:
- All instance properties need
this.
- Map provides better performance for frequent clear/set operations
What this page does
Creates the method that empties all cells for the next frame.
Part: Foundation
Section: Spatial Optimization
Depends on: Page 22
This step is part of the larger process of building the spatial hash grid.
Code (this page)
clear() {
this.cells.clear();
}
Explanation
clear() empties the Map of all cell data
- Called at the start of each physics update
- Particles are re-inserted with current positions
- Map's native
clear() is very fast
Why this matters
Particles move every frame, so their cell assignments change. Clearing and rebuilding is faster than tracking individual movements.
β If something breaks here
Common issues at this step:- Calling
this.cells = new Map() instead (slower)
- Missing parentheses on
clear() How to recover:
- Use
.clear() method, not reassignment
- Method calls need parentheses
What this page does
Creates the method that converts coordinates to a cell key string.
Part: Foundation
Section: Spatial Optimization
Depends on: Page 23
This step is part of the larger process of building the spatial hash grid.
Code (this page)
getKey(x, y) {
const col = Math.floor(x / this.cellSize);
const row = Math.floor(y / this.cellSize);
return `${col},${row}`;
}
Explanation
- Converts x,y position to grid cell coordinates
Math.floor ensures consistent cell assignment
- Returns string key like "3,5" for cell at column 3, row 5
- String keys work with JavaScript Map
Why this matters
The key uniquely identifies each cell. Particles with the same key are in the same cell and may interact with each other.
β If something breaks here
Common issues at this step:- Using
round instead of floor
- Missing template literal backticks
How to recover:
Math.floor ensures particles on cell boundary go to lower cell
- Use backticks for template strings: `
${col},${row} `
What this page does
Creates the method that adds a particle to its corresponding cell.
Part: Foundation
Section: Spatial Optimization
Depends on: Page 24
This step is part of the larger process of building the spatial hash grid.
Code (this page)
insert(particle) {
const key = this.getKey(particle.x, particle.y);
if (!this.cells.has(key)) {
this.cells.set(key, []);
}
this.cells.get(key).push(particle);
}
Explanation
- Gets the cell key from particle position
- Creates empty array if cell doesn't exist yet
- Pushes particle into the cell's array
- Multiple particles can share the same cell
Why this matters
Inserting all particles into the grid enables fast neighbor lookups. Only particles in nearby cells need to be checked for interactions.
β If something breaks here
Common issues at this step:- Forgetting to create array for new cell
- Using
cells[key] instead of cells.get(key) How to recover:
- Check
has() before first insert to cell
- Map uses
.get() and .set(), not bracket notation
What this page does
Begins the method that retrieves all particles in neighboring cells.
Part: Foundation
Section: Spatial Optimization
Depends on: Page 25
This step is part of the larger process of building the spatial hash grid.
Code (this page)
getNearby(x, y) {
const nearby = [];
const col = Math.floor(x / this.cellSize);
const row = Math.floor(y / this.cellSize);
Explanation
getNearby finds all particles that might interact with position (x,y)
- Creates empty array to collect results
- Calculates the cell coordinates of the query position
- Will check 3Γ3 grid of cells around this position
Why this matters
This is the key optimization. Instead of checking all 1500 particles, we only check the ~50-100 particles in nearby cells.
β If something breaks here
Common issues at this step:- Missing return statement (added later)
- Duplicate variable names from other methods
How to recover:
- Each method has its own scope
- Return will be added after the loops
What this page does
Iterates over the 3Γ3 grid of cells surrounding the query position.
Part: Foundation
Section: Spatial Optimization
Depends on: Page 26
This step is part of the larger process of building the spatial hash grid.
Code (this page)
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
const key = `${col + dx},${row + dy}`;
const cell = this.cells.get(key);
if (cell) {
nearby.push(...cell);
}
}
}
Explanation
dx and dy iterate from -1 to +1 (9 combinations)
- Checks the center cell plus all 8 adjacent cells
- Uses spread operator to add all particles from cell to
nearby
- Skips cells that don't exist (no particles there)
Why this matters
Checking 9 cells ensures we find all particles within MAX_DISTANCE. Particles near cell boundaries could interact with particles in adjacent cells.
β If something breaks here
Common issues at this step:- Using
< instead of <= (misses cells)
- Forgetting null check on cell
How to recover:
- Loop must include -1, 0, and +1 for both dx and dy
if (cell) prevents error on empty cells
What this page does
Returns the collected array of nearby particles.
Part: Foundation
Section: Spatial Optimization
Depends on: Page 27
This step is part of the larger process of building the spatial hash grid.
Code (this page)
Explanation
- Returns the array containing all particles from 9 neighboring cells
- Caller will iterate through this array for physics calculations
- Closing brace ends the
getNearby method
- Final closing brace ends the
SpatialHashGrid class
Why this matters
This completes the spatial hash grid. We now have all the tools needed for O(n) neighbor queries instead of O(nΒ²) brute force.
β If something breaks here
Common issues at this step:- Missing closing braces
- Returning before loops complete
How to recover:
- Count braces: method needs one, class needs one
- Return must be after both loops
What this page does
Declares the main React component that contains the entire simulation.
Part: Foundation
Section: Application Shell
Depends on: Page 28
This step is part of the larger process of building the React component structure.
Code (this page)
export default function ParticleLifeFoundation() {
Explanation
export default makes this the main export of the module
- Function component (not class) for hook compatibility
- Name indicates this is the Foundation stage version
- All simulation logic will live inside this function
Why this matters
This is the entry point for the entire simulation. React will render this component, which creates the canvas and runs the animation.
β If something breaks here
Common issues at this step:- Forgetting
export default
- Using arrow function (works, but less conventional)
How to recover:
- Include
export default for module import
- Function declaration is clearer for main components
What this page does
Creates a ref to hold the canvas DOM element.
Part: Foundation
Section: Application Shell
Depends on: Page 29
This step is part of the larger process of setting up refs for animation data.
Code (this page)
const canvasRef = useRef(null);
Explanation
canvasRef will point to the canvas DOM element
- Initial value is
null (element doesn't exist yet)
- After render,
canvasRef.current is the actual canvas
- Needed to get the 2D rendering context
Why this matters
To draw on the canvas, we need access to its 2D context. The ref provides direct DOM access without querySelector.
β If something breaks here
Common issues at this step:- Calling outside component function
- Missing
null initial value How to recover:
- Hooks must be called inside component
useRef(null) is the standard pattern for DOM refs
What this page does
Creates a ref to hold the array of all particle objects.
Part: Foundation
Section: Application Shell
Depends on: Page 30
This step is part of the larger process of setting up refs for animation data.
Code (this page)
const particlesRef = useRef([]);
Explanation
particlesRef.current will hold all 1500 particle objects
- Initial value is empty array (populated during init)
- Using ref instead of state prevents re-renders on position changes
- Particles are modified directly without triggering React updates
Why this matters
State updates trigger re-renders. With 60 FPS animation updating 1500 particles, refs prevent thousands of unnecessary re-renders per second.
β If something breaks here
Common issues at this step:- Using
useState instead (performance issue)
- Initializing with
null instead of [] How to recover:
- Always use
useRef for high-frequency updates
- Empty array is safer than null for iteration
What this page does
Creates a ref to hold the rules matrix object.
Part: Foundation
Section: Application Shell
Depends on: Page 31
This step is part of the larger process of setting up refs for animation data.
Code (this page)
const rulesRef = useRef({});
Explanation
rulesRef.current will hold the attraction/repulsion rules
- Initial value is empty object (populated during init)
- Rules change on reset, but not during animation
- Ref access is faster than state lookup
Why this matters
The rules matrix is accessed thousands of times per frame during physics calculations. Ref access has minimal overhead compared to state.
β If something breaks here
Common issues at this step:- Using wrong initial value type
- Creating inside a loop or conditional
How to recover:
- Initial value should be
{} for object
- All hooks must be at top level of component
What this page does
Creates a ref to hold the spatial hash grid instance.
Part: Foundation
Section: Application Shell
Depends on: Page 32
This step is part of the larger process of setting up refs for animation data.
Code (this page)
const gridRef = useRef(null);
Explanation
gridRef.current will hold the SpatialHashGrid instance
- Initial value is
null (created during initialization)
- Grid is rebuilt every frame (clear + insert all)
- Single instance reused throughout simulation lifetime
Why this matters
The spatial grid is cleared and rebuilt 60 times per second. Keeping it in a ref avoids recreating the object constantly.
β If something breaks here
Common issues at this step:- Creating new grid instance here (too early)
- Using array instead of null
How to recover:
- Grid instance created in init function
- Null indicates "not yet initialized"
What this page does
Creates a ref to hold the animation frame request ID.
Part: Foundation
Section: Application Shell
Depends on: Page 33
This step is part of the larger process of setting up refs for animation data.
Code (this page)
const animationRef = useRef(null);
Explanation
animationRef.current stores the ID from requestAnimationFrame
- Needed to cancel the animation on cleanup
- Initial value is
null (no animation running yet)
- Updated every frame with new ID
Why this matters
When the component unmounts, we must cancel pending animation frames. Without the ID, we couldn't stop the animation loop.
β If something breaks here
Common issues at this step:- Forgetting this ref (memory leak on unmount)
- Using wrong variable name in cleanup
How to recover:
- Always store animation IDs for cleanup
- Match variable names exactly in cleanup code
What this page does
Creates the memoized function that initializes the simulation.
Part: Foundation
Section: Initialization
Depends on: Page 34
This step is part of the larger process of setting up the simulation.
Code (this page)
const initSimulation = useCallback(() => {
Explanation
initSimulation sets up particles, rules, and grid
- Wrapped in
useCallback for stable function reference
- Called on mount and when user clicks Reset
- Empty dependency array means function never changes
Why this matters
useCallback ensures the function identity stays stable. This prevents unnecessary effect re-runs and allows safe use as a dependency.
β If something breaks here
Common issues at this step:- Missing
useCallback wrapper
- Using regular function declaration
How to recover:
- Wrap with
useCallback(() => { ... }, [])
- Callback ensures stable reference
What this page does
Initializes the array that will hold all particles and calculates particles per species.
Part: Foundation
Section: Initialization
Depends on: Page 35
This step is part of the larger process of creating the particle system.
Code (this page)
const particles = [];
const perSpecies = Math.floor(PARTICLE_COUNT / NUM_SPECIES);
Explanation
particles is the local array we'll populate
perSpecies calculates how many particles each species gets
Math.floor ensures integer division (1500 / 6 = 250)
- Total will be 1500 particles evenly distributed
Why this matters
Even distribution ensures each species has equal representation. This creates balanced emergent behavior where no single species dominates.
β If something breaks here
Common issues at this step:- Not using
Math.floor (floating point issues)
- Declaring variables outside the function
How to recover:
- Always floor division for particle counts
- Keep initialization logic inside the function
What this page does
Generates all particles distributed evenly across species.
Part: Foundation
Section: Initialization
Depends on: Page 36
This step is part of the larger process of creating the particle system.
Code (this page)
for (let species = 0; species < NUM_SPECIES; species++) {
for (let i = 0; i < perSpecies; i++) {
particles.push(createParticle(species));
}
}
Explanation
- Outer loop iterates through each species (0 to 5)
- Inner loop creates
perSpecies particles of that type
createParticle(species) returns particle with random position
- Total: 6 species Γ 250 particles = 1500 particles
Why this matters
Nested loops ensure equal distribution. Each species gets exactly the same number of particles for fair emergent competition.
β If something breaks here
Common issues at this step:- Off-by-one errors in loop bounds
- Calling wrong factory function
How to recover:
- Outer:
< NUM_SPECIES, Inner: < perSpecies
- Use
createParticle(species) with correct argument
What this page does
Assigns the created particles array to the ref for persistence.
Part: Foundation
Section: Initialization
Depends on: Page 37
This step is part of the larger process of creating the particle system.
Code (this page)
particlesRef.current = particles;
Explanation
- Assigns local array to ref's
.current property
- Ref now holds all 1500 particles
- This reference persists across renders
- Physics update accesses particles through this ref
Why this matters
The ref is how we maintain particle state without triggering renders. All physics calculations read and write through `particlesRef.current`.
β If something breaks here
Common issues at this step:- Assigning to
particlesRef instead of .current
- Forgetting this line (particles lost)
How to recover:
- Always assign to
.current property
- This line is essential for persistence
What this page does
Creates random rules and stores them in the rules ref.
Part: Foundation
Section: Initialization
Depends on: Page 38
This step is part of the larger process of setting up the rules system.
Code (this page)
rulesRef.current = generateRandomRules(NUM_SPECIES);
Explanation
- Calls
generateRandomRules to create the interaction matrix
- Stores result in
rulesRef.current
- Each call generates different random rules
- Rules persist until next reset
Why this matters
The rules matrix defines all particle behavior. Random rules mean every simulation run produces unique emergent patterns.
β If something breaks here
Common issues at this step:- Passing wrong argument to generator
- Assigning to ref instead of
.current How to recover:
- Use
NUM_SPECIES constant as argument
- Assign to
.current property
What this page does
Instantiates the spatial hash grid and stores it in the grid ref.
Part: Foundation
Section: Initialization
Depends on: Page 39
This step is part of the larger process of setting up the optimization system.
Code (this page)
gridRef.current = new SpatialHashGrid(MAX_DISTANCE, WIDTH, HEIGHT);
}, []);
Explanation
- Creates new
SpatialHashGrid with cell size = MAX_DISTANCE
- Passes canvas dimensions for bounds awareness
- Stores in
gridRef.current for physics access
- Empty dependency array
[] means stable function reference
Why this matters
The grid's cell size matching MAX_DISTANCE ensures all interacting particles are in adjacent cells. This is key to the O(n) optimization.
β If something breaks here
Common issues at this step:- Wrong order of constructor arguments
- Missing closing
}, []) for useCallback How to recover:
- Order: cellSize, width, height
- useCallback needs closing with dependency array
What this page does
Creates the memoized function that updates all particle physics.
Part: Foundation
Section: Physics Engine
Depends on: Page 40
This step is part of the larger process of building the physics system.
Code (this page)
const updatePhysics = useCallback(() => {
const particles = particlesRef.current;
const rules = rulesRef.current;
const grid = gridRef.current;
Explanation
updatePhysics calculates forces and updates positions
- Wrapped in
useCallback for stable reference
- Extracts current values from refs for cleaner code
- Local constants avoid repeated
.current access
Why this matters
Extracting to local constants improves readability and potentially performance. This function runs 60 times per second.
β If something breaks here
Common issues at this step:- Accessing refs directly throughout (verbose)
- Missing
useCallback wrapper How to recover:
- Extract to local constants at function start
- Always wrap animation functions with useCallback
What this page does
Guards against running physics before initialization completes.
Part: Foundation
Section: Physics Engine
Depends on: Page 41
This step is part of the larger process of building the physics system.
Code (this page)
Explanation
- Checks if grid exists before proceeding
- Grid is
null until initSimulation runs
- Early return prevents errors on first frame
- Simple guard pattern for safety
Why this matters
The animation loop starts immediately, but initialization is asynchronous. This guard prevents crashes if physics runs before setup completes.
β If something breaks here
Common issues at this step:- Checking wrong variable
- Using
=== null instead of falsy check How to recover:
- Check
grid (the local variable)
!grid handles both null and undefined
What this page does
Clears the grid and re-inserts all particles at current positions.
Part: Foundation
Section: Physics Engine
Depends on: Page 42
This step is part of the larger process of building the physics system.
Code (this page)
grid.clear();
for (const particle of particles) {
grid.insert(particle);
}
Explanation
clear() empties all cells from previous frame
- Loop inserts each particle into its current cell
- Particles may have moved, so cell assignments change
- This is faster than tracking individual movements
Why this matters
Rebuilding the grid each frame ensures accurate neighbor queries. Particles move constantly, so their cell memberships must update.
β If something breaks here
Common issues at this step:- Forgetting to clear (duplicate particles)
- Inserting before clearing
How to recover:
- Always clear first, then insert
- Order matters for correctness
What this page does
Starts the loop that calculates forces for each particle.
Part: Foundation
Section: Physics Engine
Depends on: Page 43
This step is part of the larger process of calculating particle interactions.
Code (this page)
for (const particle of particles) {
let fx = 0;
let fy = 0;
Explanation
- Iterates through all 1500 particles
fx, fy accumulate forces for this particle
- Start at zero, add contributions from neighbors
- Will be applied to velocity after all forces calculated
Why this matters
Each particle experiences forces from many neighbors. Accumulating into fx/fy collects all contributions before applying.
β If something breaks here
Common issues at this step:- Using
let for particle (should be const)
- Missing force accumulators
How to recover:
- Particle reference doesn't change, use
const
- Forces must accumulate, use
let with initial 0
What this page does
Retrieves all particles that could potentially interact with the current one.
Part: Foundation
Section: Physics Engine
Depends on: Page 44
This step is part of the larger process of calculating particle interactions.
Code (this page)
const nearby = grid.getNearby(particle.x, particle.y);
Explanation
- Queries the spatial grid at particle's position
- Returns array of particles in 9 neighboring cells
- Much smaller than full 1500 particles (typically 50-100)
- This is where the O(n) optimization pays off
Why this matters
Instead of checking all 1500 particles (O(nΒ²)), we only check ~80 nearby particles. This is the key to 60fps performance.
β If something breaks here
Common issues at this step:- Querying wrong coordinates
- Not storing result
How to recover:
- Use
particle.x and particle.y
- Store in
const nearby for iteration
What this page does
Starts the inner loop that processes each nearby particle.
Part: Foundation
Section: Physics Engine
Depends on: Page 45
This step is part of the larger process of calculating particle interactions.
Code (this page)
for (const other of nearby) {
if (particle === other) continue;
Explanation
- Iterates through all nearby particles
- Skip self-interaction (particle can't affect itself)
continue jumps to next iteration
- Reference equality check is fast
Why this matters
A particle exists in its own cell, so it appears in its own nearby list. We must skip self-interaction to avoid undefined behavior.
β If something breaks here
Common issues at this step:- Using
== instead of ===
- Forgetting self-check (forces explode)
How to recover:
- Use strict equality
===
- Self-check is essential for stability
What this page does
Computes the direction and distance to the other particle.
Part: Foundation
Section: Physics Engine
Depends on: Page 46
This step is part of the larger process of calculating particle interactions.
Code (this page)
const dx = other.x - particle.x;
const dy = other.y - particle.y;
const dist = Math.sqrt(dx * dx + dy * dy);
Explanation
dx, dy are the vector from particle to other
dist is the Euclidean distance (Pythagorean theorem)
- Positive dx means other is to the right
- Distance is always positive
Why this matters
We need both direction (dx, dy) and magnitude (dist) to apply forces. Direction tells us which way to push, distance affects strength.
β If something breaks here
Common issues at this step:- Subtracting in wrong order
- Forgetting
Math.sqrt How to recover:
other - particle points toward other
- Must sqrt for actual distance
What this page does
Skips particles that are too far or at zero distance.
Part: Foundation
Section: Physics Engine
Depends on: Page 47
This step is part of the larger process of calculating particle interactions.
Code (this page)
if (dist === 0 || dist > MAX_DISTANCE) continue;
Explanation
- Skip if distance is zero (same position, avoid division by zero)
- Skip if beyond interaction range
- Particles in neighboring cells may still be too far
continue proceeds to next neighbor
Why this matters
Zero distance would cause division by zero errors. Distance check ensures only truly nearby particles interact.
β If something breaks here
Common issues at this step:- Using
< instead of > for range check
- Forgetting zero check (NaN errors)
How to recover:
- Skip if
dist > MAX_DISTANCE
- Always check
dist === 0 first
What this page does
Looks up the attraction/repulsion value for this species pair.
Part: Foundation
Section: Physics Engine
Depends on: Page 48
This step is part of the larger process of calculating particle interactions.
Code (this page)
const rule = rules[particle.species]?.[other.species] || 0;
Explanation
- Looks up
rules[i][j] for this species pair
- Optional chaining
?. handles missing entries safely
- Falls back to 0 if rule undefined
- Value between -1 (repel) and +1 (attract)
Why this matters
The rule determines if these two particles attract or repel. This is the core of particle life's emergent behavior.
β If something breaks here
Common issues at this step:- Not handling undefined rules
- Wrong index order
How to recover:
- Use optional chaining and
|| 0 fallback
- Order:
rules[reactor][target]
What this page does
Computes the strength of the force based on distance and rules.
Part: Foundation
Section: Physics Engine
Depends on: Page 49
This step is part of the larger process of calculating particle interactions.
Code (this page)
let force;
if (dist < MIN_DISTANCE) {
force = (dist / MIN_DISTANCE - 1) * 0.5;
} else {
const normalizedDist = (dist - MIN_DISTANCE) / (MAX_DISTANCE - MIN_DISTANCE);
force = rule * (1 - normalizedDist) * FORCE_FACTOR;
}
Explanation
- Two distance zones: close (repel) and medium (rule-based)
- Close range: always repel, force increases as distance decreases
- Medium range: rule-based force, strength decreases with distance
normalizedDist maps distance to 0-1 range
Why this matters
Close-range repulsion prevents particle overlap. Distance-scaled force creates natural clustering without singularities.
β If something breaks here
Common issues at this step:- Wrong formula for normalized distance
- Missing
let for force variable How to recover:
- Normalized:
(dist - MIN) / (MAX - MIN)
force must be declared with let (assigned conditionally)
What this page does
Adds this neighbor's contribution to the particle's force accumulators.
Part: Foundation
Section: Physics Engine
Depends on: Page 50
This step is part of the larger process of calculating particle interactions.
Code (this page)
fx += (dx / dist) * force;
fy += (dy / dist) * force;
}
Explanation
dx / dist and dy / dist are the unit direction vector
- Multiplying by
force scales to correct magnitude
+= accumulates contributions from all neighbors
- Closing brace ends the neighbor loop
Why this matters
Direction comes from the normalized vector, magnitude from force calculation. Accumulating allows multiple neighbors to influence each particle.
β If something breaks here
Common issues at this step:- Using
= instead of +=
- Dividing by wrong value
How to recover:
- Must use
+= for accumulation
- Divide by
dist to get unit vector
What this page does
Updates particle velocity based on accumulated forces and applies friction.
Part: Foundation
Section: Physics Engine
Depends on: Page 51
This step is part of the larger process of updating particle state.
Code (this page)
particle.vx = (particle.vx + fx) * FRICTION;
particle.vy = (particle.vy + fy) * FRICTION;
Explanation
- Add force to current velocity
- Multiply by FRICTION to dampen
- FRICTION < 1 means velocity decays over time
- Order matters: add force, then apply friction
Why this matters
Forces change velocity, not position directly. Friction prevents infinite acceleration and creates smooth, natural movement.
β If something breaks here
Common issues at this step:- Applying friction before force
- Forgetting friction entirely
How to recover:
- Formula:
(v + f) * friction
- Friction must be applied each frame
What this page does
Caps particle velocity to prevent runaway speeds.
Part: Foundation
Section: Physics Engine
Depends on: Page 52
This step is part of the larger process of updating particle state.
Code (this page)
const speed = Math.sqrt(particle.vx * particle.vx + particle.vy * particle.vy);
const maxSpeed = 5;
if (speed > maxSpeed) {
particle.vx = (particle.vx / speed) * maxSpeed;
particle.vy = (particle.vy / speed) * maxSpeed;
}
}
Explanation
- Calculate current speed (magnitude of velocity)
- If exceeding max, scale down to maxSpeed
- Preserves direction while limiting magnitude
- Closing brace ends the particle force loop
Why this matters
Without speed limits, particles in strong attraction wells could accelerate infinitely. Capping ensures stable, viewable motion.
β If something breaks here
Common issues at this step:- Not preserving direction when scaling
- Using wrong speed formula
How to recover:
- Divide by speed, multiply by maxSpeed
- Speed = sqrt(vxΒ² + vyΒ²)
What this page does
Starts the loop that updates all particle positions.
Part: Foundation
Section: Physics Engine
Depends on: Page 53
This step is part of the larger process of updating particle state.
Code (this page)
for (const particle of particles) {
particle.x += particle.vx;
particle.y += particle.vy;
Explanation
- Second loop through all particles
- Position += velocity (basic kinematics)
- Separate from force loop for cleaner code
- All forces calculated before any positions update
Why this matters
Separating force calculation from position update ensures consistent behavior. All particles "see" the same state when calculating forces.
β If something breaks here
Common issues at this step:- Updating position during force loop
- Forgetting to add velocity
How to recover:
- Keep loops separate
- Position changes by
+= vx/vy
What this page does
Wraps particles that exit one edge to appear on the opposite edge.
Part: Foundation
Section: Physics Engine
Depends on: Page 54
This step is part of the larger process of handling boundaries.
Code (this page)
if (particle.x < 0) particle.x += WIDTH;
if (particle.x >= WIDTH) particle.x -= WIDTH;
if (particle.y < 0) particle.y += HEIGHT;
if (particle.y >= HEIGHT) particle.y -= HEIGHT;
}
}, []);
Explanation
- Particles exiting left appear on right (add WIDTH)
- Particles exiting right appear on left (subtract WIDTH)
- Same for top/bottom with HEIGHT
- Creates toroidal (donut-shaped) world
Why this matters
Wrapping creates an infinite world without edges. Patterns can flow continuously without bouncing off walls.
β If something breaks here
Common issues at this step:- Using
> instead of >= for right/bottom
- Adding instead of subtracting (or vice versa)
How to recover:
- Right edge:
>= WIDTH, subtract WIDTH
- Left edge:
< 0, add WIDTH
What this page does
Creates the memoized function that draws the simulation to canvas.
Part: Foundation
Section: Rendering
Depends on: Page 55
This step is part of the larger process of building the render system.
Code (this page)
const render = useCallback(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const particles = particlesRef.current;
const rules = rulesRef.current;
Explanation
render draws everything to the canvas
- Gets canvas element and 2D drawing context
- Extracts current particles and rules from refs
- Runs every frame after physics update
Why this matters
Rendering is separate from physics for clean architecture. The render function only reads state, never modifies it.
β If something breaks here
Common issues at this step:- Getting context before canvas exists
- Missing
.current on refs How to recover:
- Canvas exists after first render
- Always access
.current property
What this page does
Fills the canvas with a dark background color.
Part: Foundation
Section: Rendering
Depends on: Page 56
This step is part of the larger process of drawing the frame.
Code (this page)
ctx.fillStyle = '#0a0a14';
ctx.fillRect(0, 0, WIDTH, HEIGHT);
Explanation
- Sets fill color to dark blue-black
- Fills entire canvas, erasing previous frame
- Must clear before drawing new positions
- Dark background makes colored particles visible
Why this matters
Without clearing, particles would leave trails (which could be a feature, but not for Foundation). Each frame starts fresh.
β If something breaks here
Common issues at this step:- Wrong color format
- Not covering full canvas
How to recover:
- Use hex color with
# prefix
- fillRect from 0,0 to WIDTH,HEIGHT
What this page does
Renders each particle as a colored circle at its current position.
Part: Foundation
Section: Rendering
Depends on: Page 57
This step is part of the larger process of drawing the frame.
Code (this page)
for (const particle of particles) {
ctx.fillStyle = COLORS[particle.species % COLORS.length];
ctx.beginPath();
ctx.arc(particle.x, particle.y, 3, 0, Math.PI * 2);
ctx.fill();
}
Explanation
- Loop through all particles
- Set color based on species (modulo handles overflow)
arc draws a circle: center, radius 3, full circle
fill renders the circle solid
Why this matters
This is where the simulation becomes visible. Each particle is a small colored dot, and together they form emergent patterns.
β If something breaks here
Common issues at this step:- Forgetting
beginPath (paths connect)
- Wrong arc parameters
How to recover:
- Always
beginPath before each shape
- Arc: x, y, radius, startAngle, endAngle
What this page does
Renders a small visualization of the rules matrix in the corner.
Part: Foundation
Section: Rendering
Depends on: Page 58
This step is part of the larger process of providing visual feedback.
Code (this page)
const cellSize = 12;
const matrixX = 10;
const matrixY = HEIGHT - NUM_SPECIES * cellSize - 40;
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(
matrixX - 5,
matrixY - 20,
NUM_SPECIES * cellSize + 25,
NUM_SPECIES * cellSize + 35
);
ctx.fillStyle = '#fff';
ctx.font = '10px monospace';
ctx.fillText('Rules Matrix:', matrixX, matrixY - 5);
Explanation
- Position matrix in bottom-left corner
- Semi-transparent black background for readability
- White label text above the matrix
- Matrix shows how species interact visually
Why this matters
The rules matrix helps users understand why particles behave as they do. Green = attract, red = repel.
β If something breaks here
Common issues at this step:- Position calculation off-screen
- Wrong coordinate order in fillRect
How to recover:
- Verify Y puts matrix above bottom edge
- fillRect: x, y, width, height
What this page does
Renders the colored cells showing attraction/repulsion values.
Part: Foundation
Section: Rendering
Depends on: Page 59
This step is part of the larger process of visualizing the rules.
Code (this page)
for (let i = 0; i < NUM_SPECIES; i++) {
ctx.fillStyle = COLORS[i];
ctx.fillRect(matrixX, matrixY + (i + 1) * cellSize, cellSize - 2, cellSize - 2);
ctx.fillRect(matrixX + (i + 1) * cellSize, matrixY, cellSize - 2, cellSize - 2);
for (let j = 0; j < NUM_SPECIES; j++) {
const rule = rules[i]?.[j] || 0;
const r = rule < 0 ? Math.floor(-rule * 255) : 0;
const g = rule > 0 ? Math.floor(rule * 255) : 0;
ctx.fillStyle = `rgb(${r}, ${g}, 50)`;
ctx.fillRect(
matrixX + (j + 1) * cellSize,
matrixY + (i + 1) * cellSize,
cellSize - 2,
cellSize - 2
);
}
}
Explanation
- Row/column headers show species colors
- Inner cells show rule values as colors
- Red = negative (repulsion), Green = positive (attraction)
- Blue component (50) prevents pure black for zero
Why this matters
The color-coded matrix provides instant visual feedback about the rules governing the simulation.
β If something breaks here
Common issues at this step:- Color calculation produces invalid values
- Cell positions overlap
How to recover:
- Ensure r, g are 0-255 integers
- cellSize - 2 creates gap between cells
What this page does
Renders informational text showing particle count and species.
Part: Foundation
Section: Rendering
Depends on: Page 60
This step is part of the larger process of providing visual feedback.
Code (this page)
ctx.fillStyle = '#fff';
ctx.font = '14px monospace';
ctx.fillText(`Particles: ${particles.length}`, 10, 20);
ctx.fillText(`Species: ${NUM_SPECIES}`, 10, 40);
}, []);
Explanation
- White text for visibility on dark background
- Monospace font for consistent character width
- Shows particle count and species count
- Position at top-left corner
Why this matters
The HUD provides context about the simulation parameters. Users can verify the correct number of particles is active.
β If something breaks here
Common issues at this step:- Template literal syntax error
- Missing closing for useCallback
How to recover:
- Use backticks for template: `
text ${var}
- }, [])` closes the useCallback
What this page does
Creates the main animation loop that drives physics and rendering.
Part: Foundation
Section: Animation Loop
Depends on: Page 61
This step is part of the larger process of running the simulation.
Code (this page)
const gameLoop = useCallback(() => {
updatePhysics();
render();
animationRef.current = requestAnimationFrame(gameLoop);
}, [updatePhysics, render]);
Explanation
gameLoop runs physics then rendering each frame
requestAnimationFrame schedules next frame at ~60fps
- Stores animation ID for cleanup
- Dependencies ensure fresh function references
Why this matters
This is the heart of the simulation. The loop runs continuously, updating physics and drawing the result 60 times per second.
β If something breaks here
Common issues at this step:- Missing dependencies in array
- Calling gameLoop() instead of passing reference
How to recover:
- Include
[updatePhysics, render] in deps
- Pass
gameLoop without parentheses to requestAnimationFrame
What this page does
Creates the effect that initializes and starts the simulation.
Part: Foundation
Section: Lifecycle
Depends on: Page 62
This step is part of the larger process of managing component lifecycle.
Code (this page)
useEffect(() => {
initSimulation();
animationRef.current = requestAnimationFrame(gameLoop);
Explanation
- Effect runs after component mounts
- Calls
initSimulation to set up particles, rules, grid
- Starts the animation loop
- First frame begins rendering immediately
Why this matters
useEffect ensures the canvas exists before we try to draw. Initialization happens once on mount.
β If something breaks here
Common issues at this step:- Calling gameLoop() instead of passing to requestAnimationFrame
- Forgetting initSimulation call
How to recover:
- Pass function reference, not result
- Init must run before animation starts
What this page does
Returns the cleanup function that stops animation on unmount.
Part: Foundation
Section: Lifecycle
Depends on: Page 63
This step is part of the larger process of managing component lifecycle.
Code (this page)
return () => {
cancelAnimationFrame(animationRef.current);
};
}, [initSimulation, gameLoop]);
Explanation
- Return function runs when component unmounts
cancelAnimationFrame stops the pending frame request
- Prevents memory leaks and errors after unmount
- Dependencies include all functions used in effect
Why this matters
Without cleanup, the animation would continue after component removal, causing errors and memory leaks.
β If something breaks here
Common issues at this step:- Missing return statement
- Wrong dependency array
How to recover:
- Cleanup must be returned from effect
- Include
[initSimulation, gameLoop]
What this page does
Creates the handler function for the Reset button.
Part: Foundation
Section: Control Handlers
Depends on: Page 64
This step is part of the larger process of user interaction.
Code (this page)
const handleReset = useCallback(() => {
initSimulation();
}, [initSimulation]);
Explanation
handleReset regenerates the simulation
- Calls
initSimulation to create new particles and rules
- Wrapped in useCallback for stable reference
- Dependency on initSimulation ensures correct function
Why this matters
The Reset button lets users generate new random rules without refreshing the page. Each reset creates unique emergent behavior.
β If something breaks here
Common issues at this step:- Forgetting useCallback wrapper
- Missing dependency
How to recover:
- Wrap in useCallback for button stability
- Include
[initSimulation] dependency
What this page does
Starts the JSX return that defines the component's rendered output.
Part: Foundation
Section: UI Composition
Depends on: Page 65
This step is part of the larger process of building the user interface.
Code (this page)
return (
<div className="flex flex-col items-center gap-4 p-4 bg-gray-900 min-h-screen">
Explanation
return ( begins the JSX output
- Outer div is the main container
- Tailwind classes: flex column, centered, gaps, padding
- Dark background matching canvas aesthetic
Why this matters
The container div establishes the layout structure. Flexbox centering keeps the simulation visually balanced.
β If something breaks here
Common issues at this step:- Missing opening parenthesis
- Tailwind not available
How to recover:
- JSX needs
return ( with parenthesis
- Tailwind classes work in supported environments
What this page does
Renders the simulation title at the top of the interface.
Part: Foundation
Section: UI Composition
Depends on: Page 66
This step is part of the larger process of building the user interface.
Code (this page)
<h1 className="text-2xl font-bold text-white">
Particle Life Simulation
</h1>
Explanation
h1 is semantically correct for main title
- White text for contrast on dark background
- Bold weight for emphasis
- Text size appropriate for header
Why this matters
The title identifies the simulation and establishes visual hierarchy. Semantic HTML improves accessibility.
β If something breaks here
Common issues at this step:- Missing closing tag
- Wrong class syntax (className vs class)
How to recover:
- JSX uses
className not class
- Ensure tag is properly closed
What this page does
Renders the canvas element where the simulation draws.
Part: Foundation
Section: UI Composition
Depends on: Page 67
This step is part of the larger process of building the user interface.
Code (this page)
<canvas
ref={canvasRef}
width={WIDTH}
height={HEIGHT}
className="border border-gray-700 rounded"
/>
Explanation
ref={canvasRef} connects to our ref for drawing access
width and height set canvas resolution
- Border provides visual boundary
- Self-closing tag (no children)
Why this matters
The canvas is where all rendering happens. The ref connection is essential for the render function to access the drawing context.
β If something breaks here
Common issues at this step:- Missing ref connection
- Using style instead of width/height attributes
How to recover:
ref={canvasRef} is required
- Canvas dimensions must be attributes, not CSS
What this page does
Renders the button that resets the simulation with new rules.
Part: Foundation
Section: UI Composition
Depends on: Page 68
This step is part of the larger process of building the user interface.
Code (this page)
<button
onClick={handleReset}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
>
Reset (New Rules)
</button>
Explanation
onClick={handleReset} connects to our handler
- Blue button with hover effect
- Descriptive label explains what reset does
- Transition makes hover smooth
Why this matters
The Reset button is the primary user interaction. It lets users explore different rule sets without reloading.
β If something breaks here
Common issues at this step:- Calling handler immediately:
onClick={handleReset()}
- Missing handler function
How to recover:
- Pass reference without parentheses
- Ensure handleReset is defined above
What this page does
Renders helper text explaining how to use the simulation.
Part: Foundation
Section: UI Composition
Depends on: Page 69
This step is part of the larger process of building the user interface.
Code (this page)
<p className="text-gray-400 text-sm text-center max-w-md">
Watch particles self-organize based on attraction and repulsion rules.
Each color represents a species with unique interactions.
Click Reset to generate new random rules.
</p>
Explanation
- Gray text for secondary information
- Smaller font size for helper content
- Centered and max-width for readability
- Three sentences explain the simulation
Why this matters
Instructions help new users understand what they're seeing. Clear explanation increases engagement with the simulation.
β If something breaks here
Common issues at this step:- Text too long (wrapping issues)
- Wrong color class
How to recover:
- max-w-md constrains width
- text-gray-400 for muted appearance
What this page does
Closes the JSX return and component function.
Part: Foundation
Section: UI Composition
Depends on: Page 70
This step is part of the larger process of completing the component.
Code (this page)
Explanation
- Closes the outer container div
- Closes the return statement parenthesis
- Closes the component function
- Component is now complete
Why this matters
Proper closing ensures valid JSX and JavaScript syntax. The component is now ready to render.
β If something breaks here
Common issues at this step:- Mismatched tags/braces
- Missing semicolon (optional but conventional)
How to recover:
- Count opening and closing tags
- Verify all braces match
What this page does
Provides a verification checklist for the completed Foundation stage.
Part: Foundation
Section: Verification
Depends on: All previous pages
This step is part of the larger process of ensuring correctness.
Code (this page)
// Complete Foundation code should include:
// - 4 React imports (React, useRef, useEffect, useCallback)
// - 8 constants (WIDTH, HEIGHT, PARTICLE_COUNT, etc.)
// - createParticle factory function
// - generateRandomRules function
// - SpatialHashGrid class with 5 methods
// - ParticleLifeFoundation component with:
// - 4 refs (canvas, particles, rules, grid, animation)
// - initSimulation, updatePhysics, render, gameLoop functions
// - useEffect for lifecycle
// - handleReset handler
// - JSX with canvas and reset button
Explanation
This checklist summarizes all code that should exist after completing Foundation.
Why this matters
Verification ensures nothing was missed. A complete Foundation is the base for Configuration stage.
β If something breaks here
Common issues at this step:- Missing function or constant
- Incorrect hook dependencies
How to recover:
- Compare against the checklist
- Review each section's pages
What this page does
Confirms particles are created correctly on startup.
Part: Foundation
Section: Verification
Depends on: Page 72
This step is part of the verification process.
Explanation
Why this matters
β If something breaks here
Common issues:- Particles all in one corner (initialization bug)
- Wrong particle count (loop error)
- Missing colors (species assignment)
How to recover:
- Check createParticle random ranges
- Verify loop bounds in initSimulation
- Confirm COLORS array has enough entries
What this page does
Confirms the physics simulation runs correctly.
Part: Foundation
Section: Verification
Depends on: Page 73
This step is part of the verification process.
Explanation
Why this matters
β If something breaks here
Common issues:- No movement (physics not running)
- Jerky motion (performance issue)
- Particles disappear (wrapping bug)
How to recover:
- Verify gameLoop is calling updatePhysics
- Check spatial grid is working
- Review boundary wrapping logic
What this page does
Confirms the rules visualization renders correctly.
Part: Foundation
Section: Verification
Depends on: Page 74
This step is part of the verification process.
Explanation
Why this matters
β If something breaks here
Common issues:- Matrix off-screen (position calculation)
- All cells same color (rule lookup error)
- Missing headers (loop range)
How to recover:
- Check matrixY calculation
- Verify rules[i][j] access
- Confirm loop starts at correct index
What this page does
Confirms the Reset button works correctly.
Part: Foundation
Section: Verification
Depends on: Page 75
This step is part of the verification process.
Explanation
Why this matters
β If something breaks here
Common issues:- Nothing happens (handler not connected)
- Animation stops (gameLoop interrupted)
- Same rules every time (random seed issue)
How to recover:
- Verify onClick={handleReset}
- Ensure initSimulation doesn't stop animation
- Math.random() should vary naturally
What this page does
Confirms the simulation runs at acceptable frame rates.
Part: Foundation
Section: Verification
Depends on: Page 76
This step is part of the verification process.
Explanation
Why this matters
β If something breaks here
Common issues:- Low frame rate (spatial grid not working)
- Memory growth (leaking objects)
- GPU bottleneck (too many draw calls)
How to recover:
- Verify spatial grid reduces neighbor checks
- Ensure particles array isn't growing
- Canvas operations are efficient
What this page does
Confirms Foundation does NOT include Configuration-stage features.
Part: Foundation
Section: Verification
Depends on: Page 77
This step is part of the verification process.
Explanation
Why this matters
β If something breaks here
Common issues:- Accidentally added Configuration features early
- Imported code from later stages
How to recover:
- Remove any settings UI
- Foundation uses hardcoded constants only
What this page does
Confirms Foundation does NOT include Persistence-stage features.
Part: Foundation
Section: Verification
Depends on: Page 78
This step is part of the verification process.
Explanation
Why this matters
β If something breaks here
Common issues:- Accidentally added persistence early
- Third-party library storing data
How to recover:
- Remove any storage code
- Foundation is entirely transient
What this page does
Confirms Foundation stage is fully complete and ready for Configuration.
Part: Foundation
Section: Completion
Depends on: All previous pages
This is the final page of the Foundation Guide.
Explanation
Why this matters