Files
leefer 40dd2fffff feat: 晟算科技官网 V6 太空世界观
- 首页: Three.js破碎月亮太空之旅(warp入场/碎片悬浮/代码泄漏/腐化标题)
- 关于: 地面控制中心
- 产品: 碎片修复舱(加密悬念)
- 联系: 通讯链路
- 纯静态: HTML+CSS+原生JS+Three.js本地化
2026-08-01 11:03:38 +08:00

733 lines
26 KiB
JavaScript

/* ============================
郑州晟算科技有限公司 - 交互脚本 v3
自定义光标 · 启动加载屏 · 3D倾斜 · 磁性按钮 · 乱序解码
============================ */
(function () {
"use strict";
/* ========================================
1. 自定义光标
======================================== */
const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 0;
if (!isTouchDevice) {
const dot = document.createElement("div");
dot.className = "cursor-dot";
const ring = document.createElement("div");
ring.className = "cursor-ring";
document.body.appendChild(dot);
document.body.appendChild(ring);
let dotX = 0, dotY = 0;
let ringX = 0, ringY = 0;
let mouseX = 0, mouseY = 0;
let isVisible = false;
let cursorActivated = false;
document.addEventListener("mousemove", function (e) {
mouseX = e.clientX;
mouseY = e.clientY;
if (!isVisible) {
isVisible = true;
dot.style.opacity = "1";
ring.style.opacity = "1";
// 确认自定义光标工作后才隐藏系统光标
if (!cursorActivated) {
cursorActivated = true;
document.body.classList.add("custom-cursor-active");
}
}
});
document.addEventListener("mouseleave", function () {
isVisible = false;
dot.style.opacity = "0";
ring.style.opacity = "0";
});
document.addEventListener("mousedown", function () {
ring.classList.add("clicking");
});
document.addEventListener("mouseup", function () {
ring.classList.remove("clicking");
});
// 检测可交互元素
const hoverables = document.querySelectorAll("a, button, .card, .teaser-card, .feature-card, .belief-card, .philosophy-item, input, textarea, select, .nav-cta, .structure-item, .vision-item");
hoverables.forEach(function (el) {
el.addEventListener("mouseenter", function () { ring.classList.add("hovering"); });
el.addEventListener("mouseleave", function () { ring.classList.remove("hovering"); });
});
function animateCursor() {
// 点:即时跟随
dotX += (mouseX - dotX) * 0.35;
dotY += (mouseY - dotY) * 0.35;
dot.style.left = dotX + "px";
dot.style.top = dotY + "px";
// 环:延迟跟随
ringX += (mouseX - ringX) * 0.12;
ringY += (mouseY - ringY) * 0.12;
ring.style.left = ringX + "px";
ring.style.top = ringY + "px";
requestAnimationFrame(animateCursor);
}
animateCursor();
}
/* ========================================
2. 启动加载屏(太空模式下由space.js接管)
======================================== */
const isFirstVisit = !sessionStorage.getItem("visited");
if (!window.SPACE_MODE && isFirstVisit) {
const loadingScreen = document.createElement("div");
loadingScreen.className = "loading-screen";
loadingScreen.innerHTML =
'<div class="loading-logo">SHENG<span>SUAN</span></div>' +
'<div class="loading-terminal">' +
'<div class="terminal-header">' +
'<div class="terminal-dot"></div>' +
'<div class="terminal-dot"></div>' +
'<div class="terminal-dot"></div>' +
'<div class="terminal-title">shengsuan@tech ~ boot</div>' +
'</div>' +
'<div class="terminal-body">' +
'<div class="terminal-line" style="animation-delay:0.1s"><span class="terminal-prompt">></span><span class="terminal-text">Detecting software chaos... </span><span class="terminal-text ok">[76%]</span></div>' +
'<div class="terminal-line" style="animation-delay:0.4s"><span class="terminal-prompt">></span><span class="terminal-text">Loading fix modules... </span><span class="terminal-text ok">[OK]</span></div>' +
'<div class="terminal-line" style="animation-delay:0.7s"><span class="terminal-prompt">></span><span class="terminal-text">Mounting patches... </span><span class="terminal-text ok">[3 loaded]</span></div>' +
'<div class="terminal-line" style="animation-delay:1.0s"><span class="terminal-prompt">></span><span class="terminal-text">Rendering scene... </span><span class="terminal-text ok">[OK]</span></div>' +
'<div class="terminal-line" style="animation-delay:1.3s"><span class="terminal-prompt">></span><span class="terminal-text typing" style="color:var(--cyan)">Welcome, fixer. Let\'s repair some software.</span></div>' +
'</div>' +
'</div>';
document.body.appendChild(loadingScreen);
setTimeout(function () {
loadingScreen.classList.add("hide");
sessionStorage.setItem("visited", "true");
setTimeout(function () {
if (loadingScreen.parentNode) loadingScreen.parentNode.removeChild(loadingScreen);
}, 700);
}, 2000);
} else {
sessionStorage.setItem("visited", "true");
}
/* ========================================
3. 滚动进度条
======================================== */
const progressBar = document.createElement("div");
progressBar.className = "scroll-progress";
progressBar.style.width = "0%";
document.body.appendChild(progressBar);
window.addEventListener("scroll", function () {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollPercent = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
progressBar.style.width = scrollPercent + "%";
}, { passive: true });
/* ========================================
4. 粒子背景(太空模式下禁用,由space.js接管)
======================================== */
let canvas = document.getElementById("particlesCanvas");
if (window.SPACE_MODE) {
if (canvas) canvas.remove();
} else {
if (!canvas) {
canvas = document.createElement("canvas");
canvas.id = "particlesCanvas";
document.body.prepend(canvas);
}
const ctx = canvas.getContext("2d");
let particles = [];
const PARTICLE_COUNT = 50;
let pmouseX = 0, pmouseY = 0;
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
for (let i = 0; i < PARTICLE_COUNT; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * 0.4,
vy: (Math.random() - 0.5) * 0.4,
size: Math.random() * 1.5 + 0.5,
opacity: Math.random() * 0.35 + 0.08
});
}
document.addEventListener("mousemove", function (e) {
pmouseX = e.clientX;
pmouseY = e.clientY;
});
function drawParticles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
if (p.x < 0) p.x = canvas.width;
if (p.x > canvas.width) p.x = 0;
if (p.y < 0) p.y = canvas.height;
if (p.y > canvas.height) p.y = 0;
const dx = pmouseX - p.x;
const dy = pmouseY - p.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 180 && dist > 0) {
const force = (180 - dist) / 180 * 0.5;
p.vx += (dx / dist) * force * 0.03;
p.vy += (dy / dist) * force * 0.03;
}
p.vx *= 0.999;
p.vy *= 0.999;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = "rgba(0, 230, 118, " + p.opacity + ")";
ctx.fill();
}
for (let j = 0; j < particles.length; j++) {
for (let k = j + 1; k < particles.length; k++) {
const a = particles[j], b = particles[k];
const d = Math.sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
if (d < 120) {
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = "rgba(0, 230, 118, " + (0.06 * (1 - d / 120)) + ")";
ctx.lineWidth = 0.5;
ctx.stroke();
}
}
}
requestAnimationFrame(drawParticles);
}
drawParticles();
} /* end else (非太空模式才运行粒子) */
/* ========================================
5. 导航栏
======================================== */
const header = document.querySelector(".header");
window.addEventListener("scroll", function () {
if (window.scrollY > 10) {
header.classList.add("scrolled");
} else {
header.classList.remove("scrolled");
}
}, { passive: true });
const menuToggle = document.querySelector(".menu-toggle");
const nav = document.querySelector(".nav");
if (menuToggle && nav) {
menuToggle.addEventListener("click", function () {
menuToggle.classList.toggle("active");
nav.classList.toggle("active");
});
nav.querySelectorAll("a").forEach(function (link) {
link.addEventListener("click", function () {
menuToggle.classList.remove("active");
nav.classList.remove("active");
});
});
}
/* ========================================
6. 滚动显现
======================================== */
const revealElements = document.querySelectorAll(".reveal");
if ("IntersectionObserver" in window && revealElements.length > 0) {
const observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1, rootMargin: "0px 0px -30px 0px" });
revealElements.forEach(function (el) { observer.observe(el); });
} else {
revealElements.forEach(function (el) { el.classList.add("visible"); });
}
/* ========================================
7. 打字机效果
======================================== */
const typewriterEl = document.querySelector("#typewriter");
if (typewriterEl) {
let words;
try {
words = typewriterEl.getAttribute("data-words")
? JSON.parse(typewriterEl.getAttribute("data-words"))
: [typewriterEl.textContent.trim()];
} catch (e) {
words = [typewriterEl.textContent.trim()];
}
let wordIndex = 0;
let charIndex = 0;
let isDeleting = false;
const typeSpeed = 80;
const deleteSpeed = 40;
const pauseTime = 2000;
function type() {
const currentWord = words[wordIndex];
if (isDeleting) {
typewriterEl.textContent = currentWord.substring(0, charIndex - 1);
charIndex--;
if (charIndex === 0) {
isDeleting = false;
wordIndex = (wordIndex + 1) % words.length;
setTimeout(type, 300);
return;
}
setTimeout(type, deleteSpeed);
} else {
typewriterEl.textContent = currentWord.substring(0, charIndex + 1);
charIndex++;
if (charIndex === currentWord.length) {
setTimeout(function () {
isDeleting = true;
type();
}, pauseTime);
return;
}
setTimeout(type, typeSpeed);
}
}
const cursor = document.createElement("span");
cursor.className = "typewriter-cursor";
cursor.textContent = "_";
typewriterEl.parentNode.insertBefore(cursor, typewriterEl.nextSibling);
setTimeout(type, 500);
}
/* ========================================
8. 3D 倾斜卡片
======================================== */
const tiltCards = document.querySelectorAll(".card, .teaser-card, .feature-card, .belief-card, .philosophy-item, .teaser-main, .teaser-side-item, .about-visual-block");
tiltCards.forEach(function (card) {
card.addEventListener("mousemove", function (e) {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const rotateX = (y - centerY) / centerY * -8;
const rotateY = (x - centerX) / centerX * 8;
card.style.transform = "perspective(800px) rotateX(" + rotateX + "deg) rotateY(" + rotateY + "deg) translateZ(10px)";
// 更新光斑位置
let glow = card.querySelector(".card-glow");
if (!glow) {
glow = document.createElement("div");
glow.className = "card-glow";
card.appendChild(glow);
}
glow.style.left = (x - 100) + "px";
glow.style.top = (y - 100) + "px";
});
card.addEventListener("mouseleave", function () {
card.style.transform = "perspective(800px) rotateX(0) rotateY(0) translateZ(0)";
card.style.transition = "transform 0.6s ease";
setTimeout(function () {
card.style.transition = "";
}, 600);
});
});
/* ========================================
9. 磁性按钮
======================================== */
const magneticBtns = document.querySelectorAll(".btn, .nav-cta");
magneticBtns.forEach(function (btn) {
btn.classList.add("magnetic");
btn.addEventListener("mousemove", function (e) {
const rect = btn.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
const strength = 0.3;
btn.style.transform = "translate(" + x * strength + "px, " + y * strength + "px)";
});
btn.addEventListener("mouseleave", function () {
btn.style.transform = "translate(0, 0)";
btn.style.transition = "transform 0.4s cubic-bezier(0.22, 1, 0.36, 1)";
setTimeout(function () {
btn.style.transition = "";
}, 400);
});
});
/* ========================================
10. 文字乱序解码 (Scramble)
======================================== */
const CHARS = "!<>-_\\/[]{}—=+*^?#________";
const scrambleElements = document.querySelectorAll("[data-scramble]");
if (scrambleElements.length > 0 && "IntersectionObserver" in window) {
const scrambleObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting && !entry.target.classList.contains("scrambled")) {
entry.target.classList.add("scrambled");
scrambleText(entry.target);
scrambleObserver.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
scrambleElements.forEach(function (el) {
scrambleObserver.observe(el);
});
}
function scrambleText(el) {
const originalText = el.textContent;
const length = originalText.length;
let frame = 0;
const totalFrames = length * 3;
const revealed = new Array(length).fill(false);
function update() {
let output = "";
let done = true;
for (let i = 0; i < length; i++) {
if (revealed[i]) {
output += originalText[i];
continue;
}
if (frame > i * 2 + Math.random() * 10) {
revealed[i] = true;
output += originalText[i];
} else {
done = false;
output += CHARS[Math.floor(Math.random() * CHARS.length)];
}
}
el.textContent = output;
frame++;
if (!done) {
requestAnimationFrame(update);
} else {
el.textContent = originalText;
}
}
requestAnimationFrame(update);
}
/* ========================================
11. 视差滚动 (Orbs - 排除hero-orb避免与CSS动画冲突)
======================================== */
const orbs = document.querySelectorAll(".cta-orb, .page-hero-orb");
if (orbs.length > 0) {
window.addEventListener("scroll", function () {
const scrollY = window.scrollY;
orbs.forEach(function (orb, index) {
const speed = 0.05 + (index % 3) * 0.03;
orb.style.transform = "translateY(" + scrollY * speed + "px)";
});
}, { passive: true });
}
/* ========================================
11.5 编译进度条动画
======================================== */
const progressFills = document.querySelectorAll(".build-progress-fill[data-progress]");
if (progressFills.length > 0 && "IntersectionObserver" in window) {
const progressObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
const fill = entry.target;
const target = fill.getAttribute("data-progress");
setTimeout(function () {
fill.style.width = target + "%";
}, 200);
progressObserver.unobserve(fill);
}
});
}, { threshold: 0.5 });
progressFills.forEach(function (fill) {
fill.style.width = "0%";
progressObserver.observe(fill);
});
}
/* ========================================
11.6 失控数字滚动
======================================== */
const chaosNums = document.querySelectorAll("[data-chaos-count]");
if (chaosNums.length > 0 && "IntersectionObserver" in window) {
const chaosObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
animateChaosNum(entry.target);
chaosObserver.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
chaosNums.forEach(function (el) { chaosObserver.observe(el); });
}
function animateChaosNum(el) {
const target = parseFloat(el.getAttribute("data-chaos-count"));
const decimals = el.getAttribute("data-decimals") ? parseInt(el.getAttribute("data-decimals")) : 0;
const duration = 1600;
let startTime = null;
function step(timestamp) {
if (!startTime) startTime = timestamp;
const progress = Math.min((timestamp - startTime) / duration, 1);
const eased = progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress);
const current = eased * target;
el.textContent = decimals > 0 ? current.toFixed(decimals) : Math.floor(current).toLocaleString();
if (progress < 1) {
requestAnimationFrame(step);
} else {
el.textContent = decimals > 0 ? target.toFixed(decimals) : target.toLocaleString();
}
}
requestAnimationFrame(step);
}
/* ========================================
12.5 KPR风格滚动场景引擎
======================================== */
const scrollScenes = document.querySelectorAll(".scroll-scene");
if (scrollScenes.length > 0) {
const sceneData = [];
scrollScenes.forEach(function (scene) {
sceneData.push({
el: scene,
lines: scene.querySelectorAll(".scene-line"),
pctEls: scene.querySelectorAll("[data-scene-pct]"),
giantTexts: scene.querySelectorAll(".bg-giant-text")
});
});
let sceneTicking = false;
function updateScenes() {
const vh = window.innerHeight;
sceneData.forEach(function (data) {
const rect = data.el.getBoundingClientRect();
const scrollable = rect.height - vh;
if (scrollable <= 0) return;
const progress = Math.min(Math.max(-rect.top / scrollable, 0), 1);
// 进度百分比
const pct = Math.floor(progress * 100);
data.pctEls.forEach(function (el) {
el.textContent = (pct < 10 ? "0" : "") + pct + "%";
});
// 大字背景视差
data.giantTexts.forEach(function (gt) {
const shift = (progress - 0.5) * 120;
gt.style.transform = "translate(-50%, -50%) translateX(" + shift + "px)";
});
// 逐行揭示
const n = data.lines.length;
data.lines.forEach(function (line, i) {
const lineStart = (i / n) * 0.9;
const lineEnd = ((i + 1) / n) * 0.9 + 0.05;
if (progress >= lineStart && progress < lineEnd) {
line.classList.add("active");
line.classList.remove("exit");
} else if (progress >= lineEnd) {
line.classList.remove("active");
line.classList.add("exit");
} else {
line.classList.remove("active", "exit");
}
});
});
sceneTicking = false;
}
window.addEventListener("scroll", function () {
if (!sceneTicking) {
requestAnimationFrame(updateScenes);
sceneTicking = true;
}
}, { passive: true });
updateScenes();
}
/* ========================================
12.6 持续加密乱码(永不解码)
======================================== */
const SCRAMBLE_CHARS = "01!<>-_/[]{}=+*^?#@%&$ABCDEF";
const loopEncrypted = document.querySelectorAll("[data-scramble-loop]");
if (loopEncrypted.length > 0) {
const encStates = [];
loopEncrypted.forEach(function (el) {
const len = parseInt(el.getAttribute("data-length") || el.textContent.length, 10);
encStates.push({ el: el, len: len });
});
let encFrame = 0;
function loopScramble() {
encFrame++;
// 每3帧更新一次,降低频率更有"电流感"
if (encFrame % 3 === 0) {
encStates.forEach(function (s) {
let out = "";
for (let i = 0; i < s.len; i++) {
out += SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)];
}
s.el.textContent = out;
});
}
requestAnimationFrame(loopScramble);
}
// 仅在视口内时运行
const encObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting && !entry.target.dataset.looping) {
entry.target.dataset.looping = "1";
}
});
});
loopEncrypted.forEach(function (el) { encObserver.observe(el); });
loopScramble();
}
/* ========================================
12.7 数据流下落线条生成(带裁剪层,防页面弹跳)
======================================== */
const streamContainers = document.querySelectorAll("[data-streams]");
streamContainers.forEach(function (container) {
// 裁剪层:防止线条溢出改变文档高度导致底部弹跳
const layer = document.createElement("div");
layer.style.cssText = "position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:0";
const count = parseInt(container.getAttribute("data-streams"), 10) || 5;
for (let i = 0; i < count; i++) {
const stream = document.createElement("div");
stream.className = "data-stream";
stream.style.left = (10 + Math.random() * 80) + "%";
stream.style.height = (60 + Math.random() * 120) + "px";
stream.style.animationDelay = (Math.random() * 3) + "s";
stream.style.animationDuration = (2.5 + Math.random() * 2) + "s";
layer.appendChild(stream);
}
container.insertBefore(layer, container.firstChild);
});
/* ========================================
12. 联系表单
======================================== */
const contactForm = document.querySelector("#contactForm");
if (contactForm) {
contactForm.addEventListener("submit", function (e) {
e.preventDefault();
const successMsg = document.querySelector(".form-success");
if (successMsg) {
successMsg.classList.add("show");
successMsg.scrollIntoView({ behavior: "smooth", block: "center" });
}
contactForm.reset();
setTimeout(function () {
if (successMsg) successMsg.classList.remove("show");
}, 6000);
});
}
/* ========================================
13. 当前页面高亮导航
======================================== */
const currentPage = window.location.pathname.split("/").pop() || "index.html";
const navLinks = document.querySelectorAll(".nav-link");
navLinks.forEach(function (link) {
const href = link.getAttribute("href");
if (href === currentPage || (currentPage === "" && href === "index.html")) {
link.classList.add("active");
}
});
/* ========================================
14. 腐化标题特效 —— 标题本身就是bug
======================================== */
const corruptEl = document.querySelector(".corrupt-title[data-text]");
if (corruptEl) {
const original = corruptEl.getAttribute("data-text");
const GLITCH_CHARS = "▓█◆#@$%&!?◢◤╳╱╲─│┌┐└┘01";
// 拆分字符
corruptEl.innerHTML = "";
const charSpans = [];
for (let ci = 0; ci < original.length; ci++) {
const span = document.createElement("span");
span.className = "corrupt-char";
span.textContent = original[ci];
span.setAttribute("data-orig", original[ci]);
corruptEl.appendChild(span);
charSpans.push(span);
}
const wrap = corruptEl.closest(".corrupt-wrap");
// 持续轻微腐化: 随机字符短暂变为乱码
setInterval(function () {
if (Math.random() > 0.45) {
const idx = Math.floor(Math.random() * charSpans.length);
const span = charSpans[idx];
span.textContent = GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)];
span.classList.add("glitching");
setTimeout(function () {
span.textContent = span.getAttribute("data-orig");
span.classList.remove("glitching");
}, 60 + Math.random() * 120);
}
}, 140);
// 偶发重度腐化: 多个字符同时崩溃 + 屏幕震颤
setInterval(function () {
if (Math.random() > 0.6) {
const burst = 2 + Math.floor(Math.random() * 3);
for (let b = 0; b < burst; b++) {
const span = charSpans[Math.floor(Math.random() * charSpans.length)];
span.textContent = GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)];
span.classList.add("glitching");
(function (s) {
setTimeout(function () {
s.textContent = s.getAttribute("data-orig");
s.classList.remove("glitching");
}, 120 + Math.random() * 200);
})(span);
}
if (wrap) {
wrap.classList.remove("shake");
void wrap.offsetWidth; // 重置动画
wrap.classList.add("shake");
}
}
}, 4200);
}
})();