587 lines
17 KiB
TypeScript
587 lines
17 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useRef } from "react";
|
||
import * as THREE from "three";
|
||
|
||
export type TopicPrepVariant = "indie" | "aiagent";
|
||
|
||
type Props = {
|
||
variant: TopicPrepVariant;
|
||
};
|
||
|
||
function disposeObject3D(root: THREE.Object3D) {
|
||
const materials = new Set<THREE.Material>();
|
||
const geometries = new Set<THREE.BufferGeometry>();
|
||
root.traverse((object) => {
|
||
if (object instanceof THREE.Mesh || object instanceof THREE.LineSegments) {
|
||
const g = object.geometry;
|
||
if (g && !geometries.has(g)) {
|
||
geometries.add(g);
|
||
g.dispose();
|
||
}
|
||
const mat = object.material;
|
||
if (Array.isArray(mat)) mat.forEach((m) => materials.add(m));
|
||
else if (mat) materials.add(mat);
|
||
}
|
||
});
|
||
materials.forEach((m) => m.dispose());
|
||
}
|
||
|
||
function applyFilmicRenderer(renderer: THREE.WebGLRenderer, exposure: number) {
|
||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||
renderer.toneMappingExposure = exposure;
|
||
}
|
||
|
||
/** 独立开发:三面墙+顶+地 半封闭小房间(朝镜头开口),萌系配色与小狗 */
|
||
function buildIndieEnclosedRoom(root: THREE.Group): { dog: THREE.Group; tail: THREE.Mesh } {
|
||
const wall = new THREE.MeshStandardMaterial({
|
||
color: 0xfff0f3,
|
||
roughness: 0.94,
|
||
metalness: 0,
|
||
});
|
||
const floorMat = new THREE.MeshStandardMaterial({
|
||
color: 0xf5e0d4,
|
||
roughness: 0.88,
|
||
metalness: 0,
|
||
});
|
||
const ceilMat = new THREE.MeshStandardMaterial({
|
||
color: 0xfff7fb,
|
||
roughness: 0.92,
|
||
metalness: 0,
|
||
});
|
||
|
||
const floor = new THREE.Mesh(new THREE.PlaneGeometry(5.2, 4.2), floorMat);
|
||
floor.rotation.x = -Math.PI / 2;
|
||
floor.position.y = -1;
|
||
root.add(floor);
|
||
|
||
const back = new THREE.Mesh(new THREE.PlaneGeometry(5.2, 3.2), wall);
|
||
back.position.set(0, 0.6, -2.1);
|
||
root.add(back);
|
||
|
||
const left = new THREE.Mesh(new THREE.PlaneGeometry(4.2, 3.2), wall);
|
||
left.rotation.y = Math.PI / 2;
|
||
left.position.set(-2.6, 0.6, 0);
|
||
root.add(left);
|
||
|
||
const right = new THREE.Mesh(new THREE.PlaneGeometry(4.2, 3.2), wall);
|
||
right.rotation.y = -Math.PI / 2;
|
||
right.position.set(2.6, 0.6, 0);
|
||
root.add(right);
|
||
|
||
const ceiling = new THREE.Mesh(new THREE.PlaneGeometry(5.2, 4.2), ceilMat);
|
||
ceiling.rotation.x = Math.PI / 2;
|
||
ceiling.position.y = 2.2;
|
||
root.add(ceiling);
|
||
|
||
const lamp = new THREE.Mesh(
|
||
new THREE.PlaneGeometry(1.4, 0.9),
|
||
new THREE.MeshStandardMaterial({
|
||
color: 0xffffff,
|
||
emissive: 0xfff1f2,
|
||
emissiveIntensity: 0.55,
|
||
roughness: 1,
|
||
}),
|
||
);
|
||
lamp.rotation.x = Math.PI / 2;
|
||
lamp.position.set(0, 2.18, 0.2);
|
||
root.add(lamp);
|
||
|
||
const deskMat = new THREE.MeshStandardMaterial({
|
||
color: 0xf5e6d8,
|
||
roughness: 0.5,
|
||
metalness: 0.04,
|
||
});
|
||
const deskTop = new THREE.Mesh(new THREE.BoxGeometry(1.65, 0.055, 0.72), deskMat);
|
||
deskTop.position.set(-0.25, -0.45, 0.05);
|
||
root.add(deskTop);
|
||
|
||
const legGeo = new THREE.CylinderGeometry(0.038, 0.042, 0.36, 14);
|
||
const legMat = new THREE.MeshStandardMaterial({ color: 0xe8d5c4, roughness: 0.58 });
|
||
for (const [x, z] of [
|
||
[-0.72, -0.22],
|
||
[0.22, -0.22],
|
||
[-0.72, 0.38],
|
||
[0.22, 0.38],
|
||
]) {
|
||
const leg = new THREE.Mesh(legGeo, legMat);
|
||
leg.position.set(x, -0.66, z);
|
||
root.add(leg);
|
||
}
|
||
|
||
const bezel = new THREE.MeshStandardMaterial({
|
||
color: 0x3f3f46,
|
||
roughness: 0.42,
|
||
metalness: 0.12,
|
||
});
|
||
const monitor = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.42, 0.028), bezel);
|
||
monitor.position.set(-0.25, -0.08, -0.38);
|
||
root.add(monitor);
|
||
|
||
const screenMat = new THREE.MeshStandardMaterial({
|
||
color: 0xa5d8ff,
|
||
emissive: 0xc5eaff,
|
||
emissiveIntensity: 0.28,
|
||
roughness: 0.4,
|
||
});
|
||
const screen = new THREE.Mesh(new THREE.PlaneGeometry(0.56, 0.36), screenMat);
|
||
screen.position.set(-0.25, -0.08, -0.38 + 0.015);
|
||
root.add(screen);
|
||
|
||
const kb = new THREE.Mesh(
|
||
new THREE.BoxGeometry(0.48, 0.022, 0.18),
|
||
new THREE.MeshStandardMaterial({ color: 0xa8a29e, roughness: 0.55 }),
|
||
);
|
||
kb.position.set(-0.25, -0.4, 0.22);
|
||
kb.rotation.x = -0.035;
|
||
root.add(kb);
|
||
|
||
const tower = new THREE.Mesh(
|
||
new THREE.BoxGeometry(0.18, 0.36, 0.32),
|
||
new THREE.MeshStandardMaterial({ color: 0xfafafa, roughness: 0.32 }),
|
||
);
|
||
tower.position.set(0.38, -0.38, -0.28);
|
||
root.add(tower);
|
||
|
||
const chairSeat = new THREE.Mesh(
|
||
new THREE.BoxGeometry(0.4, 0.055, 0.36),
|
||
new THREE.MeshStandardMaterial({ color: 0x78716c, roughness: 0.72 }),
|
||
);
|
||
chairSeat.position.set(-0.25, -0.72, 0.55);
|
||
root.add(chairSeat);
|
||
|
||
const fur = new THREE.MeshStandardMaterial({
|
||
color: 0xffc4a8,
|
||
roughness: 0.82,
|
||
metalness: 0,
|
||
});
|
||
const furDeep = new THREE.MeshStandardMaterial({
|
||
color: 0xf59e7c,
|
||
roughness: 0.85,
|
||
metalness: 0,
|
||
});
|
||
|
||
const dog = new THREE.Group();
|
||
dog.position.set(0.75, -0.86, 0.22);
|
||
|
||
const body = new THREE.Mesh(new THREE.SphereGeometry(0.2, 22, 18), fur);
|
||
body.scale.set(1.05, 0.78, 1.12);
|
||
body.position.y = 0.11;
|
||
dog.add(body);
|
||
|
||
const head = new THREE.Mesh(new THREE.SphereGeometry(0.16, 20, 18), fur);
|
||
head.position.set(0, 0.3, 0.1);
|
||
dog.add(head);
|
||
|
||
const snout = new THREE.Mesh(new THREE.SphereGeometry(0.065, 14, 14), furDeep);
|
||
snout.position.set(0, 0.26, 0.22);
|
||
snout.scale.set(1, 0.88, 1.15);
|
||
dog.add(snout);
|
||
|
||
const nose = new THREE.Mesh(
|
||
new THREE.SphereGeometry(0.022, 10, 10),
|
||
new THREE.MeshStandardMaterial({ color: 0x292524, roughness: 0.35 }),
|
||
);
|
||
nose.position.set(0, 0.28, 0.3);
|
||
dog.add(nose);
|
||
|
||
const eyeWhiteL = new THREE.Mesh(
|
||
new THREE.SphereGeometry(0.045, 12, 12),
|
||
new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.25 }),
|
||
);
|
||
eyeWhiteL.position.set(-0.055, 0.32, 0.18);
|
||
const eyeWhiteR = eyeWhiteL.clone();
|
||
eyeWhiteR.position.x = 0.055;
|
||
dog.add(eyeWhiteL, eyeWhiteR);
|
||
|
||
const pupilL = new THREE.Mesh(
|
||
new THREE.SphereGeometry(0.022, 10, 10),
|
||
new THREE.MeshStandardMaterial({ color: 0x1c1917, roughness: 0.2 }),
|
||
);
|
||
pupilL.position.set(-0.052, 0.32, 0.2);
|
||
const pupilR = pupilL.clone();
|
||
pupilR.position.x = 0.052;
|
||
dog.add(pupilL, pupilR);
|
||
|
||
const blushMat = new THREE.MeshStandardMaterial({
|
||
color: 0xff9eb5,
|
||
roughness: 0.6,
|
||
emissive: 0xff8fab,
|
||
emissiveIntensity: 0.12,
|
||
});
|
||
const blushL = new THREE.Mesh(new THREE.SphereGeometry(0.035, 10, 10), blushMat);
|
||
blushL.scale.set(1.2, 0.45, 0.6);
|
||
blushL.position.set(-0.1, 0.24, 0.16);
|
||
const blushR = blushL.clone();
|
||
blushR.position.x = 0.1;
|
||
dog.add(blushL, blushR);
|
||
|
||
const earL = new THREE.Mesh(new THREE.SphereGeometry(0.055, 12, 12), furDeep);
|
||
earL.position.set(-0.11, 0.36, 0.05);
|
||
earL.scale.set(0.45, 0.95, 0.4);
|
||
const earR = earL.clone();
|
||
earR.position.x = 0.11;
|
||
dog.add(earL, earR);
|
||
|
||
const tail = new THREE.Mesh(
|
||
new THREE.CylinderGeometry(0.028, 0.036, 0.2, 12),
|
||
furDeep,
|
||
);
|
||
tail.rotation.z = Math.PI / 2.8;
|
||
tail.rotation.x = 0.25;
|
||
tail.position.set(-0.18, 0.16, -0.06);
|
||
dog.add(tail);
|
||
|
||
for (const [x, z] of [
|
||
[-0.09, 0.07],
|
||
[0.09, 0.07],
|
||
[-0.07, -0.07],
|
||
[0.07, -0.07],
|
||
]) {
|
||
const paw = new THREE.Mesh(new THREE.SphereGeometry(0.048, 12, 12), furDeep);
|
||
paw.scale.set(1, 0.58, 1);
|
||
paw.position.set(x, 0, z);
|
||
dog.add(paw);
|
||
}
|
||
|
||
root.add(dog);
|
||
return { dog, tail };
|
||
}
|
||
|
||
/** AI:赛博空间,无网格线;萌系圆润机器人 + 实体光晕环(非线框) */
|
||
function buildAiCuteCyber(root: THREE.Group): {
|
||
bot: THREE.Group;
|
||
halo: THREE.Mesh;
|
||
orbs: THREE.Mesh[];
|
||
} {
|
||
const backdrop = new THREE.Mesh(
|
||
new THREE.SphereGeometry(16, 48, 48),
|
||
new THREE.MeshBasicMaterial({ color: 0x0c0618, side: THREE.BackSide }),
|
||
);
|
||
root.add(backdrop);
|
||
|
||
const floor = new THREE.Mesh(
|
||
new THREE.CircleGeometry(4.5, 64),
|
||
new THREE.MeshStandardMaterial({
|
||
color: 0x1e1435,
|
||
roughness: 0.55,
|
||
metalness: 0.15,
|
||
emissive: 0x4c1d95,
|
||
emissiveIntensity: 0.06,
|
||
}),
|
||
);
|
||
floor.rotation.x = -Math.PI / 2;
|
||
floor.position.y = -1.05;
|
||
root.add(floor);
|
||
|
||
const haloMat = new THREE.MeshStandardMaterial({
|
||
color: 0xa78bfa,
|
||
emissive: 0x8b5cf6,
|
||
emissiveIntensity: 0.25,
|
||
roughness: 0.35,
|
||
metalness: 0.25,
|
||
transparent: true,
|
||
opacity: 0.55,
|
||
side: THREE.DoubleSide,
|
||
});
|
||
const halo = new THREE.Mesh(new THREE.TorusGeometry(1.05, 0.035, 16, 64), haloMat);
|
||
halo.rotation.x = Math.PI / 2;
|
||
halo.position.y = -0.35;
|
||
root.add(halo);
|
||
|
||
const shell = new THREE.MeshStandardMaterial({
|
||
color: 0xe8e0ff,
|
||
roughness: 0.42,
|
||
metalness: 0.18,
|
||
});
|
||
const shellDark = new THREE.MeshStandardMaterial({
|
||
color: 0xc4b5fd,
|
||
roughness: 0.45,
|
||
metalness: 0.22,
|
||
});
|
||
const eyeGlow = new THREE.MeshStandardMaterial({
|
||
color: 0x67e8f9,
|
||
emissive: 0x22d3ee,
|
||
emissiveIntensity: 0.35,
|
||
roughness: 0.28,
|
||
});
|
||
|
||
const bot = new THREE.Group();
|
||
bot.position.set(0, -0.25, 0);
|
||
|
||
const body = new THREE.Mesh(new THREE.SphereGeometry(0.32, 28, 24), shell);
|
||
body.scale.set(1, 0.88, 0.92);
|
||
body.position.y = 0.05;
|
||
bot.add(body);
|
||
|
||
const tummy = new THREE.Mesh(new THREE.CircleGeometry(0.18, 24), shellDark);
|
||
tummy.rotation.x = -0.15;
|
||
tummy.position.set(0, 0.02, 0.27);
|
||
bot.add(tummy);
|
||
|
||
const head = new THREE.Mesh(new THREE.SphereGeometry(0.26, 28, 24), shell);
|
||
head.position.y = 0.48;
|
||
bot.add(head);
|
||
|
||
const eyeWL = new THREE.Mesh(new THREE.SphereGeometry(0.07, 14, 14), new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.2 }));
|
||
eyeWL.position.set(-0.08, 0.5, 0.22);
|
||
const eyeWR = eyeWL.clone();
|
||
eyeWR.position.x = 0.08;
|
||
bot.add(eyeWL, eyeWR);
|
||
|
||
const eyeL = new THREE.Mesh(new THREE.SphereGeometry(0.038, 12, 12), eyeGlow);
|
||
eyeL.position.set(-0.078, 0.5, 0.26);
|
||
const eyeR = eyeL.clone();
|
||
eyeR.position.x = 0.078;
|
||
bot.add(eyeL, eyeR);
|
||
|
||
const cheek = new THREE.MeshStandardMaterial({
|
||
color: 0xf9a8d4,
|
||
emissive: 0xf472b6,
|
||
emissiveIntensity: 0.15,
|
||
roughness: 0.5,
|
||
});
|
||
const chL = new THREE.Mesh(new THREE.SphereGeometry(0.04, 10, 10), cheek);
|
||
chL.scale.set(1.3, 0.5, 0.5);
|
||
chL.position.set(-0.14, 0.42, 0.2);
|
||
const chR = chL.clone();
|
||
chR.position.x = 0.14;
|
||
bot.add(chL, chR);
|
||
|
||
const antStem = new THREE.Mesh(
|
||
new THREE.CylinderGeometry(0.02, 0.025, 0.16, 10),
|
||
shellDark,
|
||
);
|
||
antStem.position.set(0, 0.72, 0);
|
||
bot.add(antStem);
|
||
const antBall = new THREE.Mesh(new THREE.SphereGeometry(0.055, 16, 16), eyeGlow);
|
||
antBall.position.set(0, 0.82, 0);
|
||
bot.add(antBall);
|
||
|
||
const armGeo = new THREE.CapsuleGeometry(0.07, 0.22, 6, 12);
|
||
const armL = new THREE.Mesh(armGeo, shell);
|
||
armL.position.set(-0.42, 0.08, 0);
|
||
armL.rotation.z = 0.35;
|
||
const armR = new THREE.Mesh(armGeo, shell);
|
||
armR.position.set(0.42, 0.08, 0);
|
||
armR.rotation.z = -0.35;
|
||
bot.add(armL, armR);
|
||
|
||
const leg = new THREE.Mesh(new THREE.CapsuleGeometry(0.09, 0.12, 6, 12), shellDark);
|
||
leg.position.set(-0.12, -0.32, 0.05);
|
||
const leg2 = leg.clone();
|
||
leg2.position.x = 0.12;
|
||
bot.add(leg, leg2);
|
||
|
||
root.add(bot);
|
||
|
||
const orbMat = [
|
||
new THREE.MeshStandardMaterial({ color: 0xf0abfc, roughness: 0.35, metalness: 0.2 }),
|
||
new THREE.MeshStandardMaterial({ color: 0x7dd3fc, roughness: 0.35, metalness: 0.2 }),
|
||
new THREE.MeshStandardMaterial({ color: 0xfde68a, roughness: 0.35, metalness: 0.15 }),
|
||
];
|
||
const orbs: THREE.Mesh[] = [];
|
||
for (let i = 0; i < 3; i++) {
|
||
const o = new THREE.Mesh(new THREE.SphereGeometry(0.09, 16, 16), orbMat[i]);
|
||
const a = (i / 3) * Math.PI * 2;
|
||
o.position.set(Math.cos(a) * 1.15, 0.25 + i * 0.08, Math.sin(a) * 1.15);
|
||
root.add(o);
|
||
orbs.push(o);
|
||
}
|
||
|
||
return { bot, halo, orbs };
|
||
}
|
||
|
||
export function TopicPrepThreeScene({ variant }: Props) {
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||
|
||
useEffect(() => {
|
||
const canvas = canvasRef.current;
|
||
const container = containerRef.current;
|
||
if (!canvas || !container) return;
|
||
|
||
const renderer = new THREE.WebGLRenderer({
|
||
canvas,
|
||
antialias: true,
|
||
alpha: false,
|
||
powerPreference: "high-performance",
|
||
});
|
||
|
||
const scene = new THREE.Scene();
|
||
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
|
||
camera.position.set(0.35, 0.45, 3.85);
|
||
|
||
const group = new THREE.Group();
|
||
scene.add(group);
|
||
|
||
let anim: {
|
||
dog?: THREE.Group;
|
||
tail?: THREE.Mesh;
|
||
bot?: THREE.Group;
|
||
halo?: THREE.Mesh;
|
||
orbs?: THREE.Mesh[];
|
||
} | null = null;
|
||
|
||
if (variant === "indie") {
|
||
scene.fog = new THREE.Fog(0xfff5f7, 3.8, 11);
|
||
applyFilmicRenderer(renderer, 1.02);
|
||
|
||
const hemi = new THREE.HemisphereLight(0xffffff, 0xfce7f3, 0.95);
|
||
scene.add(hemi);
|
||
const key = new THREE.DirectionalLight(0xffffff, 0.48);
|
||
key.position.set(2, 5, 3);
|
||
scene.add(key);
|
||
const fill = new THREE.DirectionalLight(0xffe4e8, 0.28);
|
||
fill.position.set(-3, 2, -1);
|
||
scene.add(fill);
|
||
const scr = new THREE.PointLight(0xbfdbfe, 0.35, 5);
|
||
scr.position.set(-0.25, 0, 0);
|
||
scene.add(scr);
|
||
|
||
anim = buildIndieEnclosedRoom(group);
|
||
group.rotation.set(0.1, 0.28, 0);
|
||
} else {
|
||
scene.fog = new THREE.FogExp2(0x0c0618, 0.048);
|
||
applyFilmicRenderer(renderer, 1.05);
|
||
|
||
const hemi = new THREE.HemisphereLight(0xc4b5fd, 0x1e1038, 0.75);
|
||
scene.add(hemi);
|
||
const key = new THREE.DirectionalLight(0xf5f3ff, 0.5);
|
||
key.position.set(2, 4.5, 3);
|
||
scene.add(key);
|
||
const rim = new THREE.DirectionalLight(0x7c3aed, 0.22);
|
||
rim.position.set(-2.5, 1, -1.5);
|
||
scene.add(rim);
|
||
const glow = new THREE.PointLight(0xa78bfa, 0.4, 9);
|
||
glow.position.set(0.8, 1.2, 1);
|
||
scene.add(glow);
|
||
|
||
anim = buildAiCuteCyber(group);
|
||
group.rotation.set(0.06, -0.15, 0);
|
||
}
|
||
|
||
let rx = group.rotation.x;
|
||
let ry = group.rotation.y;
|
||
let vx = 0;
|
||
let vy = 0;
|
||
const sens = 0.0046;
|
||
const damping = 0.936;
|
||
const autoRy = variant === "indie" ? 0.001 : 0.0013;
|
||
|
||
let dragging = false;
|
||
let lastX = 0;
|
||
let lastY = 0;
|
||
|
||
const onPointerDown = (e: PointerEvent) => {
|
||
dragging = true;
|
||
lastX = e.clientX;
|
||
lastY = e.clientY;
|
||
canvas.setPointerCapture(e.pointerId);
|
||
};
|
||
const onPointerMove = (e: PointerEvent) => {
|
||
if (!dragging) return;
|
||
const dx = e.clientX - lastX;
|
||
const dy = e.clientY - lastY;
|
||
lastX = e.clientX;
|
||
lastY = e.clientY;
|
||
vy += dx * sens;
|
||
vx += dy * sens;
|
||
};
|
||
const onPointerEnd = (e: PointerEvent) => {
|
||
dragging = false;
|
||
try {
|
||
canvas.releasePointerCapture(e.pointerId);
|
||
} catch {
|
||
/* already released */
|
||
}
|
||
};
|
||
|
||
canvas.addEventListener("pointerdown", onPointerDown);
|
||
canvas.addEventListener("pointermove", onPointerMove);
|
||
canvas.addEventListener("pointerup", onPointerEnd);
|
||
canvas.addEventListener("pointercancel", onPointerEnd);
|
||
canvas.style.touchAction = "none";
|
||
|
||
const resize = () => {
|
||
const w = Math.max(280, container.clientWidth);
|
||
const h = Math.min(440, Math.max(240, Math.round(w * 0.52)));
|
||
camera.aspect = w / h;
|
||
camera.updateProjectionMatrix();
|
||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||
renderer.setSize(w, h, false);
|
||
};
|
||
|
||
const ro = new ResizeObserver(resize);
|
||
ro.observe(container);
|
||
resize();
|
||
|
||
let raf = 0;
|
||
const t0 = performance.now();
|
||
const tick = () => {
|
||
raf = requestAnimationFrame(tick);
|
||
const t = (performance.now() - t0) * 0.001;
|
||
if (!dragging) vy += autoRy;
|
||
rx += vx;
|
||
ry += vy;
|
||
vx *= damping;
|
||
vy *= damping;
|
||
group.rotation.x = rx;
|
||
group.rotation.y = ry;
|
||
|
||
if (anim?.dog) {
|
||
anim.dog.position.y = -0.86 + Math.sin(t * 2) * 0.016;
|
||
anim.dog.rotation.y = Math.sin(t * 0.85) * 0.05;
|
||
}
|
||
if (anim?.tail) {
|
||
anim.tail.rotation.z = Math.PI / 2.8 + Math.sin(t * 4.5) * 0.22;
|
||
}
|
||
if (anim?.bot) {
|
||
anim.bot.position.y = -0.25 + Math.sin(t * 1.5) * 0.03;
|
||
anim.bot.rotation.y = Math.sin(t * 0.6) * 0.04;
|
||
}
|
||
if (anim?.halo) {
|
||
anim.halo.rotation.z = t * 0.15;
|
||
}
|
||
if (anim?.orbs) {
|
||
anim.orbs.forEach((o, i) => {
|
||
const a = t * 0.5 + (i / 3) * Math.PI * 2;
|
||
o.position.y = 0.25 + i * 0.08 + Math.sin(t * 1.8 + i) * 0.06;
|
||
o.position.x = Math.cos(a) * 1.15;
|
||
o.position.z = Math.sin(a) * 1.15;
|
||
});
|
||
}
|
||
|
||
renderer.render(scene, camera);
|
||
};
|
||
tick();
|
||
|
||
return () => {
|
||
cancelAnimationFrame(raf);
|
||
ro.disconnect();
|
||
canvas.removeEventListener("pointerdown", onPointerDown);
|
||
canvas.removeEventListener("pointermove", onPointerMove);
|
||
canvas.removeEventListener("pointerup", onPointerEnd);
|
||
canvas.removeEventListener("pointercancel", onPointerEnd);
|
||
scene.fog = null;
|
||
disposeObject3D(group);
|
||
scene.remove(group);
|
||
renderer.dispose();
|
||
};
|
||
}, [variant]);
|
||
|
||
const label = variant === "indie" ? "独立开发互动预览" : "AI Agent 互动预览";
|
||
|
||
return (
|
||
<div ref={containerRef} className="mx-auto mt-2 w-full max-w-2xl">
|
||
<canvas
|
||
ref={canvasRef}
|
||
className="mx-auto block w-full cursor-grab touch-none rounded-2xl shadow-md outline-none active:cursor-grabbing dark:shadow-lg dark:shadow-violet-950/20"
|
||
aria-label={label}
|
||
role="img"
|
||
/>
|
||
</div>
|
||
);
|
||
}
|