diff --git a/embryo-backend/Data/CS7.5.h5ad b/embryo-backend/Data/CS7.5.h5ad new file mode 100644 index 0000000..0b84c95 Binary files /dev/null and b/embryo-backend/Data/CS7.5.h5ad differ diff --git a/embryo-backend/Data/CS8.h5ad b/embryo-backend/Data/CS8.h5ad index 8322a51..a7daa52 100644 Binary files a/embryo-backend/Data/CS8.h5ad and b/embryo-backend/Data/CS8.h5ad differ diff --git a/embryo-backend/Data/CS9.h5ad b/embryo-backend/Data/CS9.h5ad index 3c849a1..06ef344 100644 Binary files a/embryo-backend/Data/CS9.h5ad and b/embryo-backend/Data/CS9.h5ad differ diff --git a/embryo-backend/Data/FakeEmbryo.py b/embryo-backend/Data/FakeEmbryo.py index 0aaa749..dbf8410 100644 --- a/embryo-backend/Data/FakeEmbryo.py +++ b/embryo-backend/Data/FakeEmbryo.py @@ -6,7 +6,7 @@ import os np.random.seed(42) # 参数配置 -stages = [("CS7", 500), ("CS8", 1000), ("CS9", 1500)] +stages = [("CS7", 500), ("CS7.5", 750), ("CS8", 1000), ("CS9", 1500)] genes = ["SOX2", "NANOG", "T", "POU5F1", "OTX2", "ZIC2", "FOXA2", "LEFTY1"] layers = { "Ectoderm": 1.0, # 外层 diff --git a/embryo-frontend/src/App.tsx b/embryo-frontend/src/App.tsx index 39ac189..becfa1f 100644 --- a/embryo-frontend/src/App.tsx +++ b/embryo-frontend/src/App.tsx @@ -7,6 +7,7 @@ import SpatialClustering from "./pages/SpatialClustering"; import Resource from "./pages/Resource"; import Download from "./pages/Download"; import TemporalAnalysis from "./pages/TemporalAnalysis"; +import EmbryoGeneration from "./pages/EmbryoGeneration"; import "./style.css"; function App() { @@ -20,6 +21,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/embryo-frontend/src/components/Navigation.tsx b/embryo-frontend/src/components/Navigation.tsx index f7e9bd2..b187c63 100644 --- a/embryo-frontend/src/components/Navigation.tsx +++ b/embryo-frontend/src/components/Navigation.tsx @@ -9,6 +9,7 @@ const Navigation: React.FC = () => { { path: "/spatial-clustering", label: "Spatial clustering", name: "空间聚类" }, { path: "/gene-view", label: "Gene Expression", name: "基因表达" }, { path: "/temporal-analysis", label: "Temporal Analysis", name: "时间序列分析" }, + { path: "/embryo-generation", label: "Embryo Generation", name: "胚胎生成" }, { path: "/resource", label: "Resource", name: "资源" }, { path: "/download", label: "Download", name: "下载" }, ]; diff --git a/embryo-frontend/src/pages/EmbryoGeneration.tsx b/embryo-frontend/src/pages/EmbryoGeneration.tsx new file mode 100644 index 0000000..79fd777 --- /dev/null +++ b/embryo-frontend/src/pages/EmbryoGeneration.tsx @@ -0,0 +1,445 @@ +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; \ No newline at end of file