digital-embryo/embryo-frontend/src/pages/EmbryoGeneration.tsx

445 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;