
algorithm
Prompt
Create a p5.js animation that visually represents how genetic algorithms evolve solutions over generations. Start by displaying a population of simple creatures (e.g., dots, rectangles, or basic stick figures) trying to reach a target on the screen. Each generation should attempt to move toward the goal, and their "genes" (e.g., movement vectors or decision sequences) should mutate slightly over time. Visually show crossover and mutation steps during the transition to a new generation. Use colors and simple effects to indicate fitness (e.g., brightness or size), and animate the progression every few seconds. The goal is to help students intuitively grasp how evolution-inspired algorithms work.
A system prompt was added to support web rendering
```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Genetic-Algorithm Visualizer</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.min.js"></script> <style> body{margin:0;background:#111;overflow:hidden;} canvas{display:block;} </style> </head> <body> <script> // ================== CONFIGURATION ================== const lifespan = 240; // frames each generation lives const popSize = 60; // number of creatures const mutationRate = 0.015; // chance a gene mutates const maxForce = 0.4; // max acceleration magnitude const transitionLen = 120; // frames to display crossover visual // ================== GLOBAL STATE =================== let population; let generation = 1; let lifeCounter = 0; let state = "run"; // run | transition let transitionFrame = 0; let target; // ================ p5.js SETUP ====================== function setup(){ createCanvas(800,600); colorMode(HSB,360,100,100,100); target = createVector(width/2,50); population = new Population(); } // ================ p5.js DRAW LOOP ================== function draw(){ background(0,0,15); // draw target noStroke(); fill(140,80,90); ellipse(target.x,target.y,24,24); if(state==="run"){ population.run(); lifeCounter++; if(lifeCounter >= lifespan || population.allDone()){ population.evaluate(); state = "transition"; transitionFrame = 0; } }else if(state==="transition"){ population.showTransition(transitionFrame/transitionLen); transitionFrame++; if(transitionFrame>=transitionLen){ population.selection(); lifeCounter = 0; generation++; state="run"; } } // overlay stats displayHUD(); } // ================ VISUAL HUD ======================= function displayHUD(){ fill(0,0,100,80); textSize(16); textAlign(LEFT,TOP); text(`Generation: ${generation}`,10,10); text(`Life: ${lifeCounter}/${lifespan}`,10,30); } // ================= DNA CLASS ======================= class DNA{ constructor(genes){ if(genes){ this.genes = genes; }else{ this.genes = []; for(let i=0;i<lifespan;i++){ this.genes[i] = p5.Vector.random2D().setMag(maxForce); } } } // crossover returns object {dna, mutated} crossover(partner){ let newGenes = []; let midpoint = floor(random(this.genes.length)); for(let i=0;i<this.genes.length;i++){ if(i>midpoint) newGenes[i]=this.genes[i].copy(); else newGenes[i]=partner.genes[i].copy(); } let childDNA = new DNA(newGenes); let mutated = childDNA.mutate(); return {dna:childDNA, mutated}; } mutate(){ let didMutate = false; for(let i=0;i<this.genes.length;i++){ if(random(1)<mutationRate){ this.genes[i]=p5.Vector.random2D().setMag(maxForce); didMutate = true; } } return didMutate; } } // ================== CREATURE ======================= class Creature{ constructor(dna){ this.pos = createVector(width/2, height-20); this.vel = createVector(); this.acc = createVector(); this.dna = dna || new DNA(); this.geneIndex = 0; this.reached = false; this.crashed = false; this.fitness = 0; } applyForce(f){ this.acc.add(f); } update(){ if(this.reached||this.crashed) return; // follow gene this.applyForce(this.dna.genes[this.geneIndex]); this.geneIndex = (this.geneIndex+1)%this.dna.genes.length; this.vel.add(this.acc); this.pos.add(this.vel); this.acc.mult(0); // boundaries if(this.pos.x<0||this.pos.x>width||this.pos.y<0||this.pos.y>height){ this.crashed = true; } // target? if(dist(this.pos.x,this.pos.y,target.x,target.y)<12){ this.reached = true; } } calcFitness(){ const d = dist(this.pos.x,this.pos.y,target.x,target.y); this.fitness = 1/(d*d+1); // smaller distance -> higher fitness if(this.reached) this.fitness *= 4; if(this.crashed) this.fitness *= 0.1; } show(bestDist){ const b = map(this.fitness, 0, bestDist, 20, 100, true); const size = map(this.fitness,0,bestDist,4,10,true); noStroke(); fill(200,80,b, 80); ellipse(this.pos.x,this.pos.y,size,size); } } // ================= POPULATION ====================== class Population{ constructor(){ this.creatures = []; for(let i=0;i<popSize;i++) this.creatures[i] = new Creature(); this.matingPool = []; // info for transition visuals this.parentLinks = []; // {aPos,bPos,childPos,mutated} } run(){ for(let c of this.creatures){ c.update(); c.show(this.bestFitness||1); } } allDone(){ return this.creatures.every(c=>c.reached || c.crashed); } evaluate(){ // calculate fitness let maxFit = 0; for(let c of this.creatures){ c.calcFitness(); if(c.fitness > maxFit) maxFit = c.fitness; } this.bestFitness = maxFit; // build mating pool this.matingPool = []; for(let c of this.creatures){ // normalize const n = floor(map(c.fitness,0,maxFit,1,100)); for(let i=0;i<n;i++) this.matingPool.push(c); } } selection(){ // clear parent link visuals this.parentLinks = []; const newCreatures = []; for(let i=0;i<this.creatures.length;i++){ const a = random(this.matingPool); const b = random(this.matingPool); const {dna, mutated} = a.dna.crossover(b.dna); const child = new Creature(dna); // store parent/child positions for transition lines this.parentLinks.push({ aPos: a.pos.copy(), bPos: b.pos.copy(), cPos: child.pos.copy(), mutated }); newCreatures[i] = child; } this.creatures = newCreatures; } showTransition(pct){ // fade current positions for(let ln of this.parentLinks){ strokeWeight(2); const col = ln.mutated ? color(0,80,90) : color(0,0,90); stroke(col); const mid = p5.Vector.lerp(ln.aPos, ln.cPos, pct); line(ln.aPos.x, ln.aPos.y, mid.x, mid.y); const mid2 = p5.Vector.lerp(ln.bPos, ln.cPos, pct); line(ln.bPos.x, ln.bPos.y, mid2.x, mid2.y); } // overlay text noStroke(); fill(60,0,100,map(pct,0,1,0,100)); textAlign(CENTER,CENTER); textSize(32); text("Crossover & Mutation", width/2, height/2); } } </script> </body> </html> ```
This p5.js animation visually demonstrates a genetic algorithm. It simulates a population of "creatures" (simple triangles) trying to reach a target at the top of the screen. Each creature has a "DNA" sequence of movement vectors. Over generations, the algorithm selects the fittest creatures, combines their DNA through "crossover," and introduces randomness through "mutation" to evolve better solutions. * **Creatures**: Represented as small white triangles. Their color changes to green if they reach the target and red if they crash. Their brightness indicates their fitness (darker = less fit, brighter = more fit). * **Target**: A light blue circle labeled "GOAL" at the top center. * **Genes (DNA)**: Each creature has a sequence of `p5.Vector` objects defining its movement for each frame of its "lifespan." * **Fitness**: Calculated based on how close a creature gets to the target. Reaching the target quickly yields very high fitness. * **Generations**: The simulation progresses through distinct generations. After a fixed "lifespan," creatures' fitness is evaluated, and a new generation is created. * **Crossover**: DNA from two "parent" creatures is combined to form new "offspring" DNA. * **Mutation**: Random changes are introduced into the offspring's DNA to maintain diversity and explore new solution spaces. * **Visualization**: * Creatures move dynamically using their genetic instructions. * Their color and brightness reflect their performance. * A message "NEW GENERATION: EVOLVING..." appears briefly when crossover and mutation occur to transition to the next generation. * Information about the current generation and best fitness is displayed in a corner. ```html <!DOCTYPE html> <html> <head> <title>Genetic Algorithm Evolution</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <style> /* Basic styling for the page */ body { margin: 0; overflow: hidden; /* Hide scrollbars if canvas is larger than viewport */ display: flex; justify-content: center; align-items: center; min-height: 100vh; /* Full viewport height */ background-color: #333; /* Dark background */ font-family: Arial, sans-serif; } /* Canvas styling */ canvas { display: block; /* Remove extra space below canvas */ border: 2px solid #555; /* Border around the simulation area */ } /* Info display panel */ #info { position: absolute; /* Position relative to the viewport */ top: 20px; left: 20px; color: white; /* White text for readability */ font-size: 16px; background: rgba(0,0,0,0.5); /* Semi-transparent background */ padding: 10px; border-radius: 5px; /* Rounded corners */ } </style> </head> <body> <!-- Info panel for displaying generation and fitness --> <div id="info"> Generation: 0<br> Best Fitness: 0.00 </div> <!-- p5.js sketch goes here --> <script> // Global variables for the genetic algorithm simulation let population; // Array to hold all Creature objects let lifeSpan = 250; // Number of frames (steps) each generation lives let lifeCounter = 0; // Tracks current frame within the lifespan let target; // The target destination (p5.Vector) let populationSize = 200; // Number of creatures in each generation let generationCount = 0; // Current generation number let mutationRate = 0.02; // Probability (0-1) for a gene to mutate let maxForce = 0.3; // Maximum magnitude of a single movement force let infoDiv; // DOM element to display simulation info // Variables for the "New Generation" transition message let displayTransitionMessage = false; let transitionMessageAlpha = 255; // Alpha for fading effect // --- DNA Class --- // Represents the genetic code (sequence of movement vectors) for a creature class DNA { constructor(genes) { if (genes) { // If genes are provided (e.g., from crossover), use them this.genes = genes; } else { // Otherwise, initialize with random genes (for the first generation) this.genes = []; for (let i = 0; i < lifeSpan; i++) { this.genes[i] = p5.Vector.random2D(); // Random direction this.genes[i].setMag(maxForce); // With maxForce magnitude } } } // Crossover method: Combines genes from two parent DNAs crossover(partnerDNA) { let newGenes = []; // Pick a random midpoint for genetic exchange let mid = floor(random(this.genes.length)); for (let i = 0; i < this.genes.length; i++) { if (i > mid) { // Take genes from 'this' DNA after midpoint newGenes[i] = this.genes[i]; } else { // Take genes from partner DNA before/at midpoint newGenes[i] = partnerDNA.genes[i]; } } return new DNA(newGenes); // Return new DNA object } // Mutation method: Introduces random changes to genes mutate(mutationRate) { for (let i = 0; i < this.genes.length; i++) { if (random(1) < mutationRate) { // Check if mutation occurs for this gene this.genes[i] = p5.Vector.random2D(); // Randomize the gene this.genes[i].setMag(maxForce); // Set its magnitude } } } } // --- Creature Class --- // Represents a single "organism" in the simulation class Creature { constructor(dna) { this.pos = createVector(width / 2, height - 10); // Start at bottom center this.vel = createVector(0, 0); // Initial velocity this.acc = createVector(0, 0); // Initial acceleration this.dna = dna; // Assign DNA this.fitness = 0; // Initial fitness this.completed = false; // True if reached target this.crashed = false; // True if hit wall this.timeToCompletion = -1; // Steps taken to complete // Track closest distance to target achieved during lifetime this.closestDistance = dist(this.pos.x, this.pos.y, target.x, target.y); } // Applies a force to the creature (updates acceleration) applyForce(force) { this.acc.add(force); } // Updates the creature's state (position, velocity) update() { // Return if creature has already completed or crashed if (this.completed || this.crashed) { return; } // Apply the force defined by the current gene in its DNA if (lifeCounter < this.dna.genes.length) { this.applyForce(this.dna.genes[lifeCounter]); } // Update velocity and position this.vel.add(this.acc); this.pos.add(this.vel); this.acc.mult(0); // Reset acceleration for next frame // Check for completion (reaching the target) let d = dist(this.pos.x, this.pos.y, target.x, target.y); if (d < 15) { // Within target radius (15 pixels) this.completed = true; this.pos = target.copy(); // Snap to target for visual clarity this.timeToCompletion = lifeCounter; // Record time taken } // Check for crashing into canvas boundaries if (this.pos.x > width || this.pos.x < 0 || this.pos.y > height || this.pos.y < 0) { this.crashed = true; } // Update the closest distance achieved so far this.closestDistance = min(this.closestDistance, d); // Limit velocity to prevent creatures from flying too fast this.vel.limit(5); // Adjust this value to control creature speed } // Calculates the creature's fitness calcFitness() { // Higher fitness for reaching the target quickly if (this.completed) { // Fitness based on inverse square of time taken (faster = much better) this.fitness = 1000000 / (this.timeToCompletion * this.timeToCompletion); this.fitness *= 1.5; // Strong bonus for actual completion } else { // Fitness based on inverse square of closest distance achieved this.fitness = 1000000 / (this.closestDistance * this.closestDistance); } // Penalize crashing significantly if (this.crashed) { this.fitness *= 0.1; // Large penalty } // Ensure fitness is not negative or ridiculously small (for display mapping) this.fitness = max(this.fitness, 0.001); } // Draws the creature on the canvas show() { push(); // Isolate transformations for this creature translate(this.pos.x, this.pos.y); // Rotate creature to face its current direction of movement rotate(this.vel.heading() + PI / 2); // Add PI/2 to align default triangle dir noStroke(); // Determine brightness (alpha) based on fitness relative to the global maxFitness let alpha = map(this.fitness, 0, currentMaxFitness, 50, 255); alpha = constrain(alpha, 50, 255); // Keep alpha within a reasonable range // Set color based on creature's status if (this.completed) { fill(0, 255, 0, alpha); // Green if completed } else if (this.crashed) { fill(255, 0, 0, alpha); // Red if crashed } else { fill(255, 255, 255, alpha); // White otherwise } // Draw a simple triangle for the creature triangle(0, -10, -5, 10, 5, 10); // Pointing upwards initially pop(); // Restore previous transformations } } // --- Setup Function --- // Initializes the canvas and the initial population function setup() { createCanvas(800, 600); // Create a 800x600 pixel canvas frameRate(60); // Set consistent frame rate to 60 FPS target = createVector(width / 2, 50); // Target at top center infoDiv = select('#info'); // Get the HTML div for info display // Initialize the first population with random DNA population = []; for (let i = 0; i < populationSize; i++) { population[i] = new Creature(new DNA()); } } // --- Main Draw Loop --- // Runs continuously, updating and displaying the simulation let currentMaxFitness = 0; // Global variable to store max fitness of current generation function draw() { background(0); // Clear background with black // Update and show all creatures in the current population for (let i = 0; i < population.length; i++) { population[i].update(); population[i].show(); } // Draw the target fill(0, 200, 255, 150); // Light blue target with some transparency ellipse(target.x, target.y, 30, 30); // Draw target circle fill(255); textAlign(CENTER, CENTER); textSize(12); text("GOAL", target.x, target.y); // Label for the target // Update the life counter (frame count for the current generation) lifeCounter++; // Check if the current generation's lifespan is over if (lifeCounter >= lifeSpan) { evaluateAndReproduce(); // Perform GA steps: evaluation, selection, crossover, mutation generationCount++; // Increment generation number lifeCounter = 0; // Reset life counter for the new generation displayTransitionMessage = true; // Trigger the transition message transitionMessageAlpha = 255; // Reset alpha for full visibility } // Update the info display panel updateInfo(); // Display the "New Generation" message during transition if (displayTransitionMessage) { fill(255, transitionMessageAlpha); // White text, fading alpha textSize(32); textAlign(CENTER, CENTER); text("NEW GENERATION: EVOLVING...", width / 2, height / 2); transitionMessageAlpha -= 5; // Fade out effect if (transitionMessageAlpha <= 0) { displayTransitionMessage = false; // Stop displaying when fully faded transitionMessageAlpha = 255; // Reset alpha for next transition } } } // --- Genetic Algorithm Core Functions --- // Evaluates fitness of current population, then creates the next generation function evaluateAndReproduce() { // 1. Evaluate fitness for all creatures and find the maximum fitness currentMaxFitness = 0; // Reset for the current evaluation cycle for (let i = 0; i < population.length; i++) { population[i].calcFitness(); if (population[i].fitness > currentMaxFitness) { currentMaxFitness = population[i].fitness; } } // 2. Create the mating pool (for roulette wheel selection) let matingPool = []; for (let i = 0; i < population.length; i++) { // Normalize fitness to a 0-1 range based on currentMaxFitness let fitnessNormalized = map(population[i].fitness, 0, currentMaxFitness, 0, 1); // Add creature to mating pool multiple times, proportional to its normalized fitness // This ensures fitter creatures have a higher chance of being selected as parents let n = floor(fitnessNormalized * 100); // Up to 100 entries per creature for (let j = 0; j < n; j++) { matingPool.push(population[i]); } } // Handle case where mating pool is empty (e.g., all creatures crashed immediately) if (matingPool.length === 0) { console.log("Mating pool empty. Re-initializing population with random DNA to avoid stagnation."); // Re-initialize population with completely new random DNAs for (let i = 0; i < populationSize; i++) { population[i] = new Creature(new DNA()); } return; // Skip the rest of the reproduction for this generation } // 3. Create the new generation let newPopulation = []; for (let i = 0; i < population.length; i++) { // Selection: Pick two random parents from the mating pool let parentAindex = floor(random(matingPool.length)); let parentBindex = floor(random(matingPool.length)); let parentA = matingPool[parentAindex].dna; let parentB = matingPool[parentBindex].dna; // Crossover: Create offspring DNA from parents let childDNA = parentA.crossover(parentB); // Mutation: Introduce random changes to the child's DNA childDNA.mutate(mutationRate); // Create a new creature with the evolved DNA and add to new population newPopulation[i] = new Creature(childDNA); } population = newPopulation; // Replace old population with the new one } // --- Info Display Function --- // Updates the HTML div with current simulation statistics function updateInfo() { let bestFitnessVal = 0; // Iterate through population to find the absolute best fitness value for display if (population && population.length > 0) { for (let i = 0; i < population.length; i++) { // Recalculate fitness to ensure display is up-to-date, especially if creatures // were still in motion when calcFitness was last called during lifeCycle. // This can be computationally intensive for very large populations. // For this visualization, it's acceptable. population[i].calcFitness(); if (population[i].fitness > bestFitnessVal) { bestFitnessVal = population[i].fitness; } } } // Update the content of the infoDiv infoDiv.html(` Generation: ${generationCount}<br> Best Fitness: ${nf(bestFitnessVal, 0, 2)}<br> Population: ${populationSize}<br> Mutation Rate: ${mutationRate * 100}% `); } </script> </body> </html> ```
This p5.js animation creates a self-contained environment to demonstrate how a genetic algorithm works. A population of small triangles (creatures) starts at the bottom of the screen. Their goal is to reach the green target at the top while avoiding the red obstacle. Each creature has a "DNA" sequence, which is a series of pre-programmed movement instructions for its entire lifespan. - **Fitness:** At the end of each generation, creatures are scored. Those that get closer to the target receive a higher fitness score. Reaching the target gives a significant bonus, and reaching it faster is even better. Crashing into the obstacle or going off-screen results in a penalty. - **Selection:** The fittest individuals are more likely to be selected as "parents" for the next generation. - **Crossover & Mutation:** A new generation is created by combining the DNA of parent pairs (crossover) and introducing small random changes (mutation). You will see the creatures gradually learn to navigate around the obstacle and find more efficient paths to the target over many generations. The on-screen text provides context about the current generation and simulation progress. ```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Genetic Algorithm Visualization</title> <style> body { padding: 0; margin: 0; overflow: hidden; font-family: sans-serif; } canvas { display: block; } </style> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> </head> <body> <script> // --- Algorithm Parameters --- let lifespan = 400; // How many frames each generation lives let populationSize = 50; // Number of creatures in the population let mutationRate = 0.01; // Probability of a gene mutating // --- Simulation State --- let population; let lifeCounter = 0; let generationCount = 1; let target; let obstacle; let bestFitnessEver = 0; // --- SETUP FUNCTION: Initializes the simulation --- function setup() { createCanvas(windowWidth, windowHeight); const startPos = createVector(width / 2, height - 30); target = createVector(width / 2, 60); // Define an obstacle obstacle = { x: width / 2 - 150, y: height / 2, w: 300, h: 20 }; // Create the initial population population = new Population(populationSize, startPos); } // --- DRAW FUNCTION: The main animation loop --- function draw() { background(20, 20, 40); // Draw the target circle noStroke(); fill(100, 255, 100, 100); ellipse(target.x, target.y, 64, 64); fill(150, 255, 150); ellipse(target.x, target.y, 32, 32); // Draw the obstacle fill(255, 80, 80); rect(obstacle.x, obstacle.y, obstacle.w, obstacle.h); // If the generation is still alive, run the simulation if (lifeCounter < lifespan) { population.run(); lifeCounter++; } else { // Generation is over: evaluate fitness and create the next generation lifeCounter = 0; population.evaluate(); population.selection(); generationCount++; } // Display on-screen information fill(220); textSize(20); text(`Generation: ${generationCount}`, 20, 30); text(`Time in Generation: ${lifeCounter} / ${lifespan}`, 20, 60); text(`Population: ${populationSize}`, 20, 90); text(`Best Fitness: ${nf(bestFitnessEver, 0, 2)}`, 20, 120); } // --- DNA CLASS: Handles the genetic material (a sequence of vectors) --- class DNA { constructor(genes) { if (genes) { this.genes = genes; } else { // If no genes are provided, create a new random set this.genes = []; for (let i = 0; i < lifespan; i++) { // Each gene is a vector representing a force this.genes[i] = p5.Vector.random2D(); this.genes[i].setMag(0.5); // Control the max force } } } // Crossover: Combines this DNA with a partner's DNA crossover(partner) { let newGenes = []; let mid = floor(random(this.genes.length)); // Pick a random midpoint for (let i = 0; i < this.genes.length; i++) { // Take genes from either parent based on the midpoint if (i > mid) { newGenes[i] = this.genes[i]; } else { newGenes[i] = partner.genes[i]; } } return new DNA(newGenes); } // Mutation: Randomly alters genes based on mutation rate mutate(mutationRate) { for (let i = 0; i < this.genes.length; i++) { if (random(1) < mutationRate) { this.genes[i] = p5.Vector.random2D(); this.genes[i].setMag(0.5); } } } } // --- CREATURE CLASS: Represents an individual in the population --- class Creature { constructor(startPos, dna) { this.pos = startPos.copy(); this.vel = createVector(); this.acc = createVector(); this.dna = dna || new DNA(); this.fitness = 0; this.completed = false; // Did it reach the target? this.crashed = false; // Did it hit an obstacle? this.timeToTarget = lifespan; // To reward speed } applyForce(force) { this.acc.add(force); } // Calculates the creature's fitness score calcFitness() { // Fitness is inversely proportional to the final distance to the target let d = dist(this.pos.x, this.pos.y, target.x, target.y); this.fitness = map(d, 0, width, width, 0); // BIG fitness boost for reaching the target if (this.completed) { this.fitness *= 10; // Reward for finishing faster this.fitness *= (lifespan / this.timeToTarget); } // Penalty for crashing if (this.crashed) { this.fitness /= 10; } } // Updates the creature's state each frame update() { // Check if creature reached the target let d = dist(this.pos.x, this.pos.y, target.x, target.y); if (d < 20 && !this.completed) { this.completed = true; this.pos = target.copy(); // Snap to target this.timeToTarget = lifeCounter; } // Check for collision with the obstacle if (this.pos.x > obstacle.x && this.pos.x < obstacle.x + obstacle.w && this.pos.y > obstacle.y && this.pos.y < obstacle.y + obstacle.h) { this.crashed = true; } // Check for collision with canvas edges if (this.pos.x > width || this.pos.x < 0 || this.pos.y > height || this.pos.y < 0) { this.crashed = true; } // Apply the force from the current gene in its DNA sequence if (!this.completed && !this.crashed) { this.applyForce(this.dna.genes[lifeCounter]); this.vel.add(this.acc); this.pos.add(this.vel); this.acc.mult(0); this.vel.limit(4); // Limit speed } } // Renders the creature on the canvas show() { push(); noStroke(); // Set color based on status if(this.completed) fill(100, 255, 100, 200); // Green if completed else if(this.crashed) fill(255, 80, 80, 150); // Red if crashed else fill(255, 255, 255, 100); // Default white translate(this.pos.x, this.pos.y); rotate(this.vel.heading()); // Point in the direction of velocity // Draw a simple triangle shape triangle(-5, -5, -5, 5, 10, 0); pop(); } } // --- POPULATION CLASS: Manages the collection of creatures --- class Population { constructor(size, startPos) { this.creatures = []; this.startPos = startPos.copy(); this.popsize = size; this.matingPool = []; // Create the initial population of creatures for (let i = 0; i < this.popsize; i++) { this.creatures[i] = new Creature(this.startPos); } } // Run one frame of the simulation for the entire population run() { for (let i = 0; i < this.popsize; i++) { this.creatures[i].update(); this.creatures[i].show(); } } // Calculate fitness for all creatures and build a "mating pool" evaluate() { let maxFit = 0; for (let creature of this.creatures) { creature.calcFitness(); if (creature.fitness > maxFit) { maxFit = creature.fitness; } } if (maxFit > bestFitnessEver) { bestFitnessEver = maxFit; } // Clear the mating pool for the new generation this.matingPool = []; // Normalize fitness values (from 0 to 1) and add to mating pool // Fitter creatures get more entries, increasing their chance of reproduction for (let creature of this.creatures) { let fitnessNormal = creature.fitness / maxFit; let n = floor(fitnessNormal * 100); for (let j = 0; j < n; j++) { this.matingPool.push(creature); } } } // Create the next generation via crossover and mutation selection() { let newCreatures = []; // Show a visual transition before creating the new population this.showTransition(); for (let i = 0; i < this.creatures.length; i++) { let parentA, parentB; if(this.matingPool.length > 0) { parentA = random(this.matingPool).dna; parentB = random(this.matingPool).dna; } else { // If mating pool is empty (e.g. all crashed), create random parents to restart parentA = new DNA(); parentB = new DNA(); } // Create a child by crossover let child_dna = parentA.crossover(parentB); // Apply mutation to the child's DNA child_dna.mutate(mutationRate); newCreatures[i] = new Creature(this.startPos, child_dna); } this.creatures = newCreatures; } // Provides a brief visual cue during the generation change showTransition() { background(20, 20, 40, 150); fill(255); textSize(32); textAlign(CENTER, CENTER); let statusText = "GENERATION COMPLETE\n"; if (this.matingPool.length > 0) { statusText += "Selecting parents, performing crossover & mutation..."; } else { statusText += "No survivors. Restarting with random DNA."; } text(statusText, width/2, height/2 - 50); textSize(20); text("Creating new population...", width/2, height/2 + 20); textAlign(LEFT, BASELINE); } } </script> </body> </html> ```
```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Genetic Algorithm Evolution</title> <script src="https://cdn.jsdelivr.net/npm/p5@1.9.0/lib/p5.js"></script> <style> body { margin: 0; overflow: hidden; } </style> </head> <body> <script> let populationSize = 50; let creatures = []; let target; let generation = 0; let mutationRate = 0.01; let crossoverRate = 0.7; let generationTime = 300; // Frames per generation let lastGenerationChange = 0; let showCrossover = false; let showMutation = false; function setup() { createCanvas(800, 600); target = createVector(width / 2, height / 2); initializePopulation(); } function initializePopulation() { creatures = []; for (let i = 0; i < populationSize; i++) { let x = random(width); let y = random(height); let angle = random(TWO_PI); let speed = random(1, 3); creatures.push(new Creature(x, y, angle, speed)); } } function draw() { background(220); // Display Target fill(255, 0, 0); ellipse(target.x, target.y, 20, 20); // Update and display creatures for (let creature of creatures) { creature.update(); creature.display(); } // Check if it's time for a new generation if (frameCount - lastGenerationChange > generationTime) { generation++; nextGeneration(); lastGenerationChange = frameCount; showCrossover = false; showMutation = false; } // Display generation number fill(0); textSize(16); text("Generation: " + generation, 10, 20); } function nextGeneration() { // Calculate fitness let fitnesses = creatures.map(creature => calculateFitness(creature)); // Selection (roulette wheel selection) let parents = selectParents(creatures, fitnesses); // Crossover and Mutation let newCreatures = []; for (let i = 0; i < populationSize; i += 2) { let parent1 = parents[floor(random(parents.length))]; let parent2 = parents[floor(random(parents.length))]; let child1, child2; if (random(1) < crossoverRate) { showCrossover = true; [child1, child2] = crossover(parent1, parent2); } else { child1 = parent1.copy(); child2 = parent2.copy(); } if (random(1) < mutationRate) { showMutation = true; child1.mutate(); } if (random(1) < mutationRate) { showMutation = true; child2.mutate(); } newCreatures.push(child1); newCreatures.push(child2); } creatures = newCreatures; } function calculateFitness(creature) { let distance = dist(creature.x, creature.y, target.x, target.y); return map(distance, 0, max(width, height), 1, 0); // Higher fitness for closer creatures } function selectParents(creatures, fitnesses) { let totalFitness = fitnesses.reduce((a, b) => a + b, 0); let parents = []; for (let i = 0; i < creatures.length; i++) { let pick = random(totalFitness); let runningSum = 0; for (let j = 0; j < creatures.length; j++) { runningSum += fitnesses[j]; if (runningSum > pick) { parents.push(creatures[j]); break; } } } return parents; } function crossover(parent1, parent2) { let angle1 = parent1.angle; let angle2 = parent2.angle; let speed1 = parent1.speed; let speed2 = parent2.speed; let childAngle1 = lerp(angle1, angle2, random(1)); let childAngle2 = lerp(angle2, angle1, random(1)); let childSpeed1 = lerp(speed1,speed2, random(1)); let childSpeed2 = lerp(speed2, speed1, random(1)); let child1 = new Creature(parent1.x, parent1.y, childAngle1, childSpeed1); let child2 = new Creature(parent2.x, parent2.y, childAngle2, childSpeed2); return [child1, child2]; } class Creature { constructor(x, y, angle, speed) { this.x = x; this.y = y; this.angle = angle; this.speed = speed; this.color = color(random(255), random(255), random(255)); } update() { this.x += this.speed * cos(this.angle); this.y += this.speed * sin(this.angle); // Wrap around edges this.x = (this.x + width) % width; this.y = (this.y + height) % height; } display() { noStroke(); fill(this.color); let fitness = calculateFitness(this); let size = map(fitness, 0, 1, 5, 20); ellipse(this.x, this.y, size, size); if (showCrossover) { stroke(255, 255, 0); line(this.x, this.y, this.x + cos(this.angle) * 10, this.y + sin(this.angle) * 10); noStroke(); } if (showMutation) { fill(0, 255, 0); ellipse(this.x + 5, this.y + 5, 3, 3); fill(this.color); } } mutate() { this.angle += random(-0.1, 0.1); this.speed += random(-0.05, 0.05); this.speed = constrain(this.speed, 0.5, 5); } copy() { return new Creature(this.x, this.y, this.angle, this.speed); } } </script> </body> </html> ```
Here's a p5.js animation that demonstrates genetic algorithms visually. The code includes crossover and mutation visualization during generation transitions: ```html <!DOCTYPE html> <html> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <style> body { margin: 0; padding: 0; overflow: hidden; } </style> </head> <body> <script> class Creature { constructor(x, y, dx, dy) { this.position = createVector(x, y); this.velocity = createVector(dx, dy); this.fitness = 0; } update() { this.position.add(this.velocity); } calculateFitness(target) { let d = dist(this.position.x, this.position.y, target.x, target.y); this.fitness = 1 / (d + 1); } display() { let d = dist(this.position.x, this.position.y, target.x, target.y); let fitness = 1 / (d + 1); let c = map(fitness, 0, 1, 0, 255); fill(255 - c, c, 0); ellipse(this.position.x, this.position.y, 10, 10); } } class Population { constructor(popSize) { this.creatures = []; this.previousCreatures = []; this.parentChildPairs = []; for (let i = 0; i < popSize; i++) { let dx = random(-2, 2); let dy = random(-2, 2); this.creatures.push(new Creature(0, height/2, dx, dy)); } } update() { for (let creature of this.creatures) { creature.update(); } } display() { for (let creature of this.creatures) { creature.display(); } } displayTransition() { // Previous generation in red fill(255, 0, 0, 150); for (let creature of this.previousCreatures) { creature.display(); } // Current generation in green fill(0, 255, 0, 150); for (let creature of this.creatures) { creature.display(); } // Draw parent-child connections stroke(0); strokeWeight(1); for (let pair of this.parentChildPairs) { let parents = pair.parents; let child = pair.child; line(parents[0].position.x, parents[0].position.y, child.position.x, child.position.y); line(parents[1].position.x, parents[1].position.y, child.position.x, child.position.y); } } calculateFitness(target) { for (let creature of this.creatures) { creature.calculateFitness(target); } } selectParent(totalFitness) { let r = random(totalFitness); let cumulative = 0; for (let creature of this.creatures) { cumulative += creature.fitness; if (cumulative >= r) return creature; } return this.creatures[this.creatures.length - 1]; } crossover(parent1, parent2) { let dx = (parent1.velocity.x + parent2.velocity.x) / 2; let dy = (parent1.velocity.y + parent2.velocity.y) / 2; return new Creature(0, height/2, dx, dy); } mutate(child) { let mutationRate = 0.2; child.velocity.x += random(-mutationRate, mutationRate); child.velocity.y += random(-mutationRate, mutationRate); } createNewGeneration() { let newCreatures = []; let totalFitness = 0; for (let creature of this.creatures) { totalFitness += creature.fitness; } if (totalFitness < 0.01) { // Random restart if no fitness for (let i = 0; i < this.creatures.length; i++) { let dx = random(-2, 2); let dy = random(-2, 2); newCreatures.push(new Creature(0, height/2, dx, dy)); } this.creatures = newCreatures; return; } this.previousCreatures = [...this.creatures]; this.parentChildPairs = []; for (let i = 0; i < this.creatures.length; i++) { let parent1 = this.selectParent(totalFitness); let parent2 = this.selectParent(totalFitness); let child = this.crossover(parent1, parent2); this.mutate(child); newCreatures.push(child); this.parentChildPairs.push({parents: [parent1, parent2], child}); } this.creatures = newCreatures; } } let population; let target; let generationTimer = 0; let generationDuration = 60; let transitionTimer = 0; let transitionDuration = 30; function setup() { createCanvas(800, 400); target = createVector(width - 100, height/2); population = new Population(50); } function draw() { background(240); // Draw target fill(255, 0, 0); ellipse(target.x, target.y, 20, 20); // Handle generation transitions if (transitionTimer > 0) { transitionTimer--; population.displayTransition(); } else { population.update(); population.display(); } // Update generation timer generationTimer++; if (generationTimer >= generationDuration) { population.calculateFitness(target); population.createNewGeneration(); generationTimer = 0; transitionTimer = transitionDuration; } // Draw info text fill(0); textSize(16); text("Generations: " + (frameCount / generationDuration).toFixed(1), 20, 30); text("Fitness: " + population.creatures[0].fitness.toFixed(4), 20, 50); } </script> </body> </html> ``` This animation shows: 1. Red dots represent the previous generation 2. Green dots represent the new generation 3. Lines connect parents to offspring 4. Color brightness indicates fitness (closer to target = brighter) 5. The target is a red circle on the right side Key features: - Creatures start at the left edge and evolve to move toward the target - Fitness is calculated based on proximity to target - New generations are created every 60 frames - Transition visualization shows parent-child relationships - Mutation adds small random changes to velocities - Crossover blends parent velocities The animation demonstrates how genetic algorithms: - Evaluate fitness - Select parents - Create offspring through crossover - Apply mutations - Evolve solutions over generations You can adjust parameters like `generationDuration`, `mutationRate`, and population size to see different evolutionary patterns.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Genetic Algorithm Visualizer - p5.js</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.min.js"></script> <style> body { margin: 0; background: #181829; color: #eee; font-family: sans-serif;} #infoPanel { position: absolute; left: 20px; top: 20px; background: rgba(20,20,40,0.8); padding: 16px 20px; border-radius: 8px; z-index: 10; font-size: 16px; box-shadow: 0 3px 20px rgba(0,0,0,0.2); } a { color: #69c; text-decoration: underline;} </style> </head> <body> <div id="infoPanel"> <b>Genetic Algorithm Evolution Demo</b><br> Target: Pink circle. <br> Creatures: Dots (brighter = better).<br> <span id="generationTxt"></span><br> <span id="bestFitness"></span> </div> <script> let populationSize = 40; let dnaLength = 120; // how many steps in each attempt let mutationRate = 0.05; let crossoverDisplayTime = 120; // frames to show crossover/mutation visualization let maxStepSpeed = 2.7; let population = []; let gen = 1; let generationFrame = 0; let generationDuration = dnaLength + 30; // frames per generation (run dna + end pause) let target; let newGenDisplay = false; let showCrossoverData = []; let bestFitness = ''; // Obstacles (optional, but left out for clearer GA visualization) function setup() { createCanvas(730, 540); target = createVector(width - 60, height/2); makePopulation(); } function draw() { background(24,18,41); // Draw goal stroke(255, 80, 140); strokeWeight(2); fill(255, 90, 200, 137); ellipse(target.x, target.y, 35, 35); noStroke(); fill(255, 120, 240, 50); ellipse(target.x, target.y, 90, 90); // Animate one generation's run of all creatures if (generationFrame < dnaLength) { for (let c of population) c.update(); } // Draw all creatures for (let c of population) c.show(); // Show text overlays document.getElementById('generationTxt').textContent = `Generation: ${gen}`; document.getElementById('bestFitness').textContent = bestFitness; // Visualize crossover/mutation when transitioning to new generation if (newGenDisplay) { visualizeCrossover(); } // Progress to next frame/generation generationFrame++; // At end of one attempt, build next generation if (generationFrame > generationDuration) { evolvePopulation(); generationFrame = 0; gen++; newGenDisplay = true; setTimeout(()=>{ newGenDisplay = false; }, crossoverDisplayTime * 17); } } // ========== DNA CLASS (sequence of 2D movement vectors) =============== class DNA { constructor(genes) { this.genes = genes || DNA.randomGenes(); } static randomGenes() { let arr = []; for (let i=0;i<dnaLength;i++) arr.push(p5.Vector.random2D().setMag(random(0.2,maxStepSpeed))); return arr; } crossover(partner) { // 1-point crossover let mid = floor(random(this.genes.length)); let childGenes = []; for (let i=0; i<this.genes.length; i++) { childGenes[i] = i < mid ? this.genes[i].copy() : partner.genes[i].copy(); } // Visualize which parents' sections go into child showCrossoverData.push({ mid: mid, length: this.genes.length, parentACol: this.parentAColor, parentBCol: this.parentBColor, mutationIdx: [] }); return new DNA(childGenes); } mutate(mrate = mutationRate) { let mutatedIdx = []; for (let i=0;i<this.genes.length;i++) { if (random(1) < mrate) { this.genes[i] = p5.Vector.random2D().setMag(random(0.2,maxStepSpeed)); mutatedIdx.push(i); } } // Attach for visualization if (showCrossoverData.length) { let last = showCrossoverData[showCrossoverData.length-1]; last.mutationIdx = mutatedIdx; } } } // ========== CREATURE ("DOT") CLASS ================ class Creature { constructor(dna) { this.dna = dna || new DNA(); this.start = createVector(35, height/2); this.pos = this.start.copy(); this.vel = createVector(); this.acc = createVector(); this.completed = false; this.dead = false; this.step = 0; this.fitness = 0; this.col = [86, 182, 255]; } update() { if (this.completed || this.dead) return; // apply dna step if (this.step < dnaLength) { this.acc = this.dna.genes[this.step]; this.vel.add(this.acc); this.vel.limit(maxStepSpeed); this.pos.add(this.vel); this.step++; } // Died if hitting walls or out of bounds if (this.pos.x < 0 || this.pos.x > width || this.pos.y < 0 || this.pos.y > height) this.dead = true; // Finished if close to target if (p5.Vector.dist(this.pos, target) < 18) { this.completed = true; this.pos = target.copy(); } } calcFitness() { let d = p5.Vector.dist(this.pos, target); this.fitness = 1 / (d*d + 1); // lower distance == higher fitness if (this.completed) { this.fitness *= 12 + (dnaLength - this.step)/dnaLength * 12; } // Optionally add slight fitness penalty for taking more steps } show() { // Color/Size represent fitness let brightness = map(this.fitness, 0, 0.14, 0.32, 1.2, true); let sz = map(this.fitness, 0, 0.15, 7, 15, true); let ccol = lerpColor(color(36,100,181), color(240,250,120), brightness); if (this.completed) ccol = color(255,100,220); // winners = pink if (this.dead) sz *= 0.5; // Trail ("path taken") beginShape(); noFill(); stroke(ccol); strokeWeight(1.5 * brightness); let v = this.start.copy(); for (let i=0;i<this.step;i++) { v.add(this.dna.genes[i]); vertex(v.x, v.y); } endShape(); // Current position noStroke(); fill(ccol); ellipse(this.pos.x, this.pos.y, sz, sz); } clone() { // duplicate with same DNA let d = new DNA(this.dna.genes.map(v=>v.copy())); let out = new Creature(d); out.col = this.col.slice(); return out; } } // =========== HANDLE POPULATION AND GENETICS ================= function makePopulation() { population = []; for (let i=0; i<populationSize; i++) population.push(new Creature()); } function evolvePopulation() { // 1) Compute fitness let maxFit = 0; let bestIdx = 0; for (let i=0;i<population.length;i++) { population[i].calcFitness(); if (population[i].fitness > maxFit) { maxFit = population[i].fitness; bestIdx = i; } } // Show best for info text let bestDist = nf(p5.Vector.dist(population[bestIdx].pos, target),1,1); bestFitness = `Best dist: ${bestDist}, Fitness: ${nfc(maxFit,3)}` // 2) Build mating pool (roulette selection) let matingPool = []; let totalFit = population.reduce((acc, c)=>acc+c.fitness, 0); for (let c of population) { let n = floor((c.fitness/totalFit) * 410); // weight by fitness for (let j=0;j<n;j++) matingPool.push(c); } if (!matingPool.length) matingPool=population.slice(); // fallback // 3) Breed new population let newPop = []; showCrossoverData = []; for (let i=0; i<populationSize; i++) { let parentA = random(matingPool); let parentB = random(matingPool); // Assign colors for crossing visualization parentA.dna.parentAColor = color(138,219,255); parentB.dna.parentBColor = color(255,252,163); let childDNA = parentA.dna.crossover(parentB.dna); childDNA.mutate(); newPop.push(new Creature(childDNA)); } population = newPop; } // ========== ANIMATION: Show Crossover and Mutation STEPS ========== function visualizeCrossover() { // Show as a few moving "chromosomes" with colored segments, some flashing segment = mutated let x = width/2-190, y = 50; push(); textAlign(LEFT, TOP); textSize(22); fill(255); noStroke(); text('Crossover & Mutation', x+30, y-36); let cLen = min(14, dnaLength), baseSeg = 38, spacing = 28; for (let i=0; i<min(showCrossoverData.length, 7); i++) { let d = showCrossoverData[i]; let xx = x, yy = y+i*spacing*1.7; // Draw line of DNA segments for (let j=0; j<cLen; j++) { let segCol = (j < d.mid)?color(138,219,255):color(255,252,163); if (d.mutationIdx.includes(j)) segCol = color(255,70,90); fill(segCol); stroke(0,60); strokeWeight(1.4); rect(xx+j*baseSeg, yy, baseSeg-6, 17, 5); if (d.mutationIdx.includes(j)) { noStroke(); fill(255,255,140,190); ellipse(xx + j*baseSeg + 9, yy+8.5, 10, 13); // glow effect for mutation } } // label noStroke(); fill(120); textSize(12); text('offspring DNA', xx+cLen*baseSeg+15, yy+1); } pop(); } </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Genetic Algorithm Visualization</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.min.js"></script> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } </style> </head> <body> <script> let lifespan = 200; let populationSize = 30; let mutationRate = 0.01; let target; let population = []; let generation = 1; let maxFitness = 0; let cooldown = 3000; // milliseconds between generations let lastUpdateTime = 0; let showNextGen = false; function setup() { createCanvas(600, 600); colorMode(HSB, 360, 100, 100, 1); target = createVector(width / 2, 50); for (let i = 0; i < populationSize; i++) { population.push(new Rocket()); } lastUpdateTime = millis(); textAlign(LEFT, TOP); } function draw() { background(220, 50, 10); // Display target fill(0, 0, 100); noStroke(); circle(target.x, target.y, 20); fill(220, 0, 100); circle(target.x, target.y, 5); // Check cooldown between generations let allDone = true; if (!showNextGen) { for (let rocket of population) { if (!rocket.finished) { allDone = false; rocket.move(); } } } // Display creatures for (let rocket of population) { rocket.display(showNextGen); } // Handle generation transitions if (allDone && !showNextGen) { calculateFitness(); drawGenerationInfo("Evaluation Phase"); if (millis() - lastUpdateTime > cooldown) { createNewGeneration(); generation++; } } else if (allDone && showNextGen) { drawGenerationInfo("New Generation Emerges"); } if (millis() - lastUpdateTime > cooldown * 2) { showNextGen = !allDone; if (allDone) lastUpdateTime = millis(); } } function drawGenerationInfo(status) { fill(0, 0, 100); textSize(16); text(`Generation: ${generation} - ${status}`, 20, 20); textSize(14); text(`Best Fitness: ${maxFitness.toFixed(4)}`, 20, 50); } function calculateFitness() { maxFitness = 0; for (let rocket of population) { let d = dist(rocket.position.x, rocket.position.y, target.x, target.y); rocket.fitness = 1 / (d + 1); if (rocket.fitness > maxFitness) { maxFitness = rocket.fitness; } } } function createNewGeneration() { let newPopulation = []; let totalFitness = 0; for (let rocket of population) { totalFitness += rocket.fitness; } for (let i = 0; i < population.length; i++) { let parent1 = selectParent(totalFitness); let parent2 = selectParent(totalFitness); let childDNA = crossover(parent1, parent2); mutate(childDNA); newPopulation.push(new Rocket(childDNA)); } population = newPopulation; } function selectParent(totalFitness) { let rand = random(totalFitness); let currentSum = 0; for (let rocket of population) { currentSum += rocket.fitness; if (currentSum > rand) { return rocket.dna; } } return population[population.length - 1].dna; } function crossover(parent1DNA, parent2DNA) { let childDNA = []; for (let i = 0; i < lifespan; i++) { if (random() < 0.5) { childDNA[i] = p5.Vector.copy(parent1DNA[i]); } else { childDNA[i] = p5.Vector.copy(parent2DNA[i]); } } return childDNA; } function mutate(dna) { for (let v of dna) { if (random() < mutationRate) { let mutation = p5.Vector.random2D().mult(0.5); v.add(mutation); } } } class Rocket { constructor(dna) { this.position = createVector(width/2, height - 20); this.dna = dna || this.createNewDNA(); this.currentStep = 0; this.finished = false; this.fitness = 0; } createNewDNA() { let dna = []; for (let i = 0; i < lifespan; i++) { let angle = random(TWO_PI); let vec = p5.Vector.fromAngle(angle).mult(2); dna.push(vec); } return dna; } move() { if (!this.finished && this.currentStep < lifespan) { this.position.add(this.dna[this.currentStep]); this.currentStep++; if (this.currentStep >= lifespan) { this.finished = true; } } } display(showNextGen) { // Color based on fitness let hue = map(this.fitness, 0, maxFitness, 0, 280); let size = map(this.fitness, 0, maxFitness, 6, 12); // Add visual effects for new generation if (showNextGen) { hue = (hue + frameCount * 2) % 360; size *= 1.2; } fill(hue, 80, 95, 0.8); noStroke(); circle(this.position.x, this.position.y, size); } } </script> </body> </html> ```
Here's a complete HTML implementation of a genetic algorithm visualization using p5.js. It shows creatures evolving to reach a target over generations, with visual indicators for fitness, mutation, and crossover: ```html <!DOCTYPE html> <html> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <style> body { margin: 0; padding: 20px; display: flex; justify-content: center; background-color: #f0f0f0; } .container { text-align: center; } </style> </head> <body> <div class="container"> <h2>Genetic Algorithm Evolution</h2> <div id="canvas-container"></div> <div> <button id="reset-btn">Reset Simulation</button> <span id="generation-counter">Generation: 0</span> </div> </div> <script> let population = []; let target; let generation = 0; let canvasWidth = 600; let canvasHeight = 400; let animationPhase = 0; // 0: moving, 1: selecting parents, 2: reproducing let animationFrame = 0; let bestCreature; function setup() { let canvas = createCanvas(canvasWidth, canvasHeight); canvas.parent('canvas-container'); target = createVector(canvasWidth - 50, canvasHeight / 2); // Initialize population for (let i = 0; i < 20; i++) { population.push(new Creature()); } // Set up reset button document.getElementById('reset-btn').addEventListener('click', resetSimulation); } function draw() { background(240); // Draw target fill(255, 100, 100); ellipse(target.x, target.y, 30, 30); fill(0); text("Target", target.x - 20, target.y - 20); // Draw origin fill(100); text("Start", 30, canvasHeight/2 + 30); // Update animation state if (frameCount % 180 === 0) { // Every ~3 seconds (60fps) animationPhase = (animationPhase + 1) % 3; animationFrame = 0; if (animationPhase === 0) { generation++; document.getElementById('generation-counter').textContent = `Generation: ${generation}`; } } animationFrame++; if (animationPhase === 0) { // Creatures moving toward target for (let creature of population) { creature.update(); creature.show(); } // Show distance to target for (let creature of population) { creature.showDistance(); } } else if (animationPhase === 1) { // Show selection process background(240); // Draw target fill(255, 100, 100); ellipse(target.x, target.y, 30, 30); // Sort by fitness population.sort((a, b) => b.fitness - a.fitness); bestCreature = population[0]; // Highlight top 20% performers let selectedCount = Math.floor(population.length * 0.2); for (let i = 0; i < selectedCount; i++) { let interp = animationFrame / 60; // 1 second animation let alpha = min(255, interp * 255); population[i].show(); fill(0, 255, 0, alpha); ellipse(population[i].position.x, population[i].position.y, population[i].size * 2, population[i].size * 2); // Animate text if (i === 0 && animationFrame === 1) { text("Selecting parents...", canvasWidth/2 - 80, 30); } } // Show all others faded for (let i = selectedCount; i < population.length; i++) { population[i].show(); fill(100, 100, 100, 150); ellipse(population[i].position.x, population[i].position.y, population[i].size * 2, population[i].size * 2); } } else if (animationPhase === 2) { // Show reproduction with crossover and mutation background(240); // Draw target fill(255, 100, 100); ellipse(target.x, target.y, 30, 30); // Animate reproduction process let interp = animationFrame / 60; // 1 second animation let alpha = min(255, interp * 255); // Show best creature from previous generation bestCreature.show(); fill(0, 255, 0, 255 - alpha); ellipse(bestCreature.position.x, bestCreature.position.y, bestCreature.size * 2, bestCreature.size * 2); // Animate new creatures appearing if (interp >= 0.5) { text("Reproducing with crossover and mutation...", canvasWidth/2 - 150, 30); // Create next generation (already done, just visualizing) for (let i = 0; i < population.length; i++) { if (random() < interp - 0.5) { // Gradually appear population[i].show(); // Visualize mutation with some color effect fill(255, 0, 255, 100); ellipse(population[i].position.x, population[i].position.y, population[i].size * 3, population[i].position.y * 3); } } } // If we're at the end of this phase, actually create new generation if (animationFrame === 60) { createNextGeneration(); } } } function createNextGeneration() { // Select parents (top 20%) let selectedCount = Math.floor(population.length * 0.2); let selectedParents = population.slice(0, selectedCount); // Create new population let newPopulation = []; // Elitism: keep the best performer newPopulation.push(selectedParents[0].clone()); // Create children through crossover and mutation while (newPopulation.length < population.length) { // Select two parents let parentA = random(selectedParents); let parentB = random(selectedParents); // Create child through crossover let child = parentA.crossover(parentB); // Mutate the child child.mutate(); newPopulation.push(child); } population = newPopulation; } function resetSimulation() { population = []; for (let i = 0; i < 20; i++) { population.push(new Creature()); } generation = 0; animationPhase = 0; animationFrame = 0; document.getElementById('generation-counter').textContent = `Generation: 0`; } class Creature { constructor(genes) { this.size = 10; this.position = createVector(50, canvasHeight / 2); this.velocity = createVector(); this.genes = genes || this.createRandomGenes(); this.fitness = 0; this.color = color(random(100, 200), random(100, 200), random(100, 200)); this.steps = 0; } createRandomGenes() { let genes = []; for (let i = 0; i < 20; i++) { genes.push(p5.Vector.random2D().mult(random(0.5, 2))); } return genes; } update() { if (this.steps < this.genes.length) { this.velocity = this.genes[this.steps]; this.position.add(this.velocity); this.steps++; // Calculate fitness (higher is better) let distanceToTarget = p5.Vector.dist(this.position, target); this.fitness = 1000 / (1 + distanceToTarget); } } show() { fill(this.color); noStroke(); ellipse(this.position.x, this.position.y, this.size, this.size); // Show direction line stroke(0); line(this.position.x, this.position.y, this.position.x + this.velocity.x * 5, this.position.y + this.velocity.y * 5); } showDistance() { // Show fitness with transparency (more transparent = worse fitness) let alpha = map(this.fitness, 0, 100, 50, 255); fill(red(this.color), green(this.color), blue(this.color), alpha); ellipse(this.position.x, this.position.y, this.size, this.size); } crossover(partner) { // Uniform crossover let newGenes = []; for (let i = 0; i < this.genes.length; i++) { // Randomly choose from either parent newGenes.push(random() < 0.5 ? this.genes[i].copy() : partner.genes[i].copy()); } return new Creature(newGenes); } mutate() { // Random chance to mutate each gene for (let gene of this.genes) { if (random() < 0.1) { // 10% mutation rate gene.add(p5.Vector.random2D().mult(0.5)); gene.limit(2); // Max speed } } } clone() { let clonedGenes = this.genes.map(g => g.copy()); let clone = new Creature(clonedGenes); clone.fitness = this.fitness; clone.color = this.color; clone.position = this.position.copy(); clone.velocity = this.velocity.copy(); clone.steps = this.steps; return clone; } } </script> </body> </html> ``` This visualization demonstrates: 1. **Population initialization**: Random creatures with random movement patterns (genes) 2. **Fitness evaluation**: Creatures are evaluated based on how close they get to the target 3. **Selection**: The top 20% performers are selected as parents (highlighted in green) 4. **Reproduction**: New creatures are created through crossover (mixing parent genes) 5. **Mutation**: Some genes are randomly changed to introduce variation 6. **Visual indicators**: - Color intensity shows fitness (brighter = better) - Highlighted circles show selection phase - Purple effects visualize mutation - Arrows show movement direction The animation cycles through these phases every few seconds, showing how each generation improves over time. You'll see the creatures progressively get better at reaching the target as their movement patterns evolve.
Here’s a self‐contained HTML file you can drop into an iframe (or open directly) to see a p5.js‐based GA demo. It shows simple dots (“creatures”) trying to reach a target, then steps through fitness display, crossover and mutation visualizations, and proceeds to the next generation automatically. ```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Genetic Algorithm Visualization</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script> </head> <body> <script> let popSize = 50; let lifespan = 300; let mutationRate = 0.01; let population; let newPopulation; let lifeCounter = 0; let generation = 1; let target; let phase = 'run'; // 'run', 'fitness', 'crossover', 'mutation' let phaseTimer = 0; let fitnessDuration = 120; let crossoverDuration = 120; let mutationDuration = 120; // ---- Agent class ---- class Agent { constructor(genes) { this.pos = createVector(width/2, height - 20); this.vel = createVector(); this.acc = createVector(); this.genes = genes || this.randomGenes(); this.fitness = 0; this.reached = false; } randomGenes() { let g = []; for (let i = 0; i < lifespan; i++) { let angle = random(TWO_PI); let v = p5.Vector.fromAngle(angle).setMag(0.3); g.push(v); } return g; } applyForce(f) { this.acc.add(f); } update() { if (!this.reached) { this.applyForce(this.genes[lifeCounter]); this.vel.add(this.acc); this.pos.add(this.vel); this.acc.mult(0); // check reach if (p5.Vector.dist(this.pos, target) < 10) { this.reached = true; this.pos = target.copy(); } } } calcFitness() { let d = p5.Vector.dist(this.pos, target); this.fitness = map(d, 0, width, width, 0); this.fitness = pow(this.fitness / width, 2); } show() { noStroke(); fill(0, 150); ellipse(this.pos.x, this.pos.y, 8); } } // ---- p5.js setup & draw ---- function setup() { createCanvas(600, 400); target = createVector(width/2, 50); initPopulation(); } function initPopulation() { population = []; for (let i = 0; i < popSize; i++) { population.push(new Agent()); } } function draw() { background(220); // Draw target fill(255, 0, 100); noStroke(); ellipse(target.x, target.y, 16); fill(0); textSize(16); text("Generation: " + generation, 10, 20); if (phase === 'run') { // Run agents for (let agent of population) { agent.update(); agent.show(); } lifeCounter++; if (lifeCounter >= lifespan) { phase = 'fitness'; phaseTimer = 0; evaluateFitness(); breedNewPopulation(); } } else if (phase === 'fitness') { // Show fitness as size/brightness at start position for (let agent of population) { let sz = map(agent.fitness, 0, 1, 4, 20); let b = map(agent.fitness, 0, 1, 50, 255); fill(b, 200); noStroke(); ellipse(width/2, height-20, sz); } drawOverlayText("Fitness Evaluation"); phaseTimer++; if (phaseTimer > fitnessDuration) { phase = 'crossover'; phaseTimer = 0; } } else if (phase === 'crossover') { // Animate colorful crossing lines for (let i = 0; i < 5; i++) { stroke(random(100,255), random(100,255), random(100,255), 150); strokeWeight(random(1,3)); let x1 = random(width), y1 = random(height); let x2 = random(width), y2 = random(height); line(x1, y1, x2, y2); } drawOverlayText("Crossover"); phaseTimer++; if (phaseTimer > crossoverDuration) { phase = 'mutation'; phaseTimer = 0; } } else if (phase === 'mutation') { // Animate red sparkles for (let i = 0; i < 8; i++) { noStroke(); fill(255, random(100,200), random(100,200), 200); let x = random(width), y = random(height); let sz = random(2,8); ellipse(x, y, sz); } drawOverlayText("Mutation"); phaseTimer++; if (phaseTimer > mutationDuration) { // start next generation population = newPopulation; lifeCounter = 0; generation++; phase = 'run'; phaseTimer = 0; } } } function drawOverlayText(txt) { fill(0, 150); textSize(32); textAlign(CENTER, CENTER); text(txt, width/2, height/2); textAlign(LEFT, BASELINE); } // ---- Fitness & Breeding ---- function evaluateFitness() { for (let agent of population) { agent.calcFitness(); } } function selectParent(sumFitness) { let pick = random(sumFitness); let running = 0; for (let a of population) { running += a.fitness; if (running > pick) return a; } return random(population); } function breedNewPopulation() { // compute total fitness let sumFitness = population.reduce((acc, a) => acc + a.fitness, 0); newPopulation = []; for (let i = 0; i < popSize; i++) { let parentA = selectParent(sumFitness); let parentB = selectParent(sumFitness); let childGenes = []; let mid = floor(random(lifespan)); for (let g = 0; g < lifespan; g++) { if (g < mid) childGenes[g] = parentA.genes[g].copy(); else childGenes[g] = parentB.genes[g].copy(); // mutation if (random() < mutationRate) { let angle = random(TWO_PI); childGenes[g] = p5.Vector.fromAngle(angle).setMag(0.3); } } newPopulation.push(new Agent(childGenes)); } } </script> </body> </html> ``` How it works: • Each “creature” is a dot with a sequence of movement vectors (“genes”). • During the `run` phase, they move for a fixed lifespan toward a target. • At the end, we compute fitness = how close they got. • Then in three quick phases, we visualize: – Fitness display (dots sized by fitness) – Crossover (random colorful lines) – Mutation (red sparkles) • Finally we replace the population with children produced by selection, crossover, and mutation, and repeat. Feel free to tweak population size, lifespan, or mutation rate to see different behaviors!
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Genetic Algorithm Animation</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.js"></script> </head> <body> <script> // Number of creatures in the population const populationSize = 100; // Number of genes (movement vectors) per creature const numGenes = 100; // Mutation rate const mutationRate = 0.01; // Population and target position let population; let target; // Generation counter let generation = 0; function setup() { createCanvas(800, 600); // Initialize target position target = createVector(width / 2, 50); // Initialize population population = new Population(populationSize, numGenes); } function draw() { background(220); // Display target fill(255, 0, 0); ellipse(target.x, target.y, 20, 20); // Evolve and display population population.run(target); population.show(); // Display generation number fill(0); textSize(24); text(`Generation: ${generation}`, 20, 30); // Evolve to next generation every 200 frames (approx. 3-4 seconds at 60 FPS) if (frameCount % 200 === 0) { population.evaluate(target); population.selection(); population.crossover(); population.mutation(mutationRate); generation++; } } // Creature class representing an individual in the population class Creature { constructor(numGenes) { this.genes = []; this.position = createVector(random(width), random(height)); this.velocity = createVector(); this.fitness = 0; // Initialize genes (movement vectors) for (let i = 0; i < numGenes; i++) { this.genes.push(p5.Vector.random2D().setMag(random(2))); } } // Apply movement vectors (genes) to update position and velocity update() { this.velocity.add(this.genes[frameCount % this.genes.length]); this.velocity.limit(5); this.position.add(this.velocity); this.position.x = constrain(this.position.x, 0, width); this.position.y = constrain(this.position.y, 0, height); } // Calculate fitness based on distance to target calculateFitness(target) { let d = dist(this.position.x, this.position.y, target.x, target.y); this.fitness = map(d, 0, width, width, 0); } // Display creature as a circle with brightness indicating fitness show() { fill(this.fitness); ellipse(this.position.x, this.position.y, 8, 8); } // Crossover (mating) with another creature to produce offspring crossover(other) { let child = new Creature(this.genes.length); let midpoint = floor(random(this.genes.length)); for (let i = 0; i < this.genes.length; i++) { if (i > midpoint) child.genes[i] = this.genes[i]; else child.genes[i] = other.genes[i]; } return child; } // Mutate genes with a given probability mutate(mutationRate) { for (let i = 0; i < this.genes.length; i++) { if (random(1) < mutationRate) { this.genes[i] = p5.Vector.random2D().setMag(random(2)); } } } } // Population class managing a group of creatures class Population { constructor(size, numGenes) { this.creatures = []; this.matingPool = []; this.size = size; this.numGenes = numGenes; // Initialize creatures for (let i = 0; i < size; i++) { this.creatures.push(new Creature(numGenes)); } } // Run simulation for all creatures run(target) { for (let creature of this.creatures) { creature.update(); creature.show(); } } // Evaluate fitness of all creatures and create a mating pool evaluate(target) { this.matingPool = []; let maxFitness = 0; for (let creature of this.creatures) { creature.calculateFitness(target); if (creature.fitness > maxFitness) { maxFitness = creature.fitness; } } // Normalize fitness and create mating pool for (let creature of this.creatures) { let normalizedFitness = map(creature.fitness, 0, maxFitness, 0, 1); let n = floor(normalizedFitness * 100); for (let i = 0; i < n; i++) { this.matingPool.push(creature); } } } // Select parents from the mating pool and produce offspring selection() { let newCreatures = []; for (let i = 0; i < this.size; i++) { let parentA = random(this.matingPool); let parentB = random(this.matingPool); let child = parentA.crossover(parentB); newCreatures.push(child); } this.creatures = newCreatures; } // Perform crossover (mating) between creatures crossover() { // Crossover is already handled in selection } // Mutate genes of all creatures mutation(mutationRate) { for (let creature of this.creatures) { creature.mutate(mutationRate); } } // Display all creatures show() { for (let creature of this.creatures) { creature.show(); } } } </script> </body> </html> ``` This code creates a simple genetic algorithm animation in p5.js. The population of creatures (represented by small circles) tries to reach a target at the top of the screen. The creatures' movement is determined by their "genes," which are random movement vectors. The fitness of each creature is calculated based on its distance to the target. The creatures with higher fitness have a higher chance of being selected for the next generation. The animation visually represents the evolution of the population over generations, with the creatures getting closer to the target over time. The generation number is displayed at the top left of the screen. The `Creature` class represents an individual creature, and the `Population` class manages the group of creatures. The `draw` function continuously updates and displays the population, and every 200 frames (approximately 3-4 seconds at 60 FPS), it evolves the population to the next generation by evaluating fitness, selecting parents, performing crossover, and mutating genes. You can adjust parameters such as `populationSize`, `numGenes`, and `mutationRate` to explore different aspects of the genetic algorithm.
# Genetic Algorithm Visualization in p5.js Here's a complete HTML implementation of a genetic algorithm visualization using p5.js. This animation shows how populations evolve over generations to reach a target, with visual indicators for fitness, crossover, and mutation. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Genetic Algorithm Visualization</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script> <style> body { margin: 0; padding: 20px; background: #0f1626; display: flex; justify-content: center; align-items: center; min-height: 100vh; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: #f5f9fc; overflow: hidden; } #instructions { position: absolute; bottom: 20px; left: 20px; max-width: 400px; background: rgba(30, 40, 70, 0.8); padding: 15px; border-radius: 8px; font-size: 14px; line-height: 1.5; } h1 { text-align: center; margin-top: 0; color: #8d99ae; } .legend { display: flex; margin-top: 15px; gap: 15px; } .legend-item { display: flex; align-items: center; gap: 5px; } .legend-box { width: 15px; height: 15px; border-radius: 3px; } </style> </head> <body> <div id="instructions"> <h1>Genetic Algorithm Evolution</h1> <p>Watch creatures evolve over generations as they learn to reach the target.</p> <div class="legend"> <div class="legend-item"> <div class="legend-box" style="background-color: #43aa8b;"></div> <span>Parent Selected</span> </div> <div class="legend-item"> <div class="legend-box" style="background-color: #f94144;"></div> <span>Mutation</span> </div> </div> <p>Fitness: Color intensity indicates proximity<br> Size: Better individuals are larger<br> Generation: Higher generations (brighter colors)</p> <p style="margin-top: 10px;"><b>Press any key</b> to pause/resume<br> Drag to move target<br> Scroll to adjust mutation rate</p> </div> <script> let creatures = []; const creatureSize = 8; const maxSteps = 100; const gridSize = 120; let generation = 0; let target; let paused = false; let mutationRate = 0.02; // Base mutation rate let bestCreatures = []; let lifetime = 0; const popSize = 150; // Creature attributes const generationColors = [ [255, 100, 100], // Red [255, 195, 0], // Yellow [0, 200, 150], // Green [0, 150, 255], // Blue [200, 100, 255], // Purple ]; function setup() { const canvas = createCanvas(800, 600); canvas.parent('main'); frameRate(30); // Initialize creatures for (let i = 0; i < popSize; i++) { creatures.push(new Creature()); } // Initialize target at top center target = createVector(width / 2, height * 0.15); } function draw() { background(20); drawGrid(); // Draw generation progress fill(50); noStroke(); rect(20, 20, width - 40, 30, 10); fill(100, 200, 255); rect(20, 20, (width - 40) * (lifetime / maxSteps), 30, 10); // Draw generation info fill(240); textSize(16); textAlign(LEFT, TOP); text(`Generation: ${generation}`, 25, 25); // Draw mutation info text(`Mutation Rate: ${(mutationRate * 100).toFixed(1)}%`, width - 250, 25); // Draw target drawTarget(); if (!paused) { lifetime++; // Update and draw creatures for (let creature of creatures) { creature.update(); creature.display(); } // Genetic algorithm operations at end of each lifetime if (lifetime >= maxSteps) { evolve(); generation++; } } else { // Draw paused text fill(255); textSize(32); textAlign(CENTER, CENTER); text("PAUSED", width / 2, height - 80); // Display creatures even when paused for (let creature of creatures) { creature.display(); } } } function drawGrid() { stroke(40); strokeWeight(1); // Draw vertical grid lines for (let x = 0; x <= width; x += gridSize) { line(x, 0, x, height); } // Draw horizontal grid lines for (let y = 0; y <= height; y += gridSize) { line(0, y, width, y); } } function drawTarget() { // Draw target rings push(); translate(target.x, target.y); // Outer ring fill(255, 70, 70); noStroke(); ellipse(0, 0, 80, 80); // Middle ring fill(220, 20, 20); ellipse(0, 0, 40, 40); // Inner ring fill(200, 0, 0); ellipse(0, 0, 15, 15); pop(); // Draw "TARGET" text fill(255); textSize(16); textAlign(CENTER, TOP); text("TARGET", target.x, target.y + 50); } function evolve() { // Select best creatures bestCreatures = [...creatures]; bestCreatures.sort((a, b) => b.fitness - a.fitness); // Keep top 20% bestCreatures = bestCreatures.slice(0, Math.floor(popSize * 0.2)); // Create new generation const nextGen = []; // Show crossover animation drawCrossoverAnimation(); // Create next generation children while (nextGen.length < popSize) { // Pick two random parents from best creatures const parentA = random(bestCreatures); const parentB = random(bestCreatures); // Create child with crossover const child = parentA.crossover(parentB); // Mutate the child randomly child.mutate(); nextGen.push(child); } creatures = nextGen; lifetime = 0; } function drawCrossoverAnimation() { push(); blendMode(SCREEN); // Draw crossover effects for (let i = 0; i < bestCreatures.length; i++) { const creature = bestCreatures[i]; const size = map(i, 0, bestCreatures.length, 30, 100); const points = Math.floor(map(i, 0, bestCreatures.length, 4, 8)); fill(255, 150, 50, 50); noStroke(); beginShape(); for (let j = 0; j < points; j++) { const angle = j * TWO_PI / points; const r = size; const px = creature.pos.x + r * cos(angle); const py = creature.pos.y + r * sin(angle); curveVertex(px, py); } endShape(CLOSE); // Draw selection visual effect stroke(50, 200, 100); strokeWeight(2); noFill(); ellipse(creature.pos.x, creature.pos.y, size * 0.8, size * 0.8); } pop(); // Delay next frame after transition delay(200); } function mousePressed() { target.x = mouseX; target.y = mouseY; } function mouseDragged() { target.x = mouseX; target.y = mouseY; } function mouseWheel(event) { mutationRate = constrain(mutationRate + event.delta * 0.0001, 0.005, 0.1); return false; } function keyPressed() { paused = !paused; } function delay(ms) { const start = millis(); while (millis() - start <= ms) {} } // Creature class class Creature { constructor(parentA, parentB) { // Inherit genes from parents or create random ones if (parentA && parentB) { this.genes = []; for (let i = 0; i < maxSteps; i++) { // Combine genes randomly from both parents this.genes.push(random() < 0.5 ? parentA.genes[i].copy() : parentB.genes[i].copy()); } } else { this.genes = []; for (let i = 0; i < maxSteps; i++) { this.genes.push(createVector(random(-3, 3), random(-3, 3))); } } this.pos = createVector(width / 2 + random(-100, 100), height * 0.9); this.willMutate = random() < mutationRate; this.fitness = 0; this.alive = true; this.age = 0; // Stats for visuals this.sizeMultiplier = 1; this.deltaSize = random(0.1, 0.2) * (this.willMutate ? 1.5 : 1); this.colorGenerationLevel = 0; } update() { if (lifetime < maxSteps && this.alive) { this.pos.add(this.genes[lifetime]); this.age++; // Check if reached target const dis = dist(this.pos.x, this.pos.y, target.x, target.y); if (dis < 50) { this.alive = false; this.pos.x = target.x; this.pos.y = target.y; this.sizeMultiplier = 3; } // Calculate fitness // Higher fitness = closer to target this.fitness = map(dis, 0, width / 1.5, 1, 0); // Visual scaling based on fitness this.sizeMultiplier = lerp(this.sizeMultiplier, 1 + this.fitness * 3, 0.1); // Gradually increase color generation level this.colorGenerationLevel = lerp(this.colorGenerationLevel, generation, 0.02); } } display() { const genColorIndex = Math.floor(this.colorGenerationLevel) % generationColors.length; const nextIndex = (genColorIndex + 1) % generationColors.length; const blendAmt = this.colorGenerationLevel - Math.floor(this.colorGenerationLevel); const color1 = generationColors[genColorIndex]; const color2 = generationColors[nextIndex]; // Blend colors based on generation progress const blendedColor = [ lerp(color1[0], color2[0], blendAmt), lerp(color1[1], color2[1], blendAmt), lerp(color1[2], color2[2], blendAmt) ]; // Adjust color based on fitness const baseBrightness = map(this.fitness, 0, 1, 100, 255); // Size multiplier includes fitness and pulsing effect const sizeFactor = this.sizeMultiplier * (sin(frameCount * 0.1) * 0.1 + 1); // Draw creature body push(); noStroke(); // Main colored circle fill( constrain(blendedColor[0] * (baseBrightness / 255), 0, 255), constrain(blendedColor[1] * (baseBrightness / 255), 0, 255), constrain(blendedColor[2] * (baseBrightness / 255), 0, 255), 220 ); ellipse( this.pos.x, this.pos.y, creatureSize * sizeFactor, creatureSize * sizeFactor ); // Draw legs (lines pointing in movement direction) strokeWeight(1); stroke(0, 100); const step = maxSteps - lifetime; for (let i = 0; i < 4; i++) { const angle = this.genes[step].heading() + i * HALF_PI; const len = creatureSize * 0.3 * sizeFactor; line( this.pos.x, this.pos.y, this.pos.x + cos(angle) * len, this.pos.y + sin(angle) * len ); } // Highlight creatures that will mutate next generation if (this.willMutate) { drawMutationEffect( this.pos.x, this.pos.y, creatureSize * sizeFactor * 0.75 ); } pop(); } crossover(parent) { return new Creature(this, parent); } mutate() { for (let i = 0; i < this.genes.length; i++) { if (random() < mutationRate) { // Small random adjustment const adjustment = p5.Vector.random2D().mult(random(0.2, 1)); this.genes[i].add(adjustment); // Set mutation flag for visual effect this.willMutate = true; } } } } function drawMutationEffect(x, y, size) { // Draw mutation rings noFill(); strokeWeight(1.2); stroke(255, 20, 50, 150); ellipse(x, y, size * 1.8, size * 1.8); ellipse(x, y, size * 2.4, size * 2.4); // Draw plus icon const len = size * 0.4; stroke(255, 20, 50, 220); strokeWeight(1.8); push(); translate(x, y); // Draw randomized mutation crosses for (let i = 0; i < 4; i++) { const angle = random(TWO_PI); const r = size * random(0.8, 2.0); rotate(angle); line(-len, 0, len, 0); line(0, -len, 0, len); rotate(-angle); } pop(); // Draw particle bursts for (let i = 0; i < 5; i++) { const angle = random(TWO_PI); const dist = size * random(0.7, 1.5); const px = x + cos(angle) * dist; const py = y + sin(angle) * dist; strokeWeight(1.5); stroke(255, 180, 50, 200); point(px, py); } } </script> </body> </html> ``` ## Features of this Visualization 1. **Intuitive Creatures**: Simple creatures represented by colored circles that gradually evolve towards the target 2. **Genetic Operations Visualized**: - Mutation indicated by glowing effects and special patterns - Crossover shown with visual animations during transitions between generations - Selected parents highlighted with circles 3. **Fitness Representation**: - Size indicates fitness (larger creatures = more fit) - Color brightness shows proximity to target - Color hue changes with generations (showing evolution progression) 4. **Interactive Controls**: - Drag to move the target - Scroll to adjust mutation rate - Press any key to pause/resume - Watch creatures evolve in real-time 5. **Generation Information**: - Progress bar shows lifetime progression - Generation counter tracks evolution progress - Mutation rate display This visualization demonstrates how genetic algorithms use selection, mutation and crossover to evolve solutions over generations. The creatures start with random movement patterns (genes) and gradually learn to navigate toward the target through natural selection, showcasing the core mechanics of evolutionary algorithms.
```html <!DOCTYPE html> <html> <head> <title>Genetic Algorithm Visualization</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script> </head> <body> <script> let population = []; let target; let generation = 1; let populationSize = 30; let lifespan = 60; let lifeCounter = 0; let maxForce = 0.4; let mutationRate = 0.01; let showingEvolution = false; let evolutionTimer = 0; let matingPool = []; function setup() { createCanvas(800, 600); // Create target target = createVector(width - 50, height / 2); // Initialize population for (let i = 0; i < populationSize; i++) { population[i] = new Rocket(); } } function draw() { background(20, 20, 40); // Draw target push(); fill(255, 100, 100); stroke(255, 150, 150); strokeWeight(3); ellipse(target.x, target.y, 30, 30); fill(255, 200, 200); noStroke(); ellipse(target.x, target.y, 15, 15); pop(); // Draw generation info fill(255); textSize(18); text(`Generation: ${generation}`, 20, 30); text(`Life: ${lifeCounter}/${lifespan}`, 20, 55); text(`Mutation Rate: ${(mutationRate * 100).toFixed(1)}%`, 20, 80); if (!showingEvolution) { // Update and display rockets for (let rocket of population) { rocket.update(); rocket.show(); } lifeCounter++; // End of generation if (lifeCounter >= lifespan) { showingEvolution = true; evolutionTimer = 0; lifeCounter = 0; } } else { // Show evolution process displayEvolution(); } } class Rocket { constructor(dna) { this.pos = createVector(50, height / 2); this.vel = createVector(); this.acc = createVector(); this.completed = false; this.crashed = false; if (dna) { this.dna = dna.copy(); } else { this.dna = new DNA(); } this.fitness = 0; this.size = 8; this.color = color(100, 150, 255); } calcFitness() { let d = dist(this.pos.x, this.pos.y, target.x, target.y); this.fitness = map(d, 0, width, width, 0); if (this.completed) { this.fitness *= 10; } if (this.crashed) { this.fitness /= 10; } // Visual feedback based on fitness let brightness = map(this.fitness, 0, width * 10, 50, 255); this.size = map(this.fitness, 0, width * 10, 6, 12); this.color = color(brightness * 0.4, brightness * 0.6, brightness); } update() { let d = dist(this.pos.x, this.pos.y, target.x, target.y); if (d < 10) { this.completed = true; this.pos = target.copy(); } if (this.pos.x > width || this.pos.x < 0 || this.pos.y > height || this.pos.y < 0) { this.crashed = true; } this.applyForce(this.dna.genes[lifeCounter]); if (!this.completed && !this.crashed) { this.vel.add(this.acc); this.pos.add(this.vel); this.acc.mult(0); this.vel.limit(4); } this.calcFitness(); } applyForce(force) { this.acc.add(force); } show() { push(); translate(this.pos.x, this.pos.y); rotate(this.vel.heading() + PI/2); fill(this.color); stroke(255, 100); strokeWeight(1); // Draw rocket body triangle(-this.size/2, this.size, this.size/2, this.size, 0, -this.size); // Draw engines if (!this.completed && !this.crashed) { fill(255, 100, 100, 150); noStroke(); triangle(-this.size/4, this.size, this.size/4, this.size, 0, this.size + 6); } pop(); // Draw trail if (frameCount % 3 === 0 && !this.completed && !this.crashed) { fill(red(this.color), green(this.color), blue(this.color), 50); noStroke(); ellipse(this.pos.x, this.pos.y, 3, 3); } } } class DNA { constructor(genes) { if (genes) { this.genes = genes; } else { this.genes = []; for (let i = 0; i < lifespan; i++) { this.genes[i] = p5.Vector.random2D(); this.genes[i].mult(maxForce); } } } crossover(partner) { let newGenes = []; let mid = floor(random(this.genes.length)); for (let i = 0; i < this.genes.length; i++) { if (i > mid) { newGenes[i] = this.genes[i]; } else { newGenes[i] = partner.genes[i]; } } return new DNA(newGenes); } mutation() { for (let i = 0; i < this.genes.length; i++) { if (random(1) < mutationRate) { this.genes[i] = p5.Vector.random2D(); this.genes[i].mult(maxForce); } } } copy() { let newGenes = []; for (let i = 0; i < this.genes.length; i++) { newGenes[i] = this.genes[i].copy(); } return new DNA(newGenes); } } function displayEvolution() { evolutionTimer++; fill(255, 150); textAlign(CENTER); textSize(24); text("EVOLUTION IN PROGRESS", width/2, height/2 - 100); if (evolutionTimer < 60) { // Show fitness calculation textSize(16); text("Calculating Fitness...", width/2, height/2 - 60); // Display some rockets with fitness values for (let i = 0; i < min(5, population.length); i++) { let rocket = population[i]; let x = width/2 - 200 + i * 100; let y = height/2; push(); translate(x, y); fill(rocket.color); triangle(-4, 8, 4, 8, 0, -8); fill(255); textAlign(CENTER); text(rocket.fitness.toFixed(0), 0, 25); pop(); } } else if (evolutionTimer < 120) { // Show selection process textSize(16); text("Natural Selection...", width/2, height/2 - 60); if (evolutionTimer === 60) { selection(); } // Visual representation of mating pool let poolSize = min(matingPool.length, 10); for (let i = 0; i < poolSize; i++) { let x = width/2 - 150 + i * 30; let y = height/2 - 20; fill(100, 255, 100, 150); stroke(255); ellipse(x, y, 20, 20); } } else if (evolutionTimer < 180) { // Show crossover textSize(16); text("Crossover & Reproduction...", width/2, height/2 - 60); // Visual crossover animation let progress = map(evolutionTimer - 120, 0, 60, 0, 1); for (let i = 0; i < 3; i++) { let x1 = width/2 - 100 + i * 50; let x2 = width/2 - 75 + i * 50; let y = height/2 - 20; // Parent rockets fill(100, 150, 255); ellipse(x1, y, 15, 15); ellipse(x2, y, 15, 15); // Crossover lines stroke(255, 255, 100); strokeWeight(2); if (progress > 0.5) { line(x1, y, x2, y); } // Child rocket if (progress > 0.8) { fill(255, 255, 100); noStroke(); ellipse((x1 + x2) / 2, y + 30, 12, 12); } } } else if (evolutionTimer < 240) { // Show mutation textSize(16); text("Mutation...", width/2, height/2 - 60); if (evolutionTimer === 180) { reproduction(); } // Mutation sparks for (let i = 0; i < 5; i++) { if (random() < 0.3) { let x = random(width/2 - 100, width/2 + 100); let y = random(height/2 - 50, height/2 + 50); fill(255, 255, 100, 200); noStroke(); ellipse(x, y, random(3, 8), random(3, 8)); } } } else { // Evolution complete textSize(16); text("New Generation Ready!", width/2, height/2 - 60); if (evolutionTimer > 280) { showingEvolution = false; evolutionTimer = 0; generation++; // Reset positions for (let rocket of population) { rocket.pos = createVector(50, height / 2); rocket.vel = createVector(); rocket.acc = createVector(); rocket.completed = false; rocket.crashed = false; } } } textAlign(LEFT); } function selection() { matingPool = []; // Find maximum fitness let maxfit = 0; for (let rocket of population) { if (rocket.fitness > maxfit) { maxfit = rocket.fitness; } } // Add to mating pool based on fitness for (let rocket of population) { let fitness = map(rocket.fitness, 0, maxfit, 0, 1); let n = floor(fitness * 100); for (let j = 0; j < n; j++) { matingPool.push(rocket); } } } function reproduction() { for (let i = 0; i < population.length; i++) { let a = random(matingPool).dna; let b = random(matingPool).dna; let child = a.crossover(b); child.mutation(); population[i] = new Rocket(child); } } // Interactive controls function keyPressed() { if (key === 'r' || key === 'R') { // Reset generation = 1; lifeCounter = 0; showingEvolution = false; for (let i = 0; i < populationSize; i++) { population[i] = new Rocket(); } } if (key === 'm' || key === 'M') { // Adjust mutation rate mutationRate = mutationRate === 0.01 ? 0.05 : (mutationRate === 0.05 ? 0.1 : 0.01); } } function mousePressed() { // Move target to mouse position target.x = mouseX; target.y = mouseY; } </script> </body> </html> ``` This genetic algorithm visualization demonstrates: 1. **Population**: 30 rocket-shaped creatures trying to reach a red target 2. **Fitness**: Calculated by distance to target, with visual feedback through color brightness and size 3. **Evolution Process**: - Fitness calculation phase - Natural selection (creating mating pool) - Crossover visualization - Mutation effects 4. **Visual Features**: - Rockets change color/size based on fitness - Particle trails show movement - Step-by-step evolution animation - Real-time generation counter **Interactive Controls**: - Click to move the target - Press 'R' to reset - Press 'M' to cycle through mutation rates (1%, 5%, 10%) The animation clearly shows how successful traits (good navigation toward the target) become more common in subsequent generations, while poor traits are gradually eliminated through natural selection.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Genetic Algorithm Visualization</title> <script src="https://cdn.jsdelivr.net/npm/p5@1.4.0/lib/p5.js"></script> <style> body { margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f0f0; } canvas { display: block; } </style> </head> <body> <script> let population = []; let generation = 0; let target; let popSize = 50; // Number of creatures let geneLength = 100; // Length of gene sequence (e.g., steps to take) let generationTimer = 0; // Timer for new generations let generationInterval = 5000; // 5 seconds between generations let mutationRate = 0.1; // 10% chance of mutation function setup() { createCanvas(800, 600); target = createVector(width - 50, height / 2); // Target position // Initialize population with random genes for (let i = 0; i < popSize; i++) { population.push(new Creature()); } } function draw() { background(255); // Draw target fill(0, 255, 0); ellipse(target.x, target.y, 20, 20); textSize(16); fill(0); text(`Generation: ${generation}`, 10, 20); // Update and display creatures for (let creature of population) { creature.update(); creature.show(); creature.calculateFitness(); // Recalculate fitness each frame for visualization } // Handle generation progression generationTimer += deltaTime; if (generationTimer >= generationInterval) { evolvePopulation(); generationTimer = 0; } } class Creature { constructor() { this.position = createVector(50, height / 2); // Start position this.genes = []; // Array of vectors for movement this.fitness = 0; this.index = 0; // Current gene index // Generate random genes for (let i = 0; i < geneLength; i++) { this.genes[i] = p5.Vector.random2D(); // Random direction and speed this.genes[i].mult(random(1, 3)); // Scale speed } } update() { if (this.index < geneLength) { this.position.add(this.genes[this.index]); // Move based on current gene this.index++; } } show() { // Visualize creature as a circle, with size and color based on fitness let fitnessMapped = map(this.fitness, 0, 1, 10, 50); // Map fitness to size let brightness = map(this.fitness, 0, 1, 50, 255); // Map fitness to brightness fill(255, brightness, 0); // Yellow to brighter yellow noStroke(); ellipse(this.position.x, this.position.y, fitnessMapped, fitnessMapped); } calculateFitness() { // Fitness based on distance to target let distanceToTarget = p5.Vector.dist(this.position, target); this.fitness = map(distanceToTarget, 0, width, 1, 0); // 1 is best, 0 is worst this.fitness = constrain(this.fitness, 0, 1); // Clamp between 0 and 1 } } function evolvePopulation() { let newPopulation = []; // Select the best creatures and perform crossover/mutation for (let i = 0; i < popSize; i++) { // Selection: Pick two parents based on fitness (roulette wheel style) let parentA = selectParent(); let parentB = selectParent(); // Crossover: Visualize by logging or simple text (for this demo) textSize(24); fill(0, 0, 255); // Blue for crossover text('Crossover happening...', 300, 50); let child = crossover(parentA, parentB); // Mutation: Randomly alter genes mutate(child); textSize(24); fill(255, 0, 0); // Red for mutation text('Mutation applied...', 300, 80); newPopulation.push(child); } population = newPopulation; // Replace old population generation++; // Increment generation } function selectParent() { // Simple roulette wheel selection based on fitness let index = 0; let maxFitness = 0; for (let i = 0; i < population.length; i++) { if (population[i].fitness > maxFitness) { maxFitness = population[i].fitness; index = i; } } return population[index]; // For simplicity, always pick the best (could be probabilistic) } function crossover(parentA, parentB) { let child = new Creature(); let midpoint = floor(random(geneLength)); // Random crossover point for (let i = 0; i < geneLength; i++) { if (i > midpoint) child.genes[i] = parentA.genes[i]; else child.genes[i] = parentB.genes[i]; } return child; } function mutate(child) { for (let i = 0; i < child.genes.length; i++) { if (random(1) < mutationRate) { child.genes[i] = p5.Vector.random2D(); // Mutate to a new random vector child.genes[i].mult(random(1, 3)); } } } </script> </body> </html> ```