Files
OTSSigns-Website/theme/assets/js/main.js

709 lines
26 KiB
JavaScript
Raw Normal View History

2026-02-20 21:28:00 -05:00
/**
* OTS Theme - Main JS
2026-02-20 21:28:00 -05:00
*/
/* ── 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();
})();
/* ── Device Animator ──────────────────────────────────────────────────────── */
(function () {
const DEVICES = [
'da-tablet', 'da-monitor-sm', 'da-monitor-lg',
'da-tv', 'da-projector', 'da-vwall'
];
const DWELL = 2500; // ms each device is shown
document.querySelectorAll('.da-stage').forEach(function (stage) {
let current = 0;
let timer = null;
// Collect the 6 device panels
const panels = DEVICES.map(function (cls) {
return stage.querySelector('.' + cls);
});
function show(idx) {
panels.forEach(function (el, i) {
if (!el) return;
el.classList.toggle('is-active', i === idx);
el.classList.remove('is-leaving');
});
}
function advance() {
const leaving = current;
current = (current + 1) % DEVICES.length;
if (panels[leaving]) panels[leaving].classList.add('is-leaving');
show(current);
setTimeout(function () {
if (panels[leaving]) panels[leaving].classList.remove('is-leaving');
}, 600);
}
function startCycle() {
if (timer) return;
timer = setInterval(advance, DWELL);
}
function stopCycle() {
clearInterval(timer);
timer = null;
}
// Honour reduced-motion preference: show first device statically
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
show(0);
return;
}
show(0);
startCycle();
// Pause when scrolled out of view to save resources
if ('IntersectionObserver' in window) {
new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
e.isIntersecting ? startCycle() : stopCycle();
});
}, { threshold: 0.2 }).observe(stage);
}
});
})();
/* ── TV Stick Plug Animation ─────────────────────────────────────────────── */
(function () {
var stages = document.querySelectorAll('[data-tv-stick-anim]');
if (!stages.length) return;
// Honour reduced-motion: show plugged-in state immediately
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
stages.forEach(function (stage) {
stage.classList.add('is-plugged');
});
return;
}
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) {
var stage = e.target;
stage.classList.add('is-animating');
// Add plugged state after slide-in completes (1.4s)
setTimeout(function () {
stage.classList.add('is-plugged');
}, 1400);
io.unobserve(stage);
}
});
}, { threshold: 0.3 });
stages.forEach(function (stage) { io.observe(stage); });
}
})();