import React, { useState, useEffect, useRef } from "react"; import axios from "axios"; import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import "../style.css"; interface Point { x: number; y: number; z: number; value: number | string; } const EmbryoGeneration: React.FC = () => { const [cs7Data, setCs7Data] = useState([]); const [cs8Data, setCs8Data] = useState([]); const [cs75Data, setCs75Data] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [animationProgress, setAnimationProgress] = useState(0); const [isAnimating, setIsAnimating] = useState(false); const [animationSpeed, setAnimationSpeed] = useState(2); // 加载数据 useEffect(() => { const loadData = async () => { setLoading(true); setError(""); try { const [cs7Res, cs8Res, cs75Res] = await Promise.all([ axios.get("http://localhost:5000/api/cell", { params: { stage: "CS7" } }), axios.get("http://localhost:5000/api/cell", { params: { stage: "CS8" } }), axios.get("http://localhost:5000/api/cell", { params: { stage: "CS7.5" } }) ]); setCs7Data(cs7Res.data.cells); setCs8Data(cs8Res.data.cells); setCs75Data(cs75Res.data.cells); } catch (err: any) { if (err.response && err.response.data?.error) { setError(err.response.data.error); } else { setError("Failed to fetch embryo data."); } } finally { setLoading(false); } }; loadData(); }, []); // 动画逻辑 useEffect(() => { if (!isAnimating || cs7Data.length === 0 || cs8Data.length === 0 || cs75Data.length === 0) { return; } const duration = 5000 / animationSpeed; const startTime = Date.now(); const animate = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); setAnimationProgress(progress); if (progress < 1) { requestAnimationFrame(animate); } else { setIsAnimating(false); setAnimationProgress(1); } }; animate(); }, [isAnimating, cs7Data.length, cs8Data.length, cs75Data.length, animationSpeed]); const startAnimation = () => { setIsAnimating(true); setAnimationProgress(0); }; const resetAnimation = () => { setIsAnimating(false); setAnimationProgress(0); }; return (

Embryo Generation

观察胚胎发育过程中的细胞迁移和融合。CS7、CS8、CS7.5点云在同一3D视图中展示,动画模拟细胞从两侧汇聚到中间。

进度: {Math.round(animationProgress * 100)}%
{error &&
{error}
}

使用说明

🎬 动画控制

点击"开始动画"按钮观看CS7.5阶段的细胞生成过程。可以调节动画速度,使用重置按钮重新开始。

🖱️ 交互操作

在3D视图中使用鼠标拖拽旋转视角,滚轮缩放,点击数据点查看详细信息。

🎨 颜色编码

相同颜色代表相同细胞类型,便于观察细胞在发育过程中的变化。

📊 发育过程

观察从CS7和CS8到CS7.5的胚胎发育过程,了解细胞的空间分布变化和类型分化。

); }; interface UnifiedEmbryoAnimationProps { cs7Data: Point[]; cs8Data: Point[]; cs75Data: Point[]; animationProgress: number; } const UnifiedEmbryoAnimation: React.FC = ({ cs7Data, cs8Data, cs75Data, animationProgress }) => { const containerRef = useRef(null); const rendererRef = useRef(null); const sceneRef = useRef(null); const controlsRef = useRef(null); const animationIdRef = useRef(null); // 颜色映射 const getColorMap = (allData: Point[]) => { const cellTypes = Array.from(new Set(allData.map(p => p.value as string))).sort(); const colors = [ '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9', '#F8C471', '#82E0AA', '#F1948A', '#85CDCE', '#D7BDE2', '#A9DFBF', '#F9E79F', '#D5A6BD', '#AED6F1', '#E8DAEF' ]; const colorMap = new Map(); cellTypes.forEach((type, idx) => { colorMap.set(type, new THREE.Color(colors[idx % colors.length])); }); return colorMap; }; useEffect(() => { if (!containerRef.current) return; // 清理 if (animationIdRef.current !== null) { cancelAnimationFrame(animationIdRef.current); animationIdRef.current = null; } if (rendererRef.current) { rendererRef.current.dispose(); rendererRef.current = null; } while (containerRef.current.firstChild) { containerRef.current.removeChild(containerRef.current.firstChild); } // 初始化 const width = containerRef.current.clientWidth; const height = containerRef.current.clientHeight; const scene = new THREE.Scene(); sceneRef.current = scene; const camera = new THREE.PerspectiveCamera(30, width / height, 0.1, 1000); camera.position.set(0, 0, 30); camera.lookAt(0, 0, 0); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(width, height); rendererRef.current = renderer; containerRef.current.appendChild(renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controlsRef.current = controls; // 不添加坐标轴 // 颜色映射 const allData = [...cs7Data, ...cs8Data, ...cs75Data]; const colorMap = getColorMap(allData); // 计算所有点的包围盒,自动居中和缩放 let allPositions: number[] = []; cs7Data.forEach(p => { allPositions.push(p.x - 40, p.y, p.z); }); cs8Data.forEach(p => { allPositions.push(p.x + 40, p.y, p.z); }); cs75Data.forEach((p, i) => { const fromX = i % 2 === 0 ? p.x - 40 : p.x + 40; allPositions.push(fromX, p.y, p.z); allPositions.push(p.x, p.y, p.z); }); if (allPositions.length > 0) { const posArr = new Float32Array(allPositions); const box = new THREE.Box3().setFromArray(posArr); const center = new THREE.Vector3(); box.getCenter(center); const size = new THREE.Vector3(); box.getSize(size); // 让最大边长适配canvas const maxDim = Math.max(size.x, size.y, size.z); const fitScale = 32 / maxDim; // 32为经验缩放系数 scene.position.set(-center.x, -center.y, -center.z); scene.scale.set(fitScale, fitScale, fitScale); } // CS7点云(左,静态) if (cs7Data.length > 0) { const geometry = new THREE.BufferGeometry(); const positions: number[] = []; const colors: number[] = []; cs7Data.forEach((p) => { positions.push(p.x - 40, p.y, p.z); const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC'); colors.push(c.r, c.g, c.b); }); geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); const material = new THREE.PointsMaterial({ vertexColors: true, size: 0.7, sizeAttenuation: true, transparent: true, alphaTest: 0.5, depthWrite: false, blending: THREE.NormalBlending, map: (() => { const canvas = document.createElement('canvas'); canvas.width = 64; canvas.height = 64; const ctx = canvas.getContext('2d')!; ctx.clearRect(0, 0, 64, 64); // 绘制带有alpha渐变的圆形 const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 30); gradient.addColorStop(0, 'rgba(255,255,255,1)'); gradient.addColorStop(0.98, 'rgba(255,255,255,1)'); gradient.addColorStop(1, 'rgba(255,255,255,0)'); ctx.beginPath(); ctx.arc(32, 32, 30, 0, 2 * Math.PI); ctx.closePath(); ctx.fillStyle = gradient; ctx.fill(); return new THREE.CanvasTexture(canvas); })() }); const points = new THREE.Points(geometry, material); scene.add(points); } // CS8点云(右,静态) if (cs8Data.length > 0) { const geometry = new THREE.BufferGeometry(); const positions: number[] = []; const colors: number[] = []; cs8Data.forEach((p) => { positions.push(p.x + 40, p.y, p.z); const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC'); colors.push(c.r, c.g, c.b); }); geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); const material = new THREE.PointsMaterial({ vertexColors: true, size: 0.7, sizeAttenuation: true, transparent: true, alphaTest: 0.5, depthWrite: false, blending: THREE.NormalBlending, map: (() => { const canvas = document.createElement('canvas'); canvas.width = 64; canvas.height = 64; const ctx = canvas.getContext('2d')!; ctx.clearRect(0, 0, 64, 64); // 绘制带有alpha渐变的圆形 const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 30); gradient.addColorStop(0, 'rgba(255,255,255,1)'); gradient.addColorStop(0.98, 'rgba(255,255,255,1)'); gradient.addColorStop(1, 'rgba(255,255,255,0)'); ctx.beginPath(); ctx.arc(32, 32, 30, 0, 2 * Math.PI); ctx.closePath(); ctx.fillStyle = gradient; ctx.fill(); return new THREE.CanvasTexture(canvas); })() }); const points = new THREE.Points(geometry, material); scene.add(points); } // CS7.5点云(动画) if (cs75Data.length > 0) { const geometry = new THREE.BufferGeometry(); const positions: number[] = []; const colors: number[] = []; const n = cs75Data.length; const moveDuration = 0.4; // 每个点的动画持续时间(占总进度的比例) const delayPerPoint = 0.6 / n; // 剩余0.6进度用于错峰 for (let i = 0; i < n; i++) { const to = cs75Data[i]; const fromX = i % 2 === 0 ? to.x - 40 : to.x + 40; const fromY = to.y; const fromZ = to.z; const delay = i * delayPerPoint; let pointProgress = (animationProgress - delay) / moveDuration; pointProgress = Math.max(0, Math.min(1, pointProgress)); const x = fromX + (to.x - fromX) * pointProgress; const y = fromY + (to.y - fromY) * pointProgress; const z = fromZ + (to.z - fromZ) * pointProgress; positions.push(x, y, z); const c = colorMap.get(to.value as string) || new THREE.Color('#CCCCCC'); colors.push(c.r, c.g, c.b); } geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); const material = new THREE.PointsMaterial({ vertexColors: true, size: 0.7, sizeAttenuation: true, transparent: true, alphaTest: 0.5, depthWrite: false, blending: THREE.NormalBlending, map: (() => { const canvas = document.createElement('canvas'); canvas.width = 64; canvas.height = 64; const ctx = canvas.getContext('2d')!; ctx.clearRect(0, 0, 64, 64); // 绘制带有alpha渐变的圆形 const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 30); gradient.addColorStop(0, 'rgba(255,255,255,1)'); gradient.addColorStop(0.98, 'rgba(255,255,255,1)'); gradient.addColorStop(1, 'rgba(255,255,255,0)'); ctx.beginPath(); ctx.arc(32, 32, 30, 0, 2 * Math.PI); ctx.closePath(); ctx.fillStyle = gradient; ctx.fill(); return new THREE.CanvasTexture(canvas); })() }); const points = new THREE.Points(geometry, material); scene.add(points); } // 渲染循环 const animate = () => { animationIdRef.current = requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); }; animate(); // 清理 return () => { if (animationIdRef.current !== null) { cancelAnimationFrame(animationIdRef.current); } if (controlsRef.current) { controlsRef.current.dispose(); } if (rendererRef.current) { rendererRef.current.dispose(); } }; }, [cs7Data, cs8Data, cs75Data, animationProgress]); return
; }; export default EmbryoGeneration;