import * as THREE from "three"; import { quadraticBezier3D, easeOutQuad, lerp, type Vec3 } from "./BezierUtils"; export interface SlideOutConfig { slideDuration?: number; // 滑出时长 ms,默认 350 flyDuration?: number; // 飞入时长 ms,默认 400 arcHeight?: number; // 飞入弧线高度(世界单位),默认 2.0 } interface ActiveAnimation { type: "slide" | "fly"; elapsed: number; duration: number; // slide 专用 container?: THREE.Group; slideDirection?: THREE.Vector3; slideDistance?: number; // fly 专用 mesh?: THREE.Mesh; startPos?: Vec3; controlPos?: Vec3; endPos?: Vec3; startScale?: number; endScale?: number; // 回调 onComplete: () => void; } /** * Three.js 3D 滑出+飞入两阶段动画管理器 * * 阶段1:整条路径(Group)沿方向滑出棋盘 * 阶段2:Head 模型沿 3D 贝塞尔曲线飞入篮子位置(带 Y 轴弧线) */ export class AnimationManager3D { private scene: THREE.Scene; private config: Required; private activeAnimations: ActiveAnimation[] = []; constructor(scene: THREE.Scene, config?: SlideOutConfig) { this.scene = scene; this.config = { slideDuration: config?.slideDuration ?? 350, flyDuration: config?.flyDuration ?? 400, arcHeight: config?.arcHeight ?? 2.0, }; } /** * 执行完整的滑出+飞入动画 * @param pathMeshes 路径上所有 cell 的 Mesh 数组 * @param direction 滑出方向 { dx, dz } * @param stepsToExit 到边界的格子数 * @param cellSize 格子大小 * @param targetPos 篮子目标位置(世界坐标) * @param onSlideComplete 滑出完成回调 * @param onFlyComplete 飞入完成回调 */ slideOutAndFlyToTray( pathMeshes: THREE.Mesh[], direction: { dx: number; dz: number }, stepsToExit: number, cellSize: number, targetPos: THREE.Vector3, onSlideComplete: () => void, onFlyComplete: () => void, ): void { if (pathMeshes.length === 0) { onSlideComplete(); onFlyComplete(); return; } // 阶段1:创建容器,所有 mesh 放入其中统一移动 const container = new THREE.Group(); const meshPositions: THREE.Vector3[] = []; for (const mesh of pathMeshes) { meshPositions.push(mesh.position.clone()); container.add(mesh); // mesh 在 container 内保持世界位置 mesh.position.copy(mesh.position); } this.scene.add(container); const slideDistance = (stepsToExit + pathMeshes.length) * cellSize; const slideDir = new THREE.Vector3(direction.dx, 0, direction.dz).normalize(); // 启动滑出动画 this.activeAnimations.push({ type: "slide", elapsed: 0, duration: this.config.slideDuration / 1000, container, slideDirection: slideDir, slideDistance, onComplete: () => { // 滑出完成:取出 head mesh 做飞入,销毁其余 const headMesh = pathMeshes[pathMeshes.length - 1]; const headWorldPos = new THREE.Vector3(); headMesh.getWorldPosition(headWorldPos); // 销毁 body meshes for (let i = 0; i < pathMeshes.length - 1; i++) { const m = pathMeshes[i]; m.geometry.dispose(); (m.material as THREE.Material).dispose(); } container.remove(headMesh); this.scene.remove(container); // head 放回场景 headMesh.position.copy(headWorldPos); this.scene.add(headMesh); onSlideComplete(); // 阶段2:head 飞入篮子 const startPos: Vec3 = { x: headWorldPos.x, y: headWorldPos.y, z: headWorldPos.z }; const endPos: Vec3 = { x: targetPos.x, y: targetPos.y, z: targetPos.z }; // 控制点:中间位置 + Y 轴抬升 const controlPos: Vec3 = { x: (startPos.x + endPos.x) / 2, y: Math.max(startPos.y, endPos.y) + this.config.arcHeight, z: (startPos.z + endPos.z) / 2, }; this.activeAnimations.push({ type: "fly", elapsed: 0, duration: this.config.flyDuration / 1000, mesh: headMesh, startPos, controlPos, endPos, startScale: headMesh.scale.x, endScale: headMesh.scale.x * 0.5, onComplete: () => { this.scene.remove(headMesh); headMesh.geometry.dispose(); (headMesh.material as THREE.Material).dispose(); onFlyComplete(); }, }); }, }); } /** * 每帧更新(驱动动画) * @param dt 帧间隔(秒) */ update(dt: number): void { for (let i = this.activeAnimations.length - 1; i >= 0; i--) { const anim = this.activeAnimations[i]; anim.elapsed += dt; const t = Math.min(anim.elapsed / anim.duration, 1); const eased = easeOutQuad(t); if (anim.type === "slide" && anim.container && anim.slideDirection) { // 滑出:容器沿方向平移 const dist = eased * anim.slideDistance!; anim.container.position.copy(anim.slideDirection.clone().multiplyScalar(dist)); } else if (anim.type === "fly" && anim.mesh && anim.startPos && anim.controlPos && anim.endPos) { // 飞入:贝塞尔曲线 const pos = quadraticBezier3D(anim.startPos, anim.controlPos, anim.endPos, eased); anim.mesh.position.set(pos.x, pos.y, pos.z); // 缩放插值 const scale = lerp(anim.startScale!, anim.endScale!, eased); anim.mesh.scale.setScalar(scale); // 旋转衰减 anim.mesh.rotation.y *= (1 - eased * 0.5); } if (t >= 1) { anim.onComplete(); this.activeAnimations.splice(i, 1); } } } /** * 是否有动画正在播放 */ isAnimating(): boolean { return this.activeAnimations.length > 0; } /** * 销毁 */ destroy(): void { this.activeAnimations = []; } }