← Seasonal

Blooming Flowers

Stems grow up out of the grass and leaves unfurl. Then the flowers open one after another, each petal twisting out of the bud, and they sway gently in the breeze. Loop it and they close up and bloom again.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-blooming-flowers> where the flowers should grow. They come up when it scrolls into view.

Options

Add any of these attributes to the same element.

data-loopClose up after a while and bloom again.
data-countHow many flowers, 1 to 9. Default 5.
data-colorsComma-separated petal colours. Default pink, yellow, violet, rose and blue.
data-staggerGap between one flower opening and the next. Default 380 ms.

It is 8:5 with a transparent background by default; set your own height or background on the element. With reduced motion the flowers are shown in full bloom.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Blooming Flowers: stems grow up out of the grass, leaves unfurl, and the
    // flowers open their petals one after another, each petal twisting out
    // from the bud. Then they sway gently in the breeze. Loop it and they close
    // up and bloom again. Drawn as SVG that fills the element.
    // Usage: <div data-blooming-flowers></div>
    // It is 8:5 by default with a transparent background.
    // Optional: data-loop (close up and bloom again), data-count (flowers,
    // 1 to 9, default 5), data-colors (comma-separated petal colours),
    // data-stagger (ms between one flower and the next, default 380).
    var DEFAULTS = { count: 5, stagger: 380, colors: ['#f472b6', '#facc15', '#a78bfa', '#fb7185', '#60a5fa'] };
    var uid = 0;
    var GROUND = 226;
    var PETAL = 'M0 0 C-11 -8 -12 -28 0 -36 C12 -28 11 -8 0 0 Z';

    var css =
      '[data-blooming-flowers]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
      '.blf-svg{position:absolute;inset:0;width:100%;height:100%;display:block}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function build(el) {
      var d = el.dataset;
      var n = ++uid;
      var g = 'blf' + n;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var count = Math.max(1, Math.min(9, d.count !== undefined ? Math.round(+d.count) : DEFAULTS.count));
      var colors = d.colors ? d.colors.split(',').map(function (c) { return c.trim(); }) : DEFAULTS.colors;
      var stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
      if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'Flowers grow and open their petals one after another');
      if (!el.hasAttribute('role')) el.setAttribute('role', 'img');

      var HEIGHTS = [104, 66, 92, 58, 98, 74, 86, 62, 100];
      var PETALS = [8, 6, 10, 5, 8, 6, 9, 5, 7];
      var defs = '';
      colors.forEach(function (c, i) {
        defs += '<linearGradient id="' + g + '-c' + i + '" x1="0" y1="1" x2="0" y2="0"><stop offset="0" stop-color="' + c + '" stop-opacity=".85"/><stop offset=".6" stop-color="' + c + '"/><stop offset="1" stop-color="#fff" stop-opacity=".9"/></linearGradient>';
      });
      var flowers = '', span = Math.min(330, 72 * count);
      var size = Math.min(1, 5.4 / Math.max(count, 4));
      for (var i = 0; i < count; i++) {
        var x0 = 200 + (count > 1 ? -span / 2 + span * i / (count - 1) : 0);
        var hy = HEIGHTS[i % HEIGHTS.length] + (count > 6 ? 18 : 0);
        var bend = (i % 2 ? 1 : -1) * 14;
        var hx = x0 + bend * 0.6;
        var stem = 'M' + x0.toFixed(1) + ' ' + GROUND + ' Q' + (x0 + bend).toFixed(1) + ' ' + ((GROUND + hy) / 2).toFixed(1) + ' ' + hx.toFixed(1) + ' ' + hy;
        var ly = (GROUND + hy) / 2 + 18;
        var lx = x0 + bend * 0.55;
        var np = PETALS[i % PETALS.length];
        var petals = '';
        for (var k = 0; k < np; k++) petals += '<path class="blf-petal" data-a="' + (k * 360 / np).toFixed(1) + '" d="' + PETAL + '" fill="url(#' + g + '-c' + (i % colors.length) + ')" stroke="#000" stroke-opacity=".06"/>';
        flowers += '<g class="blf-flower" data-x="' + x0.toFixed(1) + '">' +
          '<path class="blf-stem" d="' + stem + '" fill="none" stroke="#15803d" stroke-width="3.4" stroke-linecap="round"/>' +
          '<g transform="translate(' + lx.toFixed(1) + ' ' + ly.toFixed(1) + ')"><path class="blf-leaf" d="M0 0 C' + (i % 2 ? '-' : '') + '10 -4 ' + (i % 2 ? '-' : '') + '22 -16 ' + (i % 2 ? '-' : '') + '30 -18 C' + (i % 2 ? '-' : '') + '24 -6 ' + (i % 2 ? '-' : '') + '12 2 0 0 Z" fill="#22c55e"/></g>' +
          '<g class="blf-head" data-hx="' + hx.toFixed(1) + '" data-hy="' + hy + '" data-s="' + size.toFixed(3) + '">' +
            '<g class="blf-bud"><path d="M0 4 C-10 0 -9 -14 0 -20 C9 -14 10 0 0 4 Z" fill="#4ade80"/><path d="M0 4 C-7 0 -12 -6 -12 -12 C-6 -8 -2 -4 0 4 Z M0 4 C7 0 12 -6 12 -12 C6 -8 2 -4 0 4 Z" fill="#16a34a"/></g>' +
            petals +
            '<g class="blf-centre"><circle r="9" fill="#f59e0b"/><circle r="9" fill="url(#' + g + '-disc)"/>' +
            '<circle cx="-3" cy="-2" r="1.2" fill="#78350f"/><circle cx="3" cy="-3" r="1.2" fill="#78350f"/><circle cx="2" cy="3" r="1.2" fill="#78350f"/><circle cx="-3" cy="3" r="1.2" fill="#78350f"/><circle cx="0" cy="0" r="1.2" fill="#78350f"/></g>' +
          '</g>' +
          '</g>';
      }
      var grass = '';
      for (var b = -60; b <= 100; b++) {
        var gx = b * 10 + ((b + 60) % 3) * 2;
        grass += 'M' + gx + ' ' + (GROUND + 6) + ' Q' + (gx + 2) + ' ' + (GROUND - 6) + ' ' + (gx + ((b + 60) % 2 ? 6 : -3)) + ' ' + (GROUND - 12 - ((b + 60) % 4) * 3) + ' ';
      }

      el.innerHTML =
        '<svg class="blf-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
        '<defs>' + defs +
        '<radialGradient id="' + g + '-disc" cx=".35" cy=".35" r=".8"><stop offset="0" stop-color="#fde68a"/><stop offset="1" stop-color="#d97706" stop-opacity="0"/></radialGradient>' +
        '</defs>' +
        flowers +
        '<path d="' + grass + '" fill="none" stroke="#16a34a" stroke-width="2.2" stroke-linecap="round"/>' +
        '<path d="M-600 ' + (GROUND + 2) + ' L-20 ' + (GROUND + 4) + ' Q100 ' + (GROUND - 4) + ' 200 ' + (GROUND + 2) + ' T420 ' + (GROUND + 2) + ' L1000 ' + (GROUND + 2) + ' L1000 600 L-600 600 Z" fill="#22c55e"/>' +
        '<path d="M-600 ' + (GROUND + 10) + ' L-20 ' + (GROUND + 12) + ' Q120 ' + (GROUND + 6) + ' 240 ' + (GROUND + 12) + ' T420 ' + (GROUND + 10) + ' L1000 ' + (GROUND + 10) + ' L1000 600 L-600 600 Z" fill="#16a34a"/>' +
        '</svg>';

      var svg = el.firstChild;
      var F = [];
      svg.querySelectorAll('.blf-flower').forEach(function (fg, i) {
        var head = fg.querySelector('.blf-head');
        var stemEl = fg.querySelector('.blf-stem');
        var len = 200;
        try { len = stemEl.getTotalLength() + 2; } catch (e) {}
        stemEl.setAttribute('stroke-dasharray', len + ' ' + len);
        var petals = [];
        fg.querySelectorAll('.blf-petal').forEach(function (p) { petals.push({ el: p, a: +p.getAttribute('data-a'), o: 0 }); });
        F.push({
          g: fg, x: +fg.getAttribute('data-x'), head: head, stemEl: stemEl, len: len,
          hx: +head.getAttribute('data-hx'), hy: +head.getAttribute('data-hy'), s: +head.getAttribute('data-s'),
          leaf: fg.querySelector('.blf-leaf'), bud: fg.querySelector('.blf-bud'), centre: fg.querySelector('.blf-centre'),
          petals: petals, st: { stem: 0, leaf: 0, bud: 0, centre: 0 }, ph: i * 1.3
        });
      });

      var t = 0;
      function render(dt) {
        t += dt || 0;
        var s = t / 1000;
        F.forEach(function (f) {
          var st = f.st;
          var sway = Math.sin(s * 1.1 + f.ph) * 2.2 * st.stem + Math.sin(s * 2.3 + f.ph) * 0.6;
          f.g.setAttribute('transform', 'rotate(' + sway.toFixed(2) + ' ' + f.x + ' ' + GROUND + ')');
          f.stemEl.setAttribute('stroke-dashoffset', (f.len * (1 - st.stem)).toFixed(1));
          f.leaf.setAttribute('transform', 'scale(' + st.leaf.toFixed(3) + ')');
          f.head.setAttribute('transform', 'translate(' + f.hx + ' ' + f.hy + ') rotate(' + (sway * 1.5).toFixed(2) + ') scale(' + f.s + ')');
          f.bud.setAttribute('transform', 'scale(' + Math.max(0, st.bud).toFixed(3) + ')');
          f.centre.setAttribute('transform', 'scale(' + Math.max(0, st.centre).toFixed(3) + ')');
          f.petals.forEach(function (p) {
            var o = p.o;
            p.el.setAttribute('transform', 'rotate(' + (p.a + 50 * (1 - o)).toFixed(1) + ') scale(' + Math.max(0, 0.05 + 0.95 * o).toFixed(3) + ' ' + Math.max(0, 0.05 + 0.95 * o).toFixed(3) + ')');
            p.el.setAttribute('opacity', Math.max(0, Math.min(1, o * 3)).toFixed(2));
          });
        });
      }

      function allPetals(f) { return f.petals; }

      if (reduced()) {
        F.forEach(function (f) {
          f.st.stem = 1; f.st.leaf = 1; f.st.bud = 1; f.st.centre = 1;
          f.petals.forEach(function (p) { p.o = 1; });
        });
        render(0);
        return;
      }
      render(0);

      var tl = null, visible = false, pending = false, grown = false;
      function bloom() {
        if (tl) tl.pause();
        tl = anime.createTimeline({
          onComplete: function () {
            tl = null;
            if (!loop) return;
            if (visible) bloom(); else pending = true;
          }
        });
        var start = 0;
        if (!grown) {
          grown = true;
          start = 1000;
          F.forEach(function (f, i) {
            var at = i * stagger * 0.6;
            tl.add(f.st, { stem: 1, duration: 900, ease: 'out(2)' }, at)
              .add(f.st, { leaf: 1, duration: 700, ease: 'outBack(1.6)' }, at + 450)
              .add(f.st, { bud: 1, duration: 500, ease: 'outBack(2)' }, at + 700);
          });
        }
        F.forEach(function (f, i) {
          var at = start + i * stagger;
          tl.add(allPetals(f), { o: [0, 1], duration: 900, ease: 'outElastic(1, .7)', delay: anime.stagger(70) }, at)
            .add(f.st, { centre: [0, 1], duration: 500, ease: 'outBack(2.5)' }, at + 260);
        });
        if (loop) {
          var shut = start + (F.length - 1) * stagger + 1600 + 2600;
          F.forEach(function (f, i) {
            var at = shut + i * 120;
            tl.add(f.st, { centre: 0, duration: 260, ease: 'in(2)' }, at)
              .add(allPetals(f), { o: 0, duration: 520, ease: 'in(2)', delay: anime.stagger(40, { from: 'last' }) }, at + 80);
          });
          tl.add({ duration: 500 });
        }
      }

      var last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = last ? Math.min(50, now - last) : 16;
          last = now;
          render(dt);
        }
      });
      var started = false;
      new IntersectionObserver(function (entries) {
        visible = entries[0].isIntersecting;
        if (visible) { last = 0; timer.play(); if (tl) tl.play(); }
        else { timer.pause(); if (tl) tl.pause(); }
        if (visible && !started) { started = true; bloom(); }
        else if (visible && pending) { pending = false; bloom(); }
      }, { threshold: 0.25 }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-blooming-flowers]').forEach(function (el) {
        if (el._blf) return;   // idempotent
        el._blf = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-blooming-flowers></div>

<!-- a long row of warm colours, opening and closing: -->
<div data-blooming-flowers data-loop data-count="8" data-colors="#f97316,#facc15,#ef4444"></div>