Files
gitlab-instance-0a899031_do…/web-console/components/LiveStudioScene3D.tsx
2026-03-29 01:54:10 -05:00

177 lines
5.4 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
type Tr = (zh: string, en: string) => string;
export function LiveStudioScene3D({ tr }: { tr: Tr }) {
const mountRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = mountRef.current;
if (!el) return;
let disposed = false;
let animationId = 0;
let teardown: (() => void) | undefined;
void import("three").then((THREE) => {
if (disposed || !mountRef.current) return;
const mount = mountRef.current;
const width = Math.max(mount.clientWidth, 280);
const height = Math.max(mount.clientHeight, 200);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(48, width / height, 0.1, 80);
camera.position.set(0, 1.1, 7.2);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(typeof window !== "undefined" ? window.devicePixelRatio : 1, 2));
renderer.setSize(width, height);
renderer.setClearColor(0x000000, 0);
mount.appendChild(renderer.domElement);
const hemi = new THREE.HemisphereLight(0xffc4ec, 0x4466ff, 0.85);
scene.add(hemi);
const dir = new THREE.DirectionalLight(0xffffff, 0.9);
dir.position.set(4, 8, 6);
scene.add(dir);
const back = new THREE.PointLight(0x7cf5ff, 0.6, 20);
back.position.set(-3, 2, -2);
scene.add(back);
const group = new THREE.Group();
const studio = new THREE.Mesh(
new THREE.BoxGeometry(3.2, 0.35, 2.2),
new THREE.MeshStandardMaterial({
color: 0x2a2540,
metalness: 0.15,
roughness: 0.55,
}),
);
studio.position.y = -0.85;
group.add(studio);
const ring = new THREE.Mesh(
new THREE.TorusGeometry(0.72, 0.09, 12, 48),
new THREE.MeshStandardMaterial({
color: 0xff3355,
emissive: 0x440011,
metalness: 0.35,
roughness: 0.25,
}),
);
ring.rotation.x = Math.PI / 2;
ring.position.set(-0.85, 0.15, 0.2);
group.add(ring);
const play = new THREE.Mesh(
new THREE.ConeGeometry(0.35, 0.5, 3),
new THREE.MeshStandardMaterial({
color: 0xffffff,
emissive: 0x222222,
metalness: 0.2,
roughness: 0.3,
}),
);
play.rotation.z = -Math.PI / 2;
play.rotation.y = Math.PI;
play.position.set(-0.55, 0.18, 0.2);
group.add(play);
const tk = new THREE.Mesh(
new THREE.CylinderGeometry(0.22, 0.28, 0.95, 20),
new THREE.MeshStandardMaterial({
color: 0x25f4ee,
emissive: 0x053038,
metalness: 0.25,
roughness: 0.35,
}),
);
tk.position.set(1.05, 0.35, -0.15);
tk.rotation.z = 0.12;
group.add(tk);
const tkCap = new THREE.Mesh(
new THREE.SphereGeometry(0.26, 16, 16),
new THREE.MeshStandardMaterial({
color: 0xfe2c55,
emissive: 0x220510,
metalness: 0.2,
roughness: 0.4,
}),
);
tkCap.position.set(1.05, 0.92, -0.15);
group.add(tkCap);
const starGeo = new THREE.BufferGeometry();
const starCount = 120;
const positions = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 14;
positions[i * 3 + 1] = (Math.random() - 0.3) * 8;
positions[i * 3 + 2] = (Math.random() - 0.5) * 8 - 2;
}
starGeo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
const stars = new THREE.Points(
starGeo,
new THREE.PointsMaterial({ color: 0xffffff, size: 0.06, transparent: true, opacity: 0.7 }),
);
scene.add(stars);
scene.add(group);
const t0 = performance.now();
const tick = () => {
if (disposed) return;
animationId = requestAnimationFrame(tick);
const t = (performance.now() - t0) / 1000;
group.rotation.y = Math.sin(t * 0.35) * 0.25 + t * 0.12;
group.position.y = Math.sin(t * 0.9) * 0.06;
ring.rotation.z = t * 0.4;
stars.rotation.y = t * 0.02;
renderer.render(scene, camera);
};
tick();
const ro = new ResizeObserver(() => {
if (!mountRef.current || disposed) return;
const w = Math.max(mountRef.current.clientWidth, 280);
const h = Math.max(mountRef.current.clientHeight, 200);
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
});
ro.observe(mount);
teardown = () => {
cancelAnimationFrame(animationId);
ro.disconnect();
renderer.dispose();
starGeo.dispose();
if (renderer.domElement.parentNode === mount) {
mount.removeChild(renderer.domElement);
}
};
});
return () => {
disposed = true;
cancelAnimationFrame(animationId);
teardown?.();
};
}, []);
return (
<div className="flex h-full min-h-[14rem] flex-col">
<p className="mb-2 px-1 text-[10px] font-medium uppercase tracking-wider text-fuchsia-200/80">
{tr("直播工作室 · 3D", "Live studio · 3D")}
</p>
<div ref={mountRef} className="relative min-h-[12rem] flex-1 overflow-hidden rounded-xl bg-gradient-to-b from-fuchsia-950/40 to-cyan-950/20 ring-1 ring-white/10" />
</div>
);
}