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>