Growing Plant
A seed wobbles in a flowerpot, then a shoot pushes up and its leaves unfurl one by one as the stem climbs. A flower opens at the top and the whole plant sways in a gentle breeze, before it fades and grows again.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-growing-plant>where the scene should go. It fills the element with an SVG scene.
Options
Add any of these attributes to the same element.
data-color | Colour of the leaves. Default #22c55e. |
data-flower | Colour of the flower, or false for just leaves. Default #f472b6. |
data-pot | Colour of the pot. Default #c2410c. |
data-duration | Time for one grow, hold and fade. Default 9000 ms. |
data-loop="false" | Grow once and stay in flower. |
It is 8:5 on a transparent background by default. It pauses when off screen. With reduced motion it shows the plant fully grown.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Growing Plant: a seed in a flowerpot sprouts, the stem climbs and its
// leaves unfurl in pairs on the way up, then a flower opens at the top.
// It sways gently in a breeze. Drawn as SVG that fills the element, and
// paused when off screen.
// Usage: <div data-growing-plant></div>
// It is 8:5 by default on a transparent background.
// Optional: data-color (leaf colour, default #22c55e), data-flower
// (flower colour, default #f472b6; "false" for no flower), data-pot (pot
// colour, default #c2410c), data-duration (one grow, hold and fade, ms,
// default 9000), data-loop="false" (grow once and stay).
var DEFAULTS = { color: '#22c55e', flower: '#f472b6', pot: '#c2410c', duration: 9000 };
var NS = 'http://www.w3.org/2000/svg';
var STEM = 'M200 184C196 160 208 132 200 108C194 88 204 72 200 56';
// Where each leaf sits along the stem (0 = soil, 1 = tip) and its size.
var LEAVES = [[0.2, 1], [0.32, 0.95], [0.5, 0.85], [0.62, 0.8], [0.78, 0.66], [0.86, 0.6]];
var css =
'[data-growing-plant]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
'.gpl-svg{position:absolute;inset:0;width:100%;height:100%;display:block;overflow:visible}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
function outCubic(k) { k = clamp01(k); return 1 - Math.pow(1 - k, 3); }
function outBack(k) { k = clamp01(k); var c = 1.6; return 1 + (c + 1) * Math.pow(k - 1, 3) + c * Math.pow(k - 1, 2); }
function inOut(k) { k = clamp01(k); return k < 0.5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2; }
function build(el) {
var d = el.dataset;
var leaf = d.color || DEFAULTS.color;
var flowerCol = d.flower === 'false' ? null : (d.flower || DEFAULTS.flower);
var pot = d.pot || DEFAULTS.pot;
var dur = Math.max(3000, +d.duration || DEFAULTS.duration);
var loop = d.loop !== 'false';
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'A seedling sprouting in a flowerpot');
if (!el.hasAttribute('role')) el.setAttribute('role', 'img');
var petals = '';
for (var i = 0; i < 6; i++) petals += '<ellipse cx="0" cy="-10" rx="6" ry="10" transform="rotate(' + i * 60 + ')"/>';
el.innerHTML =
'<svg class="gpl-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
'<ellipse cx="200" cy="240" rx="70" ry="6" fill="#1e1b2e" opacity=".08"/>' +
'<path d="M157 196L243 196L235 238L165 238Z" fill="' + pot + '"/>' +
'<path d="M157 196L243 196L241 206L159 206Z" fill="#000" opacity=".12"/>' +
'<rect x="150" y="178" width="100" height="20" rx="4" fill="' + pot + '"/>' +
'<rect x="150" y="178" width="100" height="5" rx="2.5" fill="#fff" opacity=".18"/>' +
'<ellipse cx="200" cy="182" rx="45" ry="5.5" fill="#57301a"/>' +
'<ellipse class="gpl-seed" cx="200" cy="180" rx="6" ry="4" fill="#a16207"/>' +
'<g class="gpl-plant">' +
'<path class="gpl-stem" d="' + STEM + '" fill="none" stroke="#15803d" stroke-width="4" stroke-linecap="round"/>' +
'<g class="gpl-leaves"></g>' +
'<g class="gpl-top" transform="translate(200 56)">' +
'<path class="gpl-bud" d="M0 4C-7 0 -6 -10 0 -16C6 -10 7 0 0 4Z" fill="' + leaf + '"/>' +
(flowerCol ? '<g class="gpl-flower"><g fill="' + flowerCol + '">' + petals + '</g><circle r="6" fill="#fbbf24"/><circle r="2.5" cx="-1.5" cy="-1.5" fill="#fde68a"/></g>' : '') +
'</g>' +
'</g>' +
'</svg>';
var svg = el.firstChild;
var plant = svg.querySelector('.gpl-plant');
var stem = svg.querySelector('.gpl-stem');
var seed = svg.querySelector('.gpl-seed');
var bud = svg.querySelector('.gpl-bud');
var flower = svg.querySelector('.gpl-flower');
var leavesG = svg.querySelector('.gpl-leaves');
var L = stem.getTotalLength();
stem.setAttribute('stroke-dasharray', L.toFixed(1) + ' ' + (L + 2).toFixed(1));
var leaves = LEAVES.map(function (spec, i) {
var p = stem.getPointAtLength(spec[0] * L);
var g = document.createElementNS(NS, 'g');
g.innerHTML = '<path d="M0 0C8 -10 28 -12 38 0C28 12 8 10 0 0Z" fill="' + leaf + '"/>' +
'<path d="M3 0L32 0" stroke="#15803d" stroke-width="1.2" stroke-linecap="round" opacity=".55"/>';
leavesG.appendChild(g);
return { el: g, f: spec[0], size: spec[1], x: p.x, y: p.y, side: i % 2 ? -1 : 1 };
});
function frame(p, t) {
// Seed: settles in, wobbles, then the sprout takes over.
seed.setAttribute('opacity', (clamp01(p / 0.03) * (1 - clamp01((p - 0.1) / 0.04))).toFixed(2));
seed.setAttribute('transform', 'rotate(' + (Math.sin(t * 0.03) * 12 * clamp01((p - 0.04) / 0.02)).toFixed(1) + ' 200 182)');
var g = outCubic((p - 0.08) / 0.5);
stem.setAttribute('stroke-dashoffset', (L * (1 - g)).toFixed(1));
stem.setAttribute('opacity', g > 0 ? 1 : 0);
leaves.forEach(function (lf) {
var u = outCubic((g - lf.f) / 0.24);
if (u <= 0) { lf.el.setAttribute('opacity', 0); return; }
// Unfurl: starts pointing up and narrow, opens out sideways.
var ang = lf.side > 0 ? -26 - (1 - u) * 56 : -154 + (1 - u) * 56;
var s = lf.size * (0.08 + 0.92 * u);
lf.el.setAttribute('opacity', 1);
lf.el.setAttribute('transform', 'translate(' + lf.x.toFixed(1) + ' ' + lf.y.toFixed(1) + ') rotate(' + ang.toFixed(1) + ') scale(' + s.toFixed(3) + ' ' + (s * Math.pow(u, 0.6)).toFixed(3) + ')');
});
var b = outBack((g - 0.92) / 0.08);
var q = flower ? outBack((p - 0.6) / 0.14) : 0;
bud.setAttribute('transform', 'scale(' + Math.max(0.001, b * (1 - q * 0.6)).toFixed(3) + ')');
if (flower) flower.setAttribute('transform', 'rotate(' + ((1 - q) * -60).toFixed(1) + ') scale(' + Math.max(0.001, q).toFixed(3) + ')');
var sway = Math.sin(t * 0.0013) * 2.4 + Math.sin(t * 0.0031) * 0.8;
plant.setAttribute('transform', 'rotate(' + (sway * g).toFixed(2) + ' 200 184)');
plant.setAttribute('opacity', (1 - inOut((p - 0.9) / 0.08)).toFixed(3));
}
if (reduced()) { frame(0.8, 0); return; }
var t = 0, last = 0;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
t += last ? Math.min(50, now - last) : 16;
last = now;
var p = loop ? (t % dur) / dur : Math.min(t / dur, 0.82);
frame(p, t);
}
});
frame(0, 0);
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-growing-plant]').forEach(function (el) {
if (el._gpl) return; // idempotent
el._gpl = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-growing-plant></div>
<h2>Your garden is empty</h2>
<!-- grow once, no flower: -->
<div data-growing-plant data-loop="false" data-flower="false"></div>