/*! * OSGKeyboard 主页交互与动效引擎。 * 无 JavaScript 时页面内容完整可读;本文件负责: * 1. 中 / 英双语切换(data-zh / data-en 系列属性,含视频源) * 2. 深浅色主题(默认跟随系统,手动选择后持久化;支持时以圆形扩散过渡) * 3. 移动端导航开合、导航滑块与当前区块高亮 * 4. 逐字拆分标题(data-split) * 5. 滚动引擎:进度条、Hero 设备「立起」、媒体视差、三幕滚动叙事 * 6. 声波渲染、Hero 语音胶囊打字机、数字计数 * 7. 指针光斑 / 3D 倾斜 / 磁吸按钮、FAQ 高度过渡、离屏视频暂停 * 开启「减弱动态效果」时,持续动画与指针动效全部停用,内容直接呈现最终态。 */ (function () { "use strict"; const root = document.documentElement; const siteNav = document.getElementById("siteNav"); const languageButton = document.getElementById("languageToggle"); const themeButton = document.getElementById("themeToggle"); const themeIconUse = document.getElementById("themeIconUse"); const menuButton = document.getElementById("menuToggle"); const menuIconUse = document.getElementById("menuIconUse"); const mobileNav = document.getElementById("mobileNav"); const themeColor = document.querySelector('meta[name="theme-color"]'); const themeMedia = window.matchMedia("(prefers-color-scheme: dark)"); const supportedLanguages = new Set(["zh", "en"]); const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)").matches; const canObserve = "IntersectionObserver" in window; const TITLES = { zh: "OSGKeyboard|AI 语音输入法 · 嘴替输入法 · iPhone / iPad / Mac", en: "OSGKeyboard — Agentic Keyboard & AI Voice Keyboard for iPhone and Mac" }; const clamp = (value, min = 0, max = 1) => Math.min(max, Math.max(min, value)); // 语言切换后需要重算的模块(胶囊文案、叙事文案、导航滑块)在此登记 const languageHooks = []; /* ---------------- 逐字拆分 ---------------- */ // 每个分组是一段不可断开的文字:英文按词,中文按「到下一个标点为止」的短句, // 与原标题的 word-break: keep-all 断行效果一致,标点也不会落到行首 const CJK_PUNCTUATION = /[,。、!?:;)」』》…~]/; function groupCharacters(token) { const groups = []; let current = []; for (const character of token) { current.push(character); if (CJK_PUNCTUATION.test(character)) { groups.push(current); current = []; } } if (current.length) groups.push(current); return groups; } function splitText(element) { const text = element.textContent.trim(); // 读屏读取完整句子,逐字拆分的视觉层对辅助技术隐藏 const readable = document.createElement("span"); readable.className = "sr-only"; readable.textContent = text; const visual = document.createElement("span"); visual.setAttribute("aria-hidden", "true"); let index = 0; text.split(/(\s+)/).forEach((token) => { if (!token) return; if (/^\s+$/.test(token)) { visual.append(" "); return; } groupCharacters(token).forEach((group) => { const word = document.createElement("span"); word.className = "w"; group.forEach((character) => { const span = document.createElement("span"); span.className = "ch"; span.style.setProperty("--ci", String(index++)); span.textContent = character; word.append(span); }); visual.append(word); }); }); element.replaceChildren(readable, visual); element.classList.add("is-split"); } function splitAll() { document.querySelectorAll("[data-split]").forEach(splitText); } /* ---------------- 语言 ---------------- */ function preferredLanguage() { const query = new URLSearchParams(window.location.search).get("lang"); if (supportedLanguages.has(query)) return query; const saved = localStorage.getItem("osg-site-language"); if (supportedLanguages.has(saved)) return saved; return navigator.language.toLowerCase().startsWith("zh") ? "zh" : "en"; } function applyLanguage(language, persist) { const lang = supportedLanguages.has(language) ? language : "zh"; root.lang = lang === "zh" ? "zh-Hans" : "en"; root.dataset.lang = lang; document.querySelectorAll("[data-zh][data-en]").forEach((element) => { element.textContent = element.dataset[lang] || ""; }); document.querySelectorAll("[data-zh-html][data-en-html]").forEach((element) => { element.innerHTML = element.dataset[`${lang}Html`] || ""; }); document.querySelectorAll("[data-zh-alt][data-en-alt]").forEach((element) => { element.alt = element.dataset[`${lang}Alt`] || ""; }); // 同时覆盖 与