/** * OTS Theme - Main JS */ /* ── Theme toggle (runs before DOMContentLoaded to prevent flash) ── */ (function() { const saved = localStorage.getItem('oribi-theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const theme = saved || (prefersDark ? 'dark' : 'light'); document.documentElement.setAttribute('data-theme', theme); })(); document.addEventListener('DOMContentLoaded', () => { /* ── Sticky header ──────────────────────────────────────── */ const header = document.getElementById('site-header'); if (header) { window.addEventListener('scroll', () => { header.classList.toggle('scrolled', window.scrollY > 40); }, { passive: true }); } /* ── Mobile nav toggle ──────────────────────────────────── */ const toggle = document.getElementById('nav-toggle'); const nav = document.getElementById('site-nav'); if (toggle && nav) { toggle.addEventListener('click', () => { toggle.classList.toggle('open'); nav.classList.toggle('open'); const expanded = toggle.getAttribute('aria-expanded') === 'true'; toggle.setAttribute('aria-expanded', !expanded); }); } /* ── Mobile sub-menu accordion ──────────────────────────── */ if (nav) { nav.addEventListener('click', (e) => { // Only act in mobile (nav-toggle visible = mobile view) if (!toggle || getComputedStyle(toggle).display === 'none') return; const parentLi = e.target.closest('.menu-item-has-children'); if (!parentLi) return; // If the click is on the parent link itself (not a child link), toggle const clickedLink = e.target.closest('a'); const subMenu = parentLi.querySelector(':scope > .sub-menu'); if (!subMenu) return; // If they clicked a sub-menu link, let it navigate normally if (clickedLink && subMenu.contains(clickedLink)) return; // Toggle this item; collapse siblings const isOpen = parentLi.classList.contains('submenu-open'); parentLi.closest('.nav-menu') .querySelectorAll('.menu-item-has-children.submenu-open') .forEach(li => li.classList.remove('submenu-open')); if (!isOpen) { parentLi.classList.add('submenu-open'); // Prevent the parent anchor from navigating when toggling if (clickedLink && parentLi.contains(clickedLink) && !subMenu.contains(clickedLink)) { e.preventDefault(); } } }); } /* ── Scroll-to-top ──────────────────────────────────────── */ const scrollBtn = document.getElementById('scroll-top'); if (scrollBtn) { window.addEventListener('scroll', () => { scrollBtn.classList.toggle('visible', window.scrollY > 600); }, { passive: true }); scrollBtn.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); } /* ── Light / Dark theme toggle ─────────────────────────── */ const themeToggle = document.getElementById('theme-toggle'); if (themeToggle) { themeToggle.addEventListener('click', () => { const current = document.documentElement.getAttribute('data-theme') || 'light'; const next = current === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', next); localStorage.setItem('oribi-theme', next); themeToggle.setAttribute('aria-label', next === 'dark' ? 'Switch to light mode' : 'Switch to dark mode' ); }); } /* ── Contact form (AJAX) ────────────────────────────────── */ const form = document.getElementById('contact-form'); const notice = document.getElementById('form-notice'); if (form && typeof oribiAjax !== 'undefined') { form.addEventListener('submit', async (e) => { e.preventDefault(); notice.className = 'form-notice'; notice.style.display = 'none'; const data = new FormData(form); data.append('action', 'oribi_contact'); data.append('nonce', oribiAjax.nonce); try { const res = await fetch(oribiAjax.url, { method: 'POST', body: data }); const json = await res.json(); notice.textContent = json.data; notice.className = 'form-notice ' + (json.success ? 'success' : 'error'); notice.style.display = 'block'; if (json.success) form.reset(); } catch { notice.textContent = 'Something went wrong. Please try again.'; notice.className = 'form-notice error'; notice.style.display = 'block'; } }); } /* ── Animate cards on scroll ────────────────────────────── */ const cards = document.querySelectorAll('.oribi-card, .feature-card, .industry-card, .pricing-card, .value-card, .platform-row'); if (cards.length && 'IntersectionObserver' in window) { cards.forEach(c => { c.style.opacity = '0'; c.style.transform = 'translateY(24px)'; c.style.transition = 'opacity .5s ease, transform .5s ease'; }); const io = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.style.opacity = '1'; entry.target.style.transform = 'translateY(0)'; io.unobserve(entry.target); } }); }, { threshold: 0.1 }); cards.forEach(c => io.observe(c)); } }); /* -- Datacenter hero background canvas ----------------------------------------- */ (function () { const canvas = document.getElementById('dc-canvas'); if (!canvas) return; const ctx = canvas.getContext('2d'); /* * Performance: * 1. bgCanvas -- static geometry painted once per resize, blit each frame. * 2. LED draws batched by colour -- one shadowBlur setup per colour (~4/frame). * 3. 30 fps cap. * 4. Visibility API + IntersectionObserver pause rAF when hidden/off-screen. */ /* -- colour palette -------------------------------------------------------- */ const ROOM_TOP = '#020509'; const ROOM_BOT = '#030b08'; const RACK_SHELL = '#111b2e'; /* outer frame -- dark navy */ const RACK_SHELL2 = '#0c1422'; /* outer frame shadow side */ const RACK_FACE = '#0d1728'; /* inner face panel background */ const RAIL_FACE = '#141f35'; /* mounting rail column face */ const RAIL_EDGE = '#1c2d4a'; /* rail inner-edge highlight */ const SCREW_COL = '#0a1220'; /* screw/nut recesses on rails */ const SRV_FACE = '#1a2840'; /* server 1U face -- lighter than rack */ const SRV_STRIPE = '#1f3050'; /* top-edge highlight stripe */ const SRV_SHADOW = '#0e1a2b'; /* bottom-edge shadow line */ const SRV_OFF = '#0d1624'; /* unpowered slot */ const PWRBTN_RING = '#243654'; /* power button outer ring */ const PWRBTN_FACE = '#101e30'; /* power button recessed face */ const VENT_SLOT = '#0b1421'; /* vent/louver slots */ const BAY_SLOT = '#09111e'; /* drive bay recesses */ const BAY_EDGE = '#1d2f48'; /* drive bay raised edge */ const PATCH_BODY = '#111d30'; /* patch unit body */ const PATCH_PORT = '#070d18'; /* patch port holes */ const PATCH_LBL = '#0b1422'; /* patch label strip */ const CAB_TROUGH = '#080f1c'; /* cable management trough */ const LED_OFF = '#182438'; /* unlit LED placeholder */ const LED_COLORS = { green: '#00f07a', amber: '#ffb200', red: '#ff3838', blue: '#00aaff' }; /* -- depth layers (back -> front) railW = thickness of side mounting-rail columns padTop = vertical padding inside rack before first unit -- */ const LAYERS = [ { alpha: 0.28, yShift: 0.12, rackW: 54, rackGap: 18, unitH: 12, unitGap: 1, railW: 5, padTop: 7, ledSz: 2, ledCols: 3, ledRows: 2 }, { alpha: 0.55, yShift: 0.05, rackW: 80, rackGap: 28, unitH: 18, unitGap: 2, railW: 7, padTop: 9, ledSz: 3, ledCols: 3, ledRows: 2 }, { alpha: 1.00, yShift: 0.00, rackW: 112, rackGap: 40, unitH: 25, unitGap: 2, railW: 10, padTop: 11, ledSz: 4, ledCols: 4, ledRows: 2 }, ]; /* -- shared LED-position helpers (same formula in paintBg & drawLEDs) ----- */ function ledOriginX(ux, uw, ledCols, ledSz) { return ux + uw - ledCols * (ledSz + 3) - 3; } function ledOriginY(uy, unitH, ledRows, ledSz) { return uy + ((unitH - ledRows * (ledSz + 2) + 1) / 2 | 0); } /* -- state ----------------------------------------------------------------- */ let layers = []; const bgCanvas = document.createElement('canvas'); const bgCtx = bgCanvas.getContext('2d'); let scanY = 0; let lastTs = 0; let rafId; let W = 1, H = 1; const FRAME_MS = 1000 / 30; /* 30 fps throttle */ let fpsDebt = 0; /* -- build rack data for one layer ---------------------------------------- */ function buildLayer(def) { const { rackW, rackGap, unitH, unitGap, railW, padTop, ledCols, ledRows, yShift } = def; const numRacks = Math.ceil(W / (rackW + rackGap)) + 2; const racks = []; for (let r = 0; r < numRacks; r++) { const rx = r * (rackW + rackGap) - rackGap; const numUnits = Math.floor((H * (1 - Math.abs(yShift) * 2) - padTop * 2) / (unitH + unitGap)); const units = []; const activity = { timer: Math.random() * 8000, period: 5000 + Math.random() * 12000, active: false, burstTimer: 0, burstLength: 0, }; for (let u = 0; u < numUnits; u++) { const leds = []; for (let l = 0; l < ledCols * ledRows; l++) { const rnd = Math.random(); const type = rnd < 0.68 ? 'green' : rnd < 0.86 ? 'amber' : rnd < 0.96 ? 'red' : 'blue'; leds.push({ type, on: Math.random() > 0.2, blinkPeriod: 400 + Math.random() * 5000, timer: Math.random() * 5000 }); } units.push({ leds, powered: Math.random() > 0.08 }); } racks.push({ rx, units, activity, patchRows: 2 + Math.floor(Math.random() * 2) }); } return racks; } /* -- paint static geometry to bgCanvas (once per resize) ------------------ */ function paintBg() { bgCanvas.width = W; bgCanvas.height = H; /* room background */ const bg = bgCtx.createLinearGradient(0, 0, 0, H); bg.addColorStop(0, ROOM_TOP); bg.addColorStop(0.5, '#05080f'); bg.addColorStop(1, ROOM_BOT); bgCtx.fillStyle = bg; bgCtx.fillRect(0, 0, W, H); /* ceiling fluorescent bar */ const ceil = bgCtx.createLinearGradient(0, 0, 0, H * 0.10); ceil.addColorStop(0, 'rgba(140,190,255,0.13)'); ceil.addColorStop(1, 'rgba(140,190,255,0)'); bgCtx.fillStyle = ceil; bgCtx.fillRect(0, 0, W, H * 0.10); /* floor glow */ const flr = bgCtx.createLinearGradient(0, H * 0.84, 0, H); flr.addColorStop(0, 'rgba(0,220,110,0)'); flr.addColorStop(1, 'rgba(0,180,90,0.10)'); bgCtx.fillStyle = flr; bgCtx.fillRect(0, H * 0.84, W, H * 0.16); for (const layer of layers) { const { def, racks } = layer; const { rackW, unitH, unitGap, railW, padTop, ledSz, ledCols, ledRows, alpha, yShift } = def; const layerY = H * yShift; const uw = rackW - 2 * railW; bgCtx.save(); bgCtx.globalAlpha = alpha; for (const rack of racks) { const { rx, units, patchRows } = rack; const totalRows = units.length + patchRows; const rackH = totalRows * (unitH + unitGap) - unitGap + padTop * 2 + 6; const ry = Math.floor((H - rackH) / 2) + layerY; const ux = rx + railW; /* 1. rack shadow (cartoon depth) */ bgCtx.fillStyle = RACK_SHELL2; bgCtx.fillRect(rx + 2, ry + 2, rackW, rackH); /* 2. rack outer shell */ bgCtx.fillStyle = RACK_SHELL; bgCtx.fillRect(rx, ry, rackW, rackH); /* cartoon top/left highlights */ bgCtx.fillStyle = 'rgba(255,255,255,0.13)'; bgCtx.fillRect(rx, ry, rackW, 1); bgCtx.fillStyle = 'rgba(255,255,255,0.07)'; bgCtx.fillRect(rx, ry, 1, rackH); /* 3. inner face panel */ bgCtx.fillStyle = RACK_FACE; bgCtx.fillRect(ux, ry + 1, uw, rackH - 2); /* 4. mounting rail columns */ [rx, rx + rackW - railW].forEach(function(cx) { bgCtx.fillStyle = RAIL_FACE; bgCtx.fillRect(cx, ry, railW, rackH); /* inner-edge accent */ const edgeX = (cx === rx) ? cx + railW - 1 : cx; bgCtx.fillStyle = RAIL_EDGE; bgCtx.fillRect(edgeX, ry, 1, rackH); /* screw holes */ const screwX = cx + (railW / 2 | 0) - 1; const screwSz = Math.max(2, railW * 0.28 | 0); let sy = ry + padTop; while (sy < ry + rackH - padTop) { bgCtx.fillStyle = SCREW_COL; bgCtx.fillRect(screwX, sy, screwSz, screwSz); sy += unitH + unitGap; } }); /* 5. top cap brace */ bgCtx.fillStyle = 'rgba(255,255,255,0.05)'; bgCtx.fillRect(ux, ry, uw, padTop); /* 6. cable management trough */ bgCtx.fillStyle = CAB_TROUGH; bgCtx.fillRect(ux, ry + rackH - padTop - 2, uw, padTop + 2); bgCtx.fillStyle = 'rgba(255,255,255,0.04)'; bgCtx.fillRect(ux, ry + rackH - padTop - 2, uw, 1); /* 7. patch panel rows */ for (let p = 0; p < patchRows; p++) { const py = ry + padTop + p * (unitH + unitGap); bgCtx.fillStyle = PATCH_BODY; bgCtx.fillRect(ux, py, uw, unitH); /* top bevel + bottom shadow */ bgCtx.fillStyle = 'rgba(255,255,255,0.08)'; bgCtx.fillRect(ux, py, uw, 1); bgCtx.fillStyle = 'rgba(0,0,0,0.38)'; bgCtx.fillRect(ux, py + unitH - 1, uw, 1); /* label strip (left ~18%) */ const lblW = Math.max(6, uw * 0.18 | 0); bgCtx.fillStyle = PATCH_LBL; bgCtx.fillRect(ux + 2, py + 2, lblW, unitH - 4); /* RJ-45 style port holes */ const portsX = ux + lblW + 4; const portsW = uw - lblW - 6; const portW = Math.max(3, unitH * 0.50 | 0); const portH = Math.max(2, unitH * 0.38 | 0); const portY = py + ((unitH - portH) / 2 | 0); const numPort = Math.floor(portsW / (portW + 2)); bgCtx.fillStyle = PATCH_PORT; for (let pp = 0; pp < numPort; pp++) { bgCtx.fillRect(portsX + pp * (portW + 2), portY, portW, portH); } } /* 8. server units */ for (let u = 0; u < units.length; u++) { const unit = units[u]; const uy = ry + padTop + (u + patchRows) * (unitH + unitGap); if (!unit.powered) { /* empty / powered-off slot */ bgCtx.fillStyle = SRV_OFF; bgCtx.fillRect(ux, uy, uw, unitH); bgCtx.fillStyle = 'rgba(0,0,0,0.32)'; bgCtx.fillRect(ux, uy + unitH - 1, uw, 1); continue; } /* server face */ bgCtx.fillStyle = SRV_FACE; bgCtx.fillRect(ux, uy, uw, unitH); /* top highlight stripe */ bgCtx.fillStyle = SRV_STRIPE; bgCtx.fillRect(ux, uy, uw, 1); /* bottom shadow line */ bgCtx.fillStyle = SRV_SHADOW; bgCtx.fillRect(ux, uy + unitH - 1, uw, 1); /* -- power button (left section) -- */ if (unitH >= 12) { const pBtnR = Math.max(2, unitH * 0.18 | 0); const pBtnX = ux + railW + pBtnR + 1; const pBtnY = uy + (unitH / 2 | 0) - pBtnR; /* outer ring */ bgCtx.fillStyle = PWRBTN_RING; bgCtx.fillRect(pBtnX - 1, pBtnY - 1, pBtnR * 2 + 2, pBtnR * 2 + 2); /* inner recess */ bgCtx.fillStyle = PWRBTN_FACE; bgCtx.fillRect(pBtnX, pBtnY, pBtnR * 2, pBtnR * 2); } /* -- vent / louver slots -- */ if (unitH >= 14) { const pBtnR = Math.max(2, unitH * 0.18 | 0); const ventStartX = ux + railW + pBtnR * 2 + 5; const ventH = Math.max(3, unitH - 6); const ventY = uy + 3; const numVents = Math.min(5, Math.floor(uw * 0.10 / 3)); bgCtx.fillStyle = VENT_SLOT; for (let v = 0; v < numVents; v++) { bgCtx.fillRect(ventStartX + v * 3, ventY, 1, ventH); } } /* -- drive bays (centre section) -- */ const ledPanelW = ledCols * (ledSz + 3) + 5; const rightStop = ux + uw - ledPanelW; const leftStop = ux + Math.max( railW * 2 + (unitH * 0.18 | 0) * 2 + (unitH >= 14 ? 14 : 4) + 4, uw * 0.22 | 0 ); const bayAreaW = rightStop - leftStop; if (bayAreaW > 8 && unitH >= 10) { const bayH = Math.max(3, unitH * 0.46 | 0); const bayW = Math.max(4, Math.min(11, bayAreaW / 5 | 0)); const bayY = uy + ((unitH - bayH) / 2 | 0); const numBays = Math.min(8, Math.floor(bayAreaW / (bayW + 2))); for (let b = 0; b < numBays; b++) { const bx = leftStop + b * (bayW + 2); /* bay recess */ bgCtx.fillStyle = BAY_SLOT; bgCtx.fillRect(bx, bayY, bayW, bayH); /* bay top-edge highlight */ bgCtx.fillStyle = BAY_EDGE; bgCtx.fillRect(bx, bayY, bayW, 1); } } /* -- off-state LED panel (right section) -- */ const lox = ledOriginX(ux, uw, ledCols, ledSz); const loy = ledOriginY(uy, unitH, ledRows, ledSz); /* panel inset background */ bgCtx.fillStyle = 'rgba(0,0,0,0.28)'; bgCtx.fillRect(lox - 2, uy + 2, ledCols * (ledSz + 3) + 1, unitH - 4); /* placeholder dots */ bgCtx.fillStyle = LED_OFF; for (let row = 0; row < ledRows; row++) { for (let col = 0; col < ledCols; col++) { bgCtx.fillRect( lox + col * (ledSz + 3), loy + row * (ledSz + 2), ledSz, ledSz ); } } } } bgCtx.restore(); } } /* -- collect LED state & draw all lit LEDs batched by colour -------------- */ function drawLEDs(dt) { const buckets = { green: [], amber: [], red: [], blue: [] }; for (const layer of layers) { const { def, racks } = layer; const { rackW, unitH, unitGap, railW, padTop, ledSz, ledCols, ledRows, alpha, yShift } = def; const uw = rackW - 2 * railW; const layerY = H * yShift; for (const rack of racks) { const { rx, units, activity, patchRows } = rack; const totalRows = units.length + patchRows; const rackH = totalRows * (unitH + unitGap) - unitGap + padTop * 2 + 6; const ry = Math.floor((H - rackH) / 2) + layerY; const ux = rx + railW; /* rack status strip on left rail */ buckets['green'].push({ x: rx + 1, y: ry + padTop, w: 2, h: rackH - padTop * 2, a: alpha * 0.28, }); /* per-unit LEDs */ for (let u = 0; u < units.length; u++) { const unit = units[u]; if (!unit.powered) continue; const uy = ry + padTop + (u + patchRows) * (unitH + unitGap); const lox = ledOriginX(ux, uw, ledCols, ledSz); const loy = ledOriginY(uy, unitH, ledRows, ledSz); for (let row = 0; row < ledRows; row++) { for (let col = 0; col < ledCols; col++) { const led = unit.leds[row * ledCols + col]; led.timer += dt; if (led.timer >= led.blinkPeriod) { led.timer = 0; if (led.type === 'amber' || Math.random() < 0.2) led.on = !led.on; } if (!led.on) continue; buckets[led.type].push({ x: lox + col * (ledSz + 3), y: loy + row * (ledSz + 2), w: ledSz, h: ledSz, a: alpha * 0.92, }); } } } } } /* one shadowBlur setup per colour */ for (const [key, rects] of Object.entries(buckets)) { if (!rects.length) continue; const color = LED_COLORS[key]; ctx.shadowColor = color; ctx.shadowBlur = 8; ctx.fillStyle = color; for (const r of rects) { ctx.globalAlpha = r.a; ctx.fillRect(r.x, r.y, r.w, r.h); } } ctx.shadowBlur = 0; ctx.shadowColor = 'transparent'; ctx.globalAlpha = 1; } /* -- main render loop ------------------------------------------------------ */ function draw(ts) { rafId = requestAnimationFrame(draw); const elapsed = ts - lastTs; lastTs = ts; fpsDebt += elapsed; if (fpsDebt < FRAME_MS) return; const dt = Math.min(fpsDebt, 80); fpsDebt = fpsDebt % FRAME_MS; /* blit static geometry */ ctx.drawImage(bgCanvas, 0, 0); /* mid-aisle ambient glow */ const aisle = ctx.createLinearGradient(0, H * 0.46, 0, H * 0.58); aisle.addColorStop(0, 'rgba(0,80,180,0)'); aisle.addColorStop(0.5, 'rgba(0,80,180,0.022)'); aisle.addColorStop(1, 'rgba(0,80,180,0)'); ctx.fillStyle = aisle; ctx.fillRect(0, H * 0.44, W, H * 0.16); /* lit LEDs */ drawLEDs(dt); /* sweeping scan line */ scanY = (scanY + 0.28 * (dt / 16)) % (H + 200); const sg = ctx.createLinearGradient(0, scanY - 110, 0, scanY + 110); sg.addColorStop(0, 'rgba(60,180,255,0)'); sg.addColorStop(0.45, 'rgba(60,180,255,0.025)'); sg.addColorStop(0.5, 'rgba(60,180,255,0.06)'); sg.addColorStop(0.55, 'rgba(60,180,255,0.025)'); sg.addColorStop(1, 'rgba(60,180,255,0)'); ctx.fillStyle = sg; ctx.fillRect(0, scanY - 110, W, 220); /* vignette */ const vig = ctx.createRadialGradient(W/2, H/2, H * 0.25, W/2, H/2, H * 0.88); vig.addColorStop(0, 'rgba(0,0,0,0)'); vig.addColorStop(1, 'rgba(0,0,0,0.60)'); ctx.fillStyle = vig; ctx.fillRect(0, 0, W, H); } /* -- lifecycle helpers ----------------------------------------------------- */ function start() { if (!rafId) { lastTs = performance.now(); rafId = requestAnimationFrame(draw); } } function stop() { cancelAnimationFrame(rafId); rafId = null; } document.addEventListener('visibilitychange', () => { document.hidden ? stop() : start(); }); if ('IntersectionObserver' in window) { new IntersectionObserver(entries => { entries.forEach(e => e.isIntersecting ? start() : stop()); }, { threshold: 0.01 }).observe(canvas); } window.addEventListener('resize', () => { canvas.width = canvas.offsetWidth || canvas.parentElement.offsetWidth; canvas.height = canvas.offsetHeight || canvas.parentElement.offsetHeight; W = canvas.width; H = canvas.height; layers = LAYERS.map(def => ({ def, racks: buildLayer(def) })); paintBg(); }, { passive: true }); /* -- init ------------------------------------------------------------------ */ canvas.width = canvas.offsetWidth || canvas.parentElement.offsetWidth; canvas.height = canvas.offsetHeight || canvas.parentElement.offsetHeight; W = canvas.width; H = canvas.height; layers = LAYERS.map(def => ({ def, racks: buildLayer(def) })); paintBg(); start(); })();