's'
This commit is contained in:
176
web-console/components/LiveStudioScene3D.tsx
Normal file
176
web-console/components/LiveStudioScene3D.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user