Wind Turbine
White wind turbines turn on rolling green hills, each at its own pace, while clouds drift across the sky. The far ones are smaller and paler, so the scene has depth.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-wind-turbine>where the scene should go. It fills the element with an SVG scene.
Options
Add any of these attributes to the same element.
data-speed | Speed multiplier for the blades and clouds. Default 1. |
data-clouds="false" | A clear sky. |
It is 8:5 with a sky background by default; set your own height or background on the element to change that. The hills stretch to fill any width. It pauses when off screen. With reduced motion it’s a still picture.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Wind Turbine: white wind turbines turn on rolling green hills, each at
// its own pace, while clouds drift by. Far turbines are smaller and
// paler. Drawn as SVG that fills the element, and paused when off screen.
// Usage: <div data-wind-turbine></div>
// It is 8:5 by default with a sky background; give it your own height or
// background to change that. The hills stretch to fill any width.
// Optional: data-speed (default 1), data-clouds="false" (no clouds).
var DEFAULTS = { speed: 1 };
var NS = 'http://www.w3.org/2000/svg';
var css =
'[data-wind-turbine]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5;' +
'background:linear-gradient(#93c5fd,#dbeafe 60%,#eff6ff)}' +
'.wtb-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; }
// Three layers of hills, back to front.
var HILLS = [
{ base: 156, amp: 12, f: 0.011, ph: 0.6, col: '#a3d9a5' },
{ base: 190, amp: 11, f: 0.0085, ph: 2.4, col: '#6dbb72' },
{ base: 224, amp: 8, f: 0.014, ph: 4.1, col: '#3f9d55' }
];
function hillY(h, x) { return h.base + h.amp * Math.sin(x * h.f + h.ph) + h.amp * 0.4 * Math.sin(x * h.f * 2.7 + h.ph); }
function hillPath(h) {
var p = 'M-800 600';
for (var x = -800; x <= 1200; x += 10) p += 'L' + x + ' ' + hillY(h, x).toFixed(1);
return p + 'L1200 600Z';
}
// Turbines: hill layer, x, height.
var TURBINES = [
[0, -150, 48], [0, -40, 54], [0, 64, 60], [0, 170, 50], [0, 300, 62], [0, 392, 52], [0, 520, 56],
[1, -90, 88], [1, 120, 96], [1, 268, 106], [1, 470, 90]
];
function cloud(w) {
return '<g fill="#fff"><rect x="' + (-w / 2) + '" y="-8" width="' + w + '" height="16" rx="8"/>' +
'<circle cx="' + (-w * 0.18) + '" cy="-10" r="' + (w * 0.2) + '"/><circle cx="' + (w * 0.12) + '" cy="-14" r="' + (w * 0.26) + '"/></g>';
}
function build(el) {
var d = el.dataset;
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'Wind turbines turning on rolling hills');
if (!el.hasAttribute('role')) el.setAttribute('role', 'img');
var html = '<svg class="wtb-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
'<circle cx="64" cy="48" r="34" fill="#fef9c3" opacity=".35"/><circle cx="64" cy="48" r="18" fill="#fef3c7"/>' +
'<g class="wtb-clouds"></g>';
var rotors = [];
HILLS.forEach(function (h, li) {
html += '<path d="' + hillPath(h) + '" fill="' + h.col + '"/>';
TURBINES.forEach(function (tb, i) {
if (tb[0] !== li) return;
var x = tb[1], H = tb[2], y = hillY(h, x) + 3, top = y - H;
var bw = H * 0.045, tw = H * 0.022, blade = H * 0.44;
var far = li === 0;
var col = far ? '#f1f5f9' : '#ffffff', shade = far ? '#cbd5e1' : '#d4dbe5';
html += '<g' + (far ? ' opacity=".85"' : '') + '>' +
'<path d="M' + (x - bw).toFixed(1) + ' ' + y.toFixed(1) + 'L' + (x - tw).toFixed(1) + ' ' + top.toFixed(1) + 'L' + (x + tw).toFixed(1) + ' ' + top.toFixed(1) + 'L' + (x + bw).toFixed(1) + ' ' + y.toFixed(1) + 'Z" fill="' + col + '"/>' +
'<path d="M' + x.toFixed(1) + ' ' + y.toFixed(1) + 'L' + (x + tw * 0.4).toFixed(1) + ' ' + top.toFixed(1) + 'L' + (x + tw).toFixed(1) + ' ' + top.toFixed(1) + 'L' + (x + bw).toFixed(1) + ' ' + y.toFixed(1) + 'Z" fill="' + shade + '"/>' +
'<rect x="' + (x - H * 0.05).toFixed(1) + '" y="' + (top - H * 0.035).toFixed(1) + '" width="' + (H * 0.12).toFixed(1) + '" height="' + (H * 0.06).toFixed(1) + '" rx="' + (H * 0.03).toFixed(1) + '" fill="' + col + '"/>' +
'<g transform="translate(' + x.toFixed(1) + ' ' + top.toFixed(1) + ')"><g class="wtb-rotor" data-i="' + i + '">';
for (var b = 0; b < 3; b++) {
html += '<path transform="rotate(' + b * 120 + ')" d="M' + (-blade * 0.05).toFixed(2) + ' 0C' + (-blade * 0.09).toFixed(2) + ' ' + (-blade * 0.3).toFixed(2) + ' ' + (-blade * 0.05).toFixed(2) + ' ' + (-blade * 0.8).toFixed(2) + ' 0 ' + (-blade).toFixed(2) +
'C' + (blade * 0.025).toFixed(2) + ' ' + (-blade * 0.8).toFixed(2) + ' ' + (blade * 0.06).toFixed(2) + ' ' + (-blade * 0.3).toFixed(2) + ' ' + (blade * 0.04).toFixed(2) + ' 0Z" fill="' + col + '" stroke="' + shade + '" stroke-width=".6"/>';
}
html += '<circle r="' + (H * 0.035).toFixed(2) + '" fill="' + shade + '"/></g></g></g>';
rotors.push({ i: i, rate: (0.07 + Math.random() * 0.03) * (far ? 1.15 : 1), a: Math.random() * 120 });
});
});
html += '</svg>';
el.innerHTML = html;
var svg = el.firstChild;
var rotorEls = svg.querySelectorAll('.wtb-rotor');
rotors.forEach(function (r, k) { r.el = rotorEls[k]; });
var clouds = [];
if (d.clouds !== 'false') {
var cg = svg.querySelector('.wtb-clouds');
[[-80, 40, 70, 0.9], [130, 70, 52, 0.75], [260, 32, 86, 0.95], [450, 60, 60, 0.8], [620, 36, 74, 0.85]].forEach(function (c) {
var g = document.createElementNS(NS, 'g');
g.innerHTML = cloud(c[2]);
g.setAttribute('opacity', c[3]);
cg.appendChild(g);
clouds.push({ el: g, x: c[0], y: c[1], v: 0.004 + c[2] * 0.00006 });
});
}
function frame(dt) {
rotors.forEach(function (r) {
r.a = (r.a + r.rate * dt) % 360;
r.el.setAttribute('transform', 'rotate(' + r.a.toFixed(2) + ')');
});
clouds.forEach(function (c) {
c.x += c.v * dt;
if (c.x > 800) c.x -= 1200;
c.el.setAttribute('transform', 'translate(' + c.x.toFixed(1) + ' ' + c.y + ')');
});
}
if (reduced()) { frame(0); return; }
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) * speed;
last = now;
frame(dt);
}
});
frame(0);
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-wind-turbine]').forEach(function (el) {
if (el._wtb) return; // idempotent
el._wtb = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-wind-turbine></div>
<!-- a wide, short banner: -->
<div data-wind-turbine style="aspect-ratio: auto; height: 220px"></div>