This simulation shows a dynamic animation of spirograph flowers blooming.
This project's JavaScript source code is free to use, and can be copied easily using the button below. We would love to see your work.
<script> const canvas = document.getElementById('flowerCanvas'); const ctx = canvas.getContext('2d'); // DNA structure to control the plant's traits const plantDNA = { stemHeight: 60, // Height of the stem stemThickness: 5, // Thickness of the stem stemColor: [120, 50, 50], // HSL color for the stem maxSplits: 2, // Maximum number of times a stem can split splitChance: 0.35, // Chance that a stem will split into 2 or 3 stems numSpirographs: 3, // Number of spirographs per flower (up to 5) baseSpiroSize: 30, // Base size for the largest spirograph petalBaseHue: Math.random() * 360, // Base hue of the petals petalHueVariation: 0, // Variation in petal hue bloomSpeed: 1, // Speed of the bloom lifespan: 3000 // Lifespan in milliseconds }; let livingPlants = []; let maxPlants = 5; // Create a stem object with spirograph data for each flower layer function createStem(parentX, parentY, parentAngle, depth, dna) { return { x: parentX, y: parentY, angle: parentAngle, length: 0, maxLength: dna.stemHeight, thickness: dna.stemThickness, splits: depth < dna.maxSplits && Math.random() < dna.splitChance ? 2 + Math.floor(Math.random() * 2) : 0, isGrowing: true, hasFlower: false, depth: depth, bloomStage: 0, splitChildren: [], // Store random k, l, and color for each spirograph layer spiroData: Array.from({ length: dna.numSpirographs }, () => ({ k: Math.random(), // Random k value for the layer l: Math.random(), // Random l value for the layer color: `hsl(${dna.petalBaseHue + Math.random() * 360}, 100%, 50%)`, // Fixed color for the layer })) }; } // Create a new plant with an initial stem function createPlant(dna) { const initialStem = createStem(Math.random() * canvas.width, canvas.height, -Math.PI / 2, 0, dna); return { dna: dna, stems: [initialStem], age: 0, isDead: false, }; } function getStemColor(dna) { return `hsl(${dna.stemColor[0]}, ${dna.stemColor[1]}%, ${dna.stemColor[2]}%)`; } // Helper function to find GCD (Greatest Common Divisor) function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); } // Helper function to find LCM (Least Common Multiple) function lcm(a, b) { return (a * b) / gcd(a, b); } // Draw a spirograph layer with k, l values ensuring it completes its full path function drawSpirographLayer(x, y, size, k, l, color) { const R = Math.round(size); // Outer radius, rounded to integer const r = Math.round(k * R); // Inner radius, rounded to integer const d = l * R; // Distance of the drawing point from the center of the outer circle // Calculate the LCM of R and r to determine how many full loops are needed const loopsNeeded = lcm(R, r) / r; ctx.beginPath(); ctx.moveTo(x + (R - r) + d, y); // Start at the initial point // Loop over theta to ensure full tracing of the spirograph const totalSteps = Math.PI * 2 * loopsNeeded; // Full number of rotations required for (let theta = 0; theta <= totalSteps; theta += 0.05) { // Use larger increments const newX = x + (R - r) * Math.cos(theta) + d * Math.cos(((R - r) / r) * theta); const newY = y + (R - r) * Math.sin(theta) - d * Math.sin(((R - r) / r) * theta); ctx.lineTo(newX, newY); } ctx.closePath(); ctx.strokeStyle = color; ctx.lineWidth = 1; ctx.stroke(); } // Draw a flower with its bloom stage and spirograph layers function drawFlower(bloomStage, dna, x, y, spiroData) { ctx.save(); // Save current context ctx.translate(x, y); // Translate context to flower's position const bloomScale = Math.min(bloomStage / 100, 1); // Scale the flower growth spiroData.forEach((layerData, layer) => { const layerSize = dna.baseSpiroSize * bloomScale * (1 - layer * 0.2); // Progressive size reduction const { k, l, color } = layerData; // Use stored k, l, and color values // Draw the spirograph layer for this bloom stage drawSpirographLayer(0, 0, layerSize, k, l, color); // Draw at translated origin }); ctx.restore(); // Restore context to prevent affecting other drawings } // Draw the stem function drawStem(stem, dna) { const endX = stem.x + stem.length * Math.cos(stem.angle); const endY = stem.y + stem.length * Math.sin(stem.angle); ctx.beginPath(); ctx.moveTo(stem.x, stem.y); ctx.lineTo(endX, endY); ctx.lineWidth = stem.thickness; ctx.strokeStyle = getStemColor(dna); ctx.stroke(); return { endX, endY }; } // Animation loop function animate() { ctx.fillStyle = 'rgb(0, 191, 255)'; // Equivalent to 'deepskyblue' ctx.fillRect(0, 0, canvas.width, canvas.height); livingPlants = livingPlants.filter(plant => !plant.isDead); livingPlants.forEach((plant) => { plant.age += 16.66; plant.stems.forEach(stem => { if (stem.isGrowing) { stem.length += 2; if (stem.length >= stem.maxLength) { stem.isGrowing = false; stem.hasFlower = true; } } const { endX, endY } = drawStem(stem, plant.dna); if (!stem.isGrowing && stem.splits > 0 && stem.splitChildren.length === 0) { for (let i = 0; i < stem.splits; i++) { const splitAngle = stem.angle + ((Math.random() - 0.5) * Math.PI / 3); const newStem = createStem(endX, endY, splitAngle, stem.depth + 1, plant.dna); stem.splitChildren.push(newStem); plant.stems.push(newStem); } } if (stem.hasFlower && stem.splitChildren.length === 0) { if (stem.bloomStage < 100) { stem.bloomStage += plant.dna.bloomSpeed; } drawFlower(stem.bloomStage, plant.dna, endX, endY, stem.spiroData); } }); if (plant.age > plant.dna.lifespan) { plant.isDead = true; dropSeed(plant); } }); if (livingPlants.length < maxPlants) { livingPlants.push(createPlant(generateRandomDNA())); } requestAnimationFrame(animate); } // Drop a seed to create a new plant function dropSeed(plant) { const parent1 = plant; const parent2 = getRandomPlant(); const seedDNA = generateSeedDNA(parent1.dna, parent2.dna); livingPlants.push(createPlant(seedDNA)); } // Generate the DNA for a new plant based on two parents function generateSeedDNA(parent1, parent2) { const seedDNA = {}; for (let trait in parent1) { if (Array.isArray(parent1[trait])) { seedDNA[trait] = parent1[trait].map((val, index) => { return (val + parent2[trait][index]) / 2; }); } else { seedDNA[trait] = (parent1[trait] + parent2[trait]) / 2; } } seedDNA.petalBaseHue += (Math.random() - 0.5) * 10; seedDNA.stemHeight += (Math.random() - 0.5) * 20; seedDNA.stemThickness += (Math.random() - 0.5) * 2; seedDNA.numSpirographs = Math.min(5, Math.max(1, seedDNA.numSpirographs + (Math.random() > 0.95 ? 1 : 0))); seedDNA.baseSpiroSize += (Math.random() - 0.5) * 5; return seedDNA; } // Generate random DNA for a plant function generateRandomDNA() { return { stemHeight: 60 + Math.random() * 100, stemThickness: 5, stemColor: [120, 50 + Math.random() * 20, 40 + Math.random() * 20], maxSplits: 2, splitChance: 0.35, numSpirographs: Math.floor(1 + Math.random() * 5), baseSpiroSize: 20 + Math.random() * 40, petalBaseHue: Math.random() * 360, petalHueVariation: 0, bloomSpeed: 1, lifespan: 3500 + Math.random() * 10000 }; } function getRandomPlant() { const randomIndex = Math.floor(Math.random() * livingPlants.length); return livingPlants[randomIndex]; } // Start the simulation with initial plants livingPlants.push(createPlant(plantDNA)); livingPlants.push(createPlant(plantDNA)); animate(); </script>