Add mouse cells
This commit is contained in:
parent
da843ed1c4
commit
699bf4c62e
BIN
embryo-backend/Data/GSM9046243_Embryo_E7.5_stereo_rep1.h5ad
(Stored with Git LFS)
Normal file
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
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
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
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
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
BIN
embryo-backend/Data/GSM9046248_Embryo_E8.0_stereo_rep2.h5ad
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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;
|
||||
Loading…
Reference in New Issue
Block a user