奇点的一生
2026-07-08
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>奇点的一生</title>
</head>
<body>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #010103;
font-family:
"Inter",
-apple-system,
sans-serif;
color: #fff;
}
canvas {
display: block;
}
#overlay {
position: absolute;
inset: 0;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 40px;
background: radial-gradient(
circle at center,
transparent 30%,
rgba(0, 0, 0, 0.5) 100%
);
z-index: 10;
}
.header {
text-align: center;
}
.title {
font-size: 1.2rem;
letter-spacing: 0.8em;
text-transform: uppercase;
color: #fff;
margin-bottom: 12px;
font-weight: 300;
opacity: 0.9;
}
.status-pill {
display: inline-block;
padding: 6px 20px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 30px;
font-size: 0.65rem;
letter-spacing: 0.25em;
text-transform: uppercase;
transition: all 1.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.hud-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
font-family: "JetBrains Mono", monospace;
font-size: 0.7rem;
opacity: 0.6;
letter-spacing: 1px;
}
.metric {
margin-bottom: 6px;
}
.val {
color: #00f3ff;
font-weight: bold;
transition: color 1.5s ease;
}
#vignette {
position: fixed;
inset: 0;
background: radial-gradient(circle, transparent 50%, black 150%);
pointer-events: none;
z-index: 5;
}
</style>
<script
async
src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"
></script>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/",
"gsap": "https://unpkg.com/gsap@3.12.5/index.js"
}
}
</script>
<div id="vignette"></div>
<div id="overlay">
<div class="header">
<div class="title" id="main-title">稳定奇点</div>
<div class="status-pill" id="status-text">拓扑结构:正常</div>
</div>
<div class="hud-bottom">
<div>
<div class="metric">质量指数: <span class="val">4.2M SOL</span></div>
<div class="metric">
引力透镜: <span class="val" id="lensing-val">SCHWARZSCHILD</span>
</div>
</div>
<div style="text-align: right">
<div class="metric">
相对速度: <span class="val" id="vel-val">0.45c</span>
</div>
<div class="metric">辐射探测: <span class="val">已开启</span></div>
</div>
</div>
</div>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import gsap from "gsap";
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
40,
window.innerWidth / window.innerHeight,
0.1,
1000,
);
camera.position.set(60, 30, 60);
const renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "high-performance",
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.6;
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.03;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.4;
const noiseChunk = `
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
float snoise(vec3 v) {
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy) );
vec3 x0 = v - i + dot(i, C.xxx) ;
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min( g.xyz, l.zxy );
vec3 i2 = max( g.xyz, l.zxy );
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - D.yyy;
i = mod289(i);
vec4 p = permute( permute( permute( i.z + vec4(0.0, i1.z, i2.z, 1.0 )) + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
float n_ = 0.142857142857;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_ );
vec4 x = x_ *ns.x + ns.yyyy;
vec4 y = y_ *ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4( x.xy, y.xy );
vec4 b1 = vec4( x.zw, y.zw );
vec4 s0 = floor(b0)*2.0 + 1.0;
vec4 s1 = floor(b1)*2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
vec3 p0 = vec3(a0.xy,h.x);
vec3 p1 = vec3(a0.zw,h.y);
vec3 p2 = vec3(a1.xy,h.z);
vec3 p3 = vec3(a1.zw,h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
m = m * m;
return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3) ) );
}
`;
const coreGroup = new THREE.Group();
scene.add(coreGroup);
const bhMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
const bhGeo = new THREE.SphereGeometry(4, 64, 64);
coreGroup.add(new THREE.Mesh(bhGeo, bhMat));
const auraMat = new THREE.ShaderMaterial({
uniforms: { uTime: { value: 0 }, uIntensity: { value: 1.0 } },
vertexShader: `
varying vec3 vNormal;
varying vec3 vView;
void main() {
vNormal = normalize(normalMatrix * normal);
vView = normalize(-(modelViewMatrix * vec4(position, 1.0)).xyz);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float uIntensity;
varying vec3 vNormal;
varying vec3 vView;
void main() {
float rim = pow(1.0 - max(dot(vNormal, vView), 0.0), 4.0);
gl_FragColor = vec4(vec3(1.0, 0.45, 0.1) * rim * uIntensity * 5.0, 1.0);
}
`,
side: THREE.BackSide,
transparent: true,
blending: THREE.AdditiveBlending,
});
coreGroup.add(
new THREE.Mesh(new THREE.SphereGeometry(4.25, 64, 64), auraMat),
);
const instanceCount = 5000;
const streakGeo = new THREE.CylinderGeometry(0.01, 0.12, 2.2, 3);
streakGeo.rotateX(Math.PI / 2);
const diskMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uMorph: { value: 0.1 },
uCompression: { value: 1.0 },
uIntensity: { value: 1.0 },
uOrbitScale: { value: 1.0 },
},
vertexShader: `
${noiseChunk}
uniform float uTime;
uniform float uMorph;
uniform float uCompression;
uniform float uIntensity;
uniform float uOrbitScale;
varying vec3 vColor;
varying float vOpacity;
void main() {
vec4 instPos = instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
float rOriginal = length(instPos.xz);
float r = rOriginal * uCompression;
float initialAngle = atan(instPos.z, instPos.x);
float orbitalVelocity = (1.5 / sqrt(rOriginal)) * uOrbitScale;
float currentAngle = initialAngle + (uTime * orbitalVelocity);
vec3 morphedWorldPos = vec3(cos(currentAngle) * r, instPos.y, sin(currentAngle) * r);
float noise = snoise(vec3(morphedWorldPos.x * 0.08, morphedWorldPos.z * 0.08, uTime * 0.3));
morphedWorldPos.y += noise * uMorph * 4.0;
vec3 viewDir = normalize(cameraPosition - morphedWorldPos);
vec3 orbitDir = normalize(vec3(-sin(currentAngle), 0.0, cos(currentAngle)));
float doppler = dot(orbitDir, viewDir);
vec3 hot = vec3(1.0, 0.95, 0.9);
vec3 warm = vec3(1.0, 0.45, 0.1);
vec3 cool = vec3(0.1, 0.35, 1.0);
vec3 color = mix(cool, warm, smoothstep(45.0, 12.0, r));
color = mix(color, hot, smoothstep(10.0, 4.0, r));
vColor = color * (1.3 + doppler * 0.7) * uIntensity;
vOpacity = (smoothstep(3.8, 5.5, r) * (1.0 - smoothstep(38.0, 48.0, r))) * 0.8;
float deltaAngle = currentAngle - initialAngle;
float c = cos(deltaAngle);
float s = sin(deltaAngle);
mat3 rotY = mat3(
c, 0, s,
0, 1, 0,
-s, 0, c
);
vec3 localPos = (instanceMatrix * vec4(position, 0.0)).xyz;
vec3 rotatedLocalPos = rotY * localPos;
gl_Position = projectionMatrix * viewMatrix * vec4(morphedWorldPos + rotatedLocalPos, 1.0);
}
`,
fragmentShader: `
varying vec3 vColor;
varying float vOpacity;
void main() {
gl_FragColor = vec4(vColor, vOpacity);
}
`,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const instancedDisk = new THREE.InstancedMesh(
streakGeo,
diskMaterial,
instanceCount,
);
const dummy = new THREE.Object3D();
for (let i = 0; i < instanceCount; i++) {
const r = 5 + Math.pow(Math.random(), 1.3) * 40;
const angle = Math.random() * Math.PI * 2;
dummy.position.set(
Math.cos(angle) * r,
(Math.random() - 0.5) * (8 / r),
Math.sin(angle) * r,
);
dummy.lookAt(
dummy.position.x + Math.sin(angle),
dummy.position.y,
dummy.position.z - Math.cos(angle),
);
dummy.updateMatrix();
instancedDisk.setMatrixAt(i, dummy.matrix);
}
scene.add(instancedDisk);
const config = [
{
title: "稳定奇点",
status: "拓扑结构:正常",
morph: 0.1,
compress: 1.0,
intensity: 1.0,
rotate: 0.4,
camY: 25,
camDist: 85,
orbit: 1.0,
color: "#00f3ff",
vel: "0.45c",
},
{
title: "吸积湍流",
status: "拓扑结构:波动",
morph: 4.5,
compress: 1.15,
intensity: 1.4,
rotate: 1.5,
camY: 45,
camDist: 95,
orbit: 1.8,
color: "#ffaa00",
vel: "0.78c",
},
{
title: "相对论坍缩",
status: "拓扑结构:临界",
morph: 0.8,
compress: 0.38,
intensity: 3.5,
rotate: 5.0,
camY: 12,
camDist: 55,
orbit: 4.5,
color: "#ff0044",
vel: "0.99c",
},
];
let stateIdx = 0;
const mainTitle = document.getElementById("main-title");
const statusText = document.getElementById("status-text");
const velVal = document.getElementById("vel-val");
const camControl = { distance: 85 };
function transition() {
stateIdx = (stateIdx + 1) % config.length;
const s = config[stateIdx];
const tl = gsap.timeline({
defaults: { duration: 4.0, ease: "power2.inOut" },
});
tl.to(diskMaterial.uniforms.uMorph, { value: s.morph }, 0);
tl.to(diskMaterial.uniforms.uCompression, { value: s.compress }, 0);
tl.to(diskMaterial.uniforms.uIntensity, { value: s.intensity }, 0);
tl.to(diskMaterial.uniforms.uOrbitScale, { value: s.orbit }, 0);
tl.to(auraMat.uniforms.uIntensity, { value: s.intensity }, 0);
tl.to(controls, { autoRotateSpeed: s.rotate }, 0);
tl.to(camera.position, { y: s.camY }, 0);
tl.to(camControl, { distance: s.camDist }, 0);
gsap.to([mainTitle, statusText, ".val"], {
opacity: 0,
duration: 0.8,
onComplete: () => {
mainTitle.innerText = s.title;
statusText.innerText = s.status;
statusText.style.color = s.color;
statusText.style.borderColor = s.color;
velVal.innerText = s.vel;
velVal.style.color = s.color;
gsap.to([mainTitle, statusText, ".val"], {
opacity: 1,
duration: 1.2,
});
},
});
}
setInterval(transition, 10000);
const clock = new THREE.Clock();
function animate() {
const time = clock.getElapsedTime();
diskMaterial.uniforms.uTime.value = time;
auraMat.uniforms.uTime.value = time;
instancedDisk.rotation.y += 0.0005;
const currentDir = new THREE.Vector3()
.subVectors(camera.position, controls.target)
.normalize();
camera.position.x =
controls.target.x + currentDir.x * camControl.distance;
camera.position.z =
controls.target.z + currentDir.z * camControl.distance;
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>
</body>
</html>
