/*!
* OSGKeyboard 主页交互与动效引擎。
* 无 JavaScript 时页面内容完整可读;本文件负责:
* 1. 中 / 英双语切换(data-zh / data-en 系列属性,含视频源)
* 2. 深浅色主题(默认跟随系统,手动选择后持久化;支持时以圆形扩散过渡)
* 3. 移动端导航开合、导航滑块与当前区块高亮
* 4. 逐字拆分标题(data-split)
* 5. 滚动引擎:进度条、Hero 设备「立起」、媒体视差、四幕滚动叙事(真机录屏随幕切换)
* 6. 声波渲染、Hero 语音胶囊打字机、数字计数
* 7. 亚克力反光 / 磁吸按钮、FAQ 高度过渡、离屏视频暂停
* 开启「减弱动态效果」时,持续动画与指针动效全部停用,内容直接呈现最终态。
*/
(function () {
"use strict";
const root = document.documentElement;
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 = [];
// 主题切换后需要换源的模块(真机录屏按深浅色各录一份)在此登记
const themeHooks = [];
/* ---------------- 逐字拆分 ---------------- */
// 每个分组是一段不可断开的文字:英文按词,中文按「到下一个标点为止」的短句,
// 与原标题的 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`] || "";
});
// 同时覆盖
与