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 * 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;
}
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 [cs7Data, setCs7Data] = useState<Point[]>([]);
const [cs8Data, setCs8Data] = useState<Point[]>([]);
const [cs75Data, setCs75Data] = useState<Point[]>([]);
return (
<div className="page-container">
<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 [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" } })
const [leftRes, rightRes, targetRes] = await Promise.all([
axios.get("http://localhost:5000/api/cell", { params: { stage: sourceLeft } }),
axios.get("http://localhost:5000/api/cell", { params: { stage: sourceRight } }),
axios.get("http://localhost:5000/api/cell", { params: { stage: target } })
]);
setCs7Data(cs7Res.data.cells);
setCs8Data(cs8Res.data.cells);
setCs75Data(cs75Res.data.cells);
setSourceLeftData(leftRes.data.cells || []);
setSourceRightData(rightRes.data.cells || []);
setTargetData(targetRes.data.cells || []);
setAnimationProgress(0);
setIsAnimating(false);
} catch (err: any) {
if (err.response && err.response.data?.error) {
setError(err.response.data.error);
@ -45,12 +172,12 @@ const EmbryoGeneration: React.FC = () => {
setLoading(false);
}
};
loadData();
}, []);
// 动画逻辑
loadData();
}, [sourceLeft, sourceRight, target]);
useEffect(() => {
if (!isAnimating || cs7Data.length === 0 || cs8Data.length === 0 || cs75Data.length === 0) {
if (!isAnimating || sourceLeftData.length === 0 || sourceRightData.length === 0 || targetData.length === 0) {
return;
}
const duration = 5000 / animationSpeed;
@ -67,26 +194,28 @@ const EmbryoGeneration: React.FC = () => {
}
};
animate();
}, [isAnimating, cs7Data.length, cs8Data.length, cs75Data.length, animationSpeed]);
}, [isAnimating, sourceLeftData.length, sourceRightData.length, targetData.length, animationSpeed]);
const startAnimation = () => {
setIsAnimating(true);
setAnimationProgress(0);
};
const resetAnimation = () => {
setIsAnimating(false);
setAnimationProgress(0);
};
const datasetLabel = `${sourceLeft} + ${sourceRight} -> ${target}`;
const progressPercent = Math.round(animationProgress * 100);
return (
<div className="page-container">
<div className="page-header">
<h1>Embryo Generation</h1>
<p className="page-description">
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>
<section className="section" style={{ marginBottom: "3rem" }}>
<div style={{ marginBottom: "1.5rem" }}>
<h2 style={{ marginBottom: "0.5rem" }}>{title}</h2>
<p className="page-description" style={{ marginBottom: "0.25rem" }}>{description}</p>
<p style={{ color: "var(--text-secondary)", margin: 0 }}>Dataset: {datasetLabel}</p>
</div>
<div className="page-content">
<div className="control-panel" style={{
display: "flex",
alignItems: "center",
@ -141,7 +270,7 @@ const EmbryoGeneration: React.FC = () => {
<span>{animationSpeed}x</span>
</label>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
Progress: {Math.round(animationProgress * 100)}%
Progress: {progressPercent}%
<div style={{
width: "100px",
height: "8px",
@ -160,52 +289,56 @@ const EmbryoGeneration: React.FC = () => {
</div>
{error && <div className="error-message" style={{ marginBottom: "2rem" }}>{error}</div>}
<UnifiedEmbryoAnimation
cs7Data={cs7Data}
cs8Data={cs8Data}
cs75Data={cs75Data}
sourceLeftData={sourceLeftData}
sourceRightData={sourceRightData}
targetData={targetData}
animationProgress={animationProgress}
layout={layout}
/>
<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>
</section>
);
};
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 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 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 cellTypes = Array.from(new Set(allData.map(p => p.value as string))).sort();
const colors = [
@ -221,9 +354,37 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
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(() => {
if (!containerRef.current) return;
// 清理
animationStateRef.current = null;
if (animationIdRef.current !== null) {
cancelAnimationFrame(animationIdRef.current);
animationIdRef.current = null;
@ -232,16 +393,22 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
rendererRef.current.dispose();
rendererRef.current = null;
}
if (controlsRef.current) {
controlsRef.current.dispose();
controlsRef.current = null;
}
if (containerRef.current) {
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);
const camera = new THREE.PerspectiveCamera(cameraParams.fov, width / height, cameraParams.near, cameraParams.far);
camera.position.set(cameraParams.position.x, cameraParams.position.y, cameraParams.position.z);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
@ -250,42 +417,28 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controlsRef.current = controls;
// 不添加坐标轴
// 颜色映射
const allData = [...cs7Data, ...cs8Data, ...cs75Data];
const disposableMaterials: THREE.Material[] = [];
const disposableGeometries: THREE.BufferGeometry[] = [];
const disposableTextures: THREE.Texture[] = [];
const pointTexture = createTexture();
disposableTextures.push(pointTexture);
const allData = [...sourceLeftData, ...sourceRightData, ...targetData];
const colorMap = getColorMap(allData);
const allPositions: number[] = [];
const recordPosition = (x: number, y: number, z: number) => {
allPositions.push(x, y, z);
};
// 计算所有点的包围盒,自动居中和缩放
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) {
if (sourceLeftData.length > 0) {
const geometry = new THREE.BufferGeometry();
const positions: number[] = [];
const colors: number[] = [];
cs7Data.forEach((p) => {
positions.push(p.x - 0.5, p.y, p.z);
sourceLeftData.forEach((p) => {
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');
colors.push(c.r, c.g, c.b);
});
@ -299,35 +452,22 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
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);
})()
map: pointTexture
});
const points = new THREE.Points(geometry, material);
scene.add(points);
disposableGeometries.push(geometry);
disposableMaterials.push(material);
}
// CS8点云静态
if (cs8Data.length > 0) {
if (sourceRightData.length > 0) {
const geometry = new THREE.BufferGeometry();
const positions: number[] = [];
const colors: number[] = [];
cs8Data.forEach((p) => {
positions.push(p.x + 0.5, p.y, p.z);
sourceRightData.forEach((p) => {
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');
colors.push(c.r, c.g, c.b);
});
@ -341,122 +481,118 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
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);
})()
map: pointTexture
});
const points = new THREE.Points(geometry, material);
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 positions: number[] = [];
const colors: number[] = [];
const alphas: number[] = [];
const n = cs75Data.length;
const moveDuration = 0.4; // 每个点的动画持续时间(占总进度的比例)
const delayPerPoint = 0.6 / n; // 剩余0.6进度用于错峰
const n = targetData.length;
const positionsArray = new Float32Array(n * 3);
const colorsArray = new Float32Array(n * 3);
const alphasArray = new Float32Array(n);
const fromPositions = new Float32Array(n * 3);
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的实际数据中获取
// 基于细胞类型或颜色来分配起始位置,确保相同颜色的点从两侧出现
// 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 sourceLeftByType = new Map<string, Point[]>();
sourceLeftData.forEach(point => {
const type = point.value as string;
if (!cs7ByType.has(type)) cs7ByType.set(type, []);
cs7ByType.get(type)!.push(point);
if (!sourceLeftByType.has(type)) sourceLeftByType.set(type, []);
sourceLeftByType.get(type)!.push(point);
});
cs8Data.forEach(point => {
const sourceRightByType = new Map<string, Point[]>();
sourceRightData.forEach(point => {
const type = point.value as string;
if (!cs8ByType.has(type)) cs8ByType.set(type, []);
cs8ByType.get(type)!.push(point);
if (!sourceRightByType.has(type)) sourceRightByType.set(type, []);
sourceRightByType.get(type)!.push(point);
});
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;
let fromX, fromY, fromZ;
// 基于细胞类型和索引的奇偶性来决定从哪一侧开始
const shouldStartFromLeft = i % 2 === 0;
let sourcePoint: Point;
if (shouldStartFromLeft) {
// 从左侧CS7数据开始
const cs7PointsOfType = cs7ByType.get(cellType) || [];
const sourceIndex = Math.floor(i / 2) % cs7PointsOfType.length;
const sourcePoint = cs7PointsOfType.length > 0
? cs7PointsOfType[sourceIndex]
: cs7Data[i % cs7Data.length];
fromX = sourcePoint.x - 0.5;
fromY = sourcePoint.y;
fromZ = sourcePoint.z;
const pointsOfType = sourceLeftByType.get(cellType) || [];
if (pointsOfType.length > 0) {
const sourceIndex = Math.floor(i / 2) % pointsOfType.length;
sourcePoint = pointsOfType[sourceIndex];
} else {
// 从右侧CS8数据开始
const cs8PointsOfType = cs8ByType.get(cellType) || [];
const sourceIndex = Math.floor(i / 2) % cs8PointsOfType.length;
const sourcePoint = cs8PointsOfType.length > 0
? cs8PointsOfType[sourceIndex]
: cs8Data[i % cs8Data.length];
fromX = sourcePoint.x + 0.5;
fromY = sourcePoint.y;
fromZ = sourcePoint.z;
sourcePoint = pickFallbackPoint(sourceLeftData, sourceRightData, i, to);
}
} else {
const pointsOfType = sourceRightByType.get(cellType) || [];
if (pointsOfType.length > 0) {
const sourceIndex = Math.floor(i / 2) % pointsOfType.length;
sourcePoint = pointsOfType[sourceIndex];
} else {
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 randomDelay = (Math.random() - 0.5) * 0.1; // 添加±0.05的随机延迟
const delay = 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);
const randomDelay = (Math.random() - 0.5) * 0.1;
delays[i] = baseDelay + randomDelay;
}
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
geometry.setAttribute("alpha", new THREE.Float32BufferAttribute(alphas, 1));
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positionsArray, 3));
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colorsArray, 3));
geometry.setAttribute("alpha", new THREE.Float32BufferAttribute(alphasArray, 1));
// 创建自定义着色器材质来处理透明度
const vertexShader = `
attribute float alpha;
attribute vec3 color;
@ -475,59 +611,94 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
varying float vAlpha;
varying vec3 vColor;
void main() {
// 使用纹理贴图
vec4 texColor = texture2D(pointTexture, gl_PointCoord);
// 计算当前片段在点内的相对位置
vec2 center = gl_PointCoord - vec2(0.5);
float dist = length(center);
// 创建圆形遮罩距离中心0.5半径内的片段可见
if (dist > 0.5) {
discard; // 丢弃圆形外的片段
discard;
}
// 将纹理颜色与顶点颜色混合
gl_FragColor = vec4(vColor * texColor.rgb, vAlpha * texColor.a);
}
`;
const texture = createTexture(); // 创建纹理贴图与PointsMaterial相同
const material = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
vertexShader,
fragmentShader,
transparent: true,
depthWrite: false,
blending: THREE.NormalBlending,
uniforms: {
pointTexture: { value: texture }
pointTexture: { value: pointTexture }
}
});
const points = new THREE.Points(geometry, material);
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 = () => {
animationIdRef.current = requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
animate();
// 清理
applyAnimationProgress(animationProgress);
return () => {
if (animationIdRef.current !== null) {
cancelAnimationFrame(animationIdRef.current);
animationIdRef.current = null;
}
if (controlsRef.current) {
controlsRef.current.dispose();
controlsRef.current = null;
}
if (rendererRef.current) {
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 }} />;
};
@ -551,5 +722,4 @@ const createTexture = () => {
ctx.fill();
return new THREE.CanvasTexture(canvas);
};
export default EmbryoGeneration;