Weather Radar Loop
A rain radar loop. Bands of rain, from pale green drizzle through yellow to red and purple downpours, drift across a dark map with the wind while scattered showers bubble up around them. The clock in the corner ticks on ten minutes a frame and the scrubber runs along the bottom until it reaches “Now” and starts again.
How to use it
- Paste the snippet just before
</head>. - Add
data-weather-radar-loopto an empty element and give it a height. - Pick the area with
data-viewaswest south east northin degrees, such asdata-view="-11 49 3 59"for the British Isles.
Options
Add any of these attributes to the same element.
data-view | Area to show: west, south, east and north edges in degrees. Default -127 23 -65 51, the United States. |
data-wind | Direction the rain moves, as a compass bearing. Default 70 (east-north-east). |
data-amount | How much rain, from 0 to 1. Default 0.5. |
data-speed | How fast the rain moves, as a multiplier. Default 1. |
data-period | Length of the loop in seconds. Default 10. |
data-start | Clock time of the first frame. Default 12:00. |
data-cities | Set to off to hide the city dots and names. |
The rain is made up, not real weather. It is drawn on a canvas, 300px tall unless you set a height, with simplified coastlines, and pauses off screen.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Weather Radar Loop: a rain radar on a canvas. Bands of rain, from
// light green drizzle to red downpours, drift across a dark map with the
// wind, while the clock in the corner ticks forward and the scrubber at
// the bottom runs. At the end of the loop it fades and starts again.
// Usage:
// <div data-weather-radar-loop style="height: 320px"></div>
// Optional: data-view (the part of the world to show, as "west south east
// north" in degrees, default "-127 23 -65 51", the United States),
// data-wind (direction the rain moves, compass degrees, default 70,
// roughly east-north-east), data-speed (multiplier, default 1),
// data-amount (how much rain, 0-1, default 0.5), data-start (clock time
// of the first frame, default "12:00"), data-period (seconds a loop
// lasts, default 10), data-cities (show city dots and names, on by
// default; set data-cities="off" to hide).
var DEFAULTS = { view: '-127 23 -65 51', wind: 70, speed: 1, amount: 0.5, start: '12:00', period: 10 };
var RAIN = [[0.18, '#bbf7d0'], [0.3, '#4ade80'], [0.42, '#16a34a'], [0.54, '#facc15'], [0.66, '#f97316'], [0.78, '#dc2626'], [0.9, '#c026d3']];
// Simplified coastlines: polygons separated by |, as "lon lat" pairs.
var LAND = '-168 66 -162 70 -156 71.3 -141 69.6 -128 70 -115 68.8 -95 72 -82 73.5 -80 63 -94 59 -92 57 -82 55 -79 51.5 -76 56 -78 62.5 -70 61 -64 60 -61 56 -56 52 -60 47 -66 45 -70 43.5 -70 41.5 -74 40.5 -76 38 -76 35 -81 31.5 -80 27 -80.5 25.2 -82.5 27.5 -84 30 -89 30.2 -94 29.6 -97.5 27 -97.5 22 -95 19 -91 18.6 -90.5 21 -87 21.5 -88 16 -84 15.5 -83.5 11 -79.5 9 -77.5 8.5 -80 7.5 -83 8.5 -86 11.5 -92 14.5 -96 15.8 -101 17.5 -105.5 20.5 -105.5 23.5 -109 26 -112.8 31.3 -114.7 30 -112 25 -109.5 23 -110.5 24.5 -114 28 -115 30.5 -117 32.5 -120.5 34.5 -122.5 37.5 -124 40.5 -124 46 -123 49 -127.5 50.5 -131 54 -134 58 -140 59.8 -147 60.5 -152 59 -158 56.5 -164 54.6 -158 58.5 -162 60 -165 62.5 -164.5 64.5|-73 78 -60 82 -30 83.5 -18 81 -20 75 -22 70 -32 68 -40 65 -43 60 -48 61 -52 65 -54 69 -58 75.5 -68 76.5|-80 73.5 -68 70 -62 66.5 -65 63 -72 63.5 -78 67 -90 71 -85 73.8|-95 74 -80 76 -75 79 -62 82 -90 82.5 -105 79 -118 76 -110 73 -100 72.5|-118 69 -102 68.5 -101 73 -115 73.5 -125 71.8|-77.5 8.5 -72 12 -64 10.8 -60 8.5 -57 6 -52 5 -50 1.8 -48 -1 -44 -2.5 -38 -4 -35 -7 -35 -9 -39 -13.5 -39 -17.5 -40 -20.5 -42 -23 -46 -24 -48.5 -26 -48.5 -28.5 -53 -33.5 -57 -36 -57 -38.5 -62 -39 -65 -41 -65 -45 -67.5 -46.5 -66 -48 -69 -51 -68.5 -53 -71 -54 -74 -52 -75.5 -48 -73.5 -43 -73.5 -37 -71.5 -31 -70.3 -18.5 -75.5 -15 -78 -10 -81.2 -5.5 -80 -2 -80 1 -77 4 -77.5 6.5|-9.5 37 -9 43 -2 43.5 -1.5 46.5 -4.7 48.5 -1.5 48.7 1.5 50.5 4 51.5 5 53.3 8.5 53.8 8.5 57 10.5 57.7 10.5 54.5 14 54 19 54.5 21.5 57.5 24 57.3 23.5 59.3 29 60 22.5 60.5 21.5 63 25.5 65 22 65.8 17.5 62.5 19 60 16.5 56.5 13 55.5 11 58.5 8 58 5.5 58.8 5 62 10 64 14 67.5 19 70 25 71 31 70 40 67.5 41 66 34 64.6 37 64.2 44 66 44 68.5 53 68.5 59 68.5 67 69 69 72.8 73 72.5 72 66.5 78 72.5 83 70.5 87 73.8 100 76 105 77.7 113 73.7 127 73.5 131 71 140 72.5 150 71.5 160 70.6 170 70 180 69 180 65 178.5 64.5 175 62 170 60 163 59.8 163.5 56 156.7 51 155.5 57.5 162 61.7 155 59.3 143 59.4 136 54.5 141 52.5 140 48 135 43.5 131 42.6 129.5 40.5 129.5 36 126.5 34.5 126 37.7 124.5 39.8 121.5 39 122 40.5 118 39.2 119 37 122.5 37 120 34.5 121.8 31.5 122 29 118.5 24.5 113.5 22.2 109.5 21.5 108 21.5 106.5 20 105.8 18.5 108.5 15 109.3 11.5 105 8.6 104.7 10.5 100.5 13.5 99.2 10 100.5 7.2 103.5 1.3 101 2.8 98.5 8 98.3 12.5 97.5 16.5 94.5 16 94 19 92 22 90 22 86.5 20 85 19.5 80.3 15.5 80 10.3 77.5 8 76 10 73 17 72.5 21 69 22.5 67 24.5 62 25.2 57 25.7 56.3 27 54 26.8 51.5 27.9 50 30 48 30 48.5 28 51.5 24.5 55 22.5 58.8 22.5 59.8 22 57 18.8 52 16 45 12.8 43 13 42.6 15.5 39 21.5 35.5 27.5 32.6 30 34.5 31.8 35 32.5 36 35.8 32 36.2 29 36.7 26.5 39.5 29 41 28 42 28.5 43.5 30 45.5 33.5 44.5 36 45.3 39 47 38 45 41.5 41.5 36 41.5 33 42 29 41.2 26 40.8 23 40.2 24 38 22 36.5 21 38.5 19.5 41.5 19 42.5 13.5 45.5 12.3 44.5 16 41.5 18.5 40.2 16 38 15.7 40 12 42 10 44 8.5 44.4 6 43.1 3 43.3 3.3 41.9 0 39.5 -0.5 38 -2 36.7 -5.3 36 -6.5 36.9 -8.8 37|-6 35.8 -2 35.1 3 36.8 10 37.3 11 35.5 10 33.8 15 32.3 20 30.8 20 32.5 25 31.8 29 30.9 32.3 31.3 34 27.5 37 22 38.5 18 43 12.6 44 10.4 51.2 11.8 51 10.4 49 6 46 2 42 -1 40 -3 39 -6.5 40.5 -10.5 40.5 -15 37 -17.5 35.5 -21 35.5 -24 32.8 -26 32.5 -28.6 30 -31.3 27 -33.6 22 -34.1 18.5 -34.2 18 -31.5 15 -27 14.5 -22.5 11.8 -17 13.5 -12 12 -6 9 -1 9.5 3.8 8 4.4 5.5 4.2 3 6.3 -2 4.8 -7.5 4.4 -12 7 -15 10.8 -17.1 14.7 -16 19 -16.5 22 -13 27.5 -10 29.5 -9.5 32.5|44 -25 47 -25 50.5 -15.5 49.3 -12 44.5 -16 43.3 -22|-5.7 50 1.5 51.2 1.7 52.7 -0.5 54.5 -2 55.9 -1.8 57.6 -3 58.6 -5 58.6 -6.2 56.5 -5 55 -3 54 -4.6 53.3 -5 51.6|-10 51.6 -6 52 -6 54 -7.5 55.3 -10 54.2|-24 65.5 -22 66.4 -15 66.5 -13.5 65 -18 63.4 -22.5 63.8|130 31 131.5 31.5 132 33.8 135 33.5 136.8 34.5 140 35 141 38 142 40 141.4 41.4 140 40.3 139.8 38 137 37 133 35.5 130.9 34|140 41.5 141.8 42.5 145.5 43.3 142 45.5|79.8 6 81.8 7.5 80 9.8|95.3 5.6 98 4 104 -2 106 -5.8 102 -4 98.5 1|109 2 111 1 116.5 7 119 5 117.5 1 116.3 -3.5 114.5 -4 110.5 -3|105.5 -6.5 114.5 -7.7 114.4 -8.7 108 -7.8|119 -5.5 120.5 0.7 125 1.5 121 -1 123 -4.5|131 -1.3 134 -0.8 138 -1.7 145 -4.5 151 -10.5 147 -10.2 143 -9 141 -9.2 138 -8.3 137.5 -5 133 -4|120 18.5 122.3 18.4 124.3 12.5 126.5 7 125.5 5.6 122 7 123.5 10 121 13.5 120.5 16|113.5 -22 114 -26 115 -34 118 -35 123.5 -33.8 129 -31.6 131.5 -31.5 135 -34.8 138 -35.5 140 -37.8 144 -38.5 146.3 -39 150 -37.5 151.3 -34 153.5 -28.5 153 -25.5 149 -21 145.5 -15 143.5 -14 142.5 -10.7 141.5 -13 141.5 -16.5 140 -17.5 136 -15 136.8 -12.2 132.5 -11.5 130 -13.3 129.5 -15 127 -14 124 -16.3 122 -18 119 -20 116 -21|144.6 -40.7 148.3 -40.9 148 -43 146 -43.6|172.7 -34.5 178.5 -37.7 176.8 -40 174.8 -41.4 173 -39.3 174.5 -37|172.5 -40.6 174 -41.7 171 -44.3 169 -46.7 166.5 -46 168.3 -44|-85 21.8 -81 23.1 -77 21 -74.2 20.2 -77.5 19.9 -80.5 21.8|-74.4 18.5 -72.8 19.9 -68.4 19 -70 18.2';
var CITIES = {
'London': [-0.13, 51.5], 'Paris': [2.35, 48.86], 'Berlin': [13.4, 52.52], 'Madrid': [-3.7, 40.42],
'Rome': [12.5, 41.9], 'Amsterdam': [4.9, 52.37], 'Stockholm': [18.07, 59.33], 'Moscow': [37.6, 55.75],
'Istanbul': [28.98, 41.01], 'Reykjavik': [-21.9, 64.15], 'Cairo': [31.24, 30.04], 'Lagos': [3.38, 6.52],
'Nairobi': [36.82, -1.29], 'Johannesburg': [28.05, -26.2], 'Cape Town': [18.42, -33.92], 'Dubai': [55.27, 25.2],
'Mumbai': [72.88, 19.08], 'Delhi': [77.2, 28.6], 'Bangkok': [100.5, 13.75], 'Singapore': [103.82, 1.35],
'Jakarta': [106.85, -6.2], 'Hong Kong': [114.17, 22.32], 'Shanghai': [121.47, 31.23], 'Beijing': [116.4, 39.9],
'Seoul': [126.98, 37.57], 'Tokyo': [139.69, 35.69], 'Sydney': [151.21, -33.87], 'Melbourne': [144.96, -37.81],
'Auckland': [174.76, -36.85], 'Honolulu': [-157.86, 21.3], 'Vancouver': [-123.12, 49.28], 'Seattle': [-122.33, 47.6],
'San Francisco': [-122.42, 37.77], 'Los Angeles': [-118.24, 34.05], 'Mexico City': [-99.13, 19.43], 'Chicago': [-87.63, 41.88],
'Toronto': [-79.38, 43.65], 'New York': [-74.0, 40.71], 'Miami': [-80.19, 25.76], 'Bogotá': [-74.07, 4.71],
'Lima': [-77.04, -12.05], 'São Paulo': [-46.63, -23.55], 'Rio de Janeiro': [-43.17, -22.9], 'Buenos Aires': [-58.38, -34.6]
};
function plain(t) { return t.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); }
// A city name from the list above, or "lat lon" such as "48.86 2.35".
// Returns [lon, lat, name] or null.
function place(text) {
text = text.trim();
var m = /^(-?[\d.]+)[\s,]+(-?[\d.]+)$/.exec(text);
if (m) return [+m[2], +m[1], ''];
for (var k in CITIES) if (plain(k) === plain(text)) return [CITIES[k][0], CITIES[k][1], k];
return null;
}
var css =
':where(.wrl){position:relative;width:100%;height:300px}' +
'.wrl{overflow:hidden;border-radius:.8em;background:#0d1626;line-height:1.2;color:#e6ecf5}' +
'.wrl-canvas{position:absolute;inset:0;width:100%;height:100%;display:block}' +
'.wrl-time{position:absolute;left:.7em;top:.6em;padding:.25em .6em;border-radius:.45em;background:rgba(10,18,32,.72);font-weight:700;font-variant-numeric:tabular-nums;font-size:.9em}' +
'.wrl-time small{font-weight:600;opacity:.65;margin-left:.45em;font-size:.82em}' +
'.wrl-key{position:absolute;right:.7em;top:.6em;display:flex;align-items:center;gap:.4em;padding:.3em .6em;border-radius:.45em;background:rgba(10,18,32,.72);font-size:.72em}' +
'.wrl-key i{display:block;width:5em;height:.5em;border-radius:99px}' +
'.wrl-bar{position:absolute;left:.8em;right:.8em;bottom:.7em;height:.3em;border-radius:99px;background:rgba(255,255,255,.18);overflow:hidden}' +
'.wrl-fill{position:absolute;inset:0;background:#e6ecf5;transform-origin:left;transform:scaleX(0)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// Smooth value noise.
var PERM = [];
for (var i = 0; i < 512; i++) PERM[i] = Math.floor(Math.random() * 256);
function hash(x, y) { return PERM[(PERM[x & 255] + y) & 511] / 255; }
function noise(x, y) {
var xi = Math.floor(x), yi = Math.floor(y), xf = x - xi, yf = y - yi;
var u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf);
var a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1), d = hash(xi + 1, yi + 1);
return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
}
function fbm(x, y) { return noise(x, y) * 0.55 + noise(x * 2.1 + 17, y * 2.1 + 9) * 0.3 + noise(x * 4.3 + 41, y * 4.3 + 3) * 0.15; }
function rgb(hex) { var n = parseInt(hex.slice(1), 16); return [n >> 16 & 255, n >> 8 & 255, n & 255]; }
var RAIN_RGB = RAIN.map(function (r) { return [r[0], rgb(r[1])]; });
function setup(el) {
var d = el.dataset;
var s = (el._wrl = { visible: false, raf: 0, t: 0 });
el.classList.add('wrl');
var v = (d.view || DEFAULTS.view).trim().split(/[\s,]+/).map(Number);
s.view = v.length === 4 ? v : DEFAULTS.view.split(' ').map(Number);
var wind = (d.wind !== undefined ? +d.wind : DEFAULTS.wind) * Math.PI / 180;
s.wind = [Math.sin(wind), -Math.cos(wind)];
s.speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
s.amount = Math.max(0, Math.min(1, d.amount !== undefined ? +d.amount : DEFAULTS.amount));
s.period = (+d.period || DEFAULTS.period) * 1000;
var start = (d.start || DEFAULTS.start).split(':');
s.start = (+start[0] || 0) * 60 + (+start[1] || 0);
s.seed = Math.random() * 100;
s.cities = d.cities !== 'off';
s.polys = LAND.split('|').map(function (p) {
var n = p.split(' ').map(Number), pts = [];
for (var i = 0; i < n.length; i += 2) pts.push([n[i], n[i + 1]]);
return pts;
});
s.canvas = document.createElement('canvas');
s.canvas.className = 'wrl-canvas';
s.ctx = s.canvas.getContext('2d');
s.base = document.createElement('canvas');
s.rain = document.createElement('canvas');
s.time = document.createElement('div');
s.time.className = 'wrl-time';
var key = document.createElement('div');
key.className = 'wrl-key';
key.innerHTML = 'Light<i style="background:linear-gradient(90deg,' + RAIN.map(function (r) { return r[1]; }).join(',') + ')"></i>Heavy';
var bar = document.createElement('div');
bar.className = 'wrl-bar';
s.fill = document.createElement('div');
s.fill.className = 'wrl-fill';
bar.appendChild(s.fill);
[s.canvas, s.time, key, bar].forEach(function (n) { n.setAttribute('aria-hidden', 'true'); el.appendChild(n); });
if (el.closest('[aria-hidden="true"]')) key.style.display = 'none';
el.setAttribute('role', 'img');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Weather radar loop: bands of rain moving across a map');
resize(el);
new ResizeObserver(function () {
if (Math.abs(el.clientWidth - s.w) > 2 || Math.abs(el.clientHeight - s.h) > 2) { resize(el); if (!s.raf) draw(el); }
}).observe(el);
}
// Fit the view to the box (cover), with east-west squeezed by the
// latitude so shapes look right, and draw the map once.
function resize(el) {
var s = el._wrl, dpr = Math.min(2, window.devicePixelRatio || 1);
var w = (s.w = el.clientWidth), h = (s.h = el.clientHeight);
if (!w || !h) return;
[s.canvas, s.base].forEach(function (c) { c.width = Math.round(w * dpr); c.height = Math.round(h * dpr); });
s.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
var v = s.view, midLat = (v[1] + v[3]) / 2, sq = Math.cos(midLat * Math.PI / 180);
var vw = (v[2] - v[0]) * sq, vh = v[3] - v[1];
var k = Math.max(w / vw, h / vh);
var cx = (v[0] + v[2]) / 2, cy = midLat;
s.proj = function (lon, lat) { return [w / 2 + (lon - cx) * sq * k, h / 2 - (lat - cy) * k]; };
s.k = k;
// Rain is worked out on a coarse grid and scaled up smoothly.
var span = Math.max(w, h);
s.along0 = Math.min(0, w / span * s.wind[0], h / span * s.wind[1], w / span * s.wind[0] + h / span * s.wind[1]);
s.cell = Math.max(3, Math.round(Math.min(w, h) / 110));
s.gw = Math.ceil(w / s.cell) + 2;
s.gh = Math.ceil(h / s.cell) + 2;
s.field = new Float32Array(s.gw * s.gh);
// Colours are picked on a half-size canvas, from the field smoothed
// between grid points, so the edges of the bands come out round.
s.hw = Math.ceil(w / 2);
s.hh = Math.ceil(h / 2);
s.rain.width = s.hw;
s.rain.height = s.hh;
s.rctx = s.rain.getContext('2d');
s.img = s.rctx.createImageData(s.hw, s.hh);
var b = s.base.getContext('2d');
b.setTransform(dpr, 0, 0, dpr, 0, 0);
b.fillStyle = '#0d1626';
b.fillRect(0, 0, w, h);
b.fillStyle = '#1c2a42';
b.strokeStyle = '#4a6188';
b.lineWidth = 1;
b.lineJoin = 'round';
b.beginPath();
s.polys.forEach(function (p) {
p.forEach(function (pt, i) {
var q = s.proj(pt[0], pt[1]);
if (i) b.lineTo(q[0], q[1]); else b.moveTo(q[0], q[1]);
});
b.closePath();
});
b.fill();
s.coast = new Path2D();
s.polys.forEach(function (p) {
p.forEach(function (pt, i) {
var q = s.proj(pt[0], pt[1]);
if (i) s.coast.lineTo(q[0], q[1]); else s.coast.moveTo(q[0], q[1]);
});
s.coast.closePath();
});
// Faint lines of latitude and longitude.
b.strokeStyle = 'rgba(120,150,200,.12)';
b.beginPath();
for (var lon = -180; lon <= 180; lon += 10) { var a = s.proj(lon, -80), z = s.proj(lon, 85); b.moveTo(a[0], a[1]); b.lineTo(z[0], z[1]); }
for (var lat = -80; lat <= 80; lat += 10) { var l = s.proj(-180, lat), r = s.proj(180, lat); b.moveTo(l[0], l[1]); b.lineTo(r[0], r[1]); }
b.stroke();
s.spots = [];
if (s.cities) {
for (var name in CITIES) {
var q = s.proj(CITIES[name][0], CITIES[name][1]);
if (q[0] > 12 && q[0] < w - 12 && q[1] > 34 && q[1] < h - 20) s.spots.push([q[0], q[1], name]);
}
}
}
// Rain intensity at a pixel at time t (0-1 through the loop): a broad
// front sweeping along the wind, broken up by noise, plus stray showers.
function rainAt(s, x, y, t) {
var span = Math.max(s.w, s.h), u = x / span, v = y / span;
var along = u * s.wind[0] + v * s.wind[1];
var across = -u * s.wind[1] + v * s.wind[0];
var drift = t * 1.3 * s.speed;
var front = along - s.along0 - (drift - 0.15);
var band = Math.exp(-front * front / 0.03) * (0.8 + 0.3 * Math.sin(across * 7 + s.seed));
var back = front + 0.45;
band += 0.55 * Math.exp(-back * back / 0.012);
var mx = u - s.wind[0] * drift, my = v - s.wind[1] * drift;
var n = fbm(mx * 5 + s.seed, my * 5 + t * 1.2);
var showers = Math.max(0, fbm(mx * 8 + 50 + s.seed, my * 8 + 20) - 0.52) * 2.6;
var r = (band * (0.5 + s.amount) + showers * (0.4 + s.amount)) * n * 1.3 + (s.amount - 0.5) * 0.15;
return 0.14 + (r - 0.32) * 1.15;
}
function draw(el) {
var s = el._wrl, ctx = s.ctx, t = s.t, data = s.img.data;
if (!s.w) return;
ctx.drawImage(s.base, 0, 0, s.w, s.h);
var fade = Math.min(1, t / 0.06, (1 - t) / 0.06), f = s.field, gw = s.gw;
for (var gy = 0; gy < s.gh; gy++) {
for (var gx = 0; gx < gw; gx++) f[gy * gw + gx] = rainAt(s, gx * s.cell, gy * s.cell, t);
}
var step = 2 / s.cell, alpha = 205 * fade;
for (var y = 0; y < s.hh; y++) {
var fy = y * step, y0 = Math.floor(fy), ty = fy - y0, row0 = y0 * gw, row1 = row0 + gw;
for (var x = 0; x < s.hw; x++) {
var fx = x * step, x0 = Math.floor(fx), tx = fx - x0;
var top = f[row0 + x0] + (f[row0 + x0 + 1] - f[row0 + x0]) * tx;
var bot = f[row1 + x0] + (f[row1 + x0 + 1] - f[row1 + x0]) * tx;
var r = top + (bot - top) * ty, o = (y * s.hw + x) * 4, c = null;
for (var k = RAIN_RGB.length - 1; k >= 0; k--) if (r >= RAIN_RGB[k][0]) { c = RAIN_RGB[k][1]; break; }
if (c) { data[o] = c[0]; data[o + 1] = c[1]; data[o + 2] = c[2]; data[o + 3] = alpha; }
else data[o + 3] = 0;
}
}
s.rctx.putImageData(s.img, 0, 0);
ctx.imageSmoothingEnabled = true;
ctx.drawImage(s.rain, 0, 0, s.hw * 2, s.hh * 2);
ctx.strokeStyle = 'rgba(160,185,230,.55)';
ctx.lineWidth = 1;
ctx.stroke(s.coast);
ctx.font = '600 ' + Math.max(9, Math.min(12, s.w / 55)) + 'px system-ui, sans-serif';
s.spots.forEach(function (p) {
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(p[0], p[1], 2.2, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = 'rgba(230,236,245,.85)';
ctx.fillText(p[2], p[0] + 5, p[1] + 4);
});
var mins = s.start + Math.floor(t * 12) * 10;
var hh = Math.floor(mins / 60) % 24, mm = mins % 60;
s.time.innerHTML = (hh < 10 ? '0' : '') + hh + ':' + (mm < 10 ? '0' : '') + mm + (t > 11 / 12 ? '<small>Now</small>' : '');
s.fill.style.transform = 'scaleX(' + t + ')';
}
function frame(el, now) {
var s = el._wrl;
if (!s.visible) { s.raf = 0; return; }
var dt = s.last ? Math.min(50, now - s.last) : 0;
s.last = now;
s.t += dt / s.period;
if (s.t >= 1) { s.t = 0; s.seed = Math.random() * 100; }
draw(el);
s.raf = requestAnimationFrame(function (t) { frame(el, t); });
}
function init() {
var still = reduced();
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._wrl;
s.visible = e.isIntersecting;
if (s.visible && !s.raf) { s.last = 0; s.raf = requestAnimationFrame(function (t) { frame(e.target, t); }); }
});
}, { threshold: 0.1 });
document.querySelectorAll('[data-weather-radar-loop]').forEach(function (el) {
if (el._wrl) return;
setup(el);
el._wrl.t = 0.45;
draw(el);
if (!still) { el._wrl.t = 0; io.observe(el); }
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-weather-radar-loop data-view="-11 49 3 59" style="height: 360px"></div>