Floral Spirographs

Floral Spirographs



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>
Back to blog

Leave a comment

Please note, comments need to be approved before they are published.