/** * Industry Mockup Animator * Animates SVG environment mockups for each industry on the solutions page. * Each mockup shows devices in real-world use with animated screen content. * Pauses off-screen via IntersectionObserver for performance. */ (function () { 'use strict'; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; var SPEED = 0.0018; /* ── Utility ────────────────────────────────────────────── */ function wave(t, off) { return Math.max(0, Math.min(1, 0.55 + Math.sin(t + off) * 0.25 + Math.sin(t * 1.8 + off * 1.3) * 0.15 )); } function lerp(a, b, t) { return a + (b - a) * t; } function isDark() { return document.documentElement.getAttribute('data-theme') === 'dark'; } /* ── Hospitality ────────────────────────────────────────── */ function initHospitality(svg, st) { st.menuPrices = svg.querySelectorAll('.ind-menu-price'); st.menuBars = svg.querySelectorAll('.ind-menu-bar'); st.promoBanner = svg.querySelector('.ind-promo-text'); st.promoPhase = 0; st.promos = ['HAPPY HOUR 5-7PM', 'NEW: SUMMER MENU', '20% OFF DESSERTS', 'LIVE MUSIC FRI']; } function tickHospitality(st) { // Animate menu prices (shimmer) for (var i = 0; i < st.menuPrices.length; i++) { var v = wave(st.phase, i * 1.4); var price = (5 + Math.round(v * 20)).toFixed(2); st.menuPrices[i].textContent = '$' + price; } // Animate popularity bars for (var j = 0; j < st.menuBars.length; j++) { var bv = wave(st.phase * 0.6, j * 1.8); st.menuBars[j].setAttribute('width', Math.round(bv * 50 + 10)); } // Cycle promo banner st.promoPhase += SPEED * 0.3; if (st.promoPhase > 1) { st.promoPhase = 0; if (st.promoBanner) { var idx = Math.floor(Math.random() * st.promos.length); st.promoBanner.textContent = st.promos[idx]; } } } /* ── Retail ─────────────────────────────────────────────── */ function initRetail(svg, st) { st.saleTags = svg.querySelectorAll('.ind-sale-tag'); st.footfall = svg.querySelector('.ind-footfall-val'); st.revBars = svg.querySelectorAll('.ind-rev-bar'); st.productSlots = svg.querySelectorAll('.ind-product-slot'); st.productPhase = 0; } function tickRetail(st) { // Sale tags pulse opacity for (var i = 0; i < st.saleTags.length; i++) { var op = 0.5 + wave(st.phase * 1.2, i * 2.0) * 0.5; st.saleTags[i].setAttribute('opacity', op.toFixed(2)); } // Footfall counter if (st.footfall) { var count = Math.round(wave(st.phase * 0.3, 0) * 450 + 50); st.footfall.textContent = count; } // Revenue bars for (var j = 0; j < st.revBars.length; j++) { var rv = wave(st.phase * 0.5, j * 1.5); var h = Math.round(rv * 35 + 5); st.revBars[j].setAttribute('height', h); st.revBars[j].setAttribute('y', 40 - h); } // Product slots cycle highlight st.productPhase += SPEED * 0.15; if (st.productPhase > 1) { st.productPhase = 0; var active = Math.floor(Math.random() * st.productSlots.length); for (var k = 0; k < st.productSlots.length; k++) { st.productSlots[k].setAttribute('stroke-width', k === active ? '2' : '0'); } } } /* ── Corporate ──────────────────────────────────────────── */ function initCorporate(svg, st) { st.kpiVals = svg.querySelectorAll('.ind-kpi-val'); st.kpiArrows = svg.querySelectorAll('.ind-kpi-arrow'); st.meetStatus = svg.querySelector('.ind-meet-status'); st.meetDot = svg.querySelector('.ind-meet-dot'); st.meetPhase = 0; st.statuses = ['Available', 'In Meeting', 'Reserved 2:30', 'Available']; st.statusIdx = 0; st.linePath = svg.querySelector('.ind-corp-line'); st.lineW = 140; st.linePts = 6; } function tickCorporate(st) { // KPI counters for (var i = 0; i < st.kpiVals.length; i++) { var kv = wave(st.phase * 0.6, i * 2.2); var vals = [ Math.round(kv * 98) + '%', Math.round(kv * 340 + 60), Math.round(kv * 50 + 10) + 'ms' ]; if (vals[i]) st.kpiVals[i].textContent = vals[i]; } // KPI arrows for (var j = 0; j < st.kpiArrows.length; j++) { var up = wave(st.phase * 0.6, j * 2.2) > 0.5; st.kpiArrows[j].setAttribute('transform', up ? 'rotate(0)' : 'rotate(180)'); st.kpiArrows[j].setAttribute('fill', up ? '#4CAF50' : '#ef4444'); } // Meeting room status cycle st.meetPhase += SPEED * 0.15; if (st.meetPhase > 1) { st.meetPhase = 0; st.statusIdx = (st.statusIdx + 1) % st.statuses.length; if (st.meetStatus) st.meetStatus.textContent = st.statuses[st.statusIdx]; if (st.meetDot) { st.meetDot.setAttribute('fill', st.statusIdx === 1 ? '#ef4444' : st.statusIdx === 2 ? '#f59e0b' : '#4CAF50'); } } // Line chart if (st.linePath) { var d = 'M'; for (var p = 0; p < st.linePts; p++) { var x = (p / (st.linePts - 1)) * st.lineW; var y = 10 + (1 - wave(st.phase * 0.8, p * 0.9)) * 30; d += (p ? ' L' : '') + x.toFixed(1) + ',' + y.toFixed(1); } st.linePath.setAttribute('d', d); } } /* ── Education ──────────────────────────────────────────── */ function initEducation(svg, st) { st.schedSlots = svg.querySelectorAll('.ind-sched-slot'); st.alertBar = svg.querySelector('.ind-alert-bar'); st.alertText = svg.querySelector('.ind-alert-text'); st.alertPhase = 0; st.alerts = ['Fire Drill 2:00 PM', 'Early Dismissal Fri', 'Gym Closed Today', 'Bus 12 Delayed']; st.alertIdx = 0; st.wayfindDots = svg.querySelectorAll('.ind-wf-dot'); st.wfActive = 0; st.wfPhase = 0; } function tickEducation(st) { // Schedule slots pulse (current class highlight) for (var i = 0; i < st.schedSlots.length; i++) { var isCurrent = (Math.floor(st.phase * 0.3) % st.schedSlots.length) === i; st.schedSlots[i].setAttribute('opacity', isCurrent ? '1' : '0.5'); st.schedSlots[i].setAttribute('stroke', isCurrent ? '#D83302' : 'none'); st.schedSlots[i].setAttribute('stroke-width', isCurrent ? '1.5' : '0'); } // Alert banner cycle st.alertPhase += SPEED * 0.12; if (st.alertPhase > 1) { st.alertPhase = 0; st.alertIdx = (st.alertIdx + 1) % st.alerts.length; if (st.alertText) st.alertText.textContent = st.alerts[st.alertIdx]; } // Alert bar pulse if (st.alertBar) { var ap = 0.6 + Math.sin(st.phase * 2) * 0.4; st.alertBar.setAttribute('opacity', ap.toFixed(2)); } // Wayfinding dot sequence st.wfPhase += SPEED * 0.2; if (st.wfPhase > 1) { st.wfPhase = 0; st.wfActive = (st.wfActive + 1) % Math.max(st.wayfindDots.length, 1); } for (var w = 0; w < st.wayfindDots.length; w++) { st.wayfindDots[w].setAttribute('opacity', w === st.wfActive ? '1' : '0.2'); } } /* ── Outdoor Marketplace ────────────────────────────────── */ function initOutdoor(svg, st) { st.weatherIcon = svg.querySelector('.ind-weather-icon'); st.weatherTemp = svg.querySelector('.ind-weather-temp'); st.weatherPhase = 0; st.weathers = [ { icon: '\u2600', temp: '24°C' }, { icon: '\u26C5', temp: '19°C' }, { icon: '\u2601', temp: '16°C' }, { icon: '\u2600', temp: '22°C' } ]; st.weatherIdx = 0; st.vendorBars = svg.querySelectorAll('.ind-vendor-bar'); st.busyDot = svg.querySelector('.ind-busy-dot'); st.busyLabel = svg.querySelector('.ind-busy-label'); } function tickOutdoor(st) { // Weather cycle st.weatherPhase += SPEED * 0.1; if (st.weatherPhase > 1) { st.weatherPhase = 0; st.weatherIdx = (st.weatherIdx + 1) % st.weathers.length; var w = st.weathers[st.weatherIdx]; if (st.weatherIcon) st.weatherIcon.textContent = w.icon; if (st.weatherTemp) st.weatherTemp.textContent = w.temp; } // Vendor activity bars for (var i = 0; i < st.vendorBars.length; i++) { var bv = wave(st.phase * 0.5, i * 1.6); var h = Math.round(bv * 25 + 3); st.vendorBars[i].setAttribute('height', h); st.vendorBars[i].setAttribute('y', 28 - h); } // Busy indicator pulse if (st.busyDot) { var busy = wave(st.phase * 0.3, 0) > 0.6; st.busyDot.setAttribute('fill', busy ? '#ef4444' : '#4CAF50'); if (st.busyLabel) st.busyLabel.textContent = busy ? 'Busy' : 'Quiet'; } } /* ── Live Data Displays ─────────────────────────────────── */ function initLiveData(svg, st) { st.ldBars = svg.querySelectorAll('.ind-ld-bar'); st.ldVals = svg.querySelectorAll('.ind-ld-val'); st.ldLine = svg.querySelector('.ind-ld-line'); st.ldLineW = 110; st.ldLinePts = 8; st.ldPieSegs = svg.querySelectorAll('.ind-ld-pie'); st.ldPieR = 22; st.ldAlertDot = svg.querySelector('.ind-ld-alert'); st.ldAlertText = svg.querySelector('.ind-ld-alert-text'); st.ldAlertPhase = 0; st.ldAlerts = ['All Systems OK', 'CPU: 72%', 'Latency: 12ms', 'Queue: 340']; st.ldAlertIdx = 0; } function tickLiveData(st) { // Bars animate for (var i = 0; i < st.ldBars.length; i++) { var bv = wave(st.phase, i * 1.1); var h = Math.round(bv * 30 + 5); st.ldBars[i].setAttribute('height', h); st.ldBars[i].setAttribute('y', 35 - h); } // Values update for (var v = 0; v < st.ldVals.length; v++) { var val = wave(st.phase, v * 1.1); st.ldVals[v].textContent = Math.round(val * 5000); } // Line chart if (st.ldLine) { var d = 'M'; for (var p = 0; p < st.ldLinePts; p++) { var x = (p / (st.ldLinePts - 1)) * st.ldLineW; var y = 5 + (1 - wave(st.phase * 0.8, p * 0.9)) * 30; d += (p ? ' L' : '') + x.toFixed(1) + ',' + y.toFixed(1); } st.ldLine.setAttribute('d', d); } // Pie chart if (st.ldPieSegs.length) { var n = st.ldPieSegs.length; var weights = [], total = 0; for (var pi = 0; pi < n; pi++) { var pw = 0.5 + wave(st.phase * 0.4, pi * 2.0) * 0.5; weights.push(pw); total += pw; } var angle = 0; for (var pj = 0; pj < n; pj++) { var sweep = (weights[pj] / total) * 360; var startA = angle * Math.PI / 180; var endA = (angle + sweep) * Math.PI / 180; var large = sweep > 180 ? 1 : 0; var r = st.ldPieR; var x1 = Math.sin(startA) * r, y1 = -Math.cos(startA) * r; var x2 = Math.sin(endA) * r, y2 = -Math.cos(endA) * r; var path = st.ldPieSegs[pj]; if (path) { path.setAttribute('d', 'M0,0 L' + x1.toFixed(2) + ',' + y1.toFixed(2) + ' A' + r + ',' + r + ' 0 ' + large + ',1 ' + x2.toFixed(2) + ',' + y2.toFixed(2) + ' Z'); } angle += sweep; } } // Alert text cycle st.ldAlertPhase += SPEED * 0.12; if (st.ldAlertPhase > 1) { st.ldAlertPhase = 0; st.ldAlertIdx = (st.ldAlertIdx + 1) % st.ldAlerts.length; if (st.ldAlertText) st.ldAlertText.textContent = st.ldAlerts[st.ldAlertIdx]; if (st.ldAlertDot) { st.ldAlertDot.setAttribute('fill', st.ldAlertIdx === 0 ? '#4CAF50' : '#f59e0b'); } } } /* ── Registry ───────────────────────────────────────────── */ var INDUSTRIES = { hospitality: { init: initHospitality, tick: tickHospitality }, retail: { init: initRetail, tick: tickRetail }, corporate: { init: initCorporate, tick: tickCorporate }, education: { init: initEducation, tick: tickEducation }, outdoor: { init: initOutdoor, tick: tickOutdoor }, livedata: { init: initLiveData, tick: tickLiveData } }; /* ── Main loop ──────────────────────────────────────────── */ var instances = []; function tickAll() { for (var i = 0; i < instances.length; i++) { var st = instances[i]; if (st.paused) continue; st.phase += SPEED * 16; st.handler.tick(st); } requestAnimationFrame(tickAll); } 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.el); } function boot() { var els = document.querySelectorAll('[data-industry-anim]'); if (!els.length) return; for (var i = 0; i < els.length; i++) { var el = els[i]; var type = el.getAttribute('data-industry-anim'); var handler = INDUSTRIES[type]; if (!handler) continue; if (el._indAnim) continue; var svg = el.querySelector('svg'); if (!svg) continue; var st = { el: el, svg: svg, handler: handler, phase: Math.random() * Math.PI * 2, paused: false }; handler.init(svg, st); observe(st); el._indAnim = st; instances.push(st); } if (instances.length) requestAnimationFrame(tickAll); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();