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
- Paste the snippet just before
</head>. - Add
data-cherry-blossom-petalsto 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-density | Petals per 100 × 100 px. Default 1.2. |
data-wind | Strength and direction of the breeze, from -2 to 2. Negative blows to the left. Default 1. |
data-size | Size of the biggest petals, in px. Default 24. |
data-speed | Speed multiplier. Default 1. |
data-colors | Comma-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.
<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><section data-cherry-blossom-petals>
<h1>Hanami</h1>
</section>