← Particles

Cherry Blossom Petals

Pink cherry blossom petals drift across a section on a gentle breeze that comes and goes in gusts, spinning and turning over in the air as they go.

Hanami

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-cherry-blossom-petals to a section, hero or card. It looks best on a pale sky blue or a deep night colour.

Options

Add any of these attributes to the same element.

data-densityPetals per 100 × 100 px. Default 1.2.
data-windStrength and direction of the breeze, from -2 to 2. Negative blows to the left. Default 1.
data-sizeSize of the biggest petals, in px. Default 24.
data-speedSpeed multiplier. Default 1.
data-colorsComma-separated petal colours.

It’s drawn on a transparent canvas behind your content and paused when off screen. With reduced motion the petals hang still.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Cherry Blossom Petals: pink petals drift across a section on a gentle
    // breeze that rises and falls in gusts, spinning and turning over in the
    // air as they go. Drawn on a transparent canvas behind your content, and
    // paused when off screen.
    // Usage: <section data-cherry-blossom-petals>…</section>
    // Optional: data-density (petals per 100 × 100 px, default 1.2),
    // data-wind (breeze strength and direction, -2 to 2, default 1; negative
    // blows to the left), data-speed (default 1), data-size (size of the
    // biggest petals in px, default 24), data-colors="#ffd6e0,#f9a8c9,…".
    var DEFAULTS = { density: 1.2, wind: 1, speed: 1, size: 24, colors: ['#ffe4ec', '#ffc8d9', '#f9a8c9', '#f48fb1'] };

    var css =
      '[data-cherry-blossom-petals]{position:relative;isolation:isolate;overflow:hidden}' +
      '.cbp-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // A petal on a 48 × 48 box: rounded, narrowing to the base, with the
    // little notch at its tip that cherry petals have.
    function sprite(color) {
      var c = document.createElement('canvas');
      c.width = c.height = 48;
      var g = c.getContext('2d');
      g.beginPath();
      g.moveTo(24, 46);
      g.bezierCurveTo(8, 36, 4, 16, 14, 6);
      g.quadraticCurveTo(19, 2, 24, 9);
      g.quadraticCurveTo(29, 2, 34, 6);
      g.bezierCurveTo(44, 16, 40, 36, 24, 46);
      g.closePath();
      var fill = g.createLinearGradient(24, 4, 24, 46);
      fill.addColorStop(0, color.trim());
      fill.addColorStop(0.75, color.trim());
      fill.addColorStop(1, '#ffffff');
      g.fillStyle = fill;
      g.fill();
      // A faint deeper flush along the middle.
      g.globalCompositeOperation = 'source-atop';
      var mid = g.createRadialGradient(24, 30, 0, 24, 30, 18);
      mid.addColorStop(0, 'rgba(219,39,119,.16)');
      mid.addColorStop(1, 'rgba(219,39,119,0)');
      g.fillStyle = mid;
      g.fillRect(0, 0, 48, 48);
      return c;
    }

    function build(el) {
      var d = el.dataset;
      var canvas = document.createElement('canvas');
      canvas.className = 'cbp-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var density = +d.density || DEFAULTS.density;
      var wind = d.wind !== undefined ? +d.wind : DEFAULTS.wind;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var maxSize = +d.size || DEFAULTS.size;
      var img = (d.colors ? d.colors.split(',') : DEFAULTS.colors).map(sprite);
      var R = anime.utils.random;
      var w = 0, h = 0, dpr = 1, t = 0, unit = 1, across = 1, breeze = 0;
      var petals = [];

      // New petals come in from the top, or from the side the wind blows from.
      function petal(p, anywhere) {
        p.z = Math.sqrt(Math.random());
        p.size = maxSize * (0.4 + p.z * 0.6);
        if (anywhere) {
          p.x = Math.random() * w;
          p.y = Math.random() * h;
        } else if (Math.random() < Math.min(0.7, Math.abs(wind) * 0.45)) {
          p.x = wind > 0 ? -p.size * 2 : w + p.size * 2;
          p.y = Math.random() * h * 0.8 - h * 0.1;
        } else {
          p.x = Math.random() * (w + 120) - (wind > 0 ? 120 : 0);
          p.y = -p.size * 2;
        }
        p.img = img[Math.floor(Math.random() * img.length)];
        p.fall = R(22, 42) * (0.6 + p.z * 0.6);
        p.drift = R(0.7, 1.3, 2) * (0.5 + p.z * 0.7);  // how much the breeze moves it
        p.rot = Math.random() * 6.28;
        p.spin = R(-2.2, 2.2, 2);
        p.ta = Math.random() * 6.28; p.tas = R(1.4, 3.6, 2);   // turning over, two ways
        p.tb = Math.random() * 6.28; p.tbs = R(0.8, 2.4, 2);
        p.wob = Math.random() * 6.28; p.wobs = R(0.8, 1.8, 2);
        p.alpha = 0.7 + p.z * 0.3;
        return p;
      }

      function resize() {
        dpr = Math.min(2, window.devicePixelRatio || 1);
        w = el.clientWidth;
        h = el.clientHeight;
        canvas.width = w * dpr;
        canvas.height = h * dpr;
        unit = Math.max(0.7, h / 400);
        // Narrow boxes get a gentler breeze, so petals don't rush straight through.
        across = Math.max(0.5, Math.min(1.2, w / 800));
        var n = Math.round(Math.min(200, Math.max(24, (w * h * density) / 10000)));
        while (petals.length < n) petals.push(petal({}, true));
        petals.length = n;
        petals.sort(function (a, b) { return a.z - b.z; });
      }

      function draw() {
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        for (var i = 0; i < petals.length; i++) {
          var p = petals[i];
          // Turning over in 3D, faked by squashing each axis in turn.
          var sx = Math.cos(p.ta), sy = 0.35 + 0.65 * Math.abs(Math.cos(p.tb));
          var cr = Math.cos(p.rot), sr = Math.sin(p.rot), k = dpr * p.size / 48;
          var ax = Math.max(0.1, Math.abs(sx)) * k, ay = sy * k;
          ctx.setTransform(cr * ax, sr * ax, -sr * ay, cr * ay, p.x * dpr, p.y * dpr);
          ctx.globalAlpha = p.alpha * (sx < 0 ? 0.85 : 1);
          ctx.drawImage(p.img, -24, -24);
        }
        ctx.globalAlpha = 1;
      }

      function advance(dt) {
        t += dt;
        // The breeze: a steady push plus slow gusts that come and go.
        var gust = Math.max(0, Math.sin(t * 0.45) * Math.sin(t * 0.21 + 1.3));
        breeze = wind * (30 + 28 * Math.sin(t * 0.3) * 0.5 + 90 * gust) * across;
        for (var i = 0; i < petals.length; i++) {
          var p = petals[i];
          p.ta += p.tas * dt * speed;
          p.tb += p.tbs * dt * speed;
          p.wob += p.wobs * dt * speed;
          p.rot += (p.spin + breeze * 0.004 * p.drift) * dt * speed;
          // When a petal turns flat-on it catches the air and slows its fall.
          var catchAir = Math.abs(Math.cos(p.ta));
          p.x += (breeze * p.drift + Math.sin(p.wob) * 14) * dt * speed;
          p.y += (p.fall * (1.2 - catchAir * 0.6) + Math.cos(p.wob) * 6 - gust * 10) * unit * dt * speed;
          var m = p.size * 2;
          if (p.y > h + m || p.x > w + m * 2 || p.x < -m * 2) petal(p, false);
        }
      }

      resize();
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
        draw();
        if (window.ResizeObserver) new ResizeObserver(function () { resize(); draw(); }).observe(el);
        return;
      }
      if (window.ResizeObserver) new ResizeObserver(resize).observe(el);

      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) / 1000 : 1 / 60;
          last = now;
          advance(dt);
          draw();
        }
      });
      // Pause while off screen to save battery.
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-cherry-blossom-petals]').forEach(function (el) {
        if (el._cbp) return;
        el._cbp = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-cherry-blossom-petals>
  <h1>Hanami</h1>
</section>