/** * Solutions Page Animators * Handles the two JS-driven animations on the Solutions page: * 1. Live Data board — ticking KPI values + animated sparkline * 2. Transit board — live clock, split-flap flip characters, row cycling * * Both respect prefers-reduced-motion and pause via IntersectionObserver. * Mirrors the patterns and conventions of dashboard-animator.js. */ /* ── 1. Live Data KPI Animator ─────────────────────────────────────────── */ (function () { 'use strict'; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; /* KPI definitions: label, base value, unit, variance range, display format */ var KPIS = [ { id: 'ld-orders', base: 1847, range: 120, fmt: function (v) { return v.toLocaleString(); } }, { id: 'ld-uptime', base: 9997, range: 2, fmt: function (v) { return (v / 100).toFixed(2) + '%'; } }, { id: 'ld-alerts', base: 3, range: 2, fmt: function (v) { return Math.max(0, v).toString(); } }, { id: 'ld-latency', base: 42, range: 18, fmt: function (v) { return Math.max(8, v) + 'ms'; } }, ]; /* Sparkline path parameters */ var LINE_PTS = 16; var LINE_W = 260; var LINE_H = 60; var SPEED = 0.0008; function wave(t, off) { return Math.max(0, Math.min(1, 0.5 + Math.sin(t + off) * 0.28 + Math.sin(t * 2.1 + off * 1.7) * 0.12 )); } function makeState(stage) { var kpiEls = []; for (var i = 0; i < KPIS.length; i++) { kpiEls.push(stage.querySelector('#' + KPIS[i].id)); } return { stage: stage, kpiEls: kpiEls, linePath: stage.querySelector('#ld-line-path'), fillPath: stage.querySelector('#ld-fill-path'), phase: Math.random() * Math.PI * 2, ticker: 0, /* frame counter — update KPI text every N frames */ paused: false, }; } function updateKpis(st) { for (var i = 0; i < KPIS.length; i++) { var el = st.kpiEls[i]; if (!el) continue; var k = KPIS[i]; var raw = Math.round(k.base + wave(st.phase, i * 1.5) * k.range - k.range / 2); el.textContent = k.fmt(raw); } } function updateSparkline(st) { if (!st.linePath) return; var pts = []; for (var i = 0; i < LINE_PTS; i++) { var x = (i / (LINE_PTS - 1)) * LINE_W; var y = 8 + (1 - wave(st.phase * 0.7, i * 0.8)) * (LINE_H - 16); pts.push(x.toFixed(1) + ',' + y.toFixed(1)); } var d = 'M' + pts.join(' L'); st.linePath.setAttribute('d', d); if (st.fillPath) { st.fillPath.setAttribute('d', d + ' L' + LINE_W + ',' + LINE_H + ' L0,' + LINE_H + ' Z'); } } function tick(st) { if (!st.paused) { st.phase += SPEED * 16; st.ticker++; /* Update KPI text every 12 frames (~5/sec at 60fps) for legibility */ if (st.ticker >= 12) { st.ticker = 0; updateKpis(st); } updateSparkline(st); } requestAnimationFrame(function () { tick(st); }); } function observe(st) { if (!('IntersectionObserver' in window)) return; new IntersectionObserver(function (entries) { for (var i = 0; i < entries.length; i++) { st.paused = !entries[i].isIntersecting; } }, { rootMargin: '200px', threshold: 0.05 }).observe(st.stage); } function boot() { var stages = document.querySelectorAll('.ld-stage'); if (!stages.length) return; for (var i = 0; i < stages.length; i++) { if (stages[i]._ldAnim) continue; var st = makeState(stages[i]); stages[i]._ldAnim = st; observe(st); tick(st); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })(); /* ── 2. Transit Departure Board Animator ───────────────────────────────── */ (function () { 'use strict'; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { /* Still run the clock in reduced-motion mode */ startClocks(); return; } /* Departure data sets — cycle between these every CYCLE_MS */ var CYCLE_MS = 8000; var DATA_SETS = [ [ { time: '10:14', dest: 'London Victoria', plat: '2', status: 'On Time', cls: 'on-time' }, { time: '10:22', dest: 'Brighton', plat: '4', status: 'On Time', cls: 'on-time' }, { time: '10:31', dest: 'Gatwick Airport', plat: '1', status: 'Delayed', cls: 'delayed' }, { time: '10:45', dest: 'London Bridge', plat: '3', status: 'On Time', cls: 'on-time' }, { time: '11:02', dest: 'East Croydon', plat: '2', status: 'On Time', cls: 'on-time' }, ], [ { time: '10:22', dest: 'Brighton', plat: '4', status: 'On Time', cls: 'on-time' }, { time: '10:31', dest: 'Gatwick Airport', plat: '1', status: 'Delayed', cls: 'delayed' }, { time: '10:45', dest: 'London Bridge', plat: '3', status: 'On Time', cls: 'on-time' }, { time: '11:02', dest: 'East Croydon', plat: '2', status: 'On Time', cls: 'on-time' }, { time: '11:14', dest: 'London Victoria', plat: '2', status: 'On Time', cls: 'on-time' }, ], [ { time: '10:31', dest: 'Gatwick Airport', plat: '1', status: 'Delayed', cls: 'delayed' }, { time: '10:45', dest: 'London Bridge', plat: '3', status: 'On Time', cls: 'on-time' }, { time: '11:02', dest: 'East Croydon', plat: '2', status: 'On Time', cls: 'on-time' }, { time: '11:14', dest: 'London Victoria', plat: '2', status: 'On Time', cls: 'on-time' }, { time: '11:28', dest: 'Three Bridges', plat: '4', status: 'Cancelled', cls: 'cancelled'}, ], ]; /* ── Clock ── */ function startClocks() { var clocks = document.querySelectorAll('#transit-clock'); if (!clocks.length) return; function updateClock() { var now = new Date(); var hh = String(now.getHours()).padStart(2, '0'); var mm = String(now.getMinutes()).padStart(2, '0'); var ss = String(now.getSeconds()).padStart(2, '0'); var str = hh + ':' + mm + ':' + ss; for (var i = 0; i < clocks.length; i++) clocks[i].textContent = str; } updateClock(); setInterval(updateClock, 1000); } /* ── Flip helpers ── */ function flipCells(rowEl, newDest) { var flapEls = rowEl.querySelectorAll('.transit-flap'); var chars = newDest.split(''); /* Extend or shrink the flap container to match new length */ var destCell = rowEl.querySelector('.transit-cell--dest'); if (!destCell) return; /* Animate existing flaps, create/remove extras */ var i; for (i = 0; i < chars.length; i++) { var ch = chars[i] === ' ' ? '\u00a0' : chars[i]; if (i < flapEls.length) { /* Animate existing */ (function (el, character) { el.classList.add('is-flipping'); setTimeout(function () { el.textContent = character; el.classList.remove('is-flipping'); }, 125); })(flapEls[i], ch); } else { /* Append new flap */ var newFlap = document.createElement('span'); newFlap.className = 'transit-flap is-flipping'; newFlap.textContent = ch; destCell.appendChild(newFlap); setTimeout(function (el) { el.classList.remove('is-flipping'); }, 125, newFlap); } } /* Remove surplus flaps */ for (i = chars.length; i < flapEls.length; i++) { (function (el) { el.classList.add('is-flipping'); setTimeout(function () { el.parentNode && el.parentNode.removeChild(el); }, 250); })(flapEls[i]); } } function applyRow(rowEl, departure) { var timeEl = rowEl.querySelector('.transit-cell--time'); var platEl = rowEl.querySelector('.transit-cell--plat'); var statusEl = rowEl.querySelector('.transit-cell--status'); if (timeEl) timeEl.textContent = departure.time; if (platEl) platEl.textContent = departure.platform || departure.plat; if (statusEl) { statusEl.textContent = departure.status; statusEl.className = 'transit-cell transit-cell--status transit-status--' + departure.cls; } flipCells(rowEl, departure.dest); } function cycleBoard(stage, dataIdx) { var rows = stage.querySelectorAll('.transit-row'); var set = DATA_SETS[dataIdx % DATA_SETS.length]; for (var i = 0; i < Math.min(rows.length, set.length); i++) { /* Stagger each row by 180ms */ (function (row, dep) { setTimeout(function () { applyRow(row, dep); }, i * 180); })(rows[i], set[i]); } } function initBoard(stage) { var state = { idx: 0, timer: null, paused: false }; function advance() { if (state.paused) return; state.idx++; cycleBoard(stage, state.idx); } function startTimer() { if (state.timer) return; state.timer = setInterval(advance, CYCLE_MS); } function stopTimer() { clearInterval(state.timer); state.timer = null; } if ('IntersectionObserver' in window) { new IntersectionObserver(function (entries) { entries.forEach(function (e) { state.paused = !e.isIntersecting; e.isIntersecting ? startTimer() : stopTimer(); }); }, { rootMargin: '200px', threshold: 0.05 }).observe(stage); } startTimer(); } function boot() { startClocks(); var stages = document.querySelectorAll('.transit-stage'); if (!stages.length) return; for (var i = 0; i < stages.length; i++) { if (stages[i]._transitAnim) continue; stages[i]._transitAnim = true; initBoard(stages[i]); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })(); /* ── 3. Day-Part Clock Animator ────────────────────────────────────────── */ (function () { 'use strict'; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; function initDaypart(stage) { var clockEl = stage.querySelector('[data-daypart-clock]'); var badgeEl = stage.querySelector('[data-daypart-badge]'); if (!clockEl || !badgeEl) return; var visible = true; var observer = new IntersectionObserver(function (entries) { visible = entries[0].isIntersecting; }, { threshold: 0.1 }); observer.observe(stage); var simHour = 7; var simMin = 0; var parts = ['Morning', 'Afternoon', 'Evening']; function pad(n) { return n < 10 ? '0' + n : '' + n; } function tick() { if (!visible) { requestAnimationFrame(tick); return; } simMin += 1; if (simMin >= 60) { simMin = 0; simHour = (simHour + 1) % 24; } var displayHour = simHour % 12 || 12; var ampm = simHour < 12 ? 'AM' : 'PM'; clockEl.textContent = displayHour + ':' + pad(simMin) + ' ' + ampm; if (simHour >= 5 && simHour < 12) { badgeEl.textContent = parts[0]; } else if (simHour >= 12 && simHour < 17) { badgeEl.textContent = parts[1]; } else { badgeEl.textContent = parts[2]; } requestAnimationFrame(tick); } requestAnimationFrame(tick); } function boot() { var stages = document.querySelectorAll('.daypart-stage'); for (var i = 0; i < stages.length; i++) { if (stages[i]._daypartAnim) continue; stages[i]._daypartAnim = true; initDaypart(stages[i]); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();