Add mouse cells

This commit is contained in:
wjsjwr 2025-11-09 22:29:21 +08:00
parent da843ed1c4
commit 699bf4c62e
7 changed files with 511 additions and 323 deletions

BIN
embryo-backend/Data/GSM9046243_Embryo_E7.5_stereo_rep1.h5ad (Stored with Git LFS) Normal file

Binary file not shown.

BIN
embryo-backend/Data/GSM9046244_Embryo_E7.5_stereo_rep2.h5ad (Stored with Git LFS) Normal file

Binary file not shown.

BIN
embryo-backend/Data/GSM9046245_Embryo_E7.75_stereo_rep1.h5ad (Stored with Git LFS) Normal file

Binary file not shown.

BIN
embryo-backend/Data/GSM9046246_Embryo_E7.75_stereo_rep2.h5ad (Stored with Git LFS) Normal file

Binary file not shown.

BIN
embryo-backend/Data/GSM9046247_Embryo_E8.0_stereo_rep1.h5ad (Stored with Git LFS) Normal file

Binary file not shown.

BIN
embryo-backend/Data/GSM9046248_Embryo_E8.0_stereo_rep2.h5ad (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,40 +1,167 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef, useCallback } from "react";
import axios from "axios"; import axios from "axios";
import * as THREE from "three"; import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import "../style.css"; import "../style.css";
interface Point { interface Point {
x: number; x: number;
y: number; y: number;
z: number; z: number;
value: number | string; value: number | string;
} }
interface CameraParameters {
fov: number;
near: number;
far: number;
position: THREE.Vector3;
lookAt: THREE.Vector3;
}
interface LayoutConfig {
leftOffsetX: number;
rightOffsetX: number;
targetOffsetX?: number;
cameraParams: CameraParameters;
transFunc?: (a: Point) => Point;
}
interface AnimationSectionConfig {
id: string;
title: string;
description: string;
stages: {
sourceLeft: string;
sourceRight: string;
target: string;
};
layout: LayoutConfig;
}
const ANIMATION_SECTIONS: AnimationSectionConfig[] = [
{
id: "human-cs",
title: "Human Carnegie stages",
description: "CS7 and CS8 point clouds converge into CS7.5 to highlight human embryonic development.",
stages: {
sourceLeft: "CS7",
sourceRight: "CS8",
target: "CS7.5"
},
layout: {
leftOffsetX: -0.5,
rightOffsetX: 0.5,
targetOffsetX: 0,
cameraParams: {
fov: 30,
near: 0.1,
far: 1000,
position: new THREE.Vector3(0, 0, 30),
lookAt: new THREE.Vector3(0, 0, 0)
}
}
},
{
id: "mouse-early",
title: "Mouse early gastrulation",
description: "Mouse 7.5 and 7.75 datasets merge toward stage 8.5, mirroring the human pipeline.",
stages: {
// sourceLeft: "GSM9046243_Embryo_E7.5_stereo_rep1",
// sourceRight: "GSM9046247_Embryo_E8.0_stereo_rep1",
// target: "GSM9046245_Embryo_E7.75_stereo_rep1"
sourceLeft: "GSM9046244_Embryo_E7.5_stereo_rep2",
sourceRight: "GSM9046248_Embryo_E8.0_stereo_rep2",
target: "GSM9046246_Embryo_E7.75_stereo_rep2"
},
layout: {
leftOffsetX: -0.3,
rightOffsetX: 0.3,
targetOffsetX: -0.05,
cameraParams: {
fov: 30,
near: 0.1,
far: 1000,
position: new THREE.Vector3(-10, 0, 30),
lookAt: new THREE.Vector3(0, 0, 0)
},
transFunc: (p: Point) => {
return {
x: p.x / 50,
y: p.y / 50,
z: p.z / 60,
value: p.value
};
}
}
}
];
const EmbryoGeneration: React.FC = () => { const EmbryoGeneration: React.FC = () => {
const [cs7Data, setCs7Data] = useState<Point[]>([]); return (
const [cs8Data, setCs8Data] = useState<Point[]>([]); <div className="page-container">
const [cs75Data, setCs75Data] = useState<Point[]>([]); <div className="page-header">
<h1>Embryo Generation</h1>
<p className="page-description">
Observe cell migration and merging across human CS7/CS8/CS7.5 and mouse 7.5/7.75/8.5 datasets with the same control workflow.
</p>
</div>
<div className="page-content">
{ANIMATION_SECTIONS.map((section) => (
<EmbryoAnimationSection key={section.id} config={section} />
))}
<div className="section">
<h2>User Guide</h2>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))", gap: "2rem", marginTop: "1.5rem" }}>
<div>
<h3>🎬 Animation controls</h3>
<p>Click "Start animation" to watch the CS7.5 cell formation process. Adjust speed and use Reset to start over.</p>
</div>
<div>
<h3>🖱 Interactions</h3>
<p>Drag to rotate, scroll to zoom, and click data points in the 3D view to see details.</p>
</div>
<div>
<h3>🎨 Color encoding</h3>
<p>Identical colors indicate the same cell type, making it easier to observe developmental changes.</p>
</div>
<div>
<h3>📊 Developmental process</h3>
<p>Observe the process from CS7 and CS8 to CS7.5 to understand spatial distribution changes and cell-type differentiation.</p>
</div>
</div>
</div>
</div>
</div>
);
};
interface EmbryoAnimationSectionProps {
config: AnimationSectionConfig;
}
const EmbryoAnimationSection: React.FC<EmbryoAnimationSectionProps> = ({ config }) => {
const { stages, title, description, layout } = config;
const { sourceLeft, sourceRight, target } = stages;
const [sourceLeftData, setSourceLeftData] = useState<Point[]>([]);
const [sourceRightData, setSourceRightData] = useState<Point[]>([]);
const [targetData, setTargetData] = useState<Point[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [animationProgress, setAnimationProgress] = useState(0); const [animationProgress, setAnimationProgress] = useState(0);
const [isAnimating, setIsAnimating] = useState(false); const [isAnimating, setIsAnimating] = useState(false);
const [animationSpeed, setAnimationSpeed] = useState(2); const [animationSpeed, setAnimationSpeed] = useState(2);
// 加载数据
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
setLoading(true); setLoading(true);
setError(""); setError("");
try { try {
const [cs7Res, cs8Res, cs75Res] = await Promise.all([ const [leftRes, rightRes, targetRes] = await Promise.all([
axios.get("http://localhost:5000/api/cell", { params: { stage: "CS7" } }), axios.get("http://localhost:5000/api/cell", { params: { stage: sourceLeft } }),
axios.get("http://localhost:5000/api/cell", { params: { stage: "CS8" } }), axios.get("http://localhost:5000/api/cell", { params: { stage: sourceRight } }),
axios.get("http://localhost:5000/api/cell", { params: { stage: "CS7.5" } }) axios.get("http://localhost:5000/api/cell", { params: { stage: target } })
]); ]);
setCs7Data(cs7Res.data.cells); setSourceLeftData(leftRes.data.cells || []);
setCs8Data(cs8Res.data.cells); setSourceRightData(rightRes.data.cells || []);
setCs75Data(cs75Res.data.cells); setTargetData(targetRes.data.cells || []);
setAnimationProgress(0);
setIsAnimating(false);
} catch (err: any) { } catch (err: any) {
if (err.response && err.response.data?.error) { if (err.response && err.response.data?.error) {
setError(err.response.data.error); setError(err.response.data.error);
@ -45,12 +172,12 @@ const EmbryoGeneration: React.FC = () => {
setLoading(false); setLoading(false);
} }
}; };
loadData();
}, []);
// 动画逻辑 loadData();
}, [sourceLeft, sourceRight, target]);
useEffect(() => { useEffect(() => {
if (!isAnimating || cs7Data.length === 0 || cs8Data.length === 0 || cs75Data.length === 0) { if (!isAnimating || sourceLeftData.length === 0 || sourceRightData.length === 0 || targetData.length === 0) {
return; return;
} }
const duration = 5000 / animationSpeed; const duration = 5000 / animationSpeed;
@ -67,26 +194,28 @@ const EmbryoGeneration: React.FC = () => {
} }
}; };
animate(); animate();
}, [isAnimating, cs7Data.length, cs8Data.length, cs75Data.length, animationSpeed]); }, [isAnimating, sourceLeftData.length, sourceRightData.length, targetData.length, animationSpeed]);
const startAnimation = () => { const startAnimation = () => {
setIsAnimating(true); setIsAnimating(true);
setAnimationProgress(0); setAnimationProgress(0);
}; };
const resetAnimation = () => { const resetAnimation = () => {
setIsAnimating(false); setIsAnimating(false);
setAnimationProgress(0); setAnimationProgress(0);
}; };
const datasetLabel = `${sourceLeft} + ${sourceRight} -> ${target}`;
const progressPercent = Math.round(animationProgress * 100);
return ( return (
<div className="page-container"> <section className="section" style={{ marginBottom: "3rem" }}>
<div className="page-header"> <div style={{ marginBottom: "1.5rem" }}>
<h1>Embryo Generation</h1> <h2 style={{ marginBottom: "0.5rem" }}>{title}</h2>
<p className="page-description"> <p className="page-description" style={{ marginBottom: "0.25rem" }}>{description}</p>
Observe cell migration and merging during embryonic development. CS7, CS8, and CS7.5 point clouds are shown in one 3D view, with an animation simulating cells converging from both sides. <p style={{ color: "var(--text-secondary)", margin: 0 }}>Dataset: {datasetLabel}</p>
</p>
</div> </div>
<div className="page-content">
<div className="control-panel" style={{ <div className="control-panel" style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@ -141,7 +270,7 @@ const EmbryoGeneration: React.FC = () => {
<span>{animationSpeed}x</span> <span>{animationSpeed}x</span>
</label> </label>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
Progress: {Math.round(animationProgress * 100)}% Progress: {progressPercent}%
<div style={{ <div style={{
width: "100px", width: "100px",
height: "8px", height: "8px",
@ -160,52 +289,56 @@ const EmbryoGeneration: React.FC = () => {
</div> </div>
{error && <div className="error-message" style={{ marginBottom: "2rem" }}>{error}</div>} {error && <div className="error-message" style={{ marginBottom: "2rem" }}>{error}</div>}
<UnifiedEmbryoAnimation <UnifiedEmbryoAnimation
cs7Data={cs7Data} sourceLeftData={sourceLeftData}
cs8Data={cs8Data} sourceRightData={sourceRightData}
cs75Data={cs75Data} targetData={targetData}
animationProgress={animationProgress} animationProgress={animationProgress}
layout={layout}
/> />
<div className="section"> </section>
<h2>User Guide</h2>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))", gap: "2rem", marginTop: "1.5rem" }}>
<div>
<h3>🎬 Animation controls</h3>
<p>Click "Start animation" to watch the CS7.5 cell formation process. Adjust speed and use Reset to start over.</p>
</div>
<div>
<h3>🖱 Interactions</h3>
<p>Drag to rotate, scroll to zoom, and click data points in the 3D view to see details.</p>
</div>
<div>
<h3>🎨 Color encoding</h3>
<p>Identical colors indicate the same cell type, making it easier to observe developmental changes.</p>
</div>
<div>
<h3>📊 Developmental process</h3>
<p>Observe the process from CS7 and CS8 to CS7.5 to understand spatial distribution changes and cell-type differentiation.</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 }) => { interface UnifiedEmbryoAnimationProps {
sourceLeftData: Point[];
sourceRightData: Point[];
targetData: Point[];
animationProgress: number;
layout: LayoutConfig;
}
interface TargetAnimationState {
positionsAttr: THREE.BufferAttribute;
alphaAttr: THREE.BufferAttribute;
fromPositions: Float32Array;
toPositions: Float32Array;
delays: Float32Array;
moveDuration: number;
fadeInDuration: number;
}
const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ sourceLeftData, sourceRightData, targetData, animationProgress, layout }) => {
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const rendererRef = useRef<THREE.WebGLRenderer | null>(null); const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
const sceneRef = useRef<THREE.Scene | null>(null); const sceneRef = useRef<THREE.Scene | null>(null);
const controlsRef = useRef<any>(null); const controlsRef = useRef<any>(null);
const animationIdRef = useRef<number | null>(null); const animationIdRef = useRef<number | null>(null);
const animationStateRef = useRef<TargetAnimationState | null>(null);
const { leftOffsetX, rightOffsetX, targetOffsetX = 0, cameraParams, transFunc = null } = layout;
if (transFunc) {
for (let i = 0; i < sourceLeftData.length; i++) {
if (i < 5) console.log(`Source Left Point ${i}: original (${sourceLeftData[i].x.toFixed(2)}, ${sourceLeftData[i].y.toFixed(2)}, ${sourceLeftData[i].z.toFixed(2)})`);
sourceLeftData[i] = transFunc(sourceLeftData[i]);
}
for (let i = 0; i < sourceRightData.length; i++) {
sourceRightData[i] = transFunc(sourceRightData[i]);
}
for (let i = 0; i < targetData.length; i++) {
targetData[i] = transFunc(targetData[i]);
}
}
// 颜色映射
const getColorMap = (allData: Point[]) => { const getColorMap = (allData: Point[]) => {
const cellTypes = Array.from(new Set(allData.map(p => p.value as string))).sort(); const cellTypes = Array.from(new Set(allData.map(p => p.value as string))).sort();
const colors = [ const colors = [
@ -221,9 +354,37 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
return colorMap; return colorMap;
}; };
const applyAnimationProgress = useCallback((progress: number) => {
const animationState = animationStateRef.current;
if (!animationState) return;
const { positionsAttr, alphaAttr, fromPositions, toPositions, delays, moveDuration, fadeInDuration } = animationState;
const positionsArray = positionsAttr.array as Float32Array;
const alphaArray = alphaAttr.array as Float32Array;
const total = delays.length;
for (let i = 0; i < total; i++) {
const delay = delays[i];
let pointProgress = (progress - delay) / moveDuration;
pointProgress = Math.max(0, Math.min(1, pointProgress));
const easeProgress = pointProgress < 0.5
? 2 * pointProgress * pointProgress
: 1 - Math.pow(-2 * pointProgress + 2, 2) / 2;
const baseIndex = i * 3;
positionsArray[baseIndex] = fromPositions[baseIndex] + (toPositions[baseIndex] - fromPositions[baseIndex]) * easeProgress;
positionsArray[baseIndex + 1] = fromPositions[baseIndex + 1] + (toPositions[baseIndex + 1] - fromPositions[baseIndex + 1]) * easeProgress;
positionsArray[baseIndex + 2] = fromPositions[baseIndex + 2] + (toPositions[baseIndex + 2] - fromPositions[baseIndex + 2]) * easeProgress;
const fadeProgress = Math.min(pointProgress / fadeInDuration, 1);
alphaArray[i] = fadeProgress;
}
positionsAttr.needsUpdate = true;
alphaAttr.needsUpdate = true;
}, []);
useEffect(() => { useEffect(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
// 清理
animationStateRef.current = null;
if (animationIdRef.current !== null) { if (animationIdRef.current !== null) {
cancelAnimationFrame(animationIdRef.current); cancelAnimationFrame(animationIdRef.current);
animationIdRef.current = null; animationIdRef.current = null;
@ -232,16 +393,22 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
rendererRef.current.dispose(); rendererRef.current.dispose();
rendererRef.current = null; rendererRef.current = null;
} }
if (controlsRef.current) {
controlsRef.current.dispose();
controlsRef.current = null;
}
if (containerRef.current) {
while (containerRef.current.firstChild) { while (containerRef.current.firstChild) {
containerRef.current.removeChild(containerRef.current.firstChild); containerRef.current.removeChild(containerRef.current.firstChild);
} }
// 初始化 }
const width = containerRef.current.clientWidth; const width = containerRef.current.clientWidth;
const height = containerRef.current.clientHeight; const height = containerRef.current.clientHeight;
const scene = new THREE.Scene(); const scene = new THREE.Scene();
sceneRef.current = scene; sceneRef.current = scene;
const camera = new THREE.PerspectiveCamera(30, width / height, 0.1, 1000); const camera = new THREE.PerspectiveCamera(cameraParams.fov, width / height, cameraParams.near, cameraParams.far);
camera.position.set(0, 0, 30); camera.position.set(cameraParams.position.x, cameraParams.position.y, cameraParams.position.z);
camera.lookAt(0, 0, 0); camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true }); const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height); renderer.setSize(width, height);
@ -250,42 +417,28 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
const controls = new OrbitControls(camera, renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; controls.enableDamping = true;
controlsRef.current = controls; controlsRef.current = controls;
// 不添加坐标轴
// 颜色映射 const disposableMaterials: THREE.Material[] = [];
const allData = [...cs7Data, ...cs8Data, ...cs75Data]; const disposableGeometries: THREE.BufferGeometry[] = [];
const disposableTextures: THREE.Texture[] = [];
const pointTexture = createTexture();
disposableTextures.push(pointTexture);
const allData = [...sourceLeftData, ...sourceRightData, ...targetData];
const colorMap = getColorMap(allData); const colorMap = getColorMap(allData);
const allPositions: number[] = [];
const recordPosition = (x: number, y: number, z: number) => {
allPositions.push(x, y, z);
};
// 计算所有点的包围盒,自动居中和缩放 if (sourceLeftData.length > 0) {
let allPositions: number[] = [];
cs7Data.forEach(p => { allPositions.push(p.x - 0.5, p.y, p.z); });
cs8Data.forEach(p => { allPositions.push(p.x + 0.5,p.y, p.z); });
cs75Data.forEach((p, i) => {
const fromX = i % 2 === 0 ? p.x - 0.5 : p.x + 0.5;
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 geometry = new THREE.BufferGeometry();
const positions: number[] = []; const positions: number[] = [];
const colors: number[] = []; const colors: number[] = [];
cs7Data.forEach((p) => { sourceLeftData.forEach((p) => {
positions.push(p.x - 0.5, p.y, p.z); const shiftedX = p.x + leftOffsetX;
positions.push(shiftedX, p.y, p.z);
recordPosition(shiftedX, p.y, p.z);
const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC'); const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC');
colors.push(c.r, c.g, c.b); colors.push(c.r, c.g, c.b);
}); });
@ -299,35 +452,22 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
alphaTest: 0.5, alphaTest: 0.5,
depthWrite: false, depthWrite: false,
blending: THREE.NormalBlending, blending: THREE.NormalBlending,
map: (() => { map: pointTexture
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); const points = new THREE.Points(geometry, material);
scene.add(points); scene.add(points);
disposableGeometries.push(geometry);
disposableMaterials.push(material);
} }
// CS8点云静态
if (cs8Data.length > 0) { if (sourceRightData.length > 0) {
const geometry = new THREE.BufferGeometry(); const geometry = new THREE.BufferGeometry();
const positions: number[] = []; const positions: number[] = [];
const colors: number[] = []; const colors: number[] = [];
cs8Data.forEach((p) => { sourceRightData.forEach((p) => {
positions.push(p.x + 0.5, p.y, p.z); const shiftedX = p.x + rightOffsetX;
positions.push(shiftedX, p.y, p.z);
recordPosition(shiftedX, p.y, p.z);
const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC'); const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC');
colors.push(c.r, c.g, c.b); colors.push(c.r, c.g, c.b);
}); });
@ -341,122 +481,118 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
alphaTest: 0.5, alphaTest: 0.5,
depthWrite: false, depthWrite: false,
blending: THREE.NormalBlending, blending: THREE.NormalBlending,
map: (() => { map: pointTexture
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); const points = new THREE.Points(geometry, material);
scene.add(points); scene.add(points);
disposableGeometries.push(geometry);
disposableMaterials.push(material);
} }
// CS7.5点云(动画)
if (cs75Data.length > 0) { const pickFallbackPoint = (preferred: Point[], alternative: Point[], index: number, defaultPoint: Point): Point => {
if (preferred.length > 0) {
return preferred[index % preferred.length];
}
if (alternative.length > 0) {
return alternative[index % alternative.length];
}
return defaultPoint;
};
if (targetData.length > 0) {
const geometry = new THREE.BufferGeometry(); const geometry = new THREE.BufferGeometry();
const positions: number[] = []; const n = targetData.length;
const colors: number[] = []; const positionsArray = new Float32Array(n * 3);
const alphas: number[] = []; const colorsArray = new Float32Array(n * 3);
const n = cs75Data.length; const alphasArray = new Float32Array(n);
const moveDuration = 0.4; // 每个点的动画持续时间(占总进度的比例) const fromPositions = new Float32Array(n * 3);
const delayPerPoint = 0.6 / n; // 剩余0.6进度用于错峰 const toPositions = new Float32Array(n * 3);
const delays = new Float32Array(n);
const moveDuration = 0.4;
const delayPerPoint = n > 0 ? 0.6 / n : 0;
const fadeInDuration = 0.3;
// 为CS7.5的点分配起始位置从CS7和CS8的实际数据中获取 const sourceLeftByType = new Map<string, Point[]>();
// 基于细胞类型或颜色来分配起始位置,确保相同颜色的点从两侧出现 sourceLeftData.forEach(point => {
// Left/right source point placeholders removed (unused)
// 按细胞类型分组CS7和CS8数据
const cs7ByType = new Map<string, Point[]>();
const cs8ByType = new Map<string, Point[]>();
cs7Data.forEach(point => {
const type = point.value as string; const type = point.value as string;
if (!cs7ByType.has(type)) cs7ByType.set(type, []); if (!sourceLeftByType.has(type)) sourceLeftByType.set(type, []);
cs7ByType.get(type)!.push(point); sourceLeftByType.get(type)!.push(point);
}); });
cs8Data.forEach(point => { const sourceRightByType = new Map<string, Point[]>();
sourceRightData.forEach(point => {
const type = point.value as string; const type = point.value as string;
if (!cs8ByType.has(type)) cs8ByType.set(type, []); if (!sourceRightByType.has(type)) sourceRightByType.set(type, []);
cs8ByType.get(type)!.push(point); sourceRightByType.get(type)!.push(point);
}); });
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
const to = cs75Data[i]; const to = targetData[i];
if (i < 5) console.log(`Target Point ${i}: to (${to.x.toFixed(2)}, ${to.y.toFixed(2)}, ${to.z.toFixed(2)})`);
const cellType = to.value as string; const cellType = to.value as string;
let fromX, fromY, fromZ;
// 基于细胞类型和索引的奇偶性来决定从哪一侧开始
const shouldStartFromLeft = i % 2 === 0; const shouldStartFromLeft = i % 2 === 0;
let sourcePoint: Point;
if (shouldStartFromLeft) { if (shouldStartFromLeft) {
// 从左侧CS7数据开始 const pointsOfType = sourceLeftByType.get(cellType) || [];
const cs7PointsOfType = cs7ByType.get(cellType) || []; if (pointsOfType.length > 0) {
const sourceIndex = Math.floor(i / 2) % cs7PointsOfType.length; const sourceIndex = Math.floor(i / 2) % pointsOfType.length;
const sourcePoint = cs7PointsOfType.length > 0 sourcePoint = pointsOfType[sourceIndex];
? cs7PointsOfType[sourceIndex]
: cs7Data[i % cs7Data.length];
fromX = sourcePoint.x - 0.5;
fromY = sourcePoint.y;
fromZ = sourcePoint.z;
} else { } else {
// 从右侧CS8数据开始 sourcePoint = pickFallbackPoint(sourceLeftData, sourceRightData, i, to);
const cs8PointsOfType = cs8ByType.get(cellType) || []; }
const sourceIndex = Math.floor(i / 2) % cs8PointsOfType.length; } else {
const sourcePoint = cs8PointsOfType.length > 0 const pointsOfType = sourceRightByType.get(cellType) || [];
? cs8PointsOfType[sourceIndex] if (pointsOfType.length > 0) {
: cs8Data[i % cs8Data.length]; const sourceIndex = Math.floor(i / 2) % pointsOfType.length;
fromX = sourcePoint.x + 0.5; sourcePoint = pointsOfType[sourceIndex];
fromY = sourcePoint.y; } else {
fromZ = sourcePoint.z; sourcePoint = pickFallbackPoint(sourceRightData, sourceLeftData, i, to);
}
} }
// 添加一些随机延迟,使动画更自然 const offsetX = shouldStartFromLeft ? leftOffsetX : rightOffsetX;
const fromBaseIndex = i * 3;
const fromX = sourcePoint.x + offsetX;
const fromY = sourcePoint.y;
const fromZ = sourcePoint.z;
const toBaseX = to.x + targetOffsetX;
const toBaseY = to.y;
const toBaseZ = to.z;
if (i < 5) {
console.log(`Point ${i}: from (${fromX.toFixed(2)}, ${fromY.toFixed(2)}, ${fromZ.toFixed(2)}) to (${toBaseX.toFixed(2)}, ${toBaseY.toFixed(2)}, ${toBaseZ.toFixed(2)})`);
}
fromPositions[fromBaseIndex] = fromX;
fromPositions[fromBaseIndex + 1] = fromY;
fromPositions[fromBaseIndex + 2] = fromZ;
toPositions[fromBaseIndex] = toBaseX;
toPositions[fromBaseIndex + 1] = toBaseY;
toPositions[fromBaseIndex + 2] = toBaseZ;
positionsArray[fromBaseIndex] = fromX;
positionsArray[fromBaseIndex + 1] = fromY;
positionsArray[fromBaseIndex + 2] = fromZ;
const color = colorMap.get(cellType) || new THREE.Color('#CCCCCC');
colorsArray[fromBaseIndex] = color.r;
colorsArray[fromBaseIndex + 1] = color.g;
colorsArray[fromBaseIndex + 2] = color.b;
alphasArray[i] = 0;
recordPosition(fromX, fromY, fromZ);
recordPosition(toBaseX, toBaseY, toBaseZ);
const baseDelay = i * delayPerPoint; const baseDelay = i * delayPerPoint;
const randomDelay = (Math.random() - 0.5) * 0.1; // 添加±0.05的随机延迟 const randomDelay = (Math.random() - 0.5) * 0.1;
const delay = baseDelay + randomDelay; delays[i] = baseDelay + randomDelay;
let pointProgress = (animationProgress - delay) / moveDuration;
pointProgress = Math.max(0, Math.min(1, pointProgress));
// 使用缓动函数使动画更平滑
const easeProgress = pointProgress < 0.5
? 2 * pointProgress * pointProgress
: 1 - Math.pow(-2 * pointProgress + 2, 2) / 2;
const x = fromX + (to.x - fromX) * easeProgress;
const y = fromY + (to.y - fromY) * easeProgress;
const z = fromZ + (to.z - fromZ) * easeProgress;
// 计算透明度:从透明(0)到不透明(1)
// 透明度在动画的前30%时间内从0过渡到1
const fadeInDuration = 0.3;
const fadeProgress = Math.min(pointProgress / fadeInDuration, 1);
const alpha = fadeProgress;
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);
alphas.push(alpha);
} }
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); geometry.setAttribute("position", new THREE.Float32BufferAttribute(positionsArray, 3));
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); geometry.setAttribute("color", new THREE.Float32BufferAttribute(colorsArray, 3));
geometry.setAttribute("alpha", new THREE.Float32BufferAttribute(alphas, 1)); geometry.setAttribute("alpha", new THREE.Float32BufferAttribute(alphasArray, 1));
// 创建自定义着色器材质来处理透明度
const vertexShader = ` const vertexShader = `
attribute float alpha; attribute float alpha;
attribute vec3 color; attribute vec3 color;
@ -475,59 +611,94 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
varying float vAlpha; varying float vAlpha;
varying vec3 vColor; varying vec3 vColor;
void main() { void main() {
// 使用纹理贴图
vec4 texColor = texture2D(pointTexture, gl_PointCoord); vec4 texColor = texture2D(pointTexture, gl_PointCoord);
// 计算当前片段在点内的相对位置
vec2 center = gl_PointCoord - vec2(0.5); vec2 center = gl_PointCoord - vec2(0.5);
float dist = length(center); float dist = length(center);
// 创建圆形遮罩距离中心0.5半径内的片段可见
if (dist > 0.5) { if (dist > 0.5) {
discard; // 丢弃圆形外的片段 discard;
} }
// 将纹理颜色与顶点颜色混合
gl_FragColor = vec4(vColor * texColor.rgb, vAlpha * texColor.a); gl_FragColor = vec4(vColor * texColor.rgb, vAlpha * texColor.a);
} }
`; `;
const texture = createTexture(); // 创建纹理贴图与PointsMaterial相同
const material = new THREE.ShaderMaterial({ const material = new THREE.ShaderMaterial({
vertexShader: vertexShader, vertexShader,
fragmentShader: fragmentShader, fragmentShader,
transparent: true, transparent: true,
depthWrite: false, depthWrite: false,
blending: THREE.NormalBlending, blending: THREE.NormalBlending,
uniforms: { uniforms: {
pointTexture: { value: texture } pointTexture: { value: pointTexture }
} }
}); });
const points = new THREE.Points(geometry, material); const points = new THREE.Points(geometry, material);
scene.add(points); scene.add(points);
disposableGeometries.push(geometry);
disposableMaterials.push(material);
animationStateRef.current = {
positionsAttr: geometry.getAttribute("position") as THREE.BufferAttribute,
alphaAttr: geometry.getAttribute("alpha") as THREE.BufferAttribute,
fromPositions,
toPositions,
delays,
moveDuration,
fadeInDuration
};
} else {
animationStateRef.current = null;
} }
// 渲染循环
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);
const maxDim = Math.max(size.x, size.y, size.z);
const fitScale = maxDim === 0 ? 1 : 32 / maxDim;
scene.position.set(-center.x, -center.y, -center.z);
scene.scale.set(fitScale, fitScale, fitScale);
}
const animate = () => { const animate = () => {
animationIdRef.current = requestAnimationFrame(animate); animationIdRef.current = requestAnimationFrame(animate);
controls.update(); controls.update();
renderer.render(scene, camera); renderer.render(scene, camera);
}; };
animate(); animate();
// 清理
applyAnimationProgress(animationProgress);
return () => { return () => {
if (animationIdRef.current !== null) { if (animationIdRef.current !== null) {
cancelAnimationFrame(animationIdRef.current); cancelAnimationFrame(animationIdRef.current);
animationIdRef.current = null;
} }
if (controlsRef.current) { if (controlsRef.current) {
controlsRef.current.dispose(); controlsRef.current.dispose();
controlsRef.current = null;
} }
if (rendererRef.current) { if (rendererRef.current) {
rendererRef.current.dispose(); rendererRef.current.dispose();
rendererRef.current = null;
} }
disposableMaterials.forEach((material) => material.dispose());
disposableGeometries.forEach((geometry) => geometry.dispose());
disposableTextures.forEach((texture) => texture.dispose());
if (containerRef.current) {
while (containerRef.current.firstChild) {
containerRef.current.removeChild(containerRef.current.firstChild);
}
}
animationStateRef.current = null;
}; };
}, [cs7Data, cs8Data, cs75Data, animationProgress]); }, [sourceLeftData, sourceRightData, targetData, leftOffsetX, rightOffsetX, targetOffsetX, applyAnimationProgress]);
useEffect(() => {
applyAnimationProgress(animationProgress);
}, [animationProgress, applyAnimationProgress]);
return <div ref={containerRef} className="point-cloud-container" style={{ width: "100%", height: "700px", minHeight: 600 }} />; return <div ref={containerRef} className="point-cloud-container" style={{ width: "100%", height: "700px", minHeight: 600 }} />;
}; };
@ -551,5 +722,4 @@ const createTexture = () => {
ctx.fill(); ctx.fill();
return new THREE.CanvasTexture(canvas); return new THREE.CanvasTexture(canvas);
}; };
export default EmbryoGeneration; export default EmbryoGeneration;