From 699bf4c62e577271676b5399cad7e30982b910ed Mon Sep 17 00:00:00 2001 From: wjsjwr Date: Sun, 9 Nov 2025 22:29:21 +0800 Subject: [PATCH] Add mouse cells --- .../GSM9046243_Embryo_E7.5_stereo_rep1.h5ad | 3 + .../GSM9046244_Embryo_E7.5_stereo_rep2.h5ad | 3 + .../GSM9046245_Embryo_E7.75_stereo_rep1.h5ad | 3 + .../GSM9046246_Embryo_E7.75_stereo_rep2.h5ad | 3 + .../GSM9046247_Embryo_E8.0_stereo_rep1.h5ad | 3 + .../GSM9046248_Embryo_E8.0_stereo_rep2.h5ad | 3 + .../src/pages/EmbryoGeneration.tsx | 816 +++++++++++------- 7 files changed, 511 insertions(+), 323 deletions(-) create mode 100644 embryo-backend/Data/GSM9046243_Embryo_E7.5_stereo_rep1.h5ad create mode 100644 embryo-backend/Data/GSM9046244_Embryo_E7.5_stereo_rep2.h5ad create mode 100644 embryo-backend/Data/GSM9046245_Embryo_E7.75_stereo_rep1.h5ad create mode 100644 embryo-backend/Data/GSM9046246_Embryo_E7.75_stereo_rep2.h5ad create mode 100644 embryo-backend/Data/GSM9046247_Embryo_E8.0_stereo_rep1.h5ad create mode 100644 embryo-backend/Data/GSM9046248_Embryo_E8.0_stereo_rep2.h5ad diff --git a/embryo-backend/Data/GSM9046243_Embryo_E7.5_stereo_rep1.h5ad b/embryo-backend/Data/GSM9046243_Embryo_E7.5_stereo_rep1.h5ad new file mode 100644 index 0000000..4c7cb2c --- /dev/null +++ b/embryo-backend/Data/GSM9046243_Embryo_E7.5_stereo_rep1.h5ad @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d04fb9ea6d4c3ac2b9b8b0cf1d32ff32cc5d32538548f4d0e6eaa3543f6cd0e +size 47556102 diff --git a/embryo-backend/Data/GSM9046244_Embryo_E7.5_stereo_rep2.h5ad b/embryo-backend/Data/GSM9046244_Embryo_E7.5_stereo_rep2.h5ad new file mode 100644 index 0000000..764482d --- /dev/null +++ b/embryo-backend/Data/GSM9046244_Embryo_E7.5_stereo_rep2.h5ad @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0da8148447ac4d63af4afd52edacfe52969829a8811858ea51db384c177216fb +size 32288835 diff --git a/embryo-backend/Data/GSM9046245_Embryo_E7.75_stereo_rep1.h5ad b/embryo-backend/Data/GSM9046245_Embryo_E7.75_stereo_rep1.h5ad new file mode 100644 index 0000000..056586e --- /dev/null +++ b/embryo-backend/Data/GSM9046245_Embryo_E7.75_stereo_rep1.h5ad @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97aebba7fbee43f95f206f44cda4d213f27d0812e2cc02dcb94949661738e574 +size 82098313 diff --git a/embryo-backend/Data/GSM9046246_Embryo_E7.75_stereo_rep2.h5ad b/embryo-backend/Data/GSM9046246_Embryo_E7.75_stereo_rep2.h5ad new file mode 100644 index 0000000..139c4b5 --- /dev/null +++ b/embryo-backend/Data/GSM9046246_Embryo_E7.75_stereo_rep2.h5ad @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c84b464da4b1f42dff314f61fdffcc4fc3f25e4a1b16ccfb27a49d716c8d7e32 +size 95883553 diff --git a/embryo-backend/Data/GSM9046247_Embryo_E8.0_stereo_rep1.h5ad b/embryo-backend/Data/GSM9046247_Embryo_E8.0_stereo_rep1.h5ad new file mode 100644 index 0000000..b12b771 --- /dev/null +++ b/embryo-backend/Data/GSM9046247_Embryo_E8.0_stereo_rep1.h5ad @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8eb0e02b64920c9ee18a1d4651d417407b7c8ef3c371bcdd04ce6cd694623425 +size 41431274 diff --git a/embryo-backend/Data/GSM9046248_Embryo_E8.0_stereo_rep2.h5ad b/embryo-backend/Data/GSM9046248_Embryo_E8.0_stereo_rep2.h5ad new file mode 100644 index 0000000..353faa8 --- /dev/null +++ b/embryo-backend/Data/GSM9046248_Embryo_E8.0_stereo_rep2.h5ad @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfb50245e46ba285ccdc1648d7e71a839389a29f3cdd302f5f4f021e7c033f8f +size 107093277 diff --git a/embryo-frontend/src/pages/EmbryoGeneration.tsx b/embryo-frontend/src/pages/EmbryoGeneration.tsx index 770855a..87c8af8 100644 --- a/embryo-frontend/src/pages/EmbryoGeneration.tsx +++ b/embryo-frontend/src/pages/EmbryoGeneration.tsx @@ -1,170 +1,110 @@ -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; } - -const EmbryoGeneration: React.FC = () => { - const [cs7Data, setCs7Data] = useState([]); - const [cs8Data, setCs8Data] = useState([]); - const [cs75Data, setCs75Data] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); - const [animationProgress, setAnimationProgress] = useState(0); - const [isAnimating, setIsAnimating] = useState(false); - const [animationSpeed, setAnimationSpeed] = useState(2); - - // 加载数据 - useEffect(() => { - const loadData = async () => { - setLoading(true); - setError(""); - try { - const [cs7Res, cs8Res, cs75Res] = await Promise.all([ - axios.get("http://localhost:5000/api/cell", { params: { stage: "CS7" } }), - axios.get("http://localhost:5000/api/cell", { params: { stage: "CS8" } }), - axios.get("http://localhost:5000/api/cell", { params: { stage: "CS7.5" } }) - ]); - setCs7Data(cs7Res.data.cells); - setCs8Data(cs8Res.data.cells); - setCs75Data(cs75Res.data.cells); - } catch (err: any) { - if (err.response && err.response.data?.error) { - setError(err.response.data.error); - } else { - setError("Failed to fetch embryo data."); - } - } finally { - setLoading(false); +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) } - }; - loadData(); - }, []); - - // 动画逻辑 - useEffect(() => { - if (!isAnimating || cs7Data.length === 0 || cs8Data.length === 0 || cs75Data.length === 0) { - return; } - const duration = 5000 / animationSpeed; - const startTime = Date.now(); - const animate = () => { - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / duration, 1); - setAnimationProgress(progress); - if (progress < 1) { - requestAnimationFrame(animate); - } else { - setIsAnimating(false); - setAnimationProgress(1); + }, + { + 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 + }; } - }; - animate(); - }, [isAnimating, cs7Data.length, cs8Data.length, cs75Data.length, animationSpeed]); - - const startAnimation = () => { - setIsAnimating(true); - setAnimationProgress(0); - }; - const resetAnimation = () => { - setIsAnimating(false); - setAnimationProgress(0); - }; - + } + } +]; +const EmbryoGeneration: React.FC = () => { return (

Embryo Generation

- 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. + 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.

-
- - - -
- Progress: {Math.round(animationProgress * 100)}% -
-
-
-
-
- {error &&
{error}
} - + {ANIMATION_SECTIONS.map((section) => ( + + ))}

User Guide

@@ -191,21 +131,214 @@ const EmbryoGeneration: React.FC = () => { ); }; -interface UnifiedEmbryoAnimationProps { - cs7Data: Point[]; - cs8Data: Point[]; - cs75Data: Point[]; - animationProgress: number; +interface EmbryoAnimationSectionProps { + config: AnimationSectionConfig; } -const UnifiedEmbryoAnimation: React.FC = ({ cs7Data, cs8Data, cs75Data, animationProgress }) => { +const EmbryoAnimationSection: React.FC = ({ config }) => { + const { stages, title, description, layout } = config; + const { sourceLeft, sourceRight, target } = stages; + const [sourceLeftData, setSourceLeftData] = useState([]); + const [sourceRightData, setSourceRightData] = useState([]); + const [targetData, setTargetData] = useState([]); + 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 [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 } }) + ]); + 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); + } else { + setError("Failed to fetch embryo data."); + } + } finally { + setLoading(false); + } + }; + + loadData(); + }, [sourceLeft, sourceRight, target]); + + useEffect(() => { + if (!isAnimating || sourceLeftData.length === 0 || sourceRightData.length === 0 || targetData.length === 0) { + return; + } + const duration = 5000 / animationSpeed; + const startTime = Date.now(); + const animate = () => { + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + setAnimationProgress(progress); + if (progress < 1) { + requestAnimationFrame(animate); + } else { + setIsAnimating(false); + setAnimationProgress(1); + } + }; + animate(); + }, [isAnimating, 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 ( +
+
+

{title}

+

{description}

+

Dataset: {datasetLabel}

+
+
+ + + +
+ Progress: {progressPercent}% +
+
+
+
+
+ {error &&
{error}
} + +
+ ); +}; + + +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 = ({ sourceLeftData, sourceRightData, targetData, animationProgress, layout }) => { const containerRef = useRef(null); const rendererRef = useRef(null); const sceneRef = useRef(null); const controlsRef = useRef(null); const animationIdRef = useRef(null); + const animationStateRef = useRef(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 = ({ 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 = ({ cs7Data rendererRef.current.dispose(); rendererRef.current = null; } - while (containerRef.current.firstChild) { - containerRef.current.removeChild(containerRef.current.firstChild); + 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 = ({ 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 = ({ 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 = ({ 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(); - const cs8ByType = new Map(); - - cs7Data.forEach(point => { + const sourceLeftByType = new Map(); + 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(); + 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 { + sourcePoint = pickFallbackPoint(sourceLeftData, sourceRightData, i, to); + } } 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; + 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 = ({ 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
; }; @@ -551,5 +722,4 @@ const createTexture = () => { ctx.fill(); return new THREE.CanvasTexture(canvas); }; - -export default EmbryoGeneration; \ No newline at end of file +export default EmbryoGeneration;