- 首页: Three.js破碎月亮太空之旅(warp入场/碎片悬浮/代码泄漏/腐化标题) - 关于: 地面控制中心 - 产品: 碎片修复舱(加密悬念) - 联系: 通讯链路 - 纯静态: HTML+CSS+原生JS+Three.js本地化
733 lines
26 KiB
JavaScript
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);
|
|
}
|
|
})();
|