From 2edbf9732b37c32180f5506b0c2a57fbd98e3a84 Mon Sep 17 00:00:00 2001 From: Matt Batchelder Date: Sat, 21 Feb 2026 13:59:57 -0500 Subject: [PATCH] Add industry mockup animator script for solutions page - Enqueued new script `industry-animator.js` for animated device mockups. - Implemented animation logic for various industries including hospitality, retail, corporate, education, outdoor, and live data displays. - Utilized IntersectionObserver for performance optimization by pausing animations when off-screen. --- pages/solutions.php | 12 +- theme/assets/css/main.css | 106 ++++++ theme/assets/js/industry-animator.js | 385 +++++++++++++++++++++ theme/blocks/index.php | 477 +++++++++++++++++++++++++++ theme/inc/enqueue.php | 9 + 5 files changed, 983 insertions(+), 6 deletions(-) create mode 100644 theme/assets/js/industry-animator.js diff --git a/pages/solutions.php b/pages/solutions.php index a88c6ab..b2b5c97 100644 --- a/pages/solutions.php +++ b/pages/solutions.php @@ -10,12 +10,12 @@ - - - - - - + + + + + + diff --git a/theme/assets/css/main.css b/theme/assets/css/main.css index a561889..6347abb 100644 --- a/theme/assets/css/main.css +++ b/theme/assets/css/main.css @@ -5422,3 +5422,109 @@ p:last-child { margin-bottom: 0; } .bd-ui__brand-bar { transform: scaleX(1); } .bd-ui__content { opacity: 1; transform: translateY(0); } } + +/* ═══════════════════════════════════════════════════════════════ + INDUSTRY MOCKUP ANIMATIONS (.platform-visual.has-industry) + ═══════════════════════════════════════════════════════════════ */ + +.platform-visual.has-industry { + background: none !important; + border: none !important; + border-radius: 0; + aspect-ratio: unset; + padding: 0; + overflow: visible; + box-shadow: none; + font-size: inherit; +} + +.ind-stage { + width: 100%; + max-width: 480px; + margin: 0 auto; +} +.ind-stage svg { + width: 100%; + height: auto; + display: block; + border-radius: var(--radius-md); + box-shadow: 0 8px 32px rgba(0,0,0,.3); +} + +/* Subtle glow around active screens */ +.ind-stage svg rect[fill="#1c2333"] { + transition: filter .6s ease; +} + +/* Animated content transitions */ +.ind-menu-price, +.ind-sale-tag, +.ind-footfall-val, +.ind-kpi-val, +.ind-meet-status, +.ind-alert-text, +.ind-weather-icon, +.ind-weather-temp, +.ind-busy-label, +.ind-ld-val, +.ind-ld-alert-text { + transition: opacity .3s ease; +} + +.ind-menu-bar, +.ind-rev-bar, +.ind-vendor-bar, +.ind-ld-bar { + transition: height .4s ease, y .4s ease; +} + +.ind-product-slot { + transition: stroke-width .3s ease; +} + +.ind-sched-slot { + transition: opacity .4s ease, stroke .3s ease; +} + +.ind-wf-dot { + transition: opacity .5s ease; +} + +.ind-alert-bar { + transition: opacity .4s ease; +} + +.ind-meet-dot, +.ind-busy-dot, +.ind-ld-alert { + transition: fill .4s ease; +} + +.ind-corp-line, +.ind-ld-line { + transition: d .3s ease; +} + +.ind-ld-pie { + transition: d .4s ease; +} + +/* ── Reduced-motion overrides for industry animations ── */ +@media (prefers-reduced-motion: reduce) { + .ind-menu-bar, + .ind-rev-bar, + .ind-vendor-bar, + .ind-ld-bar, + .ind-product-slot, + .ind-sched-slot, + .ind-wf-dot, + .ind-alert-bar, + .ind-corp-line, + .ind-ld-line, + .ind-ld-pie, + .ind-meet-dot, + .ind-busy-dot, + .ind-ld-alert { + transition: none !important; + } +} diff --git a/theme/assets/js/industry-animator.js b/theme/assets/js/industry-animator.js new file mode 100644 index 0000000..214dc18 --- /dev/null +++ b/theme/assets/js/industry-animator.js @@ -0,0 +1,385 @@ +/** + * 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(); + } +})(); diff --git a/theme/blocks/index.php b/theme/blocks/index.php index c9e597c..2c12f1b 100644 --- a/theme/blocks/index.php +++ b/theme/blocks/index.php @@ -558,6 +558,12 @@ add_action( 'init', function () { 'cameraAnim' => [ 'type' => 'boolean', 'default' => false ], 'neverGoesDark'=> [ 'type' => 'boolean', 'default' => false ], 'brandedAnim' => [ 'type' => 'boolean', 'default' => false ], + 'industryHospitality' => [ 'type' => 'boolean', 'default' => false ], + 'industryRetail' => [ 'type' => 'boolean', 'default' => false ], + 'industryCorporate' => [ 'type' => 'boolean', 'default' => false ], + 'industryEducation' => [ 'type' => 'boolean', 'default' => false ], + 'industryOutdoor' => [ 'type' => 'boolean', 'default' => false ], + 'industryLiveData' => [ 'type' => 'boolean', 'default' => false ], ], 'supports' => $block_supports, 'render_callback' => 'oribi_render_platform_row', @@ -1788,6 +1794,477 @@ function oribi_render_platform_row( $a ) { $ca .= ''; // cam-stage $visual_html = $ca; $visual_cls = 'platform-visual has-camera'; + + /* ── Industry: Hospitality ─────────────────────────────── */ + } elseif ( ! empty( $a['industryHospitality'] ) ) { + $visual_html = ''; + $visual_cls = 'platform-visual has-industry'; + + /* ── Industry: Retail ──────────────────────────────────── */ + } elseif ( ! empty( $a['industryRetail'] ) ) { + $visual_html = ''; + $visual_cls = 'platform-visual has-industry'; + + /* ── Industry: Corporate Office ─────────────────────────── */ + } elseif ( ! empty( $a['industryCorporate'] ) ) { + $visual_html = ''; + $visual_cls = 'platform-visual has-industry'; + + /* ── Industry: Education ───────────────────────────────── */ + } elseif ( ! empty( $a['industryEducation'] ) ) { + $visual_html = ''; + $visual_cls = 'platform-visual has-industry'; + + /* ── Industry: Outdoor Marketplace ─────────────────────── */ + } elseif ( ! empty( $a['industryOutdoor'] ) ) { + $visual_html = ''; + $visual_cls = 'platform-visual has-industry'; + + /* ── Industry: Live Data Displays ──────────────────────── */ + } elseif ( ! empty( $a['industryLiveData'] ) ) { + $visual_html = ''; + $visual_cls = 'platform-visual has-industry'; + } else { $visual_html = oribi_render_icon( $a['visual'] ?? '' ); $visual_cls = 'platform-visual'; diff --git a/theme/inc/enqueue.php b/theme/inc/enqueue.php index a46cb8b..0bcb17d 100644 --- a/theme/inc/enqueue.php +++ b/theme/inc/enqueue.php @@ -38,6 +38,15 @@ add_action( 'wp_enqueue_scripts', function () { true ); + // Industry mockup animator - animated device mockups for solutions page + wp_enqueue_script( + 'oribi-industry-animator', + ORIBI_URI . '/assets/js/industry-animator.js', + [], + ORIBI_VERSION . '.' . filemtime( ORIBI_DIR . '/assets/js/industry-animator.js' ), + true + ); + // Localize AJAX endpoint for the contact form wp_localize_script( 'oribi-main', 'oribiAjax', [ 'url' => admin_url( 'admin-ajax.php' ),