Add embryo generation page

This commit is contained in:
wjsjwr 2025-07-26 20:11:46 +08:00
parent 73e83da5fe
commit edb72bc526
7 changed files with 449 additions and 1 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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, # 外层

View File

@ -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() {
<Route path="/gene-view" element={<GeneView />} />
<Route path="/spatial-clustering" element={<SpatialClustering />} />
<Route path="/temporal-analysis" element={<TemporalAnalysis />} />
<Route path="/embryo-generation" element={<EmbryoGeneration />} />
<Route path="/resource" element={<Resource />} />
<Route path="/download" element={<Download />} />
</Routes>

View File

@ -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: "下载" },
];

View File

@ -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<Point[]>([]);
const [cs8Data, setCs8Data] = useState<Point[]>([]);
const [cs75Data, setCs75Data] = useState<Point[]>([]);
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 (
<div className="page-container">
<div className="page-header">
<h1>Embryo Generation</h1>
<p className="page-description">
CS7CS8CS7.53D视图中展示
</p>
</div>
<div className="page-content">
<div className="control-panel" style={{
display: "flex",
alignItems: "center",
gap: "2rem",
marginBottom: "2rem",
padding: "1rem",
backgroundColor: "var(--bg-secondary)",
borderRadius: "8px",
flexWrap: "wrap"
}}>
<button
onClick={startAnimation}
disabled={isAnimating || loading}
style={{
padding: "0.75rem 1.5rem",
backgroundColor: "var(--primary-color)",
color: "white",
border: "none",
borderRadius: "6px",
cursor: isAnimating || loading ? "not-allowed" : "pointer",
opacity: isAnimating || loading ? 0.6 : 1
}}
>
{isAnimating ? "动画中..." : "开始动画"}
</button>
<button
onClick={resetAnimation}
disabled={loading}
style={{
padding: "0.75rem 1.5rem",
backgroundColor: "var(--secondary-color)",
color: "white",
border: "none",
borderRadius: "6px",
cursor: loading ? "not-allowed" : "pointer",
opacity: loading ? 0.6 : 1
}}
>
</button>
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
:
<input
type="range"
min="0.5"
max="3"
step="0.1"
value={animationSpeed}
onChange={(e) => setAnimationSpeed(parseFloat(e.target.value))}
style={{ width: "100px" }}
/>
<span>{animationSpeed}x</span>
</label>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
: {Math.round(animationProgress * 100)}%
<div style={{
width: "100px",
height: "8px",
backgroundColor: "var(--bg-tertiary)",
borderRadius: "4px",
overflow: "hidden"
}}>
<div style={{
width: `${animationProgress * 100}%`,
height: "100%",
backgroundColor: "var(--primary-color)",
transition: "width 0.1s ease"
}} />
</div>
</div>
</div>
{error && <div className="error-message" style={{ marginBottom: "2rem" }}>{error}</div>}
<UnifiedEmbryoAnimation
cs7Data={cs7Data}
cs8Data={cs8Data}
cs75Data={cs75Data}
animationProgress={animationProgress}
/>
<div className="section">
<h2>使</h2>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))", gap: "2rem", marginTop: "1.5rem" }}>
<div>
<h3>🎬 </h3>
<p>"开始动画"CS7.5使</p>
</div>
<div>
<h3>🖱 </h3>
<p>3D视图中使用鼠标拖拽旋转视角</p>
</div>
<div>
<h3>🎨 </h3>
<p>便</p>
</div>
<div>
<h3>📊 </h3>
<p>CS7和CS8到CS7.5</p>
</div>
</div>
</div>
</div>
</div>
);
};
interface UnifiedEmbryoAnimationProps {
cs7Data: Point[];
cs8Data: Point[];
cs75Data: Point[];
animationProgress: number;
}
const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data, cs8Data, cs75Data, animationProgress }) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
const sceneRef = useRef<THREE.Scene | null>(null);
const controlsRef = useRef<any>(null);
const animationIdRef = useRef<number | null>(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<string, THREE.Color>();
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 <div ref={containerRef} className="point-cloud-container" style={{ width: "100%", height: "700px", minHeight: 600 }} />;
};
export default EmbryoGeneration;