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

488 lines
18 KiB
JavaScript

/* ============================================================
晟算科技 · 太空场景引擎
破碎的月亮 = 失控的软件世界 | 修复进度 85% 停住
模式: journey(首页滚动驱动) / orbit(子页面轨道展示)
============================================================ */
window.SPACE_MODE = document.body.getAttribute("data-space") || "journey";
(function () {
"use strict";
var isMobile = window.innerWidth < 768 || "ontouchstart" in window;
var reducedMotion = window.matchMedia("(prefers-reduced-motion: reduced)").matches;
var bootEl = document.getElementById("spaceBoot");
var canvasHost = document.getElementById("spaceCanvas");
/* ---------- WebGL 不可用时的兜底 ---------- */
function fallback() {
if (bootEl) bootEl.remove();
document.body.classList.add("space-fallback");
// 退化为2D星空(CSS实现,见space.css)
}
if (!window.THREE) { fallback(); return; }
var testCanvas = document.createElement("canvas");
var gl = testCanvas.getContext("webgl") || testCanvas.getContext("experimental-webgl");
if (!gl) { fallback(); return; }
/* ============================================================
程序化纹理生成
============================================================ */
// 径向光晕纹理(太阳辉光/镜头光斑)
function makeGlowTexture(inner, outer, size) {
var c = document.createElement("canvas");
c.width = c.height = size;
var ctx = c.getContext("2d");
var g = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
g.addColorStop(0, inner);
g.addColorStop(0.35, inner.replace(/[\d.]+\)$/, "0.35)"));
g.addColorStop(1, outer);
ctx.fillStyle = g;
ctx.fillRect(0, 0, size, size);
var t = new THREE.CanvasTexture(c);
return t;
}
// 月球纹理: 写实环形山 + 代码字符混合
var crackPath = []; // 裂缝路径点(供粒子泄漏/光缝定位)
function makeMoonTexture() {
var W = 1024, H = 512;
var c = document.createElement("canvas");
c.width = W; c.height = H;
var ctx = c.getContext("2d");
// 基底灰
var base = ctx.createLinearGradient(0, 0, W, H);
base.addColorStop(0, "#8d8d8f");
base.addColorStop(0.5, "#7c7c80");
base.addColorStop(1, "#6e6e73");
ctx.fillStyle = base;
ctx.fillRect(0, 0, W, H);
// 月海(大块暗斑)
for (var m = 0; m < 7; m++) {
var mx = Math.random() * W, my = Math.random() * H, mr = 60 + Math.random() * 130;
var mg = ctx.createRadialGradient(mx, my, 0, mx, my, mr);
mg.addColorStop(0, "rgba(70,70,76,0.5)");
mg.addColorStop(1, "rgba(70,70,76,0)");
ctx.fillStyle = mg;
ctx.beginPath(); ctx.arc(mx, my, mr, 0, Math.PI * 2); ctx.fill();
}
// 环形山
for (var i = 0; i < 220; i++) {
var x = Math.random() * W, y = Math.random() * H;
var r = 2 + Math.random() * 18;
ctx.fillStyle = "rgba(50,50,56," + (0.15 + Math.random() * 0.25) + ")";
ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
// 亮缘
ctx.strokeStyle = "rgba(200,200,205," + (0.1 + Math.random() * 0.2) + ")";
ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(x, y, r, -0.6, 1.8); ctx.stroke();
}
// 表面噪点
for (var n = 0; n < 2600; n++) {
var nx = Math.random() * W, ny = Math.random() * H;
ctx.fillStyle = Math.random() > 0.5
? "rgba(255,255,255," + Math.random() * 0.06 + ")"
: "rgba(0,0,0," + Math.random() * 0.08 + ")";
ctx.fillRect(nx, ny, 1.5, 1.5);
}
// 代码字符纹理(远看是月面,近看全是代码)
var glyphs = "01{}[]<>/=;+*#$&%@!?~:fnrax";
ctx.textBaseline = "top";
for (var g2 = 0; g2 < 520; g2++) {
var gx = Math.random() * W, gy = Math.random() * H;
var fs = 6 + Math.random() * 8;
ctx.font = fs + "px monospace";
var greenish = Math.random() > 0.72;
ctx.fillStyle = greenish
? "rgba(90,220,150," + (0.06 + Math.random() * 0.1) + ")"
: "rgba(230,230,235," + (0.04 + Math.random() * 0.08) + ")";
ctx.fillText(glyphs[Math.floor(Math.random() * glyphs.length)], gx, gy);
}
// 裂缝(锯齿状,贯穿一侧) —— 同时记录路径
ctx.strokeStyle = "rgba(18,18,22,0.9)";
ctx.lineCap = "round";
var cx = W * 0.62, cy = H * 0.16;
ctx.lineWidth = 7;
ctx.beginPath(); ctx.moveTo(cx, cy);
crackPath.push([cx, cy]);
while (cy < H * 0.9) {
cx += (Math.random() - 0.42) * 90;
cy += 26 + Math.random() * 30;
ctx.lineTo(cx, cy);
crackPath.push([cx, cy]);
ctx.lineWidth = Math.max(2, ctx.lineWidth * 0.94);
}
ctx.stroke();
// 裂缝两侧的次级裂纹
for (var b = 0; b < 8; b++) {
var bp = crackPath[Math.floor(Math.random() * crackPath.length)];
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(bp[0], bp[1]);
var bx = bp[0], by = bp[1];
for (var s = 0; s < 4; s++) {
bx += (Math.random() - 0.5) * 60;
by += Math.random() * 26;
ctx.lineTo(bx, by);
}
ctx.stroke();
}
var tex = new THREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
// 代码字符精灵纹理
function makeCharSprite(ch, color) {
var s = 48;
var c = document.createElement("canvas");
c.width = c.height = s;
var ctx = c.getContext("2d");
ctx.font = "bold 30px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.shadowColor = color;
ctx.shadowBlur = 8;
ctx.fillStyle = color;
ctx.fillText(ch, s / 2, s / 2);
return new THREE.CanvasTexture(c);
}
/* ============================================================
场景搭建
============================================================ */
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 2000);
var rig = new THREE.Group(); // 相机吊舱(负责呼吸式漂移)
rig.add(camera);
scene.add(rig);
var renderer;
try {
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
} catch (e) { fallback(); return; }
renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.2 : 1.6));
renderer.setSize(window.innerWidth, window.innerHeight);
canvasHost.appendChild(renderer.domElement);
/* ---------- 星空(两层) ---------- */
function makeStars(count, spread, size, opacity) {
var geo = new THREE.BufferGeometry();
var pos = new Float32Array(count * 3);
var col = new Float32Array(count * 3);
for (var i = 0; i < count; i++) {
pos[i * 3] = (Math.random() - 0.5) * spread;
pos[i * 3 + 1] = (Math.random() - 0.5) * spread;
pos[i * 3 + 2] = (Math.random() - 0.5) * spread;
var r = Math.random();
if (r > 0.85) { col[i * 3] = 1.0; col[i * 3 + 1] = 0.9; col[i * 3 + 2] = 0.75; } // 暖星
else if (r > 0.7) { col[i * 3] = 0.75; col[i * 3 + 1] = 0.85; col[i * 3 + 2] = 1.0; } // 蓝星
else { col[i * 3] = 1; col[i * 3 + 1] = 1; col[i * 3 + 2] = 1; }
}
geo.setAttribute("position", new THREE.BufferAttribute(pos, 3));
geo.setAttribute("color", new THREE.BufferAttribute(col, 3));
var mat = new THREE.PointsMaterial({
size: size, vertexColors: true, transparent: true, opacity: opacity,
sizeAttenuation: true, depthWrite: false
});
return new THREE.Points(geo, mat);
}
var starsFar = makeStars(isMobile ? 1500 : 3200, 900, 1.6, 0.85);
var starsNear = makeStars(isMobile ? 120 : 260, 220, 2.4, 0.6); // 近处星尘(景深)
scene.add(starsFar);
rig.add(starsNear);
/* ---------- 太阳(远景) ---------- */
var sunGroup = new THREE.Group();
var sunCore = new THREE.Mesh(
new THREE.SphereGeometry(14, 24, 24),
new THREE.MeshBasicMaterial({ color: 0xfff8e7 })
);
sunGroup.add(sunCore);
var glowTex = makeGlowTexture("rgba(255,244,214,1)", "rgba(255,244,214,0)", 256);
var sunGlow1 = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTex, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false }));
sunGlow1.scale.set(110, 110, 1);
sunGroup.add(sunGlow1);
var sunGlow2 = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTex, transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending, depthWrite: false }));
sunGlow2.scale.set(220, 220, 1);
sunGroup.add(sunGlow2);
// 镜头光斑链
var flareTex = makeGlowTexture("rgba(255,236,190,0.8)", "rgba(255,236,190,0)", 128);
for (var f = 0; f < 3; f++) {
var flare = new THREE.Sprite(new THREE.SpriteMaterial({ map: flareTex, transparent: true, opacity: 0.25 - f * 0.06, blending: THREE.AdditiveBlending, depthWrite: false }));
var fs2 = [22, 10, 6][f];
flare.scale.set(fs2, fs2, 1);
flare.position.set(18 + f * 14, -6 - f * 4, 0);
sunGroup.add(flare);
}
sunGroup.position.set(-150, 70, -260);
scene.add(sunGroup);
/* ---------- 月亮(破碎) ---------- */
var moonGroup = new THREE.Group();
var MOON_R = 10;
var moonTex = makeMoonTexture();
var moonMat = new THREE.MeshStandardMaterial({
map: moonTex, bumpMap: moonTex, bumpScale: 0.35,
roughness: 0.95, metalness: 0.02
});
var moon = new THREE.Mesh(new THREE.SphereGeometry(MOON_R, 64, 64), moonMat);
moonGroup.add(moon);
// 月亮轮廓微光(大气感)
var moonHalo = new THREE.Sprite(new THREE.SpriteMaterial({
map: makeGlowTexture("rgba(190,200,220,0.5)", "rgba(190,200,220,0)", 256),
transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending, depthWrite: false
}));
moonHalo.scale.set(34, 34, 1);
moonGroup.add(moonHalo);
// 碎片(3块, 不规则形状)
var fragments = [];
var fragSpecs = [
{ size: 2.6, scatter: new THREE.Vector3(9, 4.5, 3), home: new THREE.Vector3(7.6, 3.4, 2.2), spin: 0.16 },
{ size: 1.8, scatter: new THREE.Vector3(-7, -5, 4.5), home: new THREE.Vector3(-5.4, -3.9, 3.4), spin: -0.22 },
{ size: 1.3, scatter: new THREE.Vector3(4, -8, -4), home: new THREE.Vector3(3.2, -6.2, -3.1), spin: 0.3 }
];
fragSpecs.forEach(function (spec) {
var geo = new THREE.IcosahedronGeometry(spec.size, 1);
var posAttr = geo.getAttribute("position");
for (var i = 0; i < posAttr.count; i++) {
var jitter = 0.75 + Math.random() * 0.45;
posAttr.setXYZ(i, posAttr.getX(i) * jitter, posAttr.getY(i) * jitter, posAttr.getZ(i) * jitter);
}
geo.computeVertexNormals();
var mesh = new THREE.Mesh(geo, moonMat);
mesh.position.copy(spec.scatter);
mesh.rotation.set(Math.random() * 3, Math.random() * 3, Math.random() * 3);
moonGroup.add(mesh);
fragments.push({ mesh: mesh, spec: spec, phase: Math.random() * Math.PI * 2 });
});
// 修复光缝(绿色能量缝,随修复进度亮起)
var seams = [];
var seamTex = makeGlowTexture("rgba(77,255,166,1)", "rgba(77,255,166,0)", 64);
fragSpecs.forEach(function (spec) {
var seam = new THREE.Sprite(new THREE.SpriteMaterial({
map: seamTex, color: 0x4dffa6, transparent: true, opacity: 0,
blending: THREE.AdditiveBlending, depthWrite: false
}));
seam.scale.set(spec.size * 3.2, spec.size * 1.2, 1);
seam.position.copy(spec.home).multiplyScalar(0.92);
moonGroup.add(seam);
seams.push(seam);
});
moonGroup.position.set(6, 1, -26);
scene.add(moonGroup);
/* ---------- 代码泄漏粒子 ---------- */
var codeChars = "01{}<>/=;#*$fx";
var leakSprites = [];
var LEAK_COUNT = isMobile ? 22 : 46;
var charTexCache = {};
function charTex(ch) {
if (!charTexCache[ch]) charTexCache[ch] = makeCharSprite(ch, "#4dffa6");
return charTexCache[ch];
}
for (var li = 0; li < LEAK_COUNT; li++) {
var sp = new THREE.Sprite(new THREE.SpriteMaterial({
map: charTex(codeChars[Math.floor(Math.random() * codeChars.length)]),
transparent: true, opacity: 0, depthWrite: false, blending: THREE.AdditiveBlending
}));
var sc = 0.5 + Math.random() * 0.7;
sp.scale.set(sc, sc, 1);
// 从裂缝附近发射
var emit = new THREE.Vector3(
moonGroup.position.x + 3 + Math.random() * 6,
moonGroup.position.y - 4 + Math.random() * 8,
moonGroup.position.z + 2 + Math.random() * 4
);
sp.position.copy(emit);
scene.add(sp);
leakSprites.push({
sp: sp, life: Math.random(), speed: 0.06 + Math.random() * 0.1,
drift: new THREE.Vector3((Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.3),
origin: emit
});
}
/* ---------- 灯光 ---------- */
var sunLight = new THREE.DirectionalLight(0xfff3dd, 2.2);
sunLight.position.set(-150, 70, -200);
scene.add(sunLight);
scene.add(new THREE.AmbientLight(0x334455, 0.5));
var rimLight = new THREE.DirectionalLight(0x8899ff, 0.4); // 背面冷光勾勒
rimLight.position.set(80, -40, 60);
scene.add(rimLight);
/* ============================================================
相机编排: 呼吸漂移 + 鼠标惯性 + 滚动驱动
============================================================ */
var mouseX = 0, mouseY = 0;
if (!isMobile) {
document.addEventListener("mousemove", function (e) {
mouseX = (e.clientX / window.innerWidth - 0.5) * 2;
mouseY = (e.clientY / window.innerHeight - 0.5) * 2;
});
}
function scrollProgress() {
var h = document.documentElement.scrollHeight - window.innerHeight;
return h > 0 ? Math.min(Math.max(window.scrollY / h, 0), 1) : 0;
}
function easeInOut(t) { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; }
function lerp(a, b, t) { return a + (b - a) * t; }
var isJourney = window.SPACE_MODE === "journey";
var warpT = reducedMotion ? 1 : 0; // 0→1 warp入场
var clock = new THREE.Clock();
var bootDone = false;
/* ============================================================
BOOT 加载序列 → warp 穿越入场
============================================================ */
var bootLines = [
{ t: "> 接入深空观测网络 ………… ", ok: "[OK]" },
{ t: "> 望远镜阵列对焦 …………… ", ok: "[OK]" },
{ t: "> 扫描目标: MOON.SYS ……… ", ok: "[FATAL]" },
{ t: "> 检测到结构性碎裂 ………… ", ok: "[3 FRAGS]" },
{ t: "> 启动修复协议 ……………… ", ok: "[GO]" }
];
var bootBody = document.getElementById("spaceBootBody");
var bootBar = document.getElementById("spaceBootBar");
function runBoot() {
if (!bootEl || reducedMotion) { finishBoot(); return; }
var i = 0;
var total = bootLines.length;
function next() {
if (i < total) {
var line = document.createElement("div");
line.className = "terminal-line";
var isFatal = bootLines[i].ok.indexOf("FATAL") >= 0 || bootLines[i].ok.indexOf("FRAG") >= 0;
line.innerHTML = '<span class="terminal-prompt">' + bootLines[i].t + '</span><span class="terminal-text ' + (isFatal ? "log-error" : "ok") + '">' + bootLines[i].ok + "</span>";
if (bootBody) bootBody.appendChild(line);
if (bootBar) bootBar.style.width = ((i + 1) / total * 100) + "%";
i++;
setTimeout(next, 340);
} else {
setTimeout(finishBoot, 420);
}
}
next();
}
function finishBoot() {
if (bootEl) {
bootEl.classList.add("hide");
setTimeout(function () { if (bootEl.parentNode) bootEl.parentNode.removeChild(bootEl); }, 900);
}
bootDone = true;
document.body.classList.add("space-ready");
}
runBoot();
// 保险: 4.5s内必须入场
setTimeout(finishBoot, 4500);
/* ============================================================
主循环
============================================================ */
function animate() {
requestAnimationFrame(animate);
var t = clock.getElapsedTime();
var p = isJourney ? scrollProgress() : 0;
var ep = easeInOut(p);
// warp入场: 相机从远处冲刺到工位
if (warpT < 1) {
warpT = Math.min(1, warpT + 0.008);
var w = 1 - Math.pow(1 - warpT, 3);
camera.position.z = lerp(220, isJourney ? 46 : 42, w);
starsFar.rotation.z += 0.002 * (1 - warpT);
} else if (isJourney) {
// 滚动驱动: 接近月亮
camera.position.z = lerp(46, 24, ep);
camera.position.y = lerp(0, 3, ep);
} else {
camera.position.z = 42;
}
// 呼吸式失重漂移
rig.position.x = Math.sin(t * 0.24) * 0.55;
rig.position.y = Math.cos(t * 0.19) * 0.45;
rig.rotation.z = Math.sin(t * 0.1) * 0.008;
// 鼠标惯性跟随(像在太空里转头)
camera.rotation.y += ((-mouseX * 0.09) - camera.rotation.y) * 0.045;
camera.rotation.x += ((-mouseY * 0.06) - camera.rotation.x) * 0.045;
// 月亮自转 + 月组轻微摆动
moon.rotation.y += 0.0009;
moonGroup.position.y = 1 + Math.sin(t * 0.35) * 0.35;
moonGroup.rotation.z = Math.sin(t * 0.12) * 0.02;
// 修复进度: journey模式随滚动到85%停住 / orbit模式固定0.3
var repair = isJourney ? Math.min(Math.max((p - 0.42) / 0.46, 0), 1) * 0.85 : 0.3;
var rt = repair / 0.85; // 0→1
fragments.forEach(function (f) {
var target = f.spec.scatter.clone().lerp(f.spec.home, rt * rt);
// 悬浮: 在目标点附近小幅浮动
f.mesh.position.set(
target.x + Math.sin(t * 0.5 + f.phase) * 0.35 * (1 - rt),
target.y + Math.cos(t * 0.42 + f.phase) * 0.3 * (1 - rt),
target.z + Math.sin(t * 0.31 + f.phase * 2) * 0.3 * (1 - rt)
);
f.mesh.rotation.x += 0.0016 * f.spec.spin * (1 - rt * 0.8);
f.mesh.rotation.y += 0.0022 * f.spec.spin * (1 - rt * 0.8);
});
// 光缝亮起
seams.forEach(function (s, i) {
s.material.opacity = rt * 0.85 + Math.sin(t * 2 + i) * 0.06 * rt;
});
// 代码泄漏(修复越少漏得越多)
var leakRate = 1 - rt * 0.7;
leakSprites.forEach(function (L) {
L.life += L.speed * 0.016 * 60 * 0.016;
if (L.life > 1) {
L.life = 0;
L.sp.position.copy(L.origin);
L.sp.material.map = charTex(codeChars[Math.floor(Math.random() * codeChars.length)]);
}
L.sp.position.x += L.drift.x * 0.016;
L.sp.position.y += L.drift.y * 0.016;
L.sp.position.z += L.drift.z * 0.016;
L.sp.material.opacity = Math.sin(L.life * Math.PI) * 0.75 * leakRate;
});
// 太阳呼吸
sunGlow1.material.opacity = 0.85 + Math.sin(t * 0.8) * 0.1;
// 星尘缓流
starsNear.rotation.y += 0.0002;
starsFar.rotation.y += 0.00004;
renderer.render(scene, camera);
}
animate();
/* ---------- 自适应 ---------- */
window.addEventListener("resize", function () {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
})();