(() => { const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); const header = document.querySelector("[data-header]"); const menuButton = document.querySelector("[data-menu-button]"); const mobileMenu = document.querySelector("[data-mobile-menu]"); let headerScrollTimer = 0; let lastHeaderScrollY = window.scrollY; const updateHeader = () => header?.classList.toggle("is-scrolled", window.scrollY > 20); const handleHeaderScroll = () => { updateHeader(); if (!header) return; const currentScrollY = window.scrollY; if (!reducedMotion.matches && Math.abs(currentScrollY - lastHeaderScrollY) >= 2) { header.classList.add("is-scroll-hidden"); } lastHeaderScrollY = currentScrollY; window.clearTimeout(headerScrollTimer); headerScrollTimer = window.setTimeout(() => { header.classList.remove("is-scroll-hidden"); }, 180); }; updateHeader(); window.addEventListener("scroll", handleHeaderScroll, { passive: true }); reducedMotion.addEventListener("change", () => { if (reducedMotion.matches) header?.classList.remove("is-scroll-hidden"); }); const glassNav = document.querySelector(".nav-island"); if (glassNav && !reducedMotion.matches && window.matchMedia("(pointer: fine)").matches) { glassNav.addEventListener("pointermove", (event) => { const rect = glassNav.getBoundingClientRect(); glassNav.style.setProperty("--glass-x", `${event.clientX - rect.left}px`); glassNav.style.setProperty("--glass-y", `${event.clientY - rect.top}px`); }, { passive: true }); glassNav.addEventListener("pointerleave", () => { glassNav.style.setProperty("--glass-x", "50%"); glassNav.style.setProperty("--glass-y", "0%"); }); } if (menuButton && mobileMenu) { const closeMenu = () => { menuButton.setAttribute("aria-expanded", "false"); menuButton.setAttribute("aria-label", "打开导航"); mobileMenu.hidden = true; }; menuButton.addEventListener("click", () => { const opening = menuButton.getAttribute("aria-expanded") !== "true"; menuButton.setAttribute("aria-expanded", String(opening)); menuButton.setAttribute("aria-label", opening ? "关闭导航" : "打开导航"); mobileMenu.hidden = !opening; }); mobileMenu.addEventListener("click", (event) => { if (event.target.closest("a")) closeMenu(); }); document.addEventListener("pointerdown", (event) => { if (!mobileMenu.hidden && !header.contains(event.target)) closeMenu(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeMenu(); }); window.addEventListener("resize", () => { if (window.innerWidth > 960) closeMenu(); }, { passive: true }); } const activateSet = (buttons, panels, button, buttonKey, panelKey) => { const target = button.dataset[buttonKey]; buttons.forEach((item) => { const active = item === button; item.classList.toggle("is-active", active); item.setAttribute("aria-pressed", String(active)); }); panels.forEach((panel) => { const active = panel.dataset[panelKey] === target; panel.hidden = !active; panel.classList.toggle("is-active", active); }); }; const bindButtonSet = (buttonSelector, panelSelector, buttonKey, panelKey) => { const buttons = [...document.querySelectorAll(buttonSelector)]; const panels = [...document.querySelectorAll(panelSelector)]; buttons.forEach((button, index) => { button.addEventListener("click", () => activateSet(buttons, panels, button, buttonKey, panelKey)); button.addEventListener("keydown", (event) => { if (!["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp"].includes(event.key)) return; event.preventDefault(); const direction = event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : -1; const next = buttons[(index + direction + buttons.length) % buttons.length]; next.focus(); activateSet(buttons, panels, next, buttonKey, panelKey); }); }); }; bindButtonSet("[data-console-tab]", "[data-console-view]", "consoleTab", "consoleView"); const featureRail = document.querySelector("[data-feature-rail]"); const featurePrev = document.querySelector("[data-feature-prev]"); const featureNext = document.querySelector("[data-feature-next]"); if (featureRail && featurePrev && featureNext) { const originalCards = [...featureRail.querySelectorAll(".feature-card")]; const autoPauses = new Set(); let railFrame = 0; let motionFrame = 0; let lastMotionTime = 0; let manualPauseUntil = 0; // Three identical sets keep both automatic and manual movement seamless. const cloneSet = () => { const fragment = document.createDocumentFragment(); originalCards.forEach((card) => { const clone = card.cloneNode(true); clone.setAttribute("aria-hidden", "true"); fragment.append(clone); }); return fragment; }; featureRail.prepend(cloneSet()); featureRail.append(cloneSet()); const getStep = () => { const card = originalCards[0]; if (!card) return 0; const gap = parseFloat(getComputedStyle(featureRail).columnGap) || 0; return card.getBoundingClientRect().width + gap; }; const getLoopSpan = () => getStep() * originalCards.length; const normalizeRail = () => { const loopSpan = getLoopSpan(); if (!loopSpan) return; if (featureRail.scrollLeft >= loopSpan * 2) featureRail.scrollLeft -= loopSpan; if (featureRail.scrollLeft <= 1) featureRail.scrollLeft += loopSpan; }; featureRail.scrollLeft = getLoopSpan(); const scheduleRailUpdate = () => { if (railFrame) return; railFrame = requestAnimationFrame(() => { railFrame = 0; normalizeRail(); }); }; const scrollRail = (direction) => { const step = getStep(); if (!step) return; manualPauseUntil = performance.now() + 700; featureRail.scrollBy({ left: direction * step, behavior: reducedMotion.matches ? "auto" : "smooth" }); }; const stopAuto = () => { cancelAnimationFrame(motionFrame); motionFrame = 0; lastMotionTime = 0; }; const animateRail = (time) => { if (!lastMotionTime) lastMotionTime = time; const elapsed = Math.min(time - lastMotionTime, 40); lastMotionTime = time; if (!autoPauses.size && time >= manualPauseUntil) { featureRail.scrollLeft += elapsed * 0.032; normalizeRail(); } motionFrame = requestAnimationFrame(animateRail); }; const startAuto = () => { if (reducedMotion.matches || document.hidden) { stopAuto(); return; } if (!motionFrame) motionFrame = requestAnimationFrame(animateRail); }; const pauseAuto = (reason) => autoPauses.add(reason); const resumeAuto = (reason) => { autoPauses.delete(reason); startAuto(); }; const moveManually = (direction) => { scrollRail(direction); startAuto(); }; featurePrev.addEventListener("click", () => moveManually(-1)); featureNext.addEventListener("click", () => moveManually(1)); featureRail.addEventListener("scroll", scheduleRailUpdate, { passive: true }); featureRail.addEventListener("keydown", (event) => { if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return; event.preventDefault(); moveManually(event.key === "ArrowRight" ? 1 : -1); }); featureRail.addEventListener("pointerdown", () => pauseAuto("pointer"), { passive: true }); window.addEventListener("pointerup", () => resumeAuto("pointer"), { passive: true }); window.addEventListener("pointercancel", () => resumeAuto("pointer"), { passive: true }); featureRail.addEventListener("wheel", () => { manualPauseUntil = performance.now() + 900; }, { passive: true }); document.addEventListener("visibilitychange", () => { if (document.hidden) stopAuto(); else startAuto(); }); reducedMotion.addEventListener("change", startAuto); window.addEventListener("resize", () => { featureRail.scrollLeft = getLoopSpan(); scheduleRailUpdate(); }, { passive: true }); startAuto(); } const productStage = document.querySelector("[data-product-stage]"); const productWindow = document.querySelector("[data-product-window]"); if (productStage && productWindow && !reducedMotion.matches && window.matchMedia("(pointer: fine)").matches) { let currentX = 0; let currentY = 0; let targetX = 0; let targetY = 0; let velocityX = 0; let velocityY = 0; let frame = 0; const animateTilt = () => { velocityX = (velocityX + (targetX - currentX) * 0.16) * 0.72; velocityY = (velocityY + (targetY - currentY) * 0.16) * 0.72; currentX += velocityX; currentY += velocityY; productWindow.style.setProperty("--rx", `${currentX.toFixed(3)}deg`); productWindow.style.setProperty("--ry", `${currentY.toFixed(3)}deg`); const moving = Math.abs(targetX - currentX) + Math.abs(targetY - currentY) + Math.abs(velocityX) + Math.abs(velocityY) > 0.003; frame = moving ? requestAnimationFrame(animateTilt) : 0; }; const scheduleTilt = () => { if (!frame) frame = requestAnimationFrame(animateTilt); }; productStage.addEventListener("pointermove", (event) => { const rect = productStage.getBoundingClientRect(); const x = (event.clientX - rect.left) / rect.width - 0.5; const y = (event.clientY - rect.top) / rect.height - 0.5; targetX = Math.max(-1.5, Math.min(1.5, -y * 3)); targetY = Math.max(-2.1, Math.min(2.1, x * 4.2)); scheduleTilt(); }, { passive: true }); productStage.addEventListener("pointerleave", () => { targetX = 0; targetY = 0; scheduleTilt(); }); } const reveals = document.querySelectorAll(".reveal"); if (reducedMotion.matches || !("IntersectionObserver" in window)) { reveals.forEach((item) => item.classList.add("is-visible")); } else { const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; entry.target.classList.add("is-visible"); observer.unobserve(entry.target); }); }, { rootMargin: "0px 0px -7%", threshold: 0.08 }); reveals.forEach((item) => observer.observe(item)); } })();