
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>
```